/* * Copyright (c) 2015, The Linux Foundation. All rights reserved. * Not a Contribution * * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.gallery3d.ui; import android.graphics.Rect; import android.text.TextUtils; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; import android.view.animation.DecelerateInterpolator; import com.android.gallery3d.anim.Animation; import com.android.gallery3d.app.AbstractGalleryActivity; import com.android.gallery3d.common.ApiHelper.SystemProperties; import com.android.gallery3d.common.Utils; import com.android.gallery3d.glrenderer.GLCanvas; import java.util.Locale; public class TimeLineSlotView extends GLView { @SuppressWarnings("unused") private static final String TAG = "TimeLineSlotView"; public static final int INDEX_NONE = -1; private static final int mainKey = SystemProperties.getInt("qemu.hw.mainkeys", 1); public static final int RENDER_MORE_PASS = 1; public static final int RENDER_MORE_FRAME = 2; private int mWidth = 0; public interface Listener { public void onDown(int index); public void onUp(boolean followedByLongPress); public void onSingleTapUp(int index, boolean isTitle); public void onLongTap(int index, boolean isTitle); public void onScrollPositionChanged(int position, int total); } public static class SimpleListener implements Listener { @Override public void onDown(int index) {} @Override public void onUp(boolean followedByLongPress) {} @Override public void onSingleTapUp(int index, boolean isTitle) {} @Override public void onLongTap(int index, boolean isTitle) {} @Override public void onScrollPositionChanged(int position, int total) {} } private final GestureDetector mGestureDetector; private final ScrollerHelper mScroller; private Listener mListener; private SlotAnimation mAnimation = null; private final Layout mLayout = new Layout(); private int mStartIndex = INDEX_NONE; // whether the down action happened while the view is scrolling. private boolean mDownInScrolling; private TimeLineSlotRenderer mRenderer; private int[] mRequestRenderSlots = new int[16]; // Flag to check whether it is come from Photo Page. private boolean isFromPhotoPage = false; public TimeLineSlotView(AbstractGalleryActivity activity, Spec spec) { mGestureDetector = new GestureDetector(activity, new MyGestureListener()); mScroller = new ScrollerHelper(activity); setSlotSpec(spec); } public void setSlotRenderer(TimeLineSlotRenderer slotDrawer) { mRenderer = slotDrawer; if (mRenderer != null) { mRenderer.onVisibleRangeChanged(getVisibleStart(), getVisibleEnd()); } } public void setCenterIndex(int index) { int size = mLayout.getSlotSize(); if (index < 0 || index >= size) { return; } Rect rect = mLayout.getSlotRect(index); if (rect != null) { int position = (rect.top + rect.bottom - getHeight()) / 2; setScrollPosition(position); } } public void makeSlotVisible(int index) { Rect rect = mLayout.getSlotRect(index); if (rect == null) return; int visibleBegin = mScrollY; int visibleLength = getHeight(); int visibleEnd = visibleBegin + visibleLength; int slotBegin = rect.top; int slotEnd = rect.bottom; int position = visibleBegin; if (visibleLength < slotEnd - slotBegin) { position = visibleBegin; } else if (slotBegin < visibleBegin) { position = slotBegin; } else if (slotEnd > visibleEnd && mainKey == 1) { position = slotEnd - visibleLength; } else if (slotBegin > visibleEnd && mainKey == 0) { position = slotBegin - visibleLength; } setScrollPosition(position); } /** * Set the flag which used for check whether it is come from Photo Page. */ public void setIsFromPhotoPage(boolean flag) { isFromPhotoPage = flag; } public void setScrollPosition(int position) { position = Utils.clamp(position, 0, mLayout.getScrollLimit()); mScroller.setPosition(position); updateScrollPosition(position, false); } public void setSlotSpec(Spec spec) { mLayout.setSlotSpec(spec); } @Override protected void onLayout(boolean changeSize, int l, int t, int r, int b) { if (!changeSize) return; mWidth = r - l; // Make sure we are still at a reasonable scroll position after the size // is changed (like orientation change). We choose to keep the center // visible slot still visible. This is arbitrary but reasonable. int visibleIndex = (mLayout.getVisibleStart() + mLayout.getVisibleEnd()) / 2; mLayout.setSize(r - l, b - t); makeSlotVisible(visibleIndex); } public void startScatteringAnimation(RelativePosition position) { mAnimation = new ScatteringAnimation(position); mAnimation.start(); if (mLayout.getSlotSize() != 0) invalidate(); } public void startRisingAnimation() { mAnimation = new RisingAnimation(); mAnimation.start(); if (mLayout.getSlotSize() != 0) invalidate(); } private void updateScrollPosition(int position, boolean force) { if (!force && (position == mScrollY)) return; mScrollY = position; mLayout.setScrollPosition(position); onScrollPositionChanged(position); } protected void onScrollPositionChanged(int newPosition) { int limit = mLayout.getScrollLimit(); mListener.onScrollPositionChanged(newPosition, limit); } public Rect getSlotRect(int slotIndex) { return mLayout.getSlotRect(slotIndex); } @Override protected boolean onTouch(MotionEvent event) { mGestureDetector.onTouchEvent(event); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mDownInScrolling = !mScroller.isFinished(); mScroller.forceFinished(); break; case MotionEvent.ACTION_UP: invalidate(); break; } return true; } public void setListener(Listener listener) { mListener = listener; } private static int[] expandIntArray(int array[], int capacity) { while (array.length < capacity) { array = new int[array.length * 2]; } return array; } @Override protected void render(GLCanvas canvas) { super.render(canvas); if (mRenderer == null) return; mRenderer.prepareDrawing(); long animTime = AnimationTime.get(); boolean more = mScroller.advanceAnimation(animTime); int oldX = mScrollX; updateScrollPosition(mScroller.getPosition(), false); if (mAnimation != null) { more |= mAnimation.calculate(animTime); } canvas.translate(-mScrollX, -mScrollY); int requestCount = 0; int requestedSlot[] = expandIntArray(mRequestRenderSlots, mLayout.getVisibleEnd() - mLayout.getVisibleStart()); for (int i = mLayout.getVisibleEnd() - 1; i >= mLayout.getVisibleStart(); --i) { int r = renderItem(canvas, i, 0); if ((r & RENDER_MORE_FRAME) != 0) more = true; if ((r & RENDER_MORE_PASS) != 0) requestedSlot[requestCount++] = i; } for (int pass = 1; requestCount != 0; ++pass) { int newCount = 0; for (int i = 0; i < requestCount; ++i) { int r = renderItem(canvas, requestedSlot[i], pass); if ((r & RENDER_MORE_FRAME) != 0) more = false; if ((r & RENDER_MORE_PASS) != 0) requestedSlot[newCount++] = i; } requestCount = newCount; } canvas.translate(mScrollX, mScrollY); if (more) invalidate(); } private int renderItem(GLCanvas canvas, int index, int pass) { Rect rect = mLayout.getSlotRect(index); if (rect == null) return 0; canvas.save(GLCanvas.SAVE_FLAG_ALPHA | GLCanvas.SAVE_FLAG_MATRIX); canvas.translate(rect.left, rect.top, 0); if (mAnimation != null && mAnimation.isActive()) { mAnimation.apply(canvas, index, rect); } int result = mRenderer.renderSlot( canvas, index, pass, rect.right - rect.left, rect.bottom - rect.top); canvas.restore(); return result; } public static abstract class SlotAnimation extends Animation { protected float mProgress = 0; public SlotAnimation() { setInterpolator(new DecelerateInterpolator(4)); setDuration(1500); } @Override protected void onCalculate(float progress) { mProgress = progress; } abstract public void apply(GLCanvas canvas, int slotIndex, Rect target); } public static class RisingAnimation extends SlotAnimation { private static final int RISING_DISTANCE = 128; @Override public void apply(GLCanvas canvas, int slotIndex, Rect target) { canvas.translate(0, 0, RISING_DISTANCE * (1 - mProgress)); } } public static class ScatteringAnimation extends SlotAnimation { private int PHOTO_DISTANCE = 1000; private RelativePosition mCenter; public ScatteringAnimation(RelativePosition center) { mCenter = center; } @Override public void apply(GLCanvas canvas, int slotIndex, Rect target) { canvas.translate( (mCenter.getX() - target.centerX()) * (1 - mProgress), (mCenter.getY() - target.centerY()) * (1 - mProgress), slotIndex * PHOTO_DISTANCE * (1 - mProgress)); canvas.setAlpha(mProgress); } } private class MyGestureListener implements GestureDetector.OnGestureListener { private boolean isDown; // We call the listener's onDown() when our onShowPress() is called and // call the listener's onUp() when we receive any further event. @Override public void onShowPress(MotionEvent e) { GLRoot root = getGLRoot(); root.lockRenderThread(); try { if (isDown) return; Slot slot = mLayout.getSlotByPosition(e.getX(), e.getY()); if (slot != null) { isDown = true; mListener.onDown(slot.index); } } finally { root.unlockRenderThread(); } } private void cancelDown(boolean byLongPress) { if (!isDown) return; isDown = false; mListener.onUp(byLongPress); } @Override public boolean onDown(MotionEvent e) { return false; } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { cancelDown(false); int scrollLimit = mLayout.getScrollLimit(); if (scrollLimit == 0) return false; mScroller.fling((int) -velocityY, 0, scrollLimit); invalidate(); return true; } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { cancelDown(false); int overDistance = mScroller.startScroll( Math.round(distanceY), 0, mLayout.getScrollLimit()); invalidate(); return true; } @Override public boolean onSingleTapUp(MotionEvent e) { cancelDown(false); if (mDownInScrolling) return true; Slot slot = mLayout.getSlotByPosition(e.getX(), e.getY()); if (slot != null) { mListener.onSingleTapUp(slot.index, slot.isTitle); } return true; } @Override public void onLongPress(MotionEvent e) { cancelDown(true); if (mDownInScrolling) return; lockRendering(); try { Slot slot = mLayout.getSlotByPosition(e.getX(), e.getY()); if (slot != null) { mListener.onLongTap(slot.index, slot.isTitle); } } finally { unlockRendering(); } } } public void setStartIndex(int index) { mStartIndex = index; } public void setSlotCount(int[] count) { mLayout.setSlotCount(count); // mStartIndex is applied the first time setSlotCount is called. if (mStartIndex != INDEX_NONE) { setCenterIndex(mStartIndex); mStartIndex = INDEX_NONE; } // Reset the scroll position to avoid scrolling over the updated limit. setScrollPosition(mScrollY); } public int getVisibleStart() { return mLayout.getVisibleStart(); } public int getVisibleEnd() { return mLayout.getVisibleEnd(); } public int getScrollX() { return mScrollX; } public int getScrollY() { return mScrollY; } public Rect getSlotRect(int slotIndex, GLView rootPane) { // Get slot rectangle relative to this root pane. Rect offset = new Rect(); rootPane.getBoundsOf(this, offset); Rect r = getSlotRect(slotIndex); if (r != null) { r.offset(offset.left - getScrollX(), offset.top - getScrollY()); return r; } return offset; } public int getTitleWidth() { return mWidth; } // This Spec class is used to specify the size of each slot in the SlotView. // There are two ways to do it: // // Specify colsLand, colsPort, and slotGap: they specify the number // of rows in landscape/portrait mode and the gap between slots. The // width and height of each slot is determined automatically. // // The initial value of -1 means they are not specified. public static class Spec { public int colsLand = -1; public int colsPort = -1; public int titleHeight = -1; public int slotGapPort = -1; public int slotGapLand = -1; } public class Layout { private int mVisibleStart; private int mVisibleEnd; public int mSlotSize; private int mSlotWidth; private int mSlotHeight; private int mSlotGap; private int[] mSlotCount; private Spec mSpec; private int mWidth; private int mHeight; private int mUnitCount; private int mContentLength; private int mScrollPosition; public void setSlotSpec(TimeLineSlotView.Spec spec) { mSpec = spec; } public void setSlotCount(int[] count) { mSlotCount = count; if (mHeight != 0) { initLayoutParameters(); createSlots(); } } public int getSlotSize() { return mSlotSize; } public Rect getSlotRect(int index) { if (index >= mVisibleStart && index < mVisibleEnd && mVisibleEnd != 0) { int height = 0, base = 0, top = 0; for (int count : mSlotCount) { if (index == base) { return getSlotRect(getSlot(true, index, index, top)); } top += mSpec.titleHeight; ++base; if (index >= base && index < base + count) { return getSlotRect(getSlot(false, index, base, top)); } int rows = (count + mUnitCount - 1) / mUnitCount; top += mSlotHeight * rows + mSlotGap * (rows > 0 ? rows - 1 : 0); base += count; } } return null; } private void initLayoutParameters() { mUnitCount = (mWidth > mHeight) ? mSpec.colsLand : mSpec.colsPort; mSlotGap = (mWidth > mHeight) ? mSpec.slotGapLand: mSpec.slotGapPort; mSlotWidth = Math.round((mWidth - (mUnitCount - 1) * mSlotGap) / mUnitCount); mSlotHeight = mSlotWidth; if (mRenderer != null) { mRenderer.onSlotSizeChanged(mSlotWidth, mSlotHeight); } } private void setSize(int width, int height) { if (width != mWidth || height != mHeight) { mWidth = width; mHeight = height; initLayoutParameters(); createSlots(); } } public void setScrollPosition(int position) { if (mScrollPosition == position) return; mScrollPosition = position; updateVisibleSlotRange(); } private Rect getSlotRect(Slot slot) { int x, y, w, h; if (slot.isTitle) { x = 0; y = slot.top; w = mWidth; h = mSpec.titleHeight; } else { x = slot.col * (mSlotWidth + mSlotGap); y = slot.top; w = mSlotWidth; h = mSlotHeight; } return new Rect(x, y, x + w, y + h); } private synchronized void updateVisibleSlotRange() { int position = mScrollPosition; if (mSlotCount != null) { Slot begin = getSlotByPosition(0, mScrollPosition, true, false), end = getSlotByPosition(0, mScrollPosition + mHeight, true, true); if (begin == null && end != null && end.index == 0) { setVisibleRange(0, 0); } else if (begin != null && end != null) { setVisibleRange(begin.index, end.index); } } } private void setVisibleRange(int start, int end) { if (start == mVisibleStart && end == mVisibleEnd) return; if (start < end) { mVisibleStart = start; mVisibleEnd = end; } else { mVisibleStart = mVisibleEnd = 0; } if (mRenderer != null) { mRenderer.onVisibleRangeChanged(mVisibleStart, mVisibleEnd); } } public int getVisibleStart() { return mVisibleStart; } public int getVisibleEnd() { return mVisibleEnd; } private Slot getSlot(boolean isTitle, int index, int indexBase, int top) { if (isTitle) { return new Slot(true, index, 0, top); } else { int row = (index - indexBase) / mUnitCount; return new Slot(false, index, (index - indexBase) % mUnitCount, top + row * (mSlotHeight + mSlotGap)); } } public Slot getSlotByPosition(float x, float y, boolean rowStart, boolean roundUp) { if (x < 0 || y < 0 || mSlotCount == null) { return null; } int pos = (int) y, index = 0, top = 0; for (int count : mSlotCount) { int h = mSpec.titleHeight; if (pos < top + h) { if (roundUp) { return getSlot(false, index + 1, index, top + h); } else { return getSlot(true, index, index, top); } } top += h; ++index; int rows = (count + mUnitCount - 1) / mUnitCount; h = mSlotHeight * rows + mSlotGap * (rows > 0 ? rows - 1 : 0); if (pos < top + h) { int row = ((int) pos - top) / (mSlotHeight + mSlotGap); int col = 0; if (roundUp) { int idx = (row + 1) * mUnitCount; if (idx > count) idx = count + 1; return getSlot(false, index + idx, index, top + mSlotHeight); } if (!rowStart) { col = ((int) x) / (mSlotWidth + mSlotGap); if (row * mUnitCount + col >= count) { break; } } return getSlot(false, index + row * mUnitCount + col, index, top); } top += h; index += count; } if (roundUp) { return getSlot(false, index, index, top); } return null; } public Slot getSlotByPosition(float x, float y) { return getSlotByPosition(x, mScrollPosition + y, false, false); } public int getScrollLimit() { return Math.max(0, mContentLength - mHeight); } public void createSlots() { int height = 0; int size = 0; if (mSlotCount != null) { for (int count : mSlotCount) { int rows = (count + mUnitCount - 1) / mUnitCount; height += mSlotHeight * rows + mSlotGap * (rows > 0 ? rows - 1 : 0); size += 1 + count; } height += mSpec.titleHeight * mSlotCount.length; mContentLength = height; mSlotSize = size; updateVisibleSlotRange(); } } } private static class Slot { public boolean isTitle; public int index; public int col; public int top; public Slot(boolean isTitle, int index, int col, int top) { this.isTitle = isTitle; this.index = index; this.col = col; this.top = top; } } }