| /* |
| * 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.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.Utils; |
| import com.android.gallery3d.glrenderer.GLCanvas; |
| |
| import java.util.Locale; |
| |
| 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(); |
| |
| // Flag to check whether it is come from Photo Page. |
| private boolean isFromPhotoPage = false; |
| |
| 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); |
| } |
| |
| /** |
| * 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) { |
| if (View.LAYOUT_DIRECTION_RTL == TextUtils |
| .getLayoutDirectionFromLocale(Locale.getDefault()) |
| && position == 0 && !isFromPhotoPage) { |
| // If RTL and not from Photo Page, set position to max. |
| position = mLayout.getScrollLimit(); |
| } |
| 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) { |
| if (View.LAYOUT_DIRECTION_RTL == TextUtils |
| .getLayoutDirectionFromLocale(Locale.getDefault())) { |
| // If RTL, recalculate the columns and rows. |
| int count = ((mSlotCount + mUnitCount - 1) / mUnitCount); |
| col = count - index / mUnitCount - 1; |
| row = index % mUnitCount; |
| } else { |
| 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) { |
| if (View.LAYOUT_DIRECTION_RTL == TextUtils |
| .getLayoutDirectionFromLocale(Locale.getDefault())) { |
| // If RTL, recalculate the position. |
| position = mContentLength > mWidth ? (mContentLength - position - mWidth) |
| : position; |
| position = Math.max(0, position); |
| } |
| 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); |
| if (View.LAYOUT_DIRECTION_RTL == TextUtils |
| .getLayoutDirectionFromLocale(Locale.getDefault())) { |
| // If RTL, recalculate the absoluteX. |
| absoluteX = mContentLength > mWidth ? (mContentLength - absoluteX) : mWidth |
| - absoluteX; |
| } |
| 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; |
| } |
| } |
| |
| /** |
| * Get the SlotView's max scroll value. |
| */ |
| public int getScrollLimit() { |
| return mLayout.getScrollLimit(); |
| } |
| } |