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