OSDN Git Service

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