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