1 /*
2  * Copyright (C) 2007 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.widget;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Widget;
21 import android.compat.annotation.UnsupportedAppUsage;
22 import android.content.Context;
23 import android.content.res.TypedArray;
24 import android.graphics.Rect;
25 import android.os.Build;
26 import android.os.Bundle;
27 import android.util.AttributeSet;
28 import android.util.Log;
29 import android.view.ContextMenu.ContextMenuInfo;
30 import android.view.GestureDetector;
31 import android.view.Gravity;
32 import android.view.HapticFeedbackConstants;
33 import android.view.KeyEvent;
34 import android.view.MotionEvent;
35 import android.view.SoundEffectConstants;
36 import android.view.View;
37 import android.view.ViewConfiguration;
38 import android.view.ViewGroup;
39 import android.view.accessibility.AccessibilityNodeInfo;
40 import android.view.animation.Transformation;
41 
42 import com.android.internal.R;
43 
44 /**
45  * A view that shows items in a center-locked, horizontally scrolling list.
46  * <p>
47  * The default values for the Gallery assume you will be using
48  * {@link android.R.styleable#Theme_galleryItemBackground} as the background for
49  * each View given to the Gallery from the Adapter. If you are not doing this,
50  * you may need to adjust some Gallery properties, such as the spacing.
51  * <p>
52  * Views given to the Gallery should use {@link Gallery.LayoutParams} as their
53  * layout parameters type.
54  *
55  * @attr ref android.R.styleable#Gallery_animationDuration
56  * @attr ref android.R.styleable#Gallery_spacing
57  * @attr ref android.R.styleable#Gallery_gravity
58  *
59  * @deprecated This widget is no longer supported. Other horizontally scrolling
60  * widgets include {@link HorizontalScrollView} and {@link androidx.viewpager.widget.ViewPager}
61  * from the support library.
62  */
63 @Deprecated
64 @Widget
65 public class Gallery extends AbsSpinner implements GestureDetector.OnGestureListener {
66 
67     private static final String TAG = "Gallery";
68 
69     private static final boolean localLOGV = false;
70 
71     /**
72      * Duration in milliseconds from the start of a scroll during which we're
73      * unsure whether the user is scrolling or flinging.
74      */
75     private static final int SCROLL_TO_FLING_UNCERTAINTY_TIMEOUT = 250;
76 
77     /**
78      * Horizontal spacing between items.
79      */
80     @UnsupportedAppUsage
81     private int mSpacing = 0;
82 
83     /**
84      * How long the transition animation should run when a child view changes
85      * position, measured in milliseconds.
86      */
87     private int mAnimationDuration = 400;
88 
89     /**
90      * The alpha of items that are not selected.
91      */
92     private float mUnselectedAlpha;
93 
94     /**
95      * Left most edge of a child seen so far during layout.
96      */
97     private int mLeftMost;
98 
99     /**
100      * Right most edge of a child seen so far during layout.
101      */
102     private int mRightMost;
103 
104     private int mGravity;
105 
106     /**
107      * Helper for detecting touch gestures.
108      */
109     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
110     private GestureDetector mGestureDetector;
111 
112     /**
113      * The position of the item that received the user's down touch.
114      */
115     @UnsupportedAppUsage
116     private int mDownTouchPosition;
117 
118     /**
119      * The view of the item that received the user's down touch.
120      */
121     @UnsupportedAppUsage
122     private View mDownTouchView;
123 
124     /**
125      * Executes the delta scrolls from a fling or scroll movement.
126      */
127     @UnsupportedAppUsage
128     private FlingRunnable mFlingRunnable = new FlingRunnable();
129 
130     /**
131      * Sets mSuppressSelectionChanged = false. This is used to set it to false
132      * in the future. It will also trigger a selection changed.
133      */
134     private Runnable mDisableSuppressSelectionChangedRunnable = new Runnable() {
135         @Override
136         public void run() {
137             mSuppressSelectionChanged = false;
138             selectionChanged();
139         }
140     };
141 
142     /**
143      * When fling runnable runs, it resets this to false. Any method along the
144      * path until the end of its run() can set this to true to abort any
145      * remaining fling. For example, if we've reached either the leftmost or
146      * rightmost item, we will set this to true.
147      */
148     private boolean mShouldStopFling;
149 
150     /**
151      * The currently selected item's child.
152      */
153     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
154     private View mSelectedChild;
155 
156     /**
157      * Whether to continuously callback on the item selected listener during a
158      * fling.
159      */
160     private boolean mShouldCallbackDuringFling = true;
161 
162     /**
163      * Whether to callback when an item that is not selected is clicked.
164      */
165     private boolean mShouldCallbackOnUnselectedItemClick = true;
166 
167     /**
168      * If true, do not callback to item selected listener.
169      */
170     private boolean mSuppressSelectionChanged;
171 
172     /**
173      * If true, we have received the "invoke" (center or enter buttons) key
174      * down. This is checked before we action on the "invoke" key up, and is
175      * subsequently cleared.
176      */
177     private boolean mReceivedInvokeKeyDown;
178 
179     private AdapterContextMenuInfo mContextMenuInfo;
180 
181     /**
182      * If true, this onScroll is the first for this user's drag (remember, a
183      * drag sends many onScrolls).
184      */
185     private boolean mIsFirstScroll;
186 
187     /**
188      * If true, mFirstPosition is the position of the rightmost child, and
189      * the children are ordered right to left.
190      */
191     private boolean mIsRtl = true;
192 
193     /**
194      * Offset between the center of the selected child view and the center of the Gallery.
195      * Used to reset position correctly during layout.
196      */
197     private int mSelectedCenterOffset;
198 
Gallery(Context context)199     public Gallery(Context context) {
200         this(context, null);
201     }
202 
Gallery(Context context, AttributeSet attrs)203     public Gallery(Context context, AttributeSet attrs) {
204         this(context, attrs, R.attr.galleryStyle);
205     }
206 
Gallery(Context context, AttributeSet attrs, int defStyleAttr)207     public Gallery(Context context, AttributeSet attrs, int defStyleAttr) {
208         this(context, attrs, defStyleAttr, 0);
209     }
210 
Gallery(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)211     public Gallery(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
212         super(context, attrs, defStyleAttr, defStyleRes);
213 
214         final TypedArray a = context.obtainStyledAttributes(
215                 attrs, com.android.internal.R.styleable.Gallery, defStyleAttr, defStyleRes);
216         saveAttributeDataForStyleable(context, com.android.internal.R.styleable.Gallery,
217                 attrs, a, defStyleAttr, defStyleRes);
218 
219         int index = a.getInt(com.android.internal.R.styleable.Gallery_gravity, -1);
220         if (index >= 0) {
221             setGravity(index);
222         }
223 
224         int animationDuration =
225                 a.getInt(com.android.internal.R.styleable.Gallery_animationDuration, -1);
226         if (animationDuration > 0) {
227             setAnimationDuration(animationDuration);
228         }
229 
230         int spacing =
231                 a.getDimensionPixelOffset(com.android.internal.R.styleable.Gallery_spacing, 0);
232         setSpacing(spacing);
233 
234         float unselectedAlpha = a.getFloat(
235                 com.android.internal.R.styleable.Gallery_unselectedAlpha, 0.5f);
236         setUnselectedAlpha(unselectedAlpha);
237 
238         a.recycle();
239 
240         // We draw the selected item last (because otherwise the item to the
241         // right overlaps it)
242         mGroupFlags |= FLAG_USE_CHILD_DRAWING_ORDER;
243 
244         mGroupFlags |= FLAG_SUPPORT_STATIC_TRANSFORMATIONS;
245     }
246 
247     @Override
onAttachedToWindow()248     protected void onAttachedToWindow() {
249         super.onAttachedToWindow();
250 
251         if (mGestureDetector == null) {
252             mGestureDetector = new GestureDetector(getContext(), this);
253             mGestureDetector.setIsLongpressEnabled(true);
254         }
255     }
256 
257     /**
258      * Whether or not to callback on any {@link #getOnItemSelectedListener()}
259      * while the items are being flinged. If false, only the final selected item
260      * will cause the callback. If true, all items between the first and the
261      * final will cause callbacks.
262      *
263      * @param shouldCallback Whether or not to callback on the listener while
264      *            the items are being flinged.
265      */
setCallbackDuringFling(boolean shouldCallback)266     public void setCallbackDuringFling(boolean shouldCallback) {
267         mShouldCallbackDuringFling = shouldCallback;
268     }
269 
270     /**
271      * Whether or not to callback when an item that is not selected is clicked.
272      * If false, the item will become selected (and re-centered). If true, the
273      * {@link #getOnItemClickListener()} will get the callback.
274      *
275      * @param shouldCallback Whether or not to callback on the listener when a
276      *            item that is not selected is clicked.
277      * @hide
278      */
setCallbackOnUnselectedItemClick(boolean shouldCallback)279     public void setCallbackOnUnselectedItemClick(boolean shouldCallback) {
280         mShouldCallbackOnUnselectedItemClick = shouldCallback;
281     }
282 
283     /**
284      * Sets how long the transition animation should run when a child view
285      * changes position. Only relevant if animation is turned on.
286      *
287      * @param animationDurationMillis The duration of the transition, in
288      *        milliseconds.
289      *
290      * @attr ref android.R.styleable#Gallery_animationDuration
291      */
setAnimationDuration(int animationDurationMillis)292     public void setAnimationDuration(int animationDurationMillis) {
293         mAnimationDuration = animationDurationMillis;
294     }
295 
296     /**
297      * Sets the spacing between items in a Gallery
298      *
299      * @param spacing The spacing in pixels between items in the Gallery
300      *
301      * @attr ref android.R.styleable#Gallery_spacing
302      */
setSpacing(int spacing)303     public void setSpacing(int spacing) {
304         mSpacing = spacing;
305     }
306 
307     /**
308      * Sets the alpha of items that are not selected in the Gallery.
309      *
310      * @param unselectedAlpha the alpha for the items that are not selected.
311      *
312      * @attr ref android.R.styleable#Gallery_unselectedAlpha
313      */
setUnselectedAlpha(float unselectedAlpha)314     public void setUnselectedAlpha(float unselectedAlpha) {
315         mUnselectedAlpha = unselectedAlpha;
316     }
317 
318     @Override
getChildStaticTransformation(View child, Transformation t)319     protected boolean getChildStaticTransformation(View child, Transformation t) {
320 
321         t.clear();
322         t.setAlpha(child == mSelectedChild ? 1.0f : mUnselectedAlpha);
323 
324         return true;
325     }
326 
327     @Override
computeHorizontalScrollExtent()328     protected int computeHorizontalScrollExtent() {
329         // Only 1 item is considered to be selected
330         return 1;
331     }
332 
333     @Override
computeHorizontalScrollOffset()334     protected int computeHorizontalScrollOffset() {
335         // Current scroll position is the same as the selected position
336         return mSelectedPosition;
337     }
338 
339     @Override
computeHorizontalScrollRange()340     protected int computeHorizontalScrollRange() {
341         // Scroll range is the same as the item count
342         return mItemCount;
343     }
344 
345     @Override
checkLayoutParams(ViewGroup.LayoutParams p)346     protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
347         return p instanceof LayoutParams;
348     }
349 
350     @Override
generateLayoutParams(ViewGroup.LayoutParams p)351     protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
352         return new LayoutParams(p);
353     }
354 
355     @Override
generateLayoutParams(AttributeSet attrs)356     public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
357         return new LayoutParams(getContext(), attrs);
358     }
359 
360     @Override
generateDefaultLayoutParams()361     protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
362         /*
363          * Gallery expects Gallery.LayoutParams.
364          */
365         return new Gallery.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
366                 ViewGroup.LayoutParams.WRAP_CONTENT);
367     }
368 
369     @Override
onLayout(boolean changed, int l, int t, int r, int b)370     protected void onLayout(boolean changed, int l, int t, int r, int b) {
371         super.onLayout(changed, l, t, r, b);
372 
373         /*
374          * Remember that we are in layout to prevent more layout request from
375          * being generated.
376          */
377         mInLayout = true;
378         layout(0, false);
379         mInLayout = false;
380     }
381 
382     @Override
getChildHeight(View child)383     int getChildHeight(View child) {
384         return child.getMeasuredHeight();
385     }
386 
387     /**
388      * Tracks a motion scroll. In reality, this is used to do just about any
389      * movement to items (touch scroll, arrow-key scroll, set an item as selected).
390      *
391      * @param deltaX Change in X from the previous event.
392      */
393     @UnsupportedAppUsage
trackMotionScroll(int deltaX)394     void trackMotionScroll(int deltaX) {
395 
396         if (getChildCount() == 0) {
397             return;
398         }
399 
400         boolean toLeft = deltaX < 0;
401 
402         int limitedDeltaX = getLimitedMotionScrollAmount(toLeft, deltaX);
403         if (limitedDeltaX != deltaX) {
404             // The above call returned a limited amount, so stop any scrolls/flings
405             mFlingRunnable.endFling(false);
406             onFinishedMovement();
407         }
408 
409         offsetChildrenLeftAndRight(limitedDeltaX);
410 
411         detachOffScreenChildren(toLeft);
412 
413         if (toLeft) {
414             // If moved left, there will be empty space on the right
415             fillToGalleryRight();
416         } else {
417             // Similarly, empty space on the left
418             fillToGalleryLeft();
419         }
420 
421         // Clear unused views
422         mRecycler.clear();
423 
424         setSelectionToCenterChild();
425 
426         final View selChild = mSelectedChild;
427         if (selChild != null) {
428             final int childLeft = selChild.getLeft();
429             final int childCenter = selChild.getWidth() / 2;
430             final int galleryCenter = getWidth() / 2;
431             mSelectedCenterOffset = childLeft + childCenter - galleryCenter;
432         }
433 
434         // placeholder values, View's implementation does not use these.
435         onScrollChanged(0, 0, 0, 0);
436 
437         invalidate();
438     }
439 
440     int getLimitedMotionScrollAmount(boolean motionToLeft, int deltaX) {
441         int extremeItemPosition = motionToLeft != mIsRtl ? mItemCount - 1 : 0;
442         View extremeChild = getChildAt(extremeItemPosition - mFirstPosition);
443 
444         if (extremeChild == null) {
445             return deltaX;
446         }
447 
448         int extremeChildCenter = getCenterOfView(extremeChild);
449         int galleryCenter = getCenterOfGallery();
450 
451         if (motionToLeft) {
452             if (extremeChildCenter <= galleryCenter) {
453 
454                 // The extreme child is past his boundary point!
455                 return 0;
456             }
457         } else {
458             if (extremeChildCenter >= galleryCenter) {
459 
460                 // The extreme child is past his boundary point!
461                 return 0;
462             }
463         }
464 
465         int centerDifference = galleryCenter - extremeChildCenter;
466 
467         return motionToLeft
468                 ? Math.max(centerDifference, deltaX)
469                 : Math.min(centerDifference, deltaX);
470     }
471 
472     /**
473      * Offset the horizontal location of all children of this view by the
474      * specified number of pixels.
475      *
476      * @param offset the number of pixels to offset
477      */
478     private void offsetChildrenLeftAndRight(int offset) {
479         for (int i = getChildCount() - 1; i >= 0; i--) {
480             getChildAt(i).offsetLeftAndRight(offset);
481         }
482     }
483 
484     /**
485      * @return The center of this Gallery.
486      */
487     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
getCenterOfGallery()488     private int getCenterOfGallery() {
489         return (getWidth() - mPaddingLeft - mPaddingRight) / 2 + mPaddingLeft;
490     }
491 
492     /**
493      * @return The center of the given view.
494      */
495     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
getCenterOfView(View view)496     private static int getCenterOfView(View view) {
497         return view.getLeft() + view.getWidth() / 2;
498     }
499 
500     /**
501      * Detaches children that are off the screen (i.e.: Gallery bounds).
502      *
503      * @param toLeft Whether to detach children to the left of the Gallery, or
504      *            to the right.
505      */
detachOffScreenChildren(boolean toLeft)506     private void detachOffScreenChildren(boolean toLeft) {
507         int numChildren = getChildCount();
508         int firstPosition = mFirstPosition;
509         int start = 0;
510         int count = 0;
511 
512         if (toLeft) {
513             final int galleryLeft = mPaddingLeft;
514             for (int i = 0; i < numChildren; i++) {
515                 int n = mIsRtl ? (numChildren - 1 - i) : i;
516                 final View child = getChildAt(n);
517                 if (child.getRight() >= galleryLeft) {
518                     break;
519                 } else {
520                     start = n;
521                     count++;
522                     mRecycler.put(firstPosition + n, child);
523                 }
524             }
525             if (!mIsRtl) {
526                 start = 0;
527             }
528         } else {
529             final int galleryRight = getWidth() - mPaddingRight;
530             for (int i = numChildren - 1; i >= 0; i--) {
531                 int n = mIsRtl ? numChildren - 1 - i : i;
532                 final View child = getChildAt(n);
533                 if (child.getLeft() <= galleryRight) {
534                     break;
535                 } else {
536                     start = n;
537                     count++;
538                     mRecycler.put(firstPosition + n, child);
539                 }
540             }
541             if (mIsRtl) {
542                 start = 0;
543             }
544         }
545 
546         detachViewsFromParent(start, count);
547 
548         if (toLeft != mIsRtl) {
549             mFirstPosition += count;
550         }
551     }
552 
553     /**
554      * Scrolls the items so that the selected item is in its 'slot' (its center
555      * is the gallery's center).
556      */
scrollIntoSlots()557     private void scrollIntoSlots() {
558 
559         if (getChildCount() == 0 || mSelectedChild == null) return;
560 
561         int selectedCenter = getCenterOfView(mSelectedChild);
562         int targetCenter = getCenterOfGallery();
563 
564         int scrollAmount = targetCenter - selectedCenter;
565         if (scrollAmount != 0) {
566             mFlingRunnable.startUsingDistance(scrollAmount);
567         } else {
568             onFinishedMovement();
569         }
570     }
571 
onFinishedMovement()572     private void onFinishedMovement() {
573         if (mSuppressSelectionChanged) {
574             mSuppressSelectionChanged = false;
575 
576             // We haven't been callbacking during the fling, so do it now
577             super.selectionChanged();
578         }
579         mSelectedCenterOffset = 0;
580         invalidate();
581     }
582 
583     @Override
selectionChanged()584     void selectionChanged() {
585         if (!mSuppressSelectionChanged) {
586             super.selectionChanged();
587         }
588     }
589 
590     /**
591      * Looks for the child that is closest to the center and sets it as the
592      * selected child.
593      */
setSelectionToCenterChild()594     private void setSelectionToCenterChild() {
595 
596         View selView = mSelectedChild;
597         if (mSelectedChild == null) return;
598 
599         int galleryCenter = getCenterOfGallery();
600 
601         // Common case where the current selected position is correct
602         if (selView.getLeft() <= galleryCenter && selView.getRight() >= galleryCenter) {
603             return;
604         }
605 
606         // TODO better search
607         int closestEdgeDistance = Integer.MAX_VALUE;
608         int newSelectedChildIndex = 0;
609         for (int i = getChildCount() - 1; i >= 0; i--) {
610 
611             View child = getChildAt(i);
612 
613             if (child.getLeft() <= galleryCenter && child.getRight() >=  galleryCenter) {
614                 // This child is in the center
615                 newSelectedChildIndex = i;
616                 break;
617             }
618 
619             int childClosestEdgeDistance = Math.min(Math.abs(child.getLeft() - galleryCenter),
620                     Math.abs(child.getRight() - galleryCenter));
621             if (childClosestEdgeDistance < closestEdgeDistance) {
622                 closestEdgeDistance = childClosestEdgeDistance;
623                 newSelectedChildIndex = i;
624             }
625         }
626 
627         int newPos = mFirstPosition + newSelectedChildIndex;
628 
629         if (newPos != mSelectedPosition) {
630             setSelectedPositionInt(newPos);
631             setNextSelectedPositionInt(newPos);
632             checkSelectionChanged();
633         }
634     }
635 
636     /**
637      * Creates and positions all views for this Gallery.
638      * <p>
639      * We layout rarely, most of the time {@link #trackMotionScroll(int)} takes
640      * care of repositioning, adding, and removing children.
641      *
642      * @param delta Change in the selected position. +1 means the selection is
643      *            moving to the right, so views are scrolling to the left. -1
644      *            means the selection is moving to the left.
645      */
646     @Override
layout(int delta, boolean animate)647     void layout(int delta, boolean animate) {
648 
649         mIsRtl = isLayoutRtl();
650 
651         int childrenLeft = mSpinnerPadding.left;
652         int childrenWidth = mRight - mLeft - mSpinnerPadding.left - mSpinnerPadding.right;
653 
654         if (mDataChanged) {
655             handleDataChanged();
656         }
657 
658         // Handle an empty gallery by removing all views.
659         if (mItemCount == 0) {
660             resetList();
661             return;
662         }
663 
664         // Update to the new selected position.
665         if (mNextSelectedPosition >= 0) {
666             setSelectedPositionInt(mNextSelectedPosition);
667         }
668 
669         // All views go in recycler while we are in layout
670         recycleAllViews();
671 
672         // Clear out old views
673         //removeAllViewsInLayout();
674         detachAllViewsFromParent();
675 
676         /*
677          * These will be used to give initial positions to views entering the
678          * gallery as we scroll
679          */
680         mRightMost = 0;
681         mLeftMost = 0;
682 
683         // Make selected view and center it
684 
685         /*
686          * mFirstPosition will be decreased as we add views to the left later
687          * on. The 0 for x will be offset in a couple lines down.
688          */
689         mFirstPosition = mSelectedPosition;
690         View sel = makeAndAddView(mSelectedPosition, 0, 0, true);
691 
692         // Put the selected child in the center
693         int selectedOffset = childrenLeft + (childrenWidth / 2) - (sel.getWidth() / 2) +
694                 mSelectedCenterOffset;
695         sel.offsetLeftAndRight(selectedOffset);
696 
697         fillToGalleryRight();
698         fillToGalleryLeft();
699 
700         // Flush any cached views that did not get reused above
701         mRecycler.clear();
702 
703         invalidate();
704         checkSelectionChanged();
705 
706         mDataChanged = false;
707         mNeedSync = false;
708         setNextSelectedPositionInt(mSelectedPosition);
709 
710         updateSelectedItemMetadata();
711     }
712 
713     @UnsupportedAppUsage
fillToGalleryLeft()714     private void fillToGalleryLeft() {
715         if (mIsRtl) {
716             fillToGalleryLeftRtl();
717         } else {
718             fillToGalleryLeftLtr();
719         }
720     }
721 
fillToGalleryLeftRtl()722     private void fillToGalleryLeftRtl() {
723         int itemSpacing = mSpacing;
724         int galleryLeft = mPaddingLeft;
725         int numChildren = getChildCount();
726         int numItems = mItemCount;
727 
728         // Set state for initial iteration
729         View prevIterationView = getChildAt(numChildren - 1);
730         int curPosition;
731         int curRightEdge;
732 
733         if (prevIterationView != null) {
734             curPosition = mFirstPosition + numChildren;
735             curRightEdge = prevIterationView.getLeft() - itemSpacing;
736         } else {
737             // No children available!
738             mFirstPosition = curPosition = mItemCount - 1;
739             curRightEdge = mRight - mLeft - mPaddingRight;
740             mShouldStopFling = true;
741         }
742 
743         while (curRightEdge > galleryLeft && curPosition < mItemCount) {
744             prevIterationView = makeAndAddView(curPosition, curPosition - mSelectedPosition,
745                     curRightEdge, false);
746 
747             // Set state for next iteration
748             curRightEdge = prevIterationView.getLeft() - itemSpacing;
749             curPosition++;
750         }
751     }
752 
fillToGalleryLeftLtr()753     private void fillToGalleryLeftLtr() {
754         int itemSpacing = mSpacing;
755         int galleryLeft = mPaddingLeft;
756 
757         // Set state for initial iteration
758         View prevIterationView = getChildAt(0);
759         int curPosition;
760         int curRightEdge;
761 
762         if (prevIterationView != null) {
763             curPosition = mFirstPosition - 1;
764             curRightEdge = prevIterationView.getLeft() - itemSpacing;
765         } else {
766             // No children available!
767             curPosition = 0;
768             curRightEdge = mRight - mLeft - mPaddingRight;
769             mShouldStopFling = true;
770         }
771 
772         while (curRightEdge > galleryLeft && curPosition >= 0) {
773             prevIterationView = makeAndAddView(curPosition, curPosition - mSelectedPosition,
774                     curRightEdge, false);
775 
776             // Remember some state
777             mFirstPosition = curPosition;
778 
779             // Set state for next iteration
780             curRightEdge = prevIterationView.getLeft() - itemSpacing;
781             curPosition--;
782         }
783     }
784 
785     @UnsupportedAppUsage
fillToGalleryRight()786     private void fillToGalleryRight() {
787         if (mIsRtl) {
788             fillToGalleryRightRtl();
789         } else {
790             fillToGalleryRightLtr();
791         }
792     }
793 
fillToGalleryRightRtl()794     private void fillToGalleryRightRtl() {
795         int itemSpacing = mSpacing;
796         int galleryRight = mRight - mLeft - mPaddingRight;
797 
798         // Set state for initial iteration
799         View prevIterationView = getChildAt(0);
800         int curPosition;
801         int curLeftEdge;
802 
803         if (prevIterationView != null) {
804             curPosition = mFirstPosition -1;
805             curLeftEdge = prevIterationView.getRight() + itemSpacing;
806         } else {
807             curPosition = 0;
808             curLeftEdge = mPaddingLeft;
809             mShouldStopFling = true;
810         }
811 
812         while (curLeftEdge < galleryRight && curPosition >= 0) {
813             prevIterationView = makeAndAddView(curPosition, curPosition - mSelectedPosition,
814                     curLeftEdge, true);
815 
816             // Remember some state
817             mFirstPosition = curPosition;
818 
819             // Set state for next iteration
820             curLeftEdge = prevIterationView.getRight() + itemSpacing;
821             curPosition--;
822         }
823     }
824 
fillToGalleryRightLtr()825     private void fillToGalleryRightLtr() {
826         int itemSpacing = mSpacing;
827         int galleryRight = mRight - mLeft - mPaddingRight;
828         int numChildren = getChildCount();
829         int numItems = mItemCount;
830 
831         // Set state for initial iteration
832         View prevIterationView = getChildAt(numChildren - 1);
833         int curPosition;
834         int curLeftEdge;
835 
836         if (prevIterationView != null) {
837             curPosition = mFirstPosition + numChildren;
838             curLeftEdge = prevIterationView.getRight() + itemSpacing;
839         } else {
840             mFirstPosition = curPosition = mItemCount - 1;
841             curLeftEdge = mPaddingLeft;
842             mShouldStopFling = true;
843         }
844 
845         while (curLeftEdge < galleryRight && curPosition < numItems) {
846             prevIterationView = makeAndAddView(curPosition, curPosition - mSelectedPosition,
847                     curLeftEdge, true);
848 
849             // Set state for next iteration
850             curLeftEdge = prevIterationView.getRight() + itemSpacing;
851             curPosition++;
852         }
853     }
854 
855     /**
856      * Obtain a view, either by pulling an existing view from the recycler or by
857      * getting a new one from the adapter. If we are animating, make sure there
858      * is enough information in the view's layout parameters to animate from the
859      * old to new positions.
860      *
861      * @param position Position in the gallery for the view to obtain
862      * @param offset Offset from the selected position
863      * @param x X-coordinate indicating where this view should be placed. This
864      *        will either be the left or right edge of the view, depending on
865      *        the fromLeft parameter
866      * @param fromLeft Are we positioning views based on the left edge? (i.e.,
867      *        building from left to right)?
868      * @return A view that has been added to the gallery
869      */
870     @UnsupportedAppUsage
makeAndAddView(int position, int offset, int x, boolean fromLeft)871     private View makeAndAddView(int position, int offset, int x, boolean fromLeft) {
872 
873         View child;
874         if (!mDataChanged) {
875             child = mRecycler.get(position);
876             if (child != null) {
877                 // Can reuse an existing view
878                 int childLeft = child.getLeft();
879 
880                 // Remember left and right edges of where views have been placed
881                 mRightMost = Math.max(mRightMost, childLeft
882                         + child.getMeasuredWidth());
883                 mLeftMost = Math.min(mLeftMost, childLeft);
884 
885                 // Position the view
886                 setUpChild(child, offset, x, fromLeft);
887 
888                 return child;
889             }
890         }
891 
892         // Nothing found in the recycler -- ask the adapter for a view
893         child = mAdapter.getView(position, null, this);
894 
895         // Position the view
896         setUpChild(child, offset, x, fromLeft);
897 
898         return child;
899     }
900 
901     /**
902      * Helper for makeAndAddView to set the position of a view and fill out its
903      * layout parameters.
904      *
905      * @param child The view to position
906      * @param offset Offset from the selected position
907      * @param x X-coordinate indicating where this view should be placed. This
908      *        will either be the left or right edge of the view, depending on
909      *        the fromLeft parameter
910      * @param fromLeft Are we positioning views based on the left edge? (i.e.,
911      *        building from left to right)?
912      */
setUpChild(View child, int offset, int x, boolean fromLeft)913     private void setUpChild(View child, int offset, int x, boolean fromLeft) {
914 
915         // Respect layout params that are already in the view. Otherwise
916         // make some up...
917         Gallery.LayoutParams lp = (Gallery.LayoutParams) child.getLayoutParams();
918         if (lp == null) {
919             lp = (Gallery.LayoutParams) generateDefaultLayoutParams();
920         }
921 
922         addViewInLayout(child, fromLeft != mIsRtl ? -1 : 0, lp, true);
923 
924         child.setSelected(offset == 0);
925 
926         // Get measure specs
927         int childHeightSpec = ViewGroup.getChildMeasureSpec(mHeightMeasureSpec,
928                 mSpinnerPadding.top + mSpinnerPadding.bottom, lp.height);
929         int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
930                 mSpinnerPadding.left + mSpinnerPadding.right, lp.width);
931 
932         // Measure child
933         child.measure(childWidthSpec, childHeightSpec);
934 
935         int childLeft;
936         int childRight;
937 
938         // Position vertically based on gravity setting
939         int childTop = calculateTop(child, true);
940         int childBottom = childTop + child.getMeasuredHeight();
941 
942         int width = child.getMeasuredWidth();
943         if (fromLeft) {
944             childLeft = x;
945             childRight = childLeft + width;
946         } else {
947             childLeft = x - width;
948             childRight = x;
949         }
950 
951         child.layout(childLeft, childTop, childRight, childBottom);
952     }
953 
954     /**
955      * Figure out vertical placement based on mGravity
956      *
957      * @param child Child to place
958      * @return Where the top of the child should be
959      */
calculateTop(View child, boolean duringLayout)960     private int calculateTop(View child, boolean duringLayout) {
961         int myHeight = duringLayout ? getMeasuredHeight() : getHeight();
962         int childHeight = duringLayout ? child.getMeasuredHeight() : child.getHeight();
963 
964         int childTop = 0;
965 
966         switch (mGravity) {
967         case Gravity.TOP:
968             childTop = mSpinnerPadding.top;
969             break;
970         case Gravity.CENTER_VERTICAL:
971             int availableSpace = myHeight - mSpinnerPadding.bottom
972                     - mSpinnerPadding.top - childHeight;
973             childTop = mSpinnerPadding.top + (availableSpace / 2);
974             break;
975         case Gravity.BOTTOM:
976             childTop = myHeight - mSpinnerPadding.bottom - childHeight;
977             break;
978         }
979         return childTop;
980     }
981 
982     @Override
onTouchEvent(MotionEvent event)983     public boolean onTouchEvent(MotionEvent event) {
984 
985         // Give everything to the gesture detector
986         boolean retValue = mGestureDetector.onTouchEvent(event);
987 
988         int action = event.getAction();
989         if (action == MotionEvent.ACTION_UP) {
990             // Helper method for lifted finger
991             onUp();
992         } else if (action == MotionEvent.ACTION_CANCEL) {
993             onCancel();
994         }
995 
996         return retValue;
997     }
998 
999     @Override
onSingleTapUp(MotionEvent e)1000     public boolean onSingleTapUp(MotionEvent e) {
1001 
1002         if (mDownTouchPosition >= 0) {
1003 
1004             // An item tap should make it selected, so scroll to this child.
1005             scrollToChild(mDownTouchPosition - mFirstPosition);
1006 
1007             // Also pass the click so the client knows, if it wants to.
1008             if (mShouldCallbackOnUnselectedItemClick || mDownTouchPosition == mSelectedPosition) {
1009                 performItemClick(mDownTouchView, mDownTouchPosition, mAdapter
1010                         .getItemId(mDownTouchPosition));
1011             }
1012 
1013             return true;
1014         }
1015 
1016         return false;
1017     }
1018 
1019     @Override
onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)1020     public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
1021 
1022         if (!mShouldCallbackDuringFling) {
1023             // We want to suppress selection changes
1024 
1025             // Remove any future code to set mSuppressSelectionChanged = false
1026             removeCallbacks(mDisableSuppressSelectionChangedRunnable);
1027 
1028             // This will get reset once we scroll into slots
1029             if (!mSuppressSelectionChanged) mSuppressSelectionChanged = true;
1030         }
1031 
1032         // Fling the gallery!
1033         mFlingRunnable.startUsingVelocity((int) -velocityX);
1034 
1035         return true;
1036     }
1037 
1038     @Override
onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)1039     public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
1040 
1041         if (localLOGV) Log.v(TAG, String.valueOf(e2.getX() - e1.getX()));
1042 
1043         /*
1044          * Now's a good time to tell our parent to stop intercepting our events!
1045          * The user has moved more than the slop amount, since GestureDetector
1046          * ensures this before calling this method. Also, if a parent is more
1047          * interested in this touch's events than we are, it would have
1048          * intercepted them by now (for example, we can assume when a Gallery is
1049          * in the ListView, a vertical scroll would not end up in this method
1050          * since a ListView would have intercepted it by now).
1051          */
1052         mParent.requestDisallowInterceptTouchEvent(true);
1053 
1054         // As the user scrolls, we want to callback selection changes so related-
1055         // info on the screen is up-to-date with the gallery's selection
1056         if (!mShouldCallbackDuringFling) {
1057             if (mIsFirstScroll) {
1058                 /*
1059                  * We're not notifying the client of selection changes during
1060                  * the fling, and this scroll could possibly be a fling. Don't
1061                  * do selection changes until we're sure it is not a fling.
1062                  */
1063                 if (!mSuppressSelectionChanged) mSuppressSelectionChanged = true;
1064                 postDelayed(mDisableSuppressSelectionChangedRunnable, SCROLL_TO_FLING_UNCERTAINTY_TIMEOUT);
1065             }
1066         } else {
1067             if (mSuppressSelectionChanged) mSuppressSelectionChanged = false;
1068         }
1069 
1070         // Track the motion
1071         trackMotionScroll(-1 * (int) distanceX);
1072 
1073         mIsFirstScroll = false;
1074         return true;
1075     }
1076 
1077     @Override
onDown(MotionEvent e)1078     public boolean onDown(MotionEvent e) {
1079 
1080         // Kill any existing fling/scroll
1081         mFlingRunnable.stop(false);
1082 
1083         // Get the item's view that was touched
1084         mDownTouchPosition = pointToPosition((int) e.getX(), (int) e.getY());
1085 
1086         if (mDownTouchPosition >= 0) {
1087             mDownTouchView = getChildAt(mDownTouchPosition - mFirstPosition);
1088             mDownTouchView.setPressed(true);
1089         }
1090 
1091         // Reset the multiple-scroll tracking state
1092         mIsFirstScroll = true;
1093 
1094         // Must return true to get matching events for this down event.
1095         return true;
1096     }
1097 
1098     /**
1099      * Called when a touch event's action is MotionEvent.ACTION_UP.
1100      */
onUp()1101     void onUp() {
1102 
1103         if (mFlingRunnable.mScroller.isFinished()) {
1104             scrollIntoSlots();
1105         }
1106 
1107         dispatchUnpress();
1108     }
1109 
1110     /**
1111      * Called when a touch event's action is MotionEvent.ACTION_CANCEL.
1112      */
onCancel()1113     void onCancel() {
1114         onUp();
1115     }
1116 
1117     @Override
onLongPress(@onNull MotionEvent e)1118     public void onLongPress(@NonNull MotionEvent e) {
1119         if (mDownTouchPosition < 0) {
1120             return;
1121         }
1122 
1123         performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
1124 
1125         final long id = getItemIdAtPosition(mDownTouchPosition);
1126         dispatchLongPress(mDownTouchView, mDownTouchPosition, id, e.getX(), e.getY(), true);
1127     }
1128 
1129     // Unused methods from GestureDetector.OnGestureListener below
1130 
1131     @Override
onShowPress(MotionEvent e)1132     public void onShowPress(MotionEvent e) {
1133     }
1134 
1135     // Unused methods from GestureDetector.OnGestureListener above
1136 
dispatchPress(View child)1137     private void dispatchPress(View child) {
1138 
1139         if (child != null) {
1140             child.setPressed(true);
1141         }
1142 
1143         setPressed(true);
1144     }
1145 
dispatchUnpress()1146     private void dispatchUnpress() {
1147 
1148         for (int i = getChildCount() - 1; i >= 0; i--) {
1149             getChildAt(i).setPressed(false);
1150         }
1151 
1152         setPressed(false);
1153     }
1154 
1155     @Override
dispatchSetSelected(boolean selected)1156     public void dispatchSetSelected(boolean selected) {
1157         /*
1158          * We don't want to pass the selected state given from its parent to its
1159          * children since this widget itself has a selected state to give to its
1160          * children.
1161          */
1162     }
1163 
1164     @Override
dispatchSetPressed(boolean pressed)1165     protected void dispatchSetPressed(boolean pressed) {
1166 
1167         // Show the pressed state on the selected child
1168         if (mSelectedChild != null) {
1169             mSelectedChild.setPressed(pressed);
1170         }
1171     }
1172 
1173     @Override
getContextMenuInfo()1174     protected ContextMenuInfo getContextMenuInfo() {
1175         return mContextMenuInfo;
1176     }
1177 
1178     @Override
showContextMenuForChild(View originalView)1179     public boolean showContextMenuForChild(View originalView) {
1180         if (isShowingContextMenuWithCoords()) {
1181             return false;
1182         }
1183         return showContextMenuForChildInternal(originalView, 0, 0, false);
1184     }
1185 
1186     @Override
showContextMenuForChild(View originalView, float x, float y)1187     public boolean showContextMenuForChild(View originalView, float x, float y) {
1188         return showContextMenuForChildInternal(originalView, x, y, true);
1189     }
1190 
showContextMenuForChildInternal(View originalView, float x, float y, boolean useOffsets)1191     private boolean showContextMenuForChildInternal(View originalView, float x, float y,
1192             boolean useOffsets) {
1193         final int longPressPosition = getPositionForView(originalView);
1194         if (longPressPosition < 0) {
1195             return false;
1196         }
1197 
1198         final long longPressId = mAdapter.getItemId(longPressPosition);
1199         return dispatchLongPress(originalView, longPressPosition, longPressId, x, y, useOffsets);
1200     }
1201 
1202     @Override
showContextMenu()1203     public boolean showContextMenu() {
1204         return showContextMenuInternal(0, 0, false);
1205     }
1206 
1207     @Override
showContextMenu(float x, float y)1208     public boolean showContextMenu(float x, float y) {
1209         return showContextMenuInternal(x, y, true);
1210     }
1211 
showContextMenuInternal(float x, float y, boolean useOffsets)1212     private boolean showContextMenuInternal(float x, float y, boolean useOffsets) {
1213         if (isPressed() && mSelectedPosition >= 0) {
1214             final int index = mSelectedPosition - mFirstPosition;
1215             final View v = getChildAt(index);
1216             return dispatchLongPress(v, mSelectedPosition, mSelectedRowId, x, y, useOffsets);
1217         }
1218 
1219         return false;
1220     }
1221 
dispatchLongPress(View view, int position, long id, float x, float y, boolean useOffsets)1222     private boolean dispatchLongPress(View view, int position, long id, float x, float y,
1223             boolean useOffsets) {
1224         boolean handled = false;
1225 
1226         if (mOnItemLongClickListener != null) {
1227             handled = mOnItemLongClickListener.onItemLongClick(this, mDownTouchView,
1228                     mDownTouchPosition, id);
1229         }
1230 
1231         if (!handled) {
1232             mContextMenuInfo = new AdapterContextMenuInfo(view, position, id);
1233 
1234             if (useOffsets) {
1235                 handled = super.showContextMenuForChild(view, x, y);
1236             } else {
1237                 handled = super.showContextMenuForChild(this);
1238             }
1239         }
1240 
1241         if (handled) {
1242             performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
1243         }
1244 
1245         return handled;
1246     }
1247 
1248     @Override
dispatchKeyEvent(KeyEvent event)1249     public boolean dispatchKeyEvent(KeyEvent event) {
1250         // Gallery steals all key events
1251         return event.dispatch(this, null, null);
1252     }
1253 
1254     /**
1255      * Handles left, right, and clicking
1256      * @see android.view.View#onKeyDown
1257      */
1258     @Override
onKeyDown(int keyCode, KeyEvent event)1259     public boolean onKeyDown(int keyCode, KeyEvent event) {
1260         switch (keyCode) {
1261 
1262         case KeyEvent.KEYCODE_DPAD_LEFT:
1263             if (moveDirection(-1)) {
1264                 playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT);
1265                 return true;
1266             }
1267             break;
1268         case KeyEvent.KEYCODE_DPAD_RIGHT:
1269             if (moveDirection(1)) {
1270                 playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT);
1271                 return true;
1272             }
1273             break;
1274         case KeyEvent.KEYCODE_DPAD_CENTER:
1275         case KeyEvent.KEYCODE_ENTER:
1276             mReceivedInvokeKeyDown = true;
1277             // fallthrough to default handling
1278         }
1279 
1280         return super.onKeyDown(keyCode, event);
1281     }
1282 
1283     @Override
onKeyUp(int keyCode, KeyEvent event)1284     public boolean onKeyUp(int keyCode, KeyEvent event) {
1285         if (KeyEvent.isConfirmKey(keyCode)) {
1286             if (mReceivedInvokeKeyDown) {
1287                 if (mItemCount > 0) {
1288                     dispatchPress(mSelectedChild);
1289                     postDelayed(new Runnable() {
1290                         @Override
1291                         public void run() {
1292                             dispatchUnpress();
1293                         }
1294                     }, ViewConfiguration.getPressedStateDuration());
1295 
1296                     int selectedIndex = mSelectedPosition - mFirstPosition;
1297                     performItemClick(getChildAt(selectedIndex), mSelectedPosition, mAdapter
1298                             .getItemId(mSelectedPosition));
1299                 }
1300             }
1301 
1302             // Clear the flag
1303             mReceivedInvokeKeyDown = false;
1304             return true;
1305         }
1306         return super.onKeyUp(keyCode, event);
1307     }
1308 
1309     @UnsupportedAppUsage
moveDirection(int direction)1310     boolean moveDirection(int direction) {
1311         direction = isLayoutRtl() ? -direction : direction;
1312         int targetPosition = mSelectedPosition + direction;
1313 
1314         if (mItemCount > 0 && targetPosition >= 0 && targetPosition < mItemCount) {
1315             scrollToChild(targetPosition - mFirstPosition);
1316             return true;
1317         } else {
1318             return false;
1319         }
1320     }
1321 
scrollToChild(int childPosition)1322     private boolean scrollToChild(int childPosition) {
1323         View child = getChildAt(childPosition);
1324 
1325         if (child != null) {
1326             int distance = getCenterOfGallery() - getCenterOfView(child);
1327             mFlingRunnable.startUsingDistance(distance);
1328             return true;
1329         }
1330 
1331         return false;
1332     }
1333 
1334     @Override
setSelectedPositionInt(int position)1335     void setSelectedPositionInt(int position) {
1336         super.setSelectedPositionInt(position);
1337 
1338         // Updates any metadata we keep about the selected item.
1339         updateSelectedItemMetadata();
1340     }
1341 
updateSelectedItemMetadata()1342     private void updateSelectedItemMetadata() {
1343 
1344         View oldSelectedChild = mSelectedChild;
1345 
1346         View child = mSelectedChild = getChildAt(mSelectedPosition - mFirstPosition);
1347         if (child == null) {
1348             return;
1349         }
1350 
1351         child.setSelected(true);
1352         child.setFocusable(true);
1353 
1354         if (hasFocus()) {
1355             child.requestFocus();
1356         }
1357 
1358         // We unfocus the old child down here so the above hasFocus check
1359         // returns true
1360         if (oldSelectedChild != null && oldSelectedChild != child) {
1361 
1362             // Make sure its drawable state doesn't contain 'selected'
1363             oldSelectedChild.setSelected(false);
1364 
1365             // Make sure it is not focusable anymore, since otherwise arrow keys
1366             // can make this one be focused
1367             oldSelectedChild.setFocusable(false);
1368         }
1369 
1370     }
1371 
1372     /**
1373      * Describes how the child views are aligned.
1374      * @param gravity
1375      *
1376      * @attr ref android.R.styleable#Gallery_gravity
1377      */
setGravity(int gravity)1378     public void setGravity(int gravity)
1379     {
1380         if (mGravity != gravity) {
1381             mGravity = gravity;
1382             requestLayout();
1383         }
1384     }
1385 
1386     @Override
getChildDrawingOrder(int childCount, int i)1387     protected int getChildDrawingOrder(int childCount, int i) {
1388         int selectedIndex = mSelectedPosition - mFirstPosition;
1389 
1390         // Just to be safe
1391         if (selectedIndex < 0) return i;
1392 
1393         if (i == childCount - 1) {
1394             // Draw the selected child last
1395             return selectedIndex;
1396         } else if (i >= selectedIndex) {
1397             // Move the children after the selected child earlier one
1398             return i + 1;
1399         } else {
1400             // Keep the children before the selected child the same
1401             return i;
1402         }
1403     }
1404 
1405     @Override
onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect)1406     protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
1407         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
1408 
1409         /*
1410          * The gallery shows focus by focusing the selected item. So, give
1411          * focus to our selected item instead. We steal keys from our
1412          * selected item elsewhere.
1413          */
1414         if (gainFocus && mSelectedChild != null) {
1415             mSelectedChild.requestFocus(direction);
1416             mSelectedChild.setSelected(true);
1417         }
1418 
1419     }
1420 
1421     @Override
getAccessibilityClassName()1422     public CharSequence getAccessibilityClassName() {
1423         return Gallery.class.getName();
1424     }
1425 
1426     /** @hide */
1427     @Override
onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info)1428     public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
1429         super.onInitializeAccessibilityNodeInfoInternal(info);
1430         info.setScrollable(mItemCount > 1);
1431         if (isEnabled()) {
1432             if (mItemCount > 0 && mSelectedPosition < mItemCount - 1) {
1433                 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
1434             }
1435             if (isEnabled() && mItemCount > 0 && mSelectedPosition > 0) {
1436                 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
1437             }
1438         }
1439     }
1440 
1441     /** @hide */
1442     @Override
performAccessibilityActionInternal(int action, Bundle arguments)1443     public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
1444         if (super.performAccessibilityActionInternal(action, arguments)) {
1445             return true;
1446         }
1447         switch (action) {
1448             case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
1449                 if (isEnabled() && mItemCount > 0 && mSelectedPosition < mItemCount - 1) {
1450                     final int currentChildIndex = mSelectedPosition - mFirstPosition;
1451                     return scrollToChild(currentChildIndex + 1);
1452                 }
1453             } return false;
1454             case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
1455                 if (isEnabled() && mItemCount > 0 && mSelectedPosition > 0) {
1456                     final int currentChildIndex = mSelectedPosition - mFirstPosition;
1457                     return scrollToChild(currentChildIndex - 1);
1458                 }
1459             } return false;
1460         }
1461         return false;
1462     }
1463 
1464     /**
1465      * Responsible for fling behavior. Use {@link #startUsingVelocity(int)} to
1466      * initiate a fling. Each frame of the fling is handled in {@link #run()}.
1467      * A FlingRunnable will keep re-posting itself until the fling is done.
1468      */
1469     private class FlingRunnable implements Runnable {
1470         /**
1471          * Tracks the decay of a fling scroll
1472          */
1473         private Scroller mScroller;
1474 
1475         /**
1476          * X value reported by mScroller on the previous fling
1477          */
1478         private int mLastFlingX;
1479 
FlingRunnable()1480         public FlingRunnable() {
1481             mScroller = new Scroller(getContext());
1482         }
1483 
startCommon()1484         private void startCommon() {
1485             // Remove any pending flings
1486             removeCallbacks(this);
1487         }
1488 
1489         @UnsupportedAppUsage
startUsingVelocity(int initialVelocity)1490         public void startUsingVelocity(int initialVelocity) {
1491             if (initialVelocity == 0) return;
1492 
1493             startCommon();
1494 
1495             int initialX = initialVelocity < 0 ? Integer.MAX_VALUE : 0;
1496             mLastFlingX = initialX;
1497             mScroller.fling(initialX, 0, initialVelocity, 0,
1498                     0, Integer.MAX_VALUE, 0, Integer.MAX_VALUE);
1499             post(this);
1500         }
1501 
1502         public void startUsingDistance(int distance) {
1503             if (distance == 0) return;
1504 
1505             startCommon();
1506 
1507             mLastFlingX = 0;
1508             mScroller.startScroll(0, 0, -distance, 0, mAnimationDuration);
1509             post(this);
1510         }
1511 
1512         public void stop(boolean scrollIntoSlots) {
1513             removeCallbacks(this);
1514             endFling(scrollIntoSlots);
1515         }
1516 
1517         private void endFling(boolean scrollIntoSlots) {
1518             /*
1519              * Force the scroller's status to finished (without setting its
1520              * position to the end)
1521              */
1522             mScroller.forceFinished(true);
1523 
1524             if (scrollIntoSlots) scrollIntoSlots();
1525         }
1526 
1527         @Override
1528         public void run() {
1529 
1530             if (mItemCount == 0) {
1531                 endFling(true);
1532                 return;
1533             }
1534 
1535             mShouldStopFling = false;
1536 
1537             final Scroller scroller = mScroller;
1538             boolean more = scroller.computeScrollOffset();
1539             final int x = scroller.getCurrX();
1540 
1541             // Flip sign to convert finger direction to list items direction
1542             // (e.g. finger moving down means list is moving towards the top)
1543             int delta = mLastFlingX - x;
1544 
1545             // Pretend that each frame of a fling scroll is a touch scroll
1546             if (delta > 0) {
1547                 // Moving towards the left. Use leftmost view as mDownTouchPosition
1548                 mDownTouchPosition = mIsRtl ? (mFirstPosition + getChildCount() - 1) :
1549                     mFirstPosition;
1550 
1551                 // Don't fling more than 1 screen
1552                 delta = Math.min(getWidth() - mPaddingLeft - mPaddingRight - 1, delta);
1553             } else {
1554                 // Moving towards the right. Use rightmost view as mDownTouchPosition
1555                 int offsetToLast = getChildCount() - 1;
1556                 mDownTouchPosition = mIsRtl ? mFirstPosition :
1557                     (mFirstPosition + getChildCount() - 1);
1558 
1559                 // Don't fling more than 1 screen
1560                 delta = Math.max(-(getWidth() - mPaddingRight - mPaddingLeft - 1), delta);
1561             }
1562 
1563             trackMotionScroll(delta);
1564 
1565             if (more && !mShouldStopFling) {
1566                 mLastFlingX = x;
1567                 post(this);
1568             } else {
1569                endFling(true);
1570             }
1571         }
1572 
1573     }
1574 
1575     /**
1576      * Gallery extends LayoutParams to provide a place to hold current
1577      * Transformation information along with previous position/transformation
1578      * info.
1579      */
1580     public static class LayoutParams extends ViewGroup.LayoutParams {
LayoutParams(Context c, AttributeSet attrs)1581         public LayoutParams(Context c, AttributeSet attrs) {
1582             super(c, attrs);
1583         }
1584 
LayoutParams(int w, int h)1585         public LayoutParams(int w, int h) {
1586             super(w, h);
1587         }
1588 
LayoutParams(ViewGroup.LayoutParams source)1589         public LayoutParams(ViewGroup.LayoutParams source) {
1590             super(source);
1591         }
1592     }
1593 }
1594