From 9066cfe9886ac131c34d59ed0e2d287b0e3c0087 Mon Sep 17 00:00:00 2001 From: The Android Open Source Project Date: Tue, 3 Mar 2009 19:31:44 -0800 Subject: auto import from //depot/cupcake/@135843 --- core/java/android/widget/Gallery.java | 1408 +++++++++++++++++++++++++++++++++ 1 file changed, 1408 insertions(+) create mode 100644 core/java/android/widget/Gallery.java (limited to 'core/java/android/widget/Gallery.java') diff --git a/core/java/android/widget/Gallery.java b/core/java/android/widget/Gallery.java new file mode 100644 index 0000000..e7b303a --- /dev/null +++ b/core/java/android/widget/Gallery.java @@ -0,0 +1,1408 @@ +/* + * Copyright (C) 2007 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 android.widget; + +import com.android.internal.R; + +import android.annotation.Widget; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.util.Config; +import android.util.Log; +import android.view.GestureDetector; +import android.view.Gravity; +import android.view.HapticFeedbackConstants; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.SoundEffectConstants; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.animation.Transformation; +import android.widget.AbsSpinner; +import android.widget.Scroller; + +/** + * A view that shows items in a center-locked, horizontally scrolling list. + *

+ * The default values for the Gallery assume you will be using + * {@link android.R.styleable#Theme_galleryItemBackground} as the background for + * each View given to the Gallery from the Adapter. If you are not doing this, + * you may need to adjust some Gallery properties, such as the spacing. + *

+ * Views given to the Gallery should use {@link Gallery.LayoutParams} as their + * layout parameters type. + * + * @attr ref android.R.styleable#Gallery_animationDuration + * @attr ref android.R.styleable#Gallery_spacing + * @attr ref android.R.styleable#Gallery_gravity + */ +@Widget +public class Gallery extends AbsSpinner implements GestureDetector.OnGestureListener { + + private static final String TAG = "Gallery"; + + private static final boolean localLOGV = Config.LOGV; + + /** + * Duration in milliseconds from the start of a scroll during which we're + * unsure whether the user is scrolling or flinging. + */ + private static final int SCROLL_TO_FLING_UNCERTAINTY_TIMEOUT = 250; + + /** + * Horizontal spacing between items. + */ + private int mSpacing = 0; + + /** + * How long the transition animation should run when a child view changes + * position, measured in milliseconds. + */ + private int mAnimationDuration = 400; + + /** + * The alpha of items that are not selected. + */ + private float mUnselectedAlpha; + + /** + * Left most edge of a child seen so far during layout. + */ + private int mLeftMost; + + /** + * Right most edge of a child seen so far during layout. + */ + private int mRightMost; + + private int mGravity; + + /** + * Helper for detecting touch gestures. + */ + private GestureDetector mGestureDetector; + + /** + * The position of the item that received the user's down touch. + */ + private int mDownTouchPosition; + + /** + * The view of the item that received the user's down touch. + */ + private View mDownTouchView; + + /** + * Executes the delta scrolls from a fling or scroll movement. + */ + private FlingRunnable mFlingRunnable = new FlingRunnable(); + + /** + * Sets mSuppressSelectionChanged = false. This is used to set it to false + * in the future. It will also trigger a selection changed. + */ + private Runnable mDisableSuppressSelectionChangedRunnable = new Runnable() { + public void run() { + mSuppressSelectionChanged = false; + selectionChanged(); + } + }; + + /** + * When fling runnable runs, it resets this to false. Any method along the + * path until the end of its run() can set this to true to abort any + * remaining fling. For example, if we've reached either the leftmost or + * rightmost item, we will set this to true. + */ + private boolean mShouldStopFling; + + /** + * The currently selected item's child. + */ + private View mSelectedChild; + + /** + * Whether to continuously callback on the item selected listener during a + * fling. + */ + private boolean mShouldCallbackDuringFling = true; + + /** + * Whether to callback when an item that is not selected is clicked. + */ + private boolean mShouldCallbackOnUnselectedItemClick = true; + + /** + * If true, do not callback to item selected listener. + */ + private boolean mSuppressSelectionChanged; + + /** + * If true, we have received the "invoke" (center or enter buttons) key + * down. This is checked before we action on the "invoke" key up, and is + * subsequently cleared. + */ + private boolean mReceivedInvokeKeyDown; + + private AdapterContextMenuInfo mContextMenuInfo; + + /** + * If true, this onScroll is the first for this user's drag (remember, a + * drag sends many onScrolls). + */ + private boolean mIsFirstScroll; + + public Gallery(Context context) { + this(context, null); + } + + public Gallery(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.galleryStyle); + } + + public Gallery(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + mGestureDetector = new GestureDetector(context, this); + mGestureDetector.setIsLongpressEnabled(true); + + TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.Gallery, defStyle, 0); + + int index = a.getInt(com.android.internal.R.styleable.Gallery_gravity, -1); + if (index >= 0) { + setGravity(index); + } + + int animationDuration = + a.getInt(com.android.internal.R.styleable.Gallery_animationDuration, -1); + if (animationDuration > 0) { + setAnimationDuration(animationDuration); + } + + int spacing = + a.getDimensionPixelOffset(com.android.internal.R.styleable.Gallery_spacing, 0); + setSpacing(spacing); + + float unselectedAlpha = a.getFloat( + com.android.internal.R.styleable.Gallery_unselectedAlpha, 0.5f); + setUnselectedAlpha(unselectedAlpha); + + a.recycle(); + + // We draw the selected item last (because otherwise the item to the + // right overlaps it) + mGroupFlags |= FLAG_USE_CHILD_DRAWING_ORDER; + + mGroupFlags |= FLAG_SUPPORT_STATIC_TRANSFORMATIONS; + } + + /** + * Whether or not to callback on any {@link #getOnItemSelectedListener()} + * while the items are being flinged. If false, only the final selected item + * will cause the callback. If true, all items between the first and the + * final will cause callbacks. + * + * @param shouldCallback Whether or not to callback on the listener while + * the items are being flinged. + */ + public void setCallbackDuringFling(boolean shouldCallback) { + mShouldCallbackDuringFling = shouldCallback; + } + + /** + * Whether or not to callback when an item that is not selected is clicked. + * If false, the item will become selected (and re-centered). If true, the + * {@link #getOnItemClickListener()} will get the callback. + * + * @param shouldCallback Whether or not to callback on the listener when a + * item that is not selected is clicked. + * @hide + */ + public void setCallbackOnUnselectedItemClick(boolean shouldCallback) { + mShouldCallbackOnUnselectedItemClick = shouldCallback; + } + + /** + * Sets how long the transition animation should run when a child view + * changes position. Only relevant if animation is turned on. + * + * @param animationDurationMillis The duration of the transition, in + * milliseconds. + * + * @attr ref android.R.styleable#Gallery_animationDuration + */ + public void setAnimationDuration(int animationDurationMillis) { + mAnimationDuration = animationDurationMillis; + } + + /** + * Sets the spacing between items in a Gallery + * + * @param spacing The spacing in pixels between items in the Gallery + * + * @attr ref android.R.styleable#Gallery_spacing + */ + public void setSpacing(int spacing) { + mSpacing = spacing; + } + + /** + * Sets the alpha of items that are not selected in the Gallery. + * + * @param unselectedAlpha the alpha for the items that are not selected. + * + * @attr ref android.R.styleable#Gallery_unselectedAlpha + */ + public void setUnselectedAlpha(float unselectedAlpha) { + mUnselectedAlpha = unselectedAlpha; + } + + @Override + protected boolean getChildStaticTransformation(View child, Transformation t) { + + t.clear(); + t.setAlpha(child == mSelectedChild ? 1.0f : mUnselectedAlpha); + + return true; + } + + @Override + protected int computeHorizontalScrollExtent() { + // Only 1 item is considered to be selected + return 1; + } + + @Override + protected int computeHorizontalScrollOffset() { + // Current scroll position is the same as the selected position + return mSelectedPosition; + } + + @Override + protected int computeHorizontalScrollRange() { + // Scroll range is the same as the item count + return mItemCount; + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams; + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return new LayoutParams(p); + } + + @Override + public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LayoutParams(getContext(), attrs); + } + + @Override + protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + /* + * Gallery expects Gallery.LayoutParams. + */ + return new Gallery.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + + /* + * Remember that we are in layout to prevent more layout request from + * being generated. + */ + mInLayout = true; + layout(0, false); + mInLayout = false; + } + + @Override + int getChildHeight(View child) { + return child.getMeasuredHeight(); + } + + /** + * Tracks a motion scroll. In reality, this is used to do just about any + * movement to items (touch scroll, arrow-key scroll, set an item as selected). + * + * @param deltaX Change in X from the previous event. + */ + void trackMotionScroll(int deltaX) { + + if (getChildCount() == 0) { + return; + } + + boolean toLeft = deltaX < 0; + + int limitedDeltaX = getLimitedMotionScrollAmount(toLeft, deltaX); + if (limitedDeltaX != deltaX) { + // The above call returned a limited amount, so stop any scrolls/flings + mFlingRunnable.endFling(false); + onFinishedMovement(); + } + + offsetChildrenLeftAndRight(limitedDeltaX); + + detachOffScreenChildren(toLeft); + + if (toLeft) { + // If moved left, there will be empty space on the right + fillToGalleryRight(); + } else { + // Similarly, empty space on the left + fillToGalleryLeft(); + } + + // Clear unused views + mRecycler.clear(); + + setSelectionToCenterChild(); + + invalidate(); + } + + int getLimitedMotionScrollAmount(boolean motionToLeft, int deltaX) { + int extremeItemPosition = motionToLeft ? mItemCount - 1 : 0; + View extremeChild = getChildAt(extremeItemPosition - mFirstPosition); + + if (extremeChild == null) { + return deltaX; + } + + int extremeChildCenter = getCenterOfView(extremeChild); + int galleryCenter = getCenterOfGallery(); + + if (motionToLeft) { + if (extremeChildCenter <= galleryCenter) { + + // The extreme child is past his boundary point! + return 0; + } + } else { + if (extremeChildCenter >= galleryCenter) { + + // The extreme child is past his boundary point! + return 0; + } + } + + int centerDifference = galleryCenter - extremeChildCenter; + + return motionToLeft + ? Math.max(centerDifference, deltaX) + : Math.min(centerDifference, deltaX); + } + + /** + * Offset the horizontal location of all children of this view by the + * specified number of pixels. + * + * @param offset the number of pixels to offset + */ + private void offsetChildrenLeftAndRight(int offset) { + for (int i = getChildCount() - 1; i >= 0; i--) { + getChildAt(i).offsetLeftAndRight(offset); + } + } + + /** + * @return The center of this Gallery. + */ + private int getCenterOfGallery() { + return (getWidth() - mPaddingLeft - mPaddingRight) / 2 + mPaddingLeft; + } + + /** + * @return The center of the given view. + */ + private static int getCenterOfView(View view) { + return view.getLeft() + view.getWidth() / 2; + } + + /** + * Detaches children that are off the screen (i.e.: Gallery bounds). + * + * @param toLeft Whether to detach children to the left of the Gallery, or + * to the right. + */ + private void detachOffScreenChildren(boolean toLeft) { + int numChildren = getChildCount(); + int firstPosition = mFirstPosition; + int start = 0; + int count = 0; + + if (toLeft) { + final int galleryLeft = mPaddingLeft; + for (int i = 0; i < numChildren; i++) { + final View child = getChildAt(i); + if (child.getRight() >= galleryLeft) { + break; + } else { + count++; + mRecycler.put(firstPosition + i, child); + } + } + } else { + final int galleryRight = getWidth() - mPaddingRight; + for (int i = numChildren - 1; i >= 0; i--) { + final View child = getChildAt(i); + if (child.getLeft() <= galleryRight) { + break; + } else { + start = i; + count++; + mRecycler.put(firstPosition + i, child); + } + } + } + + detachViewsFromParent(start, count); + + if (toLeft) { + mFirstPosition += count; + } + } + + /** + * Scrolls the items so that the selected item is in its 'slot' (its center + * is the gallery's center). + */ + private void scrollIntoSlots() { + + if (getChildCount() == 0 || mSelectedChild == null) return; + + int selectedCenter = getCenterOfView(mSelectedChild); + int targetCenter = getCenterOfGallery(); + + int scrollAmount = targetCenter - selectedCenter; + if (scrollAmount != 0) { + mFlingRunnable.startUsingDistance(scrollAmount); + } else { + onFinishedMovement(); + } + } + + private void onFinishedMovement() { + if (mSuppressSelectionChanged) { + mSuppressSelectionChanged = false; + + // We haven't been callbacking during the fling, so do it now + super.selectionChanged(); + } + } + + @Override + void selectionChanged() { + if (!mSuppressSelectionChanged) { + super.selectionChanged(); + } + } + + /** + * Looks for the child that is closest to the center and sets it as the + * selected child. + */ + private void setSelectionToCenterChild() { + + View selView = mSelectedChild; + if (mSelectedChild == null) return; + + int galleryCenter = getCenterOfGallery(); + + if (selView != null) { + + // Common case where the current selected position is correct + if (selView.getLeft() <= galleryCenter && selView.getRight() >= galleryCenter) { + return; + } + } + + // TODO better search + int closestEdgeDistance = Integer.MAX_VALUE; + int newSelectedChildIndex = 0; + for (int i = getChildCount() - 1; i >= 0; i--) { + + View child = getChildAt(i); + + if (child.getLeft() <= galleryCenter && child.getRight() >= galleryCenter) { + // This child is in the center + newSelectedChildIndex = i; + break; + } + + int childClosestEdgeDistance = Math.min(Math.abs(child.getLeft() - galleryCenter), + Math.abs(child.getRight() - galleryCenter)); + if (childClosestEdgeDistance < closestEdgeDistance) { + closestEdgeDistance = childClosestEdgeDistance; + newSelectedChildIndex = i; + } + } + + int newPos = mFirstPosition + newSelectedChildIndex; + + if (newPos != mSelectedPosition) { + setSelectedPositionInt(newPos); + setNextSelectedPositionInt(newPos); + checkSelectionChanged(); + } + } + + /** + * Creates and positions all views for this Gallery. + *

+ * We layout rarely, most of the time {@link #trackMotionScroll(int)} takes + * care of repositioning, adding, and removing children. + * + * @param delta Change in the selected position. +1 means the selection is + * moving to the right, so views are scrolling to the left. -1 + * means the selection is moving to the left. + */ + @Override + void layout(int delta, boolean animate) { + + int childrenLeft = mSpinnerPadding.left; + int childrenWidth = mRight - mLeft - mSpinnerPadding.left - mSpinnerPadding.right; + + if (mDataChanged) { + handleDataChanged(); + } + + // Handle an empty gallery by removing all views. + if (mItemCount == 0) { + resetList(); + return; + } + + // Update to the new selected position. + if (mNextSelectedPosition >= 0) { + setSelectedPositionInt(mNextSelectedPosition); + } + + // All views go in recycler while we are in layout + recycleAllViews(); + + // Clear out old views + //removeAllViewsInLayout(); + detachAllViewsFromParent(); + + /* + * These will be used to give initial positions to views entering the + * gallery as we scroll + */ + mRightMost = 0; + mLeftMost = 0; + + // Make selected view and center it + + /* + * mFirstPosition will be decreased as we add views to the left later + * on. The 0 for x will be offset in a couple lines down. + */ + mFirstPosition = mSelectedPosition; + View sel = makeAndAddView(mSelectedPosition, 0, 0, true); + + // Put the selected child in the center + Gallery.LayoutParams lp = (Gallery.LayoutParams) sel.getLayoutParams(); + int selectedOffset = childrenLeft + (childrenWidth / 2) - (sel.getWidth() / 2); + sel.offsetLeftAndRight(selectedOffset); + + fillToGalleryRight(); + fillToGalleryLeft(); + + // Flush any cached views that did not get reused above + mRecycler.clear(); + + invalidate(); + checkSelectionChanged(); + + mDataChanged = false; + mNeedSync = false; + setNextSelectedPositionInt(mSelectedPosition); + + updateSelectedItemMetadata(); + } + + private void fillToGalleryLeft() { + int itemSpacing = mSpacing; + int galleryLeft = mPaddingLeft; + + // Set state for initial iteration + View prevIterationView = getChildAt(0); + int curPosition; + int curRightEdge; + + if (prevIterationView != null) { + curPosition = mFirstPosition - 1; + curRightEdge = prevIterationView.getLeft() - itemSpacing; + } else { + // No children available! + curPosition = 0; + curRightEdge = mRight - mLeft - mPaddingRight; + mShouldStopFling = true; + } + + while (curRightEdge > galleryLeft && curPosition >= 0) { + prevIterationView = makeAndAddView(curPosition, curPosition - mSelectedPosition, + curRightEdge, false); + + // Remember some state + mFirstPosition = curPosition; + + // Set state for next iteration + curRightEdge = prevIterationView.getLeft() - itemSpacing; + curPosition--; + } + } + + private void fillToGalleryRight() { + int itemSpacing = mSpacing; + int galleryRight = mRight - mLeft - mPaddingRight; + int numChildren = getChildCount(); + int numItems = mItemCount; + + // Set state for initial iteration + View prevIterationView = getChildAt(numChildren - 1); + int curPosition; + int curLeftEdge; + + if (prevIterationView != null) { + curPosition = mFirstPosition + numChildren; + curLeftEdge = prevIterationView.getRight() + itemSpacing; + } else { + mFirstPosition = curPosition = mItemCount - 1; + curLeftEdge = mPaddingLeft; + mShouldStopFling = true; + } + + while (curLeftEdge < galleryRight && curPosition < numItems) { + prevIterationView = makeAndAddView(curPosition, curPosition - mSelectedPosition, + curLeftEdge, true); + + // Set state for next iteration + curLeftEdge = prevIterationView.getRight() + itemSpacing; + curPosition++; + } + } + + /** + * Obtain a view, either by pulling an existing view from the recycler or by + * getting a new one from the adapter. If we are animating, make sure there + * is enough information in the view's layout parameters to animate from the + * old to new positions. + * + * @param position Position in the gallery for the view to obtain + * @param offset Offset from the selected position + * @param x X-coordintate indicating where this view should be placed. This + * will either be the left or right edge of the view, depending on + * the fromLeft paramter + * @param fromLeft Are we posiitoning views based on the left edge? (i.e., + * building from left to right)? + * @return A view that has been added to the gallery + */ + private View makeAndAddView(int position, int offset, int x, + boolean fromLeft) { + + View child; + + if (!mDataChanged) { + child = mRecycler.get(position); + if (child != null) { + // Can reuse an existing view + Gallery.LayoutParams lp = (Gallery.LayoutParams) + child.getLayoutParams(); + + int childLeft = child.getLeft(); + + // Remember left and right edges of where views have been placed + mRightMost = Math.max(mRightMost, childLeft + + child.getMeasuredWidth()); + mLeftMost = Math.min(mLeftMost, childLeft); + + // Position the view + setUpChild(child, offset, x, fromLeft); + + return child; + } + } + + // Nothing found in the recycler -- ask the adapter for a view + child = mAdapter.getView(position, null, this); + + // Position the view + setUpChild(child, offset, x, fromLeft); + + return child; + } + + /** + * Helper for makeAndAddView to set the position of a view and fill out its + * layout paramters. + * + * @param child The view to position + * @param offset Offset from the selected position + * @param x X-coordintate indicating where this view should be placed. This + * will either be the left or right edge of the view, depending on + * the fromLeft paramter + * @param fromLeft Are we posiitoning views based on the left edge? (i.e., + * building from left to right)? + */ + private void setUpChild(View child, int offset, int x, boolean fromLeft) { + + // Respect layout params that are already in the view. Otherwise + // make some up... + Gallery.LayoutParams lp = (Gallery.LayoutParams) + child.getLayoutParams(); + if (lp == null) { + lp = (Gallery.LayoutParams) generateDefaultLayoutParams(); + } + + addViewInLayout(child, fromLeft ? -1 : 0, lp); + + child.setSelected(offset == 0); + + // Get measure specs + int childHeightSpec = ViewGroup.getChildMeasureSpec(mHeightMeasureSpec, + mSpinnerPadding.top + mSpinnerPadding.bottom, lp.height); + int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, + mSpinnerPadding.left + mSpinnerPadding.right, lp.width); + + // Measure child + child.measure(childWidthSpec, childHeightSpec); + + int childLeft; + int childRight; + + // Position vertically based on gravity setting + int childTop = calculateTop(child, lp, true); + int childBottom = childTop + child.getMeasuredHeight(); + + int width = child.getMeasuredWidth(); + if (fromLeft) { + childLeft = x; + childRight = childLeft + width; + } else { + childLeft = x - width; + childRight = x; + } + + child.layout(childLeft, childTop, childRight, childBottom); + } + + /** + * Figure out vertical placement based on mGravity + * + * @param child Child to place + * @param lp LayoutParams for this view (just so we don't keep looking them + * up) + * @return Where the top of the child should be + */ + private int calculateTop(View child, Gallery.LayoutParams lp, boolean duringLayout) { + int myHeight = duringLayout ? mMeasuredHeight : getHeight(); + int childHeight = duringLayout ? child.getMeasuredHeight() : child.getHeight(); + + int childTop = 0; + + switch (mGravity) { + case Gravity.TOP: + childTop = mSpinnerPadding.top; + break; + case Gravity.CENTER_VERTICAL: + int availableSpace = myHeight - mSpinnerPadding.bottom + - mSpinnerPadding.top - childHeight; + childTop = mSpinnerPadding.top + (availableSpace / 2); + break; + case Gravity.BOTTOM: + childTop = myHeight - mSpinnerPadding.bottom - childHeight; + break; + } + return childTop; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + + // Give everything to the gesture detector + boolean retValue = mGestureDetector.onTouchEvent(event); + + int action = event.getAction(); + if (action == MotionEvent.ACTION_UP) { + // Helper method for lifted finger + onUp(); + } else if (action == MotionEvent.ACTION_CANCEL) { + onCancel(); + } + + return retValue; + } + + /** + * {@inheritDoc} + */ + public boolean onSingleTapUp(MotionEvent e) { + + if (mDownTouchPosition >= 0) { + + // An item tap should make it selected, so scroll to this child. + scrollToChild(mDownTouchPosition - mFirstPosition); + + // Also pass the click so the client knows, if it wants to. + if (mShouldCallbackOnUnselectedItemClick || mDownTouchPosition == mSelectedPosition) { + performItemClick(mDownTouchView, mDownTouchPosition, mAdapter + .getItemId(mDownTouchPosition)); + } + + return true; + } + + return false; + } + + /** + * {@inheritDoc} + */ + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + + if (!mShouldCallbackDuringFling) { + // We want to suppress selection changes + + // Remove any future code to set mSuppressSelectionChanged = false + removeCallbacks(mDisableSuppressSelectionChangedRunnable); + + // This will get reset once we scroll into slots + if (!mSuppressSelectionChanged) mSuppressSelectionChanged = true; + } + + // Fling the gallery! + mFlingRunnable.startUsingVelocity((int) -velocityX); + + return true; + } + + /** + * {@inheritDoc} + */ + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + + if (localLOGV) Log.v(TAG, String.valueOf(e2.getX() - e1.getX())); + + /* + * Now's a good time to tell our parent to stop intercepting our events! + * The user has moved more than the slop amount, since GestureDetector + * ensures this before calling this method. Also, if a parent is more + * interested in this touch's events than we are, it would have + * intercepted them by now (for example, we can assume when a Gallery is + * in the ListView, a vertical scroll would not end up in this method + * since a ListView would have intercepted it by now). + */ + mParent.requestDisallowInterceptTouchEvent(true); + + // As the user scrolls, we want to callback selection changes so related- + // info on the screen is up-to-date with the gallery's selection + if (!mShouldCallbackDuringFling) { + if (mIsFirstScroll) { + /* + * We're not notifying the client of selection changes during + * the fling, and this scroll could possibly be a fling. Don't + * do selection changes until we're sure it is not a fling. + */ + if (!mSuppressSelectionChanged) mSuppressSelectionChanged = true; + postDelayed(mDisableSuppressSelectionChangedRunnable, SCROLL_TO_FLING_UNCERTAINTY_TIMEOUT); + } + } else { + if (mSuppressSelectionChanged) mSuppressSelectionChanged = false; + } + + // Track the motion + trackMotionScroll(-1 * (int) distanceX); + + mIsFirstScroll = false; + return true; + } + + /** + * {@inheritDoc} + */ + public boolean onDown(MotionEvent e) { + + // Kill any existing fling/scroll + mFlingRunnable.stop(false); + + // Get the item's view that was touched + mDownTouchPosition = pointToPosition((int) e.getX(), (int) e.getY()); + + if (mDownTouchPosition >= 0) { + mDownTouchView = getChildAt(mDownTouchPosition - mFirstPosition); + mDownTouchView.setPressed(true); + } + + // Reset the multiple-scroll tracking state + mIsFirstScroll = true; + + // Must return true to get matching events for this down event. + return true; + } + + /** + * Called when a touch event's action is MotionEvent.ACTION_UP. + */ + void onUp() { + + if (mFlingRunnable.mScroller.isFinished()) { + scrollIntoSlots(); + } + + dispatchUnpress(); + } + + /** + * Called when a touch event's action is MotionEvent.ACTION_CANCEL. + */ + void onCancel() { + onUp(); + } + + /** + * {@inheritDoc} + */ + public void onLongPress(MotionEvent e) { + + if (mDownTouchPosition < 0) { + return; + } + + performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + long id = getItemIdAtPosition(mDownTouchPosition); + dispatchLongPress(mDownTouchView, mDownTouchPosition, id); + } + + // Unused methods from GestureDetector.OnGestureListener below + + /** + * {@inheritDoc} + */ + public void onShowPress(MotionEvent e) { + } + + // Unused methods from GestureDetector.OnGestureListener above + + private void dispatchPress(View child) { + + if (child != null) { + child.setPressed(true); + } + + setPressed(true); + } + + private void dispatchUnpress() { + + for (int i = getChildCount() - 1; i >= 0; i--) { + getChildAt(i).setPressed(false); + } + + setPressed(false); + } + + @Override + public void dispatchSetSelected(boolean selected) { + /* + * We don't want to pass the selected state given from its parent to its + * children since this widget itself has a selected state to give to its + * children. + */ + } + + @Override + protected void dispatchSetPressed(boolean pressed) { + + // Show the pressed state on the selected child + if (mSelectedChild != null) { + mSelectedChild.setPressed(pressed); + } + } + + @Override + protected ContextMenuInfo getContextMenuInfo() { + return mContextMenuInfo; + } + + @Override + public boolean showContextMenuForChild(View originalView) { + + final int longPressPosition = getPositionForView(originalView); + if (longPressPosition < 0) { + return false; + } + + final long longPressId = mAdapter.getItemId(longPressPosition); + return dispatchLongPress(originalView, longPressPosition, longPressId); + } + + @Override + public boolean showContextMenu() { + + if (isPressed() && mSelectedPosition >= 0) { + int index = mSelectedPosition - mFirstPosition; + View v = getChildAt(index); + return dispatchLongPress(v, mSelectedPosition, mSelectedRowId); + } + + return false; + } + + private boolean dispatchLongPress(View view, int position, long id) { + boolean handled = false; + + if (mOnItemLongClickListener != null) { + handled = mOnItemLongClickListener.onItemLongClick(this, mDownTouchView, + mDownTouchPosition, id); + } + + if (!handled) { + mContextMenuInfo = new AdapterContextMenuInfo(view, position, id); + handled = super.showContextMenuForChild(this); + } + + if (handled) { + performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + } + + return handled; + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + // Gallery steals all key events + return event.dispatch(this); + } + + /** + * Handles left, right, and clicking + * @see android.view.View#onKeyDown + */ + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + switch (keyCode) { + + case KeyEvent.KEYCODE_DPAD_LEFT: + if (movePrevious()) { + playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); + } + return true; + + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (moveNext()) { + playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); + } + return true; + + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_ENTER: + mReceivedInvokeKeyDown = true; + // fallthrough to default handling + } + + return super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_ENTER: { + + if (mReceivedInvokeKeyDown) { + if (mItemCount > 0) { + + dispatchPress(mSelectedChild); + postDelayed(new Runnable() { + public void run() { + dispatchUnpress(); + } + }, ViewConfiguration.getPressedStateDuration()); + + int selectedIndex = mSelectedPosition - mFirstPosition; + performItemClick(getChildAt(selectedIndex), mSelectedPosition, mAdapter + .getItemId(mSelectedPosition)); + } + } + + // Clear the flag + mReceivedInvokeKeyDown = false; + + return true; + } + } + + return super.onKeyUp(keyCode, event); + } + + boolean movePrevious() { + if (mItemCount > 0 && mSelectedPosition > 0) { + scrollToChild(mSelectedPosition - mFirstPosition - 1); + return true; + } else { + return false; + } + } + + boolean moveNext() { + if (mItemCount > 0 && mSelectedPosition < mItemCount - 1) { + scrollToChild(mSelectedPosition - mFirstPosition + 1); + return true; + } else { + return false; + } + } + + private boolean scrollToChild(int childPosition) { + View child = getChildAt(childPosition); + + if (child != null) { + int distance = getCenterOfGallery() - getCenterOfView(child); + mFlingRunnable.startUsingDistance(distance); + return true; + } + + return false; + } + + @Override + void setSelectedPositionInt(int position) { + super.setSelectedPositionInt(position); + + // Updates any metadata we keep about the selected item. + updateSelectedItemMetadata(); + } + + private void updateSelectedItemMetadata() { + + View oldSelectedChild = mSelectedChild; + + View child = mSelectedChild = getChildAt(mSelectedPosition - mFirstPosition); + if (child == null) { + return; + } + + child.setSelected(true); + child.setFocusable(true); + + if (hasFocus()) { + child.requestFocus(); + } + + // We unfocus the old child down here so the above hasFocus check + // returns true + if (oldSelectedChild != null) { + + // Make sure its drawable state doesn't contain 'selected' + oldSelectedChild.setSelected(false); + + // Make sure it is not focusable anymore, since otherwise arrow keys + // can make this one be focused + oldSelectedChild.setFocusable(false); + } + + } + + /** + * Describes how the child views are aligned. + * @param gravity + * + * @attr ref android.R.styleable#Gallery_gravity + */ + public void setGravity(int gravity) + { + if (mGravity != gravity) { + mGravity = gravity; + requestLayout(); + } + } + + @Override + protected int getChildDrawingOrder(int childCount, int i) { + int selectedIndex = mSelectedPosition - mFirstPosition; + + // Just to be safe + if (selectedIndex < 0) return i; + + if (i == childCount - 1) { + // Draw the selected child last + return selectedIndex; + } else if (i >= selectedIndex) { + // Move the children to the right of the selected child earlier one + return i + 1; + } else { + // Keep the children to the left of the selected child the same + return i; + } + } + + @Override + protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); + + /* + * The gallery shows focus by focusing the selected item. So, give + * focus to our selected item instead. We steal keys from our + * selected item elsewhere. + */ + if (gainFocus && mSelectedChild != null) { + mSelectedChild.requestFocus(direction); + } + + } + + /** + * Responsible for fling behavior. Use {@link #startUsingVelocity(int)} to + * initiate a fling. Each frame of the fling is handled in {@link #run()}. + * A FlingRunnable will keep re-posting itself until the fling is done. + * + */ + private class FlingRunnable implements Runnable { + /** + * Tracks the decay of a fling scroll + */ + private Scroller mScroller; + + /** + * X value reported by mScroller on the previous fling + */ + private int mLastFlingX; + + public FlingRunnable() { + mScroller = new Scroller(getContext()); + } + + private void startCommon() { + // Remove any pending flings + removeCallbacks(this); + } + + public void startUsingVelocity(int initialVelocity) { + if (initialVelocity == 0) return; + + startCommon(); + + int initialX = initialVelocity < 0 ? Integer.MAX_VALUE : 0; + mLastFlingX = initialX; + mScroller.fling(initialX, 0, initialVelocity, 0, + 0, Integer.MAX_VALUE, 0, Integer.MAX_VALUE); + post(this); + } + + public void startUsingDistance(int distance) { + if (distance == 0) return; + + startCommon(); + + mLastFlingX = 0; + mScroller.startScroll(0, 0, -distance, 0, mAnimationDuration); + post(this); + } + + public void stop(boolean scrollIntoSlots) { + removeCallbacks(this); + endFling(scrollIntoSlots); + } + + private void endFling(boolean scrollIntoSlots) { + /* + * Force the scroller's status to finished (without setting its + * position to the end) + */ + mScroller.forceFinished(true); + + if (scrollIntoSlots) scrollIntoSlots(); + } + + public void run() { + + if (mItemCount == 0) { + endFling(true); + return; + } + + mShouldStopFling = false; + + final Scroller scroller = mScroller; + boolean more = scroller.computeScrollOffset(); + final int x = scroller.getCurrX(); + + // Flip sign to convert finger direction to list items direction + // (e.g. finger moving down means list is moving towards the top) + int delta = mLastFlingX - x; + + // Pretend that each frame of a fling scroll is a touch scroll + if (delta > 0) { + // Moving towards the left. Use first view as mDownTouchPosition + mDownTouchPosition = mFirstPosition; + + // Don't fling more than 1 screen + delta = Math.min(getWidth() - mPaddingLeft - mPaddingRight - 1, delta); + } else { + // Moving towards the right. Use last view as mDownTouchPosition + int offsetToLast = getChildCount() - 1; + mDownTouchPosition = mFirstPosition + offsetToLast; + + // Don't fling more than 1 screen + delta = Math.max(-(getWidth() - mPaddingRight - mPaddingLeft - 1), delta); + } + + trackMotionScroll(delta); + + if (more && !mShouldStopFling) { + mLastFlingX = x; + post(this); + } else { + endFling(true); + } + } + + } + + /** + * Gallery extends LayoutParams to provide a place to hold current + * Transformation information along with previous position/transformation + * info. + * + */ + public static class LayoutParams extends ViewGroup.LayoutParams { + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + } + + public LayoutParams(int w, int h) { + super(w, h); + } + + public LayoutParams(ViewGroup.LayoutParams source) { + super(source); + } + } +} -- cgit v1.1