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