/* * 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.os.Handler; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.animation.DecelerateInterpolator; import com.android.gallery3d.anim.Animation; import com.android.gallery3d.app.AbstractGalleryActivity; import com.android.gallery3d.common.Utils; import com.android.gallery3d.glrenderer.GLCanvas; public class SlotView extends GLView { @SuppressWarnings("unused") private static final String TAG = "SlotView"; private static final boolean WIDE = true; private static final int INDEX_NONE = -1; public static final int RENDER_MORE_PASS = 1; public static final int RENDER_MORE_FRAME = 2; public interface Listener { public void onDown(int index); public void onUp(boolean followedByLongPress); public void onSingleTapUp(int index); public void onLongTap(int index); 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) {} @Override public void onLongTap(int index) {} @Override public void onScrollPositionChanged(int position, int total) {} } public static interface SlotRenderer { public void prepareDrawing(); public void onVisibleRangeChanged(int visibleStart, int visibleEnd); public void onSlotSizeChanged(int width, int height); public int renderSlot(GLCanvas canvas, int index, int pass, int width, int height); } private final GestureDetector mGestureDetector; private final ScrollerHelper mScroller; private final Paper mPaper = new Paper(); private Listener mListener; private UserInteractionListener mUIListener; private boolean mMoreAnimation = false; 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 int mOverscrollEffect = OVERSCROLL_3D; private final Handler mHandler; private SlotRenderer mRenderer; private int[] mRequestRenderSlots = new int[16]; public static final int OVERSCROLL_3D = 0; public static final int OVERSCROLL_SYSTEM = 1; public static final int OVERSCROLL_NONE = 2; // to prevent allocating memory private final Rect mTempRect = new Rect(); public SlotView(AbstractGalleryActivity activity, Spec spec) { mGestureDetector = new GestureDetector(activity, new MyGestureListener()); mScroller = new ScrollerHelper(activity); mHandler = new SynchronizedHandler(activity.getGLRoot()); setSlotSpec(spec); } public void setSlotRenderer(SlotRenderer slotDrawer) { mRenderer = slotDrawer; if (mRenderer != null) { mRenderer.onSlotSizeChanged(mLayout.mSlotWidth, mLayout.mSlotHeight); mRenderer.onVisibleRangeChanged(getVisibleStart(), getVisibleEnd()); } } public void setCenterIndex(int index) { int slotCount = mLayout.mSlotCount; if (index < 0 || index >= slotCount) { return; } Rect rect = mLayout.getSlotRect(index, mTempRect); int position = WIDE ? (rect.left + rect.right - getWidth()) / 2 : (rect.top + rect.bottom - getHeight()) / 2; setScrollPosition(position); } public void makeSlotVisible(int index) { Rect rect = mLayout.getSlotRect(index, mTempRect); int visibleBegin = WIDE ? mScrollX : mScrollY; int visibleLength = WIDE ? getWidth() : getHeight(); int visibleEnd = visibleBegin + visibleLength; int slotBegin = WIDE ? rect.left : rect.top; int slotEnd = WIDE ? rect.right : rect.bottom; int position = visibleBegin; if (visibleLength < slotEnd - slotBegin) { position = visibleBegin; } else if (slotBegin < visibleBegin) { position = slotBegin; } else if (slotEnd > visibleEnd) { position = slotEnd - visibleLength; } setScrollPosition(position); } 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 public void addComponent(GLView view) { throw new UnsupportedOperationException(); } @Override protected void onLayout(boolean changeSize, int l, int t, int r, int b) { if (!changeSize) return; // Make sure we are still at a resonable 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); if (mOverscrollEffect == OVERSCROLL_3D) { mPaper.setSize(r - l, b - t); } } public void startScatteringAnimation(RelativePosition position) { mAnimation = new ScatteringAnimation(position); mAnimation.start(); if (mLayout.mSlotCount != 0) invalidate(); } public void startRisingAnimation() { mAnimation = new RisingAnimation(); mAnimation.start(); if (mLayout.mSlotCount != 0) invalidate(); } private void updateScrollPosition(int position, boolean force) { if (!force && (WIDE ? position == mScrollX : position == mScrollY)) return; if (WIDE) { mScrollX = position; } else { 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, new Rect()); } @Override protected boolean onTouch(MotionEvent event) { if (mUIListener != null) mUIListener.onUserInteraction(); mGestureDetector.onTouchEvent(event); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mDownInScrolling = !mScroller.isFinished(); mScroller.forceFinished(); break; case MotionEvent.ACTION_UP: mPaper.onRelease(); invalidate(); break; } return true; } public void setListener(Listener listener) { mListener = listener; } public void setUserInteractionListener(UserInteractionListener listener) { mUIListener = listener; } public void setOverscrollEffect(int kind) { mOverscrollEffect = kind; mScroller.setOverfling(kind == OVERSCROLL_SYSTEM); } 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); more |= mLayout.advanceAnimation(animTime); int oldX = mScrollX; updateScrollPosition(mScroller.getPosition(), false); boolean paperActive = false; if (mOverscrollEffect == OVERSCROLL_3D) { // Check if an edge is reached and notify mPaper if so. int newX = mScrollX; int limit = mLayout.getScrollLimit(); if (oldX > 0 && newX == 0 || oldX < limit && newX == limit) { float v = mScroller.getCurrVelocity(); if (newX == limit) v = -v; // I don't know why, but getCurrVelocity() can return NaN. if (!Float.isNaN(v)) { mPaper.edgeReached(v); } } paperActive = mPaper.advanceAnimation(); } more |= paperActive; if (mAnimation != null) { more |= mAnimation.calculate(animTime); } canvas.translate(-mScrollX, -mScrollY); int requestCount = 0; int requestedSlot[] = expandIntArray(mRequestRenderSlots, mLayout.mVisibleEnd - mLayout.mVisibleStart); for (int i = mLayout.mVisibleEnd - 1; i >= mLayout.mVisibleStart; --i) { int r = renderItem(canvas, i, 0, paperActive); 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, paperActive); if ((r & RENDER_MORE_FRAME) != 0) more = true; if ((r & RENDER_MORE_PASS) != 0) requestedSlot[newCount++] = i; } requestCount = newCount; } canvas.translate(mScrollX, mScrollY); if (more) invalidate(); final UserInteractionListener listener = mUIListener; if (mMoreAnimation && !more && listener != null) { mHandler.post(new Runnable() { @Override public void run() { listener.onUserInteractionEnd(); } }); } mMoreAnimation = more; } private int renderItem( GLCanvas canvas, int index, int pass, boolean paperActive) { canvas.save(GLCanvas.SAVE_FLAG_ALPHA | GLCanvas.SAVE_FLAG_MATRIX); Rect rect = mLayout.getSlotRect(index, mTempRect); if (paperActive) { canvas.multiplyMatrix(mPaper.getTransform(rect, mScrollX), 0); } else { 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); } } // This Spec class is used to specify the size of each slot in the SlotView. // There are two ways to do it: // // (1) Specify slotWidth and slotHeight: they specify the width and height // of each slot. The number of rows and the gap between slots will be // determined automatically. // (2) Specify rowsLand, rowsPort, 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 slotWidth = -1; public int slotHeight = -1; public int slotHeightAdditional = 0; public int rowsLand = -1; public int rowsPort = -1; public int slotGap = -1; } public class Layout { private int mVisibleStart; private int mVisibleEnd; private int mSlotCount; private int mSlotWidth; private int mSlotHeight; private int mSlotGap; private Spec mSpec; private int mWidth; private int mHeight; private int mUnitCount; private int mContentLength; private int mScrollPosition; private IntegerAnimation mVerticalPadding = new IntegerAnimation(); private IntegerAnimation mHorizontalPadding = new IntegerAnimation(); public void setSlotSpec(Spec spec) { mSpec = spec; } public boolean setSlotCount(int slotCount) { if (slotCount == mSlotCount) return false; if (mSlotCount != 0) { mHorizontalPadding.setEnabled(true); mVerticalPadding.setEnabled(true); } mSlotCount = slotCount; int hPadding = mHorizontalPadding.getTarget(); int vPadding = mVerticalPadding.getTarget(); initLayoutParameters(); return vPadding != mVerticalPadding.getTarget() || hPadding != mHorizontalPadding.getTarget(); } public Rect getSlotRect(int index, Rect rect) { int col, row; if (WIDE) { col = index / mUnitCount; row = index - col * mUnitCount; } else { row = index / mUnitCount; col = index - row * mUnitCount; } int x = mHorizontalPadding.get() + col * (mSlotWidth + mSlotGap); int y = mVerticalPadding.get() + row * (mSlotHeight + mSlotGap); rect.set(x, y, x + mSlotWidth, y + mSlotHeight); return rect; } public int getSlotWidth() { return mSlotWidth; } public int getSlotHeight() { return mSlotHeight; } // Calculate // (1) mUnitCount: the number of slots we can fit into one column (or row). // (2) mContentLength: the width (or height) we need to display all the // columns (rows). // (3) padding[]: the vertical and horizontal padding we need in order // to put the slots towards to the center of the display. // // The "major" direction is the direction the user can scroll. The other // direction is the "minor" direction. // // The comments inside this method are the description when the major // directon is horizontal (X), and the minor directon is vertical (Y). private void initLayoutParameters( int majorLength, int minorLength, /* The view width and height */ int majorUnitSize, int minorUnitSize, /* The slot width and height */ int[] padding) { int unitCount = (minorLength + mSlotGap) / (minorUnitSize + mSlotGap); if (unitCount == 0) unitCount = 1; mUnitCount = unitCount; // We put extra padding above and below the column. int availableUnits = Math.min(mUnitCount, mSlotCount); int usedMinorLength = availableUnits * minorUnitSize + (availableUnits - 1) * mSlotGap; padding[0] = (minorLength - usedMinorLength) / 2; // Then calculate how many columns we need for all slots. int count = ((mSlotCount + mUnitCount - 1) / mUnitCount); mContentLength = count * majorUnitSize + (count - 1) * mSlotGap; // If the content length is less then the screen width, put // extra padding in left and right. padding[1] = Math.max(0, (majorLength - mContentLength) / 2); } private void initLayoutParameters() { // Initialize mSlotWidth and mSlotHeight from mSpec if (mSpec.slotWidth != -1) { mSlotGap = 0; mSlotWidth = mSpec.slotWidth; mSlotHeight = mSpec.slotHeight; } else { int rows = (mWidth > mHeight) ? mSpec.rowsLand : mSpec.rowsPort; mSlotGap = mSpec.slotGap; mSlotHeight = Math.max(1, (mHeight - (rows - 1) * mSlotGap) / rows); mSlotWidth = mSlotHeight - mSpec.slotHeightAdditional; } if (mRenderer != null) { mRenderer.onSlotSizeChanged(mSlotWidth, mSlotHeight); } int[] padding = new int[2]; if (WIDE) { initLayoutParameters(mWidth, mHeight, mSlotWidth, mSlotHeight, padding); mVerticalPadding.startAnimateTo(padding[0]); mHorizontalPadding.startAnimateTo(padding[1]); } else { initLayoutParameters(mHeight, mWidth, mSlotHeight, mSlotWidth, padding); mVerticalPadding.startAnimateTo(padding[1]); mHorizontalPadding.startAnimateTo(padding[0]); } updateVisibleSlotRange(); } public void setSize(int width, int height) { mWidth = width; mHeight = height; initLayoutParameters(); } private void updateVisibleSlotRange() { int position = mScrollPosition; if (WIDE) { int startCol = position / (mSlotWidth + mSlotGap); int start = Math.max(0, mUnitCount * startCol); int endCol = (position + mWidth + mSlotWidth + mSlotGap - 1) / (mSlotWidth + mSlotGap); int end = Math.min(mSlotCount, mUnitCount * endCol); setVisibleRange(start, end); } else { int startRow = position / (mSlotHeight + mSlotGap); int start = Math.max(0, mUnitCount * startRow); int endRow = (position + mHeight + mSlotHeight + mSlotGap - 1) / (mSlotHeight + mSlotGap); int end = Math.min(mSlotCount, mUnitCount * endRow); setVisibleRange(start, end); } } public void setScrollPosition(int position) { if (mScrollPosition == position) return; mScrollPosition = position; updateVisibleSlotRange(); } 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; } public int getSlotIndexByPosition(float x, float y) { int absoluteX = Math.round(x) + (WIDE ? mScrollPosition : 0); int absoluteY = Math.round(y) + (WIDE ? 0 : mScrollPosition); absoluteX -= mHorizontalPadding.get(); absoluteY -= mVerticalPadding.get(); if (absoluteX < 0 || absoluteY < 0) { return INDEX_NONE; } int columnIdx = absoluteX / (mSlotWidth + mSlotGap); int rowIdx = absoluteY / (mSlotHeight + mSlotGap); if (!WIDE && columnIdx >= mUnitCount) { return INDEX_NONE; } if (WIDE && rowIdx >= mUnitCount) { return INDEX_NONE; } if (absoluteX % (mSlotWidth + mSlotGap) >= mSlotWidth) { return INDEX_NONE; } if (absoluteY % (mSlotHeight + mSlotGap) >= mSlotHeight) { return INDEX_NONE; } int index = WIDE ? (columnIdx * mUnitCount + rowIdx) : (rowIdx * mUnitCount + columnIdx); return index >= mSlotCount ? INDEX_NONE : index; } public int getScrollLimit() { int limit = WIDE ? mContentLength - mWidth : mContentLength - mHeight; return limit <= 0 ? 0 : limit; } public boolean advanceAnimation(long animTime) { // use '|' to make sure both sides will be executed return mVerticalPadding.calculate(animTime) | mHorizontalPadding.calculate(animTime); } } 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; int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY()); if (index != INDEX_NONE) { isDown = true; mListener.onDown(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; float velocity = WIDE ? velocityX : velocityY; mScroller.fling((int) -velocity, 0, scrollLimit); if (mUIListener != null) mUIListener.onUserInteractionBegin(); invalidate(); return true; } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { cancelDown(false); float distance = WIDE ? distanceX : distanceY; int overDistance = mScroller.startScroll( Math.round(distance), 0, mLayout.getScrollLimit()); if (mOverscrollEffect == OVERSCROLL_3D && overDistance != 0) { mPaper.overScroll(overDistance); } invalidate(); return true; } @Override public boolean onSingleTapUp(MotionEvent e) { cancelDown(false); if (mDownInScrolling) return true; int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY()); if (index != INDEX_NONE) mListener.onSingleTapUp(index); return true; } @Override public void onLongPress(MotionEvent e) { cancelDown(true); if (mDownInScrolling) return; lockRendering(); try { int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY()); if (index != INDEX_NONE) mListener.onLongTap(index); } finally { unlockRendering(); } } } public void setStartIndex(int index) { mStartIndex = index; } // Return true if the layout parameters have been changed public boolean setSlotCount(int slotCount) { boolean changed = mLayout.setSlotCount(slotCount); // 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(WIDE ? mScrollX : mScrollY); return changed; } 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); r.offset(offset.left - getScrollX(), offset.top - getScrollY()); return r; } private static class IntegerAnimation extends Animation { private int mTarget; private int mCurrent = 0; private int mFrom = 0; private boolean mEnabled = false; public void setEnabled(boolean enabled) { mEnabled = enabled; } public void startAnimateTo(int target) { if (!mEnabled) { mTarget = mCurrent = target; return; } if (target == mTarget) return; mFrom = mCurrent; mTarget = target; setDuration(180); start(); } public int get() { return mCurrent; } public int getTarget() { return mTarget; } @Override protected void onCalculate(float progress) { mCurrent = Math.round(mFrom + progress * (mTarget - mFrom)); if (progress == 1f) mEnabled = false; } } }