|  | /* | 
|  | * 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 android.annotation.NonNull; | 
|  | import android.annotation.Widget; | 
|  | import android.compat.annotation.UnsupportedAppUsage; | 
|  | import android.content.Context; | 
|  | import android.content.res.TypedArray; | 
|  | import android.graphics.Rect; | 
|  | import android.os.Build; | 
|  | import android.os.Bundle; | 
|  | import android.util.AttributeSet; | 
|  | import android.util.Log; | 
|  | import android.view.ContextMenu.ContextMenuInfo; | 
|  | import android.view.GestureDetector; | 
|  | import android.view.Gravity; | 
|  | import android.view.HapticFeedbackConstants; | 
|  | import android.view.KeyEvent; | 
|  | import android.view.MotionEvent; | 
|  | import android.view.SoundEffectConstants; | 
|  | import android.view.View; | 
|  | import android.view.ViewConfiguration; | 
|  | import android.view.ViewGroup; | 
|  | import android.view.accessibility.AccessibilityNodeInfo; | 
|  | import android.view.animation.Transformation; | 
|  |  | 
|  | import com.android.internal.R; | 
|  |  | 
|  | /** | 
|  | * A view that shows items in a center-locked, horizontally scrolling list. | 
|  | * <p> | 
|  | * 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. | 
|  | * <p> | 
|  | * 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 | 
|  | * | 
|  | * @deprecated This widget is no longer supported. Other horizontally scrolling | 
|  | * widgets include {@link HorizontalScrollView} and {@link android.support.v4.view.ViewPager} | 
|  | * from the support library. | 
|  | */ | 
|  | @Deprecated | 
|  | @Widget | 
|  | public class Gallery extends AbsSpinner implements GestureDetector.OnGestureListener { | 
|  |  | 
|  | private static final String TAG = "Gallery"; | 
|  |  | 
|  | private static final boolean localLOGV = false; | 
|  |  | 
|  | /** | 
|  | * 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. | 
|  | */ | 
|  | @UnsupportedAppUsage | 
|  | 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. | 
|  | */ | 
|  | @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) | 
|  | private GestureDetector mGestureDetector; | 
|  |  | 
|  | /** | 
|  | * The position of the item that received the user's down touch. | 
|  | */ | 
|  | @UnsupportedAppUsage | 
|  | private int mDownTouchPosition; | 
|  |  | 
|  | /** | 
|  | * The view of the item that received the user's down touch. | 
|  | */ | 
|  | @UnsupportedAppUsage | 
|  | private View mDownTouchView; | 
|  |  | 
|  | /** | 
|  | * Executes the delta scrolls from a fling or scroll movement. | 
|  | */ | 
|  | @UnsupportedAppUsage | 
|  | 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() { | 
|  | @Override | 
|  | 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. | 
|  | */ | 
|  | @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) | 
|  | 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; | 
|  |  | 
|  | /** | 
|  | * If true, mFirstPosition is the position of the rightmost child, and | 
|  | * the children are ordered right to left. | 
|  | */ | 
|  | private boolean mIsRtl = true; | 
|  |  | 
|  | /** | 
|  | * Offset between the center of the selected child view and the center of the Gallery. | 
|  | * Used to reset position correctly during layout. | 
|  | */ | 
|  | private int mSelectedCenterOffset; | 
|  |  | 
|  | 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 defStyleAttr) { | 
|  | this(context, attrs, defStyleAttr, 0); | 
|  | } | 
|  |  | 
|  | public Gallery(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { | 
|  | super(context, attrs, defStyleAttr, defStyleRes); | 
|  |  | 
|  | final TypedArray a = context.obtainStyledAttributes( | 
|  | attrs, com.android.internal.R.styleable.Gallery, defStyleAttr, defStyleRes); | 
|  | saveAttributeDataForStyleable(context, com.android.internal.R.styleable.Gallery, | 
|  | attrs, a, defStyleAttr, defStyleRes); | 
|  |  | 
|  | 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; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | protected void onAttachedToWindow() { | 
|  | super.onAttachedToWindow(); | 
|  |  | 
|  | if (mGestureDetector == null) { | 
|  | mGestureDetector = new GestureDetector(getContext(), this); | 
|  | mGestureDetector.setIsLongpressEnabled(true); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * 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. | 
|  | */ | 
|  | @UnsupportedAppUsage | 
|  | 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(); | 
|  |  | 
|  | final View selChild = mSelectedChild; | 
|  | if (selChild != null) { | 
|  | final int childLeft = selChild.getLeft(); | 
|  | final int childCenter = selChild.getWidth() / 2; | 
|  | final int galleryCenter = getWidth() / 2; | 
|  | mSelectedCenterOffset = childLeft + childCenter - galleryCenter; | 
|  | } | 
|  |  | 
|  | onScrollChanged(0, 0, 0, 0); // dummy values, View's implementation does not use these. | 
|  |  | 
|  | invalidate(); | 
|  | } | 
|  |  | 
|  | int getLimitedMotionScrollAmount(boolean motionToLeft, int deltaX) { | 
|  | int extremeItemPosition = motionToLeft != mIsRtl ? 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. | 
|  | */ | 
|  | @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) | 
|  | private int getCenterOfGallery() { | 
|  | return (getWidth() - mPaddingLeft - mPaddingRight) / 2 + mPaddingLeft; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @return The center of the given view. | 
|  | */ | 
|  | @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) | 
|  | 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++) { | 
|  | int n = mIsRtl ? (numChildren - 1 - i) : i; | 
|  | final View child = getChildAt(n); | 
|  | if (child.getRight() >= galleryLeft) { | 
|  | break; | 
|  | } else { | 
|  | start = n; | 
|  | count++; | 
|  | mRecycler.put(firstPosition + n, child); | 
|  | } | 
|  | } | 
|  | if (!mIsRtl) { | 
|  | start = 0; | 
|  | } | 
|  | } else { | 
|  | final int galleryRight = getWidth() - mPaddingRight; | 
|  | for (int i = numChildren - 1; i >= 0; i--) { | 
|  | int n = mIsRtl ? numChildren - 1 - i : i; | 
|  | final View child = getChildAt(n); | 
|  | if (child.getLeft() <= galleryRight) { | 
|  | break; | 
|  | } else { | 
|  | start = n; | 
|  | count++; | 
|  | mRecycler.put(firstPosition + n, child); | 
|  | } | 
|  | } | 
|  | if (mIsRtl) { | 
|  | start = 0; | 
|  | } | 
|  | } | 
|  |  | 
|  | detachViewsFromParent(start, count); | 
|  |  | 
|  | if (toLeft != mIsRtl) { | 
|  | 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(); | 
|  | } | 
|  | mSelectedCenterOffset = 0; | 
|  | invalidate(); | 
|  | } | 
|  |  | 
|  | @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(); | 
|  |  | 
|  | // 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. | 
|  | * <p> | 
|  | * 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) { | 
|  |  | 
|  | mIsRtl = isLayoutRtl(); | 
|  |  | 
|  | 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 | 
|  | int selectedOffset = childrenLeft + (childrenWidth / 2) - (sel.getWidth() / 2) + | 
|  | mSelectedCenterOffset; | 
|  | 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(); | 
|  | } | 
|  |  | 
|  | @UnsupportedAppUsage | 
|  | private void fillToGalleryLeft() { | 
|  | if (mIsRtl) { | 
|  | fillToGalleryLeftRtl(); | 
|  | } else { | 
|  | fillToGalleryLeftLtr(); | 
|  | } | 
|  | } | 
|  |  | 
|  | private void fillToGalleryLeftRtl() { | 
|  | int itemSpacing = mSpacing; | 
|  | int galleryLeft = mPaddingLeft; | 
|  | int numChildren = getChildCount(); | 
|  | int numItems = mItemCount; | 
|  |  | 
|  | // Set state for initial iteration | 
|  | View prevIterationView = getChildAt(numChildren - 1); | 
|  | int curPosition; | 
|  | int curRightEdge; | 
|  |  | 
|  | if (prevIterationView != null) { | 
|  | curPosition = mFirstPosition + numChildren; | 
|  | curRightEdge = prevIterationView.getLeft() - itemSpacing; | 
|  | } else { | 
|  | // No children available! | 
|  | mFirstPosition = curPosition = mItemCount - 1; | 
|  | curRightEdge = mRight - mLeft - mPaddingRight; | 
|  | mShouldStopFling = true; | 
|  | } | 
|  |  | 
|  | while (curRightEdge > galleryLeft && curPosition < mItemCount) { | 
|  | prevIterationView = makeAndAddView(curPosition, curPosition - mSelectedPosition, | 
|  | curRightEdge, false); | 
|  |  | 
|  | // Set state for next iteration | 
|  | curRightEdge = prevIterationView.getLeft() - itemSpacing; | 
|  | curPosition++; | 
|  | } | 
|  | } | 
|  |  | 
|  | private void fillToGalleryLeftLtr() { | 
|  | 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--; | 
|  | } | 
|  | } | 
|  |  | 
|  | @UnsupportedAppUsage | 
|  | private void fillToGalleryRight() { | 
|  | if (mIsRtl) { | 
|  | fillToGalleryRightRtl(); | 
|  | } else { | 
|  | fillToGalleryRightLtr(); | 
|  | } | 
|  | } | 
|  |  | 
|  | private void fillToGalleryRightRtl() { | 
|  | int itemSpacing = mSpacing; | 
|  | int galleryRight = mRight - mLeft - mPaddingRight; | 
|  |  | 
|  | // Set state for initial iteration | 
|  | View prevIterationView = getChildAt(0); | 
|  | int curPosition; | 
|  | int curLeftEdge; | 
|  |  | 
|  | if (prevIterationView != null) { | 
|  | curPosition = mFirstPosition -1; | 
|  | curLeftEdge = prevIterationView.getRight() + itemSpacing; | 
|  | } else { | 
|  | curPosition = 0; | 
|  | curLeftEdge = mPaddingLeft; | 
|  | mShouldStopFling = true; | 
|  | } | 
|  |  | 
|  | while (curLeftEdge < galleryRight && curPosition >= 0) { | 
|  | prevIterationView = makeAndAddView(curPosition, curPosition - mSelectedPosition, | 
|  | curLeftEdge, true); | 
|  |  | 
|  | // Remember some state | 
|  | mFirstPosition = curPosition; | 
|  |  | 
|  | // Set state for next iteration | 
|  | curLeftEdge = prevIterationView.getRight() + itemSpacing; | 
|  | curPosition--; | 
|  | } | 
|  | } | 
|  |  | 
|  | private void fillToGalleryRightLtr() { | 
|  | 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-coordinate indicating where this view should be placed. This | 
|  | *        will either be the left or right edge of the view, depending on | 
|  | *        the fromLeft parameter | 
|  | * @param fromLeft Are we positioning views based on the left edge? (i.e., | 
|  | *        building from left to right)? | 
|  | * @return A view that has been added to the gallery | 
|  | */ | 
|  | @UnsupportedAppUsage | 
|  | 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 | 
|  | 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 parameters. | 
|  | * | 
|  | * @param child The view to position | 
|  | * @param offset Offset from the selected position | 
|  | * @param x X-coordinate indicating where this view should be placed. This | 
|  | *        will either be the left or right edge of the view, depending on | 
|  | *        the fromLeft parameter | 
|  | * @param fromLeft Are we positioning 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 != mIsRtl ? -1 : 0, lp, true); | 
|  |  | 
|  | 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, 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 | 
|  | * @return Where the top of the child should be | 
|  | */ | 
|  | private int calculateTop(View child, boolean duringLayout) { | 
|  | int myHeight = duringLayout ? getMeasuredHeight() : 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; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | 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; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | 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; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | 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; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | 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(); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void onLongPress(@NonNull MotionEvent e) { | 
|  | if (mDownTouchPosition < 0) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); | 
|  |  | 
|  | final long id = getItemIdAtPosition(mDownTouchPosition); | 
|  | dispatchLongPress(mDownTouchView, mDownTouchPosition, id, e.getX(), e.getY(), true); | 
|  | } | 
|  |  | 
|  | // Unused methods from GestureDetector.OnGestureListener below | 
|  |  | 
|  | @Override | 
|  | 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) { | 
|  | if (isShowingContextMenuWithCoords()) { | 
|  | return false; | 
|  | } | 
|  | return showContextMenuForChildInternal(originalView, 0, 0, false); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public boolean showContextMenuForChild(View originalView, float x, float y) { | 
|  | return showContextMenuForChildInternal(originalView, x, y, true); | 
|  | } | 
|  |  | 
|  | private boolean showContextMenuForChildInternal(View originalView, float x, float y, | 
|  | boolean useOffsets) { | 
|  | final int longPressPosition = getPositionForView(originalView); | 
|  | if (longPressPosition < 0) { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | final long longPressId = mAdapter.getItemId(longPressPosition); | 
|  | return dispatchLongPress(originalView, longPressPosition, longPressId, x, y, useOffsets); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public boolean showContextMenu() { | 
|  | return showContextMenuInternal(0, 0, false); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public boolean showContextMenu(float x, float y) { | 
|  | return showContextMenuInternal(x, y, true); | 
|  | } | 
|  |  | 
|  | private boolean showContextMenuInternal(float x, float y, boolean useOffsets) { | 
|  | if (isPressed() && mSelectedPosition >= 0) { | 
|  | final int index = mSelectedPosition - mFirstPosition; | 
|  | final View v = getChildAt(index); | 
|  | return dispatchLongPress(v, mSelectedPosition, mSelectedRowId, x, y, useOffsets); | 
|  | } | 
|  |  | 
|  | return false; | 
|  | } | 
|  |  | 
|  | private boolean dispatchLongPress(View view, int position, long id, float x, float y, | 
|  | boolean useOffsets) { | 
|  | boolean handled = false; | 
|  |  | 
|  | if (mOnItemLongClickListener != null) { | 
|  | handled = mOnItemLongClickListener.onItemLongClick(this, mDownTouchView, | 
|  | mDownTouchPosition, id); | 
|  | } | 
|  |  | 
|  | if (!handled) { | 
|  | mContextMenuInfo = new AdapterContextMenuInfo(view, position, id); | 
|  |  | 
|  | if (useOffsets) { | 
|  | handled = super.showContextMenuForChild(view, x, y); | 
|  | } else { | 
|  | 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, null, null); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * 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 (moveDirection(-1)) { | 
|  | playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); | 
|  | return true; | 
|  | } | 
|  | break; | 
|  | case KeyEvent.KEYCODE_DPAD_RIGHT: | 
|  | if (moveDirection(1)) { | 
|  | playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); | 
|  | return true; | 
|  | } | 
|  | break; | 
|  | 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) { | 
|  | if (KeyEvent.isConfirmKey(keyCode)) { | 
|  | if (mReceivedInvokeKeyDown) { | 
|  | if (mItemCount > 0) { | 
|  | dispatchPress(mSelectedChild); | 
|  | postDelayed(new Runnable() { | 
|  | @Override | 
|  | 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); | 
|  | } | 
|  |  | 
|  | @UnsupportedAppUsage | 
|  | boolean moveDirection(int direction) { | 
|  | direction = isLayoutRtl() ? -direction : direction; | 
|  | int targetPosition = mSelectedPosition + direction; | 
|  |  | 
|  | if (mItemCount > 0 && targetPosition >= 0 && targetPosition < mItemCount) { | 
|  | scrollToChild(targetPosition - mFirstPosition); | 
|  | 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 && oldSelectedChild != child) { | 
|  |  | 
|  | // 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 after the selected child earlier one | 
|  | return i + 1; | 
|  | } else { | 
|  | // Keep the children before 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); | 
|  | mSelectedChild.setSelected(true); | 
|  | } | 
|  |  | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public CharSequence getAccessibilityClassName() { | 
|  | return Gallery.class.getName(); | 
|  | } | 
|  |  | 
|  | /** @hide */ | 
|  | @Override | 
|  | public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { | 
|  | super.onInitializeAccessibilityNodeInfoInternal(info); | 
|  | info.setScrollable(mItemCount > 1); | 
|  | if (isEnabled()) { | 
|  | if (mItemCount > 0 && mSelectedPosition < mItemCount - 1) { | 
|  | info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); | 
|  | } | 
|  | if (isEnabled() && mItemCount > 0 && mSelectedPosition > 0) { | 
|  | info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | /** @hide */ | 
|  | @Override | 
|  | public boolean performAccessibilityActionInternal(int action, Bundle arguments) { | 
|  | if (super.performAccessibilityActionInternal(action, arguments)) { | 
|  | return true; | 
|  | } | 
|  | switch (action) { | 
|  | case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: { | 
|  | if (isEnabled() && mItemCount > 0 && mSelectedPosition < mItemCount - 1) { | 
|  | final int currentChildIndex = mSelectedPosition - mFirstPosition; | 
|  | return scrollToChild(currentChildIndex + 1); | 
|  | } | 
|  | } return false; | 
|  | case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: { | 
|  | if (isEnabled() && mItemCount > 0 && mSelectedPosition > 0) { | 
|  | final int currentChildIndex = mSelectedPosition - mFirstPosition; | 
|  | return scrollToChild(currentChildIndex - 1); | 
|  | } | 
|  | } return false; | 
|  | } | 
|  | return false; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * 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); | 
|  | } | 
|  |  | 
|  | @UnsupportedAppUsage | 
|  | 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(); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | 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 leftmost view as mDownTouchPosition | 
|  | mDownTouchPosition = mIsRtl ? (mFirstPosition + getChildCount() - 1) : | 
|  | mFirstPosition; | 
|  |  | 
|  | // Don't fling more than 1 screen | 
|  | delta = Math.min(getWidth() - mPaddingLeft - mPaddingRight - 1, delta); | 
|  | } else { | 
|  | // Moving towards the right. Use rightmost view as mDownTouchPosition | 
|  | int offsetToLast = getChildCount() - 1; | 
|  | mDownTouchPosition = mIsRtl ? mFirstPosition : | 
|  | (mFirstPosition + getChildCount() - 1); | 
|  |  | 
|  | // 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); | 
|  | } | 
|  | } | 
|  | } |