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 android.support.v4.view.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         onScrollChanged(0, 0, 0, 0); // dummy values, View's implementation does not use these.
435 
436         invalidate();
437     }
438 
439     int getLimitedMotionScrollAmount(boolean motionToLeft, int deltaX) {
440         int extremeItemPosition = motionToLeft != mIsRtl ? mItemCount - 1 : 0;
441         View extremeChild = getChildAt(extremeItemPosition - mFirstPosition);
442 
443         if (extremeChild == null) {
444             return deltaX;
445         }
446 
447         int extremeChildCenter = getCenterOfView(extremeChild);
448         int galleryCenter = getCenterOfGallery();
449 
450         if (motionToLeft) {
451             if (extremeChildCenter <= galleryCenter) {
452 
453                 // The extreme child is past his boundary point!
454                 return 0;
455             }
456         } else {
457             if (extremeChildCenter >= galleryCenter) {
458 
459                 // The extreme child is past his boundary point!
460                 return 0;
461             }
462         }
463 
464         int centerDifference = galleryCenter - extremeChildCenter;
465 
466         return motionToLeft
467                 ? Math.max(centerDifference, deltaX)
468                 : Math.min(centerDifference, deltaX);
469     }
470 
471     /**
472      * Offset the horizontal location of all children of this view by the
473      * specified number of pixels.
474      *
475      * @param offset the number of pixels to offset
476      */
477     private void offsetChildrenLeftAndRight(int offset) {
478         for (int i = getChildCount() - 1; i >= 0; i--) {
479             getChildAt(i).offsetLeftAndRight(offset);
480         }
481     }
482 
483     /**
484      * @return The center of this Gallery.
485      */
486     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
getCenterOfGallery()487     private int getCenterOfGallery() {
488         return (getWidth() - mPaddingLeft - mPaddingRight) / 2 + mPaddingLeft;
489     }
490 
491     /**
492      * @return The center of the given view.
493      */
494     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
getCenterOfView(View view)495     private static int getCenterOfView(View view) {
496         return view.getLeft() + view.getWidth() / 2;
497     }
498 
499     /**
500      * Detaches children that are off the screen (i.e.: Gallery bounds).
501      *
502      * @param toLeft Whether to detach children to the left of the Gallery, or
503      *            to the right.
504      */
detachOffScreenChildren(boolean toLeft)505     private void detachOffScreenChildren(boolean toLeft) {
506         int numChildren = getChildCount();
507         int firstPosition = mFirstPosition;
508         int start = 0;
509         int count = 0;
510 
511         if (toLeft) {
512             final int galleryLeft = mPaddingLeft;
513             for (int i = 0; i < numChildren; i++) {
514                 int n = mIsRtl ? (numChildren - 1 - i) : i;
515                 final View child = getChildAt(n);
516                 if (child.getRight() >= galleryLeft) {
517                     break;
518                 } else {
519                     start = n;
520                     count++;
521                     mRecycler.put(firstPosition + n, child);
522                 }
523             }
524             if (!mIsRtl) {
525                 start = 0;
526             }
527         } else {
528             final int galleryRight = getWidth() - mPaddingRight;
529             for (int i = numChildren - 1; i >= 0; i--) {
530                 int n = mIsRtl ? numChildren - 1 - i : i;
531                 final View child = getChildAt(n);
532                 if (child.getLeft() <= galleryRight) {
533                     break;
534                 } else {
535                     start = n;
536                     count++;
537                     mRecycler.put(firstPosition + n, child);
538                 }
539             }
540             if (mIsRtl) {
541                 start = 0;
542             }
543         }
544 
545         detachViewsFromParent(start, count);
546 
547         if (toLeft != mIsRtl) {
548             mFirstPosition += count;
549         }
550     }
551 
552     /**
553      * Scrolls the items so that the selected item is in its 'slot' (its center
554      * is the gallery's center).
555      */
scrollIntoSlots()556     private void scrollIntoSlots() {
557 
558         if (getChildCount() == 0 || mSelectedChild == null) return;
559 
560         int selectedCenter = getCenterOfView(mSelectedChild);
561         int targetCenter = getCenterOfGallery();
562 
563         int scrollAmount = targetCenter - selectedCenter;
564         if (scrollAmount != 0) {
565             mFlingRunnable.startUsingDistance(scrollAmount);
566         } else {
567             onFinishedMovement();
568         }
569     }
570 
onFinishedMovement()571     private void onFinishedMovement() {
572         if (mSuppressSelectionChanged) {
573             mSuppressSelectionChanged = false;
574 
575             // We haven't been callbacking during the fling, so do it now
576             super.selectionChanged();
577         }
578         mSelectedCenterOffset = 0;
579         invalidate();
580     }
581 
582     @Override
selectionChanged()583     void selectionChanged() {
584         if (!mSuppressSelectionChanged) {
585             super.selectionChanged();
586         }
587     }
588 
589     /**
590      * Looks for the child that is closest to the center and sets it as the
591      * selected child.
592      */
setSelectionToCenterChild()593     private void setSelectionToCenterChild() {
594 
595         View selView = mSelectedChild;
596         if (mSelectedChild == null) return;
597 
598         int galleryCenter = getCenterOfGallery();
599 
600         // Common case where the current selected position is correct
601         if (selView.getLeft() <= galleryCenter && selView.getRight() >= galleryCenter) {
602             return;
603         }
604 
605         // TODO better search
606         int closestEdgeDistance = Integer.MAX_VALUE;
607         int newSelectedChildIndex = 0;
608         for (int i = getChildCount() - 1; i >= 0; i--) {
609 
610             View child = getChildAt(i);
611 
612             if (child.getLeft() <= galleryCenter && child.getRight() >=  galleryCenter) {
613                 // This child is in the center
614                 newSelectedChildIndex = i;
615                 break;
616             }
617 
618             int childClosestEdgeDistance = Math.min(Math.abs(child.getLeft() - galleryCenter),
619                     Math.abs(child.getRight() - galleryCenter));
620             if (childClosestEdgeDistance < closestEdgeDistance) {
621                 closestEdgeDistance = childClosestEdgeDistance;
622                 newSelectedChildIndex = i;
623             }
624         }
625 
626         int newPos = mFirstPosition + newSelectedChildIndex;
627 
628         if (newPos != mSelectedPosition) {
629             setSelectedPositionInt(newPos);
630             setNextSelectedPositionInt(newPos);
631             checkSelectionChanged();
632         }
633     }
634 
635     /**
636      * Creates and positions all views for this Gallery.
637      * <p>
638      * We layout rarely, most of the time {@link #trackMotionScroll(int)} takes
639      * care of repositioning, adding, and removing children.
640      *
641      * @param delta Change in the selected position. +1 means the selection is
642      *            moving to the right, so views are scrolling to the left. -1
643      *            means the selection is moving to the left.
644      */
645     @Override
layout(int delta, boolean animate)646     void layout(int delta, boolean animate) {
647 
648         mIsRtl = isLayoutRtl();
649 
650         int childrenLeft = mSpinnerPadding.left;
651         int childrenWidth = mRight - mLeft - mSpinnerPadding.left - mSpinnerPadding.right;
652 
653         if (mDataChanged) {
654             handleDataChanged();
655         }
656 
657         // Handle an empty gallery by removing all views.
658         if (mItemCount == 0) {
659             resetList();
660             return;
661         }
662 
663         // Update to the new selected position.
664         if (mNextSelectedPosition >= 0) {
665             setSelectedPositionInt(mNextSelectedPosition);
666         }
667 
668         // All views go in recycler while we are in layout
669         recycleAllViews();
670 
671         // Clear out old views
672         //removeAllViewsInLayout();
673         detachAllViewsFromParent();
674 
675         /*
676          * These will be used to give initial positions to views entering the
677          * gallery as we scroll
678          */
679         mRightMost = 0;
680         mLeftMost = 0;
681 
682         // Make selected view and center it
683 
684         /*
685          * mFirstPosition will be decreased as we add views to the left later
686          * on. The 0 for x will be offset in a couple lines down.
687          */
688         mFirstPosition = mSelectedPosition;
689         View sel = makeAndAddView(mSelectedPosition, 0, 0, true);
690 
691         // Put the selected child in the center
692         int selectedOffset = childrenLeft + (childrenWidth / 2) - (sel.getWidth() / 2) +
693                 mSelectedCenterOffset;
694         sel.offsetLeftAndRight(selectedOffset);
695 
696         fillToGalleryRight();
697         fillToGalleryLeft();
698 
699         // Flush any cached views that did not get reused above
700         mRecycler.clear();
701 
702         invalidate();
703         checkSelectionChanged();
704 
705         mDataChanged = false;
706         mNeedSync = false;
707         setNextSelectedPositionInt(mSelectedPosition);
708 
709         updateSelectedItemMetadata();
710     }
711 
712     @UnsupportedAppUsage
fillToGalleryLeft()713     private void fillToGalleryLeft() {
714         if (mIsRtl) {
715             fillToGalleryLeftRtl();
716         } else {
717             fillToGalleryLeftLtr();
718         }
719     }
720 
fillToGalleryLeftRtl()721     private void fillToGalleryLeftRtl() {
722         int itemSpacing = mSpacing;
723         int galleryLeft = mPaddingLeft;
724         int numChildren = getChildCount();
725         int numItems = mItemCount;
726 
727         // Set state for initial iteration
728         View prevIterationView = getChildAt(numChildren - 1);
729         int curPosition;
730         int curRightEdge;
731 
732         if (prevIterationView != null) {
733             curPosition = mFirstPosition + numChildren;
734             curRightEdge = prevIterationView.getLeft() - itemSpacing;
735         } else {
736             // No children available!
737             mFirstPosition = curPosition = mItemCount - 1;
738             curRightEdge = mRight - mLeft - mPaddingRight;
739             mShouldStopFling = true;
740         }
741 
742         while (curRightEdge > galleryLeft && curPosition < mItemCount) {
743             prevIterationView = makeAndAddView(curPosition, curPosition - mSelectedPosition,
744                     curRightEdge, false);
745 
746             // Set state for next iteration
747             curRightEdge = prevIterationView.getLeft() - itemSpacing;
748             curPosition++;
749         }
750     }
751 
fillToGalleryLeftLtr()752     private void fillToGalleryLeftLtr() {
753         int itemSpacing = mSpacing;
754         int galleryLeft = mPaddingLeft;
755 
756         // Set state for initial iteration
757         View prevIterationView = getChildAt(0);
758         int curPosition;
759         int curRightEdge;
760 
761         if (prevIterationView != null) {
762             curPosition = mFirstPosition - 1;
763             curRightEdge = prevIterationView.getLeft() - itemSpacing;
764         } else {
765             // No children available!
766             curPosition = 0;
767             curRightEdge = mRight - mLeft - mPaddingRight;
768             mShouldStopFling = true;
769         }
770 
771         while (curRightEdge > galleryLeft && curPosition >= 0) {
772             prevIterationView = makeAndAddView(curPosition, curPosition - mSelectedPosition,
773                     curRightEdge, false);
774 
775             // Remember some state
776             mFirstPosition = curPosition;
777 
778             // Set state for next iteration
779             curRightEdge = prevIterationView.getLeft() - itemSpacing;
780             curPosition--;
781         }
782     }
783 
784     @UnsupportedAppUsage
fillToGalleryRight()785     private void fillToGalleryRight() {
786         if (mIsRtl) {
787             fillToGalleryRightRtl();
788         } else {
789             fillToGalleryRightLtr();
790         }
791     }
792 
fillToGalleryRightRtl()793     private void fillToGalleryRightRtl() {
794         int itemSpacing = mSpacing;
795         int galleryRight = mRight - mLeft - mPaddingRight;
796 
797         // Set state for initial iteration
798         View prevIterationView = getChildAt(0);
799         int curPosition;
800         int curLeftEdge;
801 
802         if (prevIterationView != null) {
803             curPosition = mFirstPosition -1;
804             curLeftEdge = prevIterationView.getRight() + itemSpacing;
805         } else {
806             curPosition = 0;
807             curLeftEdge = mPaddingLeft;
808             mShouldStopFling = true;
809         }
810 
811         while (curLeftEdge < galleryRight && curPosition >= 0) {
812             prevIterationView = makeAndAddView(curPosition, curPosition - mSelectedPosition,
813                     curLeftEdge, true);
814 
815             // Remember some state
816             mFirstPosition = curPosition;
817 
818             // Set state for next iteration
819             curLeftEdge = prevIterationView.getRight() + itemSpacing;
820             curPosition--;
821         }
822     }
823 
fillToGalleryRightLtr()824     private void fillToGalleryRightLtr() {
825         int itemSpacing = mSpacing;
826         int galleryRight = mRight - mLeft - mPaddingRight;
827         int numChildren = getChildCount();
828         int numItems = mItemCount;
829 
830         // Set state for initial iteration
831         View prevIterationView = getChildAt(numChildren - 1);
832         int curPosition;
833         int curLeftEdge;
834 
835         if (prevIterationView != null) {
836             curPosition = mFirstPosition + numChildren;
837             curLeftEdge = prevIterationView.getRight() + itemSpacing;
838         } else {
839             mFirstPosition = curPosition = mItemCount - 1;
840             curLeftEdge = mPaddingLeft;
841             mShouldStopFling = true;
842         }
843 
844         while (curLeftEdge < galleryRight && curPosition < numItems) {
845             prevIterationView = makeAndAddView(curPosition, curPosition - mSelectedPosition,
846                     curLeftEdge, true);
847 
848             // Set state for next iteration
849             curLeftEdge = prevIterationView.getRight() + itemSpacing;
850             curPosition++;
851         }
852     }
853 
854     /**
855      * Obtain a view, either by pulling an existing view from the recycler or by
856      * getting a new one from the adapter. If we are animating, make sure there
857      * is enough information in the view's layout parameters to animate from the
858      * old to new positions.
859      *
860      * @param position Position in the gallery for the view to obtain
861      * @param offset Offset from the selected position
862      * @param x X-coordinate indicating where this view should be placed. This
863      *        will either be the left or right edge of the view, depending on
864      *        the fromLeft parameter
865      * @param fromLeft Are we positioning views based on the left edge? (i.e.,
866      *        building from left to right)?
867      * @return A view that has been added to the gallery
868      */
869     @UnsupportedAppUsage
makeAndAddView(int position, int offset, int x, boolean fromLeft)870     private View makeAndAddView(int position, int offset, int x, boolean fromLeft) {
871 
872         View child;
873         if (!mDataChanged) {
874             child = mRecycler.get(position);
875             if (child != null) {
876                 // Can reuse an existing view
877                 int childLeft = child.getLeft();
878 
879                 // Remember left and right edges of where views have been placed
880                 mRightMost = Math.max(mRightMost, childLeft
881                         + child.getMeasuredWidth());
882                 mLeftMost = Math.min(mLeftMost, childLeft);
883 
884                 // Position the view
885                 setUpChild(child, offset, x, fromLeft);
886 
887                 return child;
888             }
889         }
890 
891         // Nothing found in the recycler -- ask the adapter for a view
892         child = mAdapter.getView(position, null, this);
893 
894         // Position the view
895         setUpChild(child, offset, x, fromLeft);
896 
897         return child;
898     }
899 
900     /**
901      * Helper for makeAndAddView to set the position of a view and fill out its
902      * layout parameters.
903      *
904      * @param child The view to position
905      * @param offset Offset from the selected position
906      * @param x X-coordinate indicating where this view should be placed. This
907      *        will either be the left or right edge of the view, depending on
908      *        the fromLeft parameter
909      * @param fromLeft Are we positioning views based on the left edge? (i.e.,
910      *        building from left to right)?
911      */
setUpChild(View child, int offset, int x, boolean fromLeft)912     private void setUpChild(View child, int offset, int x, boolean fromLeft) {
913 
914         // Respect layout params that are already in the view. Otherwise
915         // make some up...
916         Gallery.LayoutParams lp = (Gallery.LayoutParams) child.getLayoutParams();
917         if (lp == null) {
918             lp = (Gallery.LayoutParams) generateDefaultLayoutParams();
919         }
920 
921         addViewInLayout(child, fromLeft != mIsRtl ? -1 : 0, lp, true);
922 
923         child.setSelected(offset == 0);
924 
925         // Get measure specs
926         int childHeightSpec = ViewGroup.getChildMeasureSpec(mHeightMeasureSpec,
927                 mSpinnerPadding.top + mSpinnerPadding.bottom, lp.height);
928         int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
929                 mSpinnerPadding.left + mSpinnerPadding.right, lp.width);
930 
931         // Measure child
932         child.measure(childWidthSpec, childHeightSpec);
933 
934         int childLeft;
935         int childRight;
936 
937         // Position vertically based on gravity setting
938         int childTop = calculateTop(child, true);
939         int childBottom = childTop + child.getMeasuredHeight();
940 
941         int width = child.getMeasuredWidth();
942         if (fromLeft) {
943             childLeft = x;
944             childRight = childLeft + width;
945         } else {
946             childLeft = x - width;
947             childRight = x;
948         }
949 
950         child.layout(childLeft, childTop, childRight, childBottom);
951     }
952 
953     /**
954      * Figure out vertical placement based on mGravity
955      *
956      * @param child Child to place
957      * @return Where the top of the child should be
958      */
calculateTop(View child, boolean duringLayout)959     private int calculateTop(View child, boolean duringLayout) {
960         int myHeight = duringLayout ? getMeasuredHeight() : getHeight();
961         int childHeight = duringLayout ? child.getMeasuredHeight() : child.getHeight();
962 
963         int childTop = 0;
964 
965         switch (mGravity) {
966         case Gravity.TOP:
967             childTop = mSpinnerPadding.top;
968             break;
969         case Gravity.CENTER_VERTICAL:
970             int availableSpace = myHeight - mSpinnerPadding.bottom
971                     - mSpinnerPadding.top - childHeight;
972             childTop = mSpinnerPadding.top + (availableSpace / 2);
973             break;
974         case Gravity.BOTTOM:
975             childTop = myHeight - mSpinnerPadding.bottom - childHeight;
976             break;
977         }
978         return childTop;
979     }
980 
981     @Override
onTouchEvent(MotionEvent event)982     public boolean onTouchEvent(MotionEvent event) {
983 
984         // Give everything to the gesture detector
985         boolean retValue = mGestureDetector.onTouchEvent(event);
986 
987         int action = event.getAction();
988         if (action == MotionEvent.ACTION_UP) {
989             // Helper method for lifted finger
990             onUp();
991         } else if (action == MotionEvent.ACTION_CANCEL) {
992             onCancel();
993         }
994 
995         return retValue;
996     }
997 
998     @Override
onSingleTapUp(MotionEvent e)999     public boolean onSingleTapUp(MotionEvent e) {
1000 
1001         if (mDownTouchPosition >= 0) {
1002 
1003             // An item tap should make it selected, so scroll to this child.
1004             scrollToChild(mDownTouchPosition - mFirstPosition);
1005 
1006             // Also pass the click so the client knows, if it wants to.
1007             if (mShouldCallbackOnUnselectedItemClick || mDownTouchPosition == mSelectedPosition) {
1008                 performItemClick(mDownTouchView, mDownTouchPosition, mAdapter
1009                         .getItemId(mDownTouchPosition));
1010             }
1011 
1012             return true;
1013         }
1014 
1015         return false;
1016     }
1017 
1018     @Override
onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)1019     public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
1020 
1021         if (!mShouldCallbackDuringFling) {
1022             // We want to suppress selection changes
1023 
1024             // Remove any future code to set mSuppressSelectionChanged = false
1025             removeCallbacks(mDisableSuppressSelectionChangedRunnable);
1026 
1027             // This will get reset once we scroll into slots
1028             if (!mSuppressSelectionChanged) mSuppressSelectionChanged = true;
1029         }
1030 
1031         // Fling the gallery!
1032         mFlingRunnable.startUsingVelocity((int) -velocityX);
1033 
1034         return true;
1035     }
1036 
1037     @Override
onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)1038     public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
1039 
1040         if (localLOGV) Log.v(TAG, String.valueOf(e2.getX() - e1.getX()));
1041 
1042         /*
1043          * Now's a good time to tell our parent to stop intercepting our events!
1044          * The user has moved more than the slop amount, since GestureDetector
1045          * ensures this before calling this method. Also, if a parent is more
1046          * interested in this touch's events than we are, it would have
1047          * intercepted them by now (for example, we can assume when a Gallery is
1048          * in the ListView, a vertical scroll would not end up in this method
1049          * since a ListView would have intercepted it by now).
1050          */
1051         mParent.requestDisallowInterceptTouchEvent(true);
1052 
1053         // As the user scrolls, we want to callback selection changes so related-
1054         // info on the screen is up-to-date with the gallery's selection
1055         if (!mShouldCallbackDuringFling) {
1056             if (mIsFirstScroll) {
1057                 /*
1058                  * We're not notifying the client of selection changes during
1059                  * the fling, and this scroll could possibly be a fling. Don't
1060                  * do selection changes until we're sure it is not a fling.
1061                  */
1062                 if (!mSuppressSelectionChanged) mSuppressSelectionChanged = true;
1063                 postDelayed(mDisableSuppressSelectionChangedRunnable, SCROLL_TO_FLING_UNCERTAINTY_TIMEOUT);
1064             }
1065         } else {
1066             if (mSuppressSelectionChanged) mSuppressSelectionChanged = false;
1067         }
1068 
1069         // Track the motion
1070         trackMotionScroll(-1 * (int) distanceX);
1071 
1072         mIsFirstScroll = false;
1073         return true;
1074     }
1075 
1076     @Override
onDown(MotionEvent e)1077     public boolean onDown(MotionEvent e) {
1078 
1079         // Kill any existing fling/scroll
1080         mFlingRunnable.stop(false);
1081 
1082         // Get the item's view that was touched
1083         mDownTouchPosition = pointToPosition((int) e.getX(), (int) e.getY());
1084 
1085         if (mDownTouchPosition >= 0) {
1086             mDownTouchView = getChildAt(mDownTouchPosition - mFirstPosition);
1087             mDownTouchView.setPressed(true);
1088         }
1089 
1090         // Reset the multiple-scroll tracking state
1091         mIsFirstScroll = true;
1092 
1093         // Must return true to get matching events for this down event.
1094         return true;
1095     }
1096 
1097     /**
1098      * Called when a touch event's action is MotionEvent.ACTION_UP.
1099      */
onUp()1100     void onUp() {
1101 
1102         if (mFlingRunnable.mScroller.isFinished()) {
1103             scrollIntoSlots();
1104         }
1105 
1106         dispatchUnpress();
1107     }
1108 
1109     /**
1110      * Called when a touch event's action is MotionEvent.ACTION_CANCEL.
1111      */
onCancel()1112     void onCancel() {
1113         onUp();
1114     }
1115 
1116     @Override
onLongPress(@onNull MotionEvent e)1117     public void onLongPress(@NonNull MotionEvent e) {
1118         if (mDownTouchPosition < 0) {
1119             return;
1120         }
1121 
1122         performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
1123 
1124         final long id = getItemIdAtPosition(mDownTouchPosition);
1125         dispatchLongPress(mDownTouchView, mDownTouchPosition, id, e.getX(), e.getY(), true);
1126     }
1127 
1128     // Unused methods from GestureDetector.OnGestureListener below
1129 
1130     @Override
onShowPress(MotionEvent e)1131     public void onShowPress(MotionEvent e) {
1132     }
1133 
1134     // Unused methods from GestureDetector.OnGestureListener above
1135 
dispatchPress(View child)1136     private void dispatchPress(View child) {
1137 
1138         if (child != null) {
1139             child.setPressed(true);
1140         }
1141 
1142         setPressed(true);
1143     }
1144 
dispatchUnpress()1145     private void dispatchUnpress() {
1146 
1147         for (int i = getChildCount() - 1; i >= 0; i--) {
1148             getChildAt(i).setPressed(false);
1149         }
1150 
1151         setPressed(false);
1152     }
1153 
1154     @Override
dispatchSetSelected(boolean selected)1155     public void dispatchSetSelected(boolean selected) {
1156         /*
1157          * We don't want to pass the selected state given from its parent to its
1158          * children since this widget itself has a selected state to give to its
1159          * children.
1160          */
1161     }
1162 
1163     @Override
dispatchSetPressed(boolean pressed)1164     protected void dispatchSetPressed(boolean pressed) {
1165 
1166         // Show the pressed state on the selected child
1167         if (mSelectedChild != null) {
1168             mSelectedChild.setPressed(pressed);
1169         }
1170     }
1171 
1172     @Override
getContextMenuInfo()1173     protected ContextMenuInfo getContextMenuInfo() {
1174         return mContextMenuInfo;
1175     }
1176 
1177     @Override
showContextMenuForChild(View originalView)1178     public boolean showContextMenuForChild(View originalView) {
1179         if (isShowingContextMenuWithCoords()) {
1180             return false;
1181         }
1182         return showContextMenuForChildInternal(originalView, 0, 0, false);
1183     }
1184 
1185     @Override
showContextMenuForChild(View originalView, float x, float y)1186     public boolean showContextMenuForChild(View originalView, float x, float y) {
1187         return showContextMenuForChildInternal(originalView, x, y, true);
1188     }
1189 
showContextMenuForChildInternal(View originalView, float x, float y, boolean useOffsets)1190     private boolean showContextMenuForChildInternal(View originalView, float x, float y,
1191             boolean useOffsets) {
1192         final int longPressPosition = getPositionForView(originalView);
1193         if (longPressPosition < 0) {
1194             return false;
1195         }
1196 
1197         final long longPressId = mAdapter.getItemId(longPressPosition);
1198         return dispatchLongPress(originalView, longPressPosition, longPressId, x, y, useOffsets);
1199     }
1200 
1201     @Override
showContextMenu()1202     public boolean showContextMenu() {
1203         return showContextMenuInternal(0, 0, false);
1204     }
1205 
1206     @Override
showContextMenu(float x, float y)1207     public boolean showContextMenu(float x, float y) {
1208         return showContextMenuInternal(x, y, true);
1209     }
1210 
showContextMenuInternal(float x, float y, boolean useOffsets)1211     private boolean showContextMenuInternal(float x, float y, boolean useOffsets) {
1212         if (isPressed() && mSelectedPosition >= 0) {
1213             final int index = mSelectedPosition - mFirstPosition;
1214             final View v = getChildAt(index);
1215             return dispatchLongPress(v, mSelectedPosition, mSelectedRowId, x, y, useOffsets);
1216         }
1217 
1218         return false;
1219     }
1220 
dispatchLongPress(View view, int position, long id, float x, float y, boolean useOffsets)1221     private boolean dispatchLongPress(View view, int position, long id, float x, float y,
1222             boolean useOffsets) {
1223         boolean handled = false;
1224 
1225         if (mOnItemLongClickListener != null) {
1226             handled = mOnItemLongClickListener.onItemLongClick(this, mDownTouchView,
1227                     mDownTouchPosition, id);
1228         }
1229 
1230         if (!handled) {
1231             mContextMenuInfo = new AdapterContextMenuInfo(view, position, id);
1232 
1233             if (useOffsets) {
1234                 handled = super.showContextMenuForChild(view, x, y);
1235             } else {
1236                 handled = super.showContextMenuForChild(this);
1237             }
1238         }
1239 
1240         if (handled) {
1241             performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
1242         }
1243 
1244         return handled;
1245     }
1246 
1247     @Override
dispatchKeyEvent(KeyEvent event)1248     public boolean dispatchKeyEvent(KeyEvent event) {
1249         // Gallery steals all key events
1250         return event.dispatch(this, null, null);
1251     }
1252 
1253     /**
1254      * Handles left, right, and clicking
1255      * @see android.view.View#onKeyDown
1256      */
1257     @Override
onKeyDown(int keyCode, KeyEvent event)1258     public boolean onKeyDown(int keyCode, KeyEvent event) {
1259         switch (keyCode) {
1260 
1261         case KeyEvent.KEYCODE_DPAD_LEFT:
1262             if (moveDirection(-1)) {
1263                 playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT);
1264                 return true;
1265             }
1266             break;
1267         case KeyEvent.KEYCODE_DPAD_RIGHT:
1268             if (moveDirection(1)) {
1269                 playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT);
1270                 return true;
1271             }
1272             break;
1273         case KeyEvent.KEYCODE_DPAD_CENTER:
1274         case KeyEvent.KEYCODE_ENTER:
1275             mReceivedInvokeKeyDown = true;
1276             // fallthrough to default handling
1277         }
1278 
1279         return super.onKeyDown(keyCode, event);
1280     }
1281 
1282     @Override
onKeyUp(int keyCode, KeyEvent event)1283     public boolean onKeyUp(int keyCode, KeyEvent event) {
1284         if (KeyEvent.isConfirmKey(keyCode)) {
1285             if (mReceivedInvokeKeyDown) {
1286                 if (mItemCount > 0) {
1287                     dispatchPress(mSelectedChild);
1288                     postDelayed(new Runnable() {
1289                         @Override
1290                         public void run() {
1291                             dispatchUnpress();
1292                         }
1293                     }, ViewConfiguration.getPressedStateDuration());
1294 
1295                     int selectedIndex = mSelectedPosition - mFirstPosition;
1296                     performItemClick(getChildAt(selectedIndex), mSelectedPosition, mAdapter
1297                             .getItemId(mSelectedPosition));
1298                 }
1299             }
1300 
1301             // Clear the flag
1302             mReceivedInvokeKeyDown = false;
1303             return true;
1304         }
1305         return super.onKeyUp(keyCode, event);
1306     }
1307 
1308     @UnsupportedAppUsage
moveDirection(int direction)1309     boolean moveDirection(int direction) {
1310         direction = isLayoutRtl() ? -direction : direction;
1311         int targetPosition = mSelectedPosition + direction;
1312 
1313         if (mItemCount > 0 && targetPosition >= 0 && targetPosition < mItemCount) {
1314             scrollToChild(targetPosition - mFirstPosition);
1315             return true;
1316         } else {
1317             return false;
1318         }
1319     }
1320 
scrollToChild(int childPosition)1321     private boolean scrollToChild(int childPosition) {
1322         View child = getChildAt(childPosition);
1323 
1324         if (child != null) {
1325             int distance = getCenterOfGallery() - getCenterOfView(child);
1326             mFlingRunnable.startUsingDistance(distance);
1327             return true;
1328         }
1329 
1330         return false;
1331     }
1332 
1333     @Override
setSelectedPositionInt(int position)1334     void setSelectedPositionInt(int position) {
1335         super.setSelectedPositionInt(position);
1336 
1337         // Updates any metadata we keep about the selected item.
1338         updateSelectedItemMetadata();
1339     }
1340 
updateSelectedItemMetadata()1341     private void updateSelectedItemMetadata() {
1342 
1343         View oldSelectedChild = mSelectedChild;
1344 
1345         View child = mSelectedChild = getChildAt(mSelectedPosition - mFirstPosition);
1346         if (child == null) {
1347             return;
1348         }
1349 
1350         child.setSelected(true);
1351         child.setFocusable(true);
1352 
1353         if (hasFocus()) {
1354             child.requestFocus();
1355         }
1356 
1357         // We unfocus the old child down here so the above hasFocus check
1358         // returns true
1359         if (oldSelectedChild != null && oldSelectedChild != child) {
1360 
1361             // Make sure its drawable state doesn't contain 'selected'
1362             oldSelectedChild.setSelected(false);
1363 
1364             // Make sure it is not focusable anymore, since otherwise arrow keys
1365             // can make this one be focused
1366             oldSelectedChild.setFocusable(false);
1367         }
1368 
1369     }
1370 
1371     /**
1372      * Describes how the child views are aligned.
1373      * @param gravity
1374      *
1375      * @attr ref android.R.styleable#Gallery_gravity
1376      */
setGravity(int gravity)1377     public void setGravity(int gravity)
1378     {
1379         if (mGravity != gravity) {
1380             mGravity = gravity;
1381             requestLayout();
1382         }
1383     }
1384 
1385     @Override
getChildDrawingOrder(int childCount, int i)1386     protected int getChildDrawingOrder(int childCount, int i) {
1387         int selectedIndex = mSelectedPosition - mFirstPosition;
1388 
1389         // Just to be safe
1390         if (selectedIndex < 0) return i;
1391 
1392         if (i == childCount - 1) {
1393             // Draw the selected child last
1394             return selectedIndex;
1395         } else if (i >= selectedIndex) {
1396             // Move the children after the selected child earlier one
1397             return i + 1;
1398         } else {
1399             // Keep the children before the selected child the same
1400             return i;
1401         }
1402     }
1403 
1404     @Override
onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect)1405     protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
1406         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
1407 
1408         /*
1409          * The gallery shows focus by focusing the selected item. So, give
1410          * focus to our selected item instead. We steal keys from our
1411          * selected item elsewhere.
1412          */
1413         if (gainFocus && mSelectedChild != null) {
1414             mSelectedChild.requestFocus(direction);
1415             mSelectedChild.setSelected(true);
1416         }
1417 
1418     }
1419 
1420     @Override
getAccessibilityClassName()1421     public CharSequence getAccessibilityClassName() {
1422         return Gallery.class.getName();
1423     }
1424 
1425     /** @hide */
1426     @Override
onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info)1427     public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
1428         super.onInitializeAccessibilityNodeInfoInternal(info);
1429         info.setScrollable(mItemCount > 1);
1430         if (isEnabled()) {
1431             if (mItemCount > 0 && mSelectedPosition < mItemCount - 1) {
1432                 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
1433             }
1434             if (isEnabled() && mItemCount > 0 && mSelectedPosition > 0) {
1435                 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
1436             }
1437         }
1438     }
1439 
1440     /** @hide */
1441     @Override
performAccessibilityActionInternal(int action, Bundle arguments)1442     public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
1443         if (super.performAccessibilityActionInternal(action, arguments)) {
1444             return true;
1445         }
1446         switch (action) {
1447             case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
1448                 if (isEnabled() && mItemCount > 0 && mSelectedPosition < mItemCount - 1) {
1449                     final int currentChildIndex = mSelectedPosition - mFirstPosition;
1450                     return scrollToChild(currentChildIndex + 1);
1451                 }
1452             } return false;
1453             case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
1454                 if (isEnabled() && mItemCount > 0 && mSelectedPosition > 0) {
1455                     final int currentChildIndex = mSelectedPosition - mFirstPosition;
1456                     return scrollToChild(currentChildIndex - 1);
1457                 }
1458             } return false;
1459         }
1460         return false;
1461     }
1462 
1463     /**
1464      * Responsible for fling behavior. Use {@link #startUsingVelocity(int)} to
1465      * initiate a fling. Each frame of the fling is handled in {@link #run()}.
1466      * A FlingRunnable will keep re-posting itself until the fling is done.
1467      */
1468     private class FlingRunnable implements Runnable {
1469         /**
1470          * Tracks the decay of a fling scroll
1471          */
1472         private Scroller mScroller;
1473 
1474         /**
1475          * X value reported by mScroller on the previous fling
1476          */
1477         private int mLastFlingX;
1478 
FlingRunnable()1479         public FlingRunnable() {
1480             mScroller = new Scroller(getContext());
1481         }
1482 
startCommon()1483         private void startCommon() {
1484             // Remove any pending flings
1485             removeCallbacks(this);
1486         }
1487 
1488         @UnsupportedAppUsage
startUsingVelocity(int initialVelocity)1489         public void startUsingVelocity(int initialVelocity) {
1490             if (initialVelocity == 0) return;
1491 
1492             startCommon();
1493 
1494             int initialX = initialVelocity < 0 ? Integer.MAX_VALUE : 0;
1495             mLastFlingX = initialX;
1496             mScroller.fling(initialX, 0, initialVelocity, 0,
1497                     0, Integer.MAX_VALUE, 0, Integer.MAX_VALUE);
1498             post(this);
1499         }
1500 
1501         public void startUsingDistance(int distance) {
1502             if (distance == 0) return;
1503 
1504             startCommon();
1505 
1506             mLastFlingX = 0;
1507             mScroller.startScroll(0, 0, -distance, 0, mAnimationDuration);
1508             post(this);
1509         }
1510 
1511         public void stop(boolean scrollIntoSlots) {
1512             removeCallbacks(this);
1513             endFling(scrollIntoSlots);
1514         }
1515 
1516         private void endFling(boolean scrollIntoSlots) {
1517             /*
1518              * Force the scroller's status to finished (without setting its
1519              * position to the end)
1520              */
1521             mScroller.forceFinished(true);
1522 
1523             if (scrollIntoSlots) scrollIntoSlots();
1524         }
1525 
1526         @Override
1527         public void run() {
1528 
1529             if (mItemCount == 0) {
1530                 endFling(true);
1531                 return;
1532             }
1533 
1534             mShouldStopFling = false;
1535 
1536             final Scroller scroller = mScroller;
1537             boolean more = scroller.computeScrollOffset();
1538             final int x = scroller.getCurrX();
1539 
1540             // Flip sign to convert finger direction to list items direction
1541             // (e.g. finger moving down means list is moving towards the top)
1542             int delta = mLastFlingX - x;
1543 
1544             // Pretend that each frame of a fling scroll is a touch scroll
1545             if (delta > 0) {
1546                 // Moving towards the left. Use leftmost view as mDownTouchPosition
1547                 mDownTouchPosition = mIsRtl ? (mFirstPosition + getChildCount() - 1) :
1548                     mFirstPosition;
1549 
1550                 // Don't fling more than 1 screen
1551                 delta = Math.min(getWidth() - mPaddingLeft - mPaddingRight - 1, delta);
1552             } else {
1553                 // Moving towards the right. Use rightmost view as mDownTouchPosition
1554                 int offsetToLast = getChildCount() - 1;
1555                 mDownTouchPosition = mIsRtl ? mFirstPosition :
1556                     (mFirstPosition + getChildCount() - 1);
1557 
1558                 // Don't fling more than 1 screen
1559                 delta = Math.max(-(getWidth() - mPaddingRight - mPaddingLeft - 1), delta);
1560             }
1561 
1562             trackMotionScroll(delta);
1563 
1564             if (more && !mShouldStopFling) {
1565                 mLastFlingX = x;
1566                 post(this);
1567             } else {
1568                endFling(true);
1569             }
1570         }
1571 
1572     }
1573 
1574     /**
1575      * Gallery extends LayoutParams to provide a place to hold current
1576      * Transformation information along with previous position/transformation
1577      * info.
1578      */
1579     public static class LayoutParams extends ViewGroup.LayoutParams {
LayoutParams(Context c, AttributeSet attrs)1580         public LayoutParams(Context c, AttributeSet attrs) {
1581             super(c, attrs);
1582         }
1583 
LayoutParams(int w, int h)1584         public LayoutParams(int w, int h) {
1585             super(w, h);
1586         }
1587 
LayoutParams(ViewGroup.LayoutParams source)1588         public LayoutParams(ViewGroup.LayoutParams source) {
1589             super(source);
1590         }
1591     }
1592 }
1593