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