/* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.aviary.android.feather.widget; import android.R; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Rect; import android.os.Handler; import android.os.Message; import android.os.Vibrator; import android.util.AttributeSet; import android.util.Log; import android.view.ContextMenu.ContextMenuInfo; import android.view.GestureDetector; import android.view.Gravity; import android.view.HapticFeedbackConstants; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.SoundEffectConstants; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.animation.Transformation; import com.aviary.android.feather.library.utils.ReflectionUtils; import com.aviary.android.feather.library.utils.ReflectionUtils.ReflectionException; import com.aviary.android.feather.widget.IFlingRunnable.FlingRunnableView; // TODO: Auto-generated Javadoc /** * A view that shows items in a center-locked, horizontally scrolling list. * <p> * The default values for the Gallery assume you will be using {@link android.R.styleable#Theme_galleryItemBackground} as the * background for each View given to the Gallery from the Adapter. If you are not doing this, you may need to adjust some Gallery * properties, such as the spacing. * <p> * Views given to the Gallery should use {@link Gallery.LayoutParams} as their layout parameters type. * * <p> * See the <a href="{@docRoot}resources/tutorials/views/hello-gallery.html">Gallery tutorial</a>. * </p> * * @attr ref android.R.styleable#Gallery_animationDuration * @attr ref android.R.styleable#Gallery_spacing * @attr ref android.R.styleable#Gallery_gravity */ public class Gallery extends AbsSpinner implements GestureDetector.OnGestureListener, FlingRunnableView, VibrationWidget { /** * The listener interface for receiving onItemsScroll events. The class that is interested in processing a onItemsScroll event * implements this interface, and the object created with that class is registered with a component using the component's * <code>addOnItemsScrollListener<code> method. When * the onItemsScroll event occurs, that object's appropriate * method is invoked. * * @see OnItemsScrollEvent */ public interface OnItemsScrollListener { /** * On scroll started. * * @param parent * the parent * @param view * the view * @param position * the position * @param id * the id */ void onScrollStarted( AdapterView<?> parent, View view, int position, long id ); /** * On scroll. * * @param parent * the parent * @param view * the view * @param position * the position * @param id * the id */ void onScroll( AdapterView<?> parent, View view, int position, long id ); /** * On scroll finished. * * @param parent * the parent * @param view * the view * @param position * the position * @param id * the id */ void onScrollFinished( AdapterView<?> parent, View view, int position, long id ); } /** The Constant TAG. */ private static final String TAG = "gallery"; /** The Constant MSG_VIBRATE. */ private static final int MSG_VIBRATE = 1; /** Vibration. */ Vibrator mVibrator; /** The m vibration handler. */ static Handler mVibrationHandler; /** set child selected automatically. */ private boolean mAutoSelectChild = false; /** The m items scroll listener. */ private OnItemsScrollListener mItemsScrollListener = null; /** * Duration in milliseconds from the start of a scroll during which we're unsure whether the user is scrolling or flinging. */ private static final int SCROLL_TO_FLING_UNCERTAINTY_TIMEOUT = 250; /** * Horizontal spacing between items. */ private int mSpacing = 0; /** * How long the transition animation should run when a child view changes position, measured in milliseconds. */ private int mAnimationDuration = 400; /** * The alpha of items that are not selected. */ private float mUnselectedAlpha; /** * Left most edge of a child seen so far during layout. */ private int mLeftMost; /** * Right most edge of a child seen so far during layout. */ private int mRightMost; /** The m gravity. */ private int mGravity; /** * Helper for detecting touch gestures. */ private GestureDetector mGestureDetector; /** * The position of the item that received the user's down touch. */ private int mDownTouchPosition; /** * The view of the item that received the user's down touch. */ private View mDownTouchView; /** * Executes the delta scrolls from a fling or scroll movement. */ private IFlingRunnable mFlingRunnable; /** The m auto scroll to center. */ private boolean mAutoScrollToCenter = true; /** The m touch slop. */ int mTouchSlop; /** * Sets mSuppressSelectionChanged = false. This is used to set it to false in the future. It will also trigger a selection * changed. */ private Runnable mDisableSuppressSelectionChangedRunnable = new Runnable() { @Override public void run() { mSuppressSelectionChanged = false; selectionChanged(); } }; /** * When fling runnable runs, it resets this to false. Any method along the path until the end of its run() can set this to true * to abort any remaining fling. For example, if we've reached either the leftmost or rightmost item, we will set this to true. */ @SuppressWarnings("unused") private boolean mShouldStopFling; /** * The currently selected item's child. */ private View mSelectedChild; /** * Whether to continuously callback on the item selected listener during a fling. */ private boolean mShouldCallbackDuringFling = false; /** * Whether to callback when an item that is not selected is clicked. */ private boolean mShouldCallbackOnUnselectedItemClick = true; /** * If true, do not callback to item selected listener. */ private boolean mSuppressSelectionChanged = true; /** * If true, we have received the "invoke" (center or enter buttons) key down. This is checked before we action on the "invoke" * key up, and is subsequently cleared. */ private boolean mReceivedInvokeKeyDown; /** The m context menu info. */ private AdapterContextMenuInfo mContextMenuInfo; /** * If true, this onScroll is the first for this user's drag (remember, a drag sends many onScrolls). */ private boolean mIsFirstScroll; /** * If true, mFirstPosition is the position of the rightmost child, and the children are ordered right to left. */ private boolean mIsRtl = true; /** The m last motion value. */ private int mLastMotionValue; /** * Instantiates a new gallery. * * @param context * the context */ public Gallery( Context context ) { this( context, null ); } /** * Instantiates a new gallery. * * @param context * the context * @param attrs * the attrs */ public Gallery( Context context, AttributeSet attrs ) { this( context, attrs, R.attr.galleryStyle ); } /** * Instantiates a new gallery. * * @param context * the context * @param attrs * the attrs * @param defStyle * the def style */ public Gallery( Context context, AttributeSet attrs, int defStyle ) { super( context, attrs, defStyle ); mGestureDetector = new GestureDetector( context, this ); mGestureDetector.setIsLongpressEnabled( false ); ViewConfiguration configuration = ViewConfiguration.get( context ); mTouchSlop = configuration.getScaledTouchSlop(); if ( android.os.Build.VERSION.SDK_INT > 8 ) { try { mFlingRunnable = (IFlingRunnable) ReflectionUtils.newInstance( "com.aviary.android.feather.widget.Fling9Runnable", new Class<?>[] { FlingRunnableView.class, int.class }, this, mAnimationDuration ); } catch ( ReflectionException e ) { mFlingRunnable = new Fling8Runnable( this, mAnimationDuration ); } } else { mFlingRunnable = new Fling8Runnable( this, mAnimationDuration ); } Log.d( VIEW_LOG_TAG, "fling class: " + mFlingRunnable.getClass().getName() ); try { mVibrator = (Vibrator) context.getSystemService( Context.VIBRATOR_SERVICE ); } catch ( Exception e ) { Log.e( TAG, e.toString() ); } if ( mVibrator != null ) { setVibrationEnabled( true ); } } @Override public synchronized void setVibrationEnabled( boolean value ) { if ( !value ) { mVibrationHandler = null; } else { if ( null == mVibrationHandler ) { mVibrationHandler = new Handler() { @Override public void handleMessage( Message msg ) { super.handleMessage( msg ); switch ( msg.what ) { case MSG_VIBRATE: try { mVibrator.vibrate( 10 ); } catch ( SecurityException e ) { // missing VIBRATE permission } } } }; } } } @Override @SuppressLint("HandlerLeak") public synchronized boolean getVibrationEnabled() { return mVibrationHandler != null; } /** * Sets the on items scroll listener. * * @param value * the new on items scroll listener */ public void setOnItemsScrollListener( OnItemsScrollListener value ) { mItemsScrollListener = value; } /** * Sets the auto scroll to center. * * @param value * the new auto scroll to center */ public void setAutoScrollToCenter( boolean value ) { mAutoScrollToCenter = value; } /** * Whether or not to callback on any {@link #getOnItemSelectedListener()} while the items are being flinged. If false, only the * final selected item will cause the callback. If true, all items between the first and the final will cause callbacks. * * @param shouldCallback * Whether or not to callback on the listener while the items are being flinged. */ public void setCallbackDuringFling( boolean shouldCallback ) { mShouldCallbackDuringFling = shouldCallback; } /* * (non-Javadoc) * * @see com.aviary.android.feather.widget.AdapterView#onDetachedFromWindow() */ @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); removeCallbacks( mScrollSelectionNotifier ); } /** * Whether or not to callback when an item that is not selected is clicked. If false, the item will become selected (and * re-centered). If true, the {@link #getOnItemClickListener()} will get the callback. * * @param shouldCallback * Whether or not to callback on the listener when a item that is not selected is clicked. * @hide */ public void setCallbackOnUnselectedItemClick( boolean shouldCallback ) { mShouldCallbackOnUnselectedItemClick = shouldCallback; } /** * Sets how long the transition animation should run when a child view changes position. Only relevant if animation is turned on. * * @param animationDurationMillis * The duration of the transition, in milliseconds. * * @attr ref android.R.styleable#Gallery_animationDuration */ public void setAnimationDuration( int animationDurationMillis ) { mAnimationDuration = animationDurationMillis; } /** * Sets the spacing between items in a Gallery. * * @param spacing * The spacing in pixels between items in the Gallery * @attr ref android.R.styleable#Gallery_spacing */ public void setSpacing( int spacing ) { mSpacing = spacing; } /** * Sets the alpha of items that are not selected in the Gallery. * * @param unselectedAlpha * the alpha for the items that are not selected. * * @attr ref android.R.styleable#Gallery_unselectedAlpha */ public void setUnselectedAlpha( float unselectedAlpha ) { mUnselectedAlpha = unselectedAlpha; } /* * (non-Javadoc) * * @see android.view.ViewGroup#getChildStaticTransformation(android.view.View, android.view.animation.Transformation) */ @Override protected boolean getChildStaticTransformation( View child, Transformation t ) { t.clear(); t.setAlpha( child == mSelectedChild ? 1.0f : mUnselectedAlpha ); return true; } /* * (non-Javadoc) * * @see android.view.View#computeHorizontalScrollExtent() */ @Override protected int computeHorizontalScrollExtent() { // Only 1 item is considered to be selected return 1; } /* * (non-Javadoc) * * @see android.view.View#computeHorizontalScrollOffset() */ @Override protected int computeHorizontalScrollOffset() { return mSelectedPosition; } /* * (non-Javadoc) * * @see android.view.View#computeHorizontalScrollRange() */ @Override protected int computeHorizontalScrollRange() { // Scroll range is the same as the item count return mItemCount; } /* * (non-Javadoc) * * @see android.view.ViewGroup#checkLayoutParams(android.view.ViewGroup.LayoutParams) */ @Override protected boolean checkLayoutParams( ViewGroup.LayoutParams p ) { return p instanceof LayoutParams; } /* * (non-Javadoc) * * @see android.view.ViewGroup#generateLayoutParams(android.view.ViewGroup.LayoutParams) */ @Override protected ViewGroup.LayoutParams generateLayoutParams( ViewGroup.LayoutParams p ) { return new LayoutParams( p ); } /* * (non-Javadoc) * * @see android.view.ViewGroup#generateLayoutParams(android.util.AttributeSet) */ @Override public ViewGroup.LayoutParams generateLayoutParams( AttributeSet attrs ) { return new LayoutParams( getContext(), attrs ); } /* * (non-Javadoc) * * @see com.aviary.android.feather.widget.AbsSpinner#generateDefaultLayoutParams() */ @Override protected ViewGroup.LayoutParams generateDefaultLayoutParams() { /* * Gallery expects Gallery.LayoutParams. */ return new Gallery.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT ); } /* * (non-Javadoc) * * @see com.aviary.android.feather.widget.AdapterView#onLayout(boolean, int, int, int, int) */ @Override protected void onLayout( boolean changed, int l, int t, int r, int b ) { super.onLayout( changed, l, t, r, b ); /* * Remember that we are in layout to prevent more layout request from being generated. */ mInLayout = true; layout( 0, false, changed ); mInLayout = false; } /* * (non-Javadoc) * * @see com.aviary.android.feather.widget.AbsSpinner#getChildHeight(android.view.View) */ @Override int getChildHeight( View child ) { return child.getMeasuredHeight(); } /** * Tracks a motion scroll. In reality, this is used to do just about any movement to items (touch scroll, arrow-key scroll, set * an item as selected). * * @param deltaX * Change in X from the previous event. */ @Override public void trackMotionScroll( int delta ) { int deltaX = mFlingRunnable.getLastFlingX() - delta; // Pretend that each frame of a fling scroll is a touch scroll if ( delta > 0 ) { mDownTouchPosition = mIsRtl ? ( mFirstPosition + getChildCount() - 1 ) : mFirstPosition; // Don't fling more than 1 screen delta = Math.min( getWidth() - mPaddingLeft - mPaddingRight - 1, delta ); } else { mDownTouchPosition = mIsRtl ? mFirstPosition : ( mFirstPosition + getChildCount() - 1 ); // Don't fling more than 1 screen delta = Math.max( -( getWidth() - mPaddingRight - mPaddingLeft - 1 ), delta ); } if ( getChildCount() == 0 ) { return; } boolean toLeft = deltaX < 0; int limitedDeltaX = deltaX; // getLimitedMotionScrollAmount(toLeft, deltaX); int realDeltaX = getLimitedMotionScrollAmount( toLeft, deltaX ); if ( realDeltaX != deltaX ) { // mFlingRunnable.springBack( realDeltaX ); limitedDeltaX = realDeltaX; } if ( limitedDeltaX != deltaX ) { mFlingRunnable.endFling( false ); if ( limitedDeltaX == 0 ) onFinishedMovement(); } offsetChildrenLeftAndRight( limitedDeltaX ); detachOffScreenChildren( toLeft ); if ( toLeft ) { // If moved left, there will be empty space on the right fillToGalleryRight(); } else { // Similarly, empty space on the left fillToGalleryLeft(); } // Clear unused views // mRecycler.clear(); // mRecyclerInvalidItems.clear(); setSelectionToCenterChild(); onScrollChanged( 0, 0, 0, 0 ); // dummy values, View's implementation does not use these. invalidate(); } /** * Gets the limited motion scroll amount. * * @param motionToLeft * the motion to left * @param deltaX * the delta x * @return the limited motion scroll amount */ int getLimitedMotionScrollAmount( boolean motionToLeft, int deltaX ) { int extremeItemPosition = motionToLeft != mIsRtl ? mItemCount - 1 : 0; View extremeChild = getChildAt( extremeItemPosition - mFirstPosition ); if ( extremeChild == null ) { return deltaX; } int extremeChildCenter = getCenterOfView( extremeChild ) + ( motionToLeft ? extremeChild.getWidth() / 2 : -extremeChild.getWidth() / 2 ); int galleryCenter = getCenterOfGallery(); if ( motionToLeft ) { if ( extremeChildCenter <= galleryCenter ) { // The extreme child is past his boundary point! return 0; } } else { if ( extremeChildCenter >= galleryCenter ) { // The extreme child is past his boundary point! return 0; } } int centerDifference = galleryCenter - extremeChildCenter; return motionToLeft ? Math.max( centerDifference, deltaX ) : Math.min( centerDifference, deltaX ); } /** * Gets the limited motion scroll amount2. * * @param motionToLeft * the motion to left * @param deltaX * the delta x * @return the limited motion scroll amount2 */ int getLimitedMotionScrollAmount2( boolean motionToLeft, int deltaX ) { int extremeItemPosition = motionToLeft != mIsRtl ? mItemCount - 1 : 0; View extremeChild = getChildAt( extremeItemPosition - mFirstPosition ); if ( extremeChild == null ) { return deltaX; } int extremeChildCenter = getCenterOfView( extremeChild ) + ( motionToLeft ? extremeChild.getWidth() / 2 : -extremeChild.getWidth() / 2 ); int galleryCenter = getCenterOfGallery(); int centerDifference = galleryCenter - extremeChildCenter; return motionToLeft ? Math.max( centerDifference, deltaX ) : Math.min( centerDifference, deltaX ); } /** * Gets the over scroll delta. * * @param motionToLeft * the motion to left * @param deltaX * the delta x * @return the over scroll delta */ int getOverScrollDelta( boolean motionToLeft, int deltaX ) { int extremeItemPosition = motionToLeft != mIsRtl ? mItemCount - 1 : 0; View extremeChild = getChildAt( extremeItemPosition - mFirstPosition ); if ( extremeChild == null ) { return 0; } int extremeChildCenter = getCenterOfView( extremeChild ); int galleryCenter = getCenterOfGallery(); if ( motionToLeft ) { if ( extremeChildCenter < galleryCenter ) { return extremeChildCenter - galleryCenter; } } else { if ( extremeChildCenter > galleryCenter ) { return galleryCenter - extremeChildCenter; } } return 0; } protected void onOverScrolled( int scrollX, int scrollY, boolean clampedX, boolean clampedY ) {} /** * Offset the horizontal location of all children of this view by the specified number of pixels. * * @param offset * the number of pixels to offset */ private void offsetChildrenLeftAndRight( int offset ) { for ( int i = getChildCount() - 1; i >= 0; i-- ) { getChildAt( i ).offsetLeftAndRight( offset ); } } /** * Gets the center of gallery. * * @return The center of this Gallery. */ private int getCenterOfGallery() { return ( getWidth() - mPaddingLeft - mPaddingRight ) / 2 + mPaddingLeft; } /** * Gets the center of view. * * @param view * the view * @return The center of the given view. */ private static int getCenterOfView( View view ) { return view.getLeft() + view.getWidth() / 2; } /** * Detaches children that are off the screen (i.e.: Gallery bounds). * * @param toLeft * Whether to detach children to the left of the Gallery, or to the right. */ private void detachOffScreenChildren( boolean toLeft ) { int numChildren = getChildCount(); int firstPosition = mFirstPosition; int start = 0; int count = 0; if ( toLeft ) { final int galleryLeft = mPaddingLeft; for ( int i = 0; i < numChildren; i++ ) { int n = mIsRtl ? ( numChildren - 1 - i ) : i; final View child = getChildAt( n ); if ( child.getRight() >= galleryLeft ) { break; } else { start = n; count++; int viewType = mAdapter.getItemViewType( firstPosition + n ); mRecycleBin.get( viewType ).add( child ); //if ( firstPosition + n < 0 ) { // mRecyclerInvalidItems.put( firstPosition + n, child ); //} else { // mRecycler.put( firstPosition + n, child ); //} } } if ( !mIsRtl ) { start = 0; } } else { final int galleryRight = getWidth() - mPaddingRight; for ( int i = numChildren - 1; i >= 0; i-- ) { int n = mIsRtl ? numChildren - 1 - i : i; final View child = getChildAt( n ); if ( child.getLeft() <= galleryRight ) { break; } else { start = n; count++; int viewType = mAdapter.getItemViewType( firstPosition + n ); mRecycleBin.get( viewType ).add( child ); //if ( firstPosition + n >= mItemCount ) { // mRecyclerInvalidItems.put( firstPosition + n, child ); //} else { // mRecycler.put( firstPosition + n, child ); //} } } if ( mIsRtl ) { start = 0; } } detachViewsFromParent( start, count ); if ( toLeft != mIsRtl ) { mFirstPosition += count; } } /** * Scrolls the items so that the selected item is in its 'slot' (its center is the gallery's center). */ @Override public void scrollIntoSlots() { if ( getChildCount() == 0 || mSelectedChild == null ) return; if ( mAutoScrollToCenter ) { int selectedCenter = getCenterOfView( mSelectedChild ); int targetCenter = getCenterOfGallery(); int scrollAmount = targetCenter - selectedCenter; if ( scrollAmount != 0 ) { mFlingRunnable.startUsingDistance( 0, -scrollAmount ); // fireVibration(); } else { onFinishedMovement(); } } else { onFinishedMovement(); } } /** * Scrolls the items so that the selected item is in its 'slot' (its center is the gallery's center). * * @return true, if is over scrolled */ private boolean isOverScrolled() { if ( getChildCount() < 2 || mSelectedChild == null ) return false; if ( mSelectedPosition == 0 || mSelectedPosition == mItemCount - 1 ) { int selectedCenter0 = getCenterOfView( mSelectedChild ); int targetCenter = getCenterOfGallery(); if ( mSelectedPosition == 0 && selectedCenter0 > targetCenter ) return true; if ( ( mSelectedPosition == mItemCount - 1 ) && selectedCenter0 < targetCenter ) return true; } return false; } /** * On finished movement. */ private void onFinishedMovement() { if ( isDown ) return; if ( mSuppressSelectionChanged ) { mSuppressSelectionChanged = false; // We haven't been callbacking during the fling, so do it now super.selectionChanged(); } scrollCompleted(); invalidate(); } /* * (non-Javadoc) * * @see com.aviary.android.feather.widget.AdapterView#selectionChanged() */ @Override void selectionChanged() { if ( !mSuppressSelectionChanged ) { super.selectionChanged(); } } /** * Looks for the child that is closest to the center and sets it as the selected child. */ private void setSelectionToCenterChild() { View selView = mSelectedChild; if ( mSelectedChild == null ) return; int galleryCenter = getCenterOfGallery(); // Common case where the current selected position is correct if ( selView.getLeft() <= galleryCenter && selView.getRight() >= galleryCenter ) { return; } // TODO better search int closestEdgeDistance = Integer.MAX_VALUE; int newSelectedChildIndex = 0; for ( int i = getChildCount() - 1; i >= 0; i-- ) { View child = getChildAt( i ); if ( child.getLeft() <= galleryCenter && child.getRight() >= galleryCenter ) { // This child is in the center newSelectedChildIndex = i; break; } int childClosestEdgeDistance = Math.min( Math.abs( child.getLeft() - galleryCenter ), Math.abs( child.getRight() - galleryCenter ) ); if ( childClosestEdgeDistance < closestEdgeDistance ) { closestEdgeDistance = childClosestEdgeDistance; newSelectedChildIndex = i; } } int newPos = mFirstPosition + newSelectedChildIndex; if ( newPos != mSelectedPosition ) { newPos = Math.min( Math.max( newPos, 0 ), mItemCount - 1 ); setSelectedPositionInt( newPos, true ); setNextSelectedPositionInt( newPos ); checkSelectionChanged(); } } /** * Creates and positions all views for this Gallery. * <p> * We layout rarely, most of the time {@link #trackMotionScroll(int)} takes care of repositioning, adding, and removing children. * * @param delta * Change in the selected position. +1 means the selection is moving to the right, so views are scrolling to the left. * -1 means the selection is moving to the left. * @param animate * the animate */ @Override void layout( int delta, boolean animate, boolean changed ) { mIsRtl = false; int childrenLeft = mSpinnerPadding.left; int childrenWidth = getRight() - getLeft() - mSpinnerPadding.left - mSpinnerPadding.right; if ( mDataChanged ) { handleDataChanged(); } // Handle an empty gallery by removing all views. if ( mItemCount == 0 ) { resetList(); return; } // Update to the new selected position. if ( mNextSelectedPosition >= 0 ) { setSelectedPositionInt( mNextSelectedPosition, animate ); } // All views go in recycler while we are in layout recycleAllViews(); // Clear out old views // removeAllViewsInLayout(); detachAllViewsFromParent(); /* * These will be used to give initial positions to views entering the gallery as we scroll */ mRightMost = 0; mLeftMost = 0; // Make selected view and center it /* * mFirstPosition will be decreased as we add views to the left later on. The 0 for x will be offset in a couple lines down. */ mFirstPosition = mSelectedPosition; View sel = makeAndAddView( mSelectedPosition, 0, 0, true ); // Put the selected child in the center int selectedOffset = childrenLeft + ( childrenWidth / 2 ) - ( sel.getWidth() / 2 ); sel.offsetLeftAndRight( selectedOffset ); fillToGalleryRight(); fillToGalleryLeft(); // Flush any cached views that did not get reused above //mRecycler.clear(); //mRecyclerInvalidItems.clear(); emptySubRecycler(); invalidate(); checkSelectionChanged(); mDataChanged = false; mNeedSync = false; setNextSelectedPositionInt( mSelectedPosition ); updateSelectedItemMetadata( animate, changed ); } /** * Fill to gallery left. */ private void fillToGalleryLeft() { if ( mIsRtl ) { fillToGalleryLeftRtl(); } else { fillToGalleryLeftLtr(); } } /** * Fill to gallery left rtl. */ private void fillToGalleryLeftRtl() { int itemSpacing = mSpacing; int galleryLeft = mPaddingLeft; int numChildren = getChildCount(); // Set state for initial iteration View prevIterationView = getChildAt( numChildren - 1 ); int curPosition; int curRightEdge; if ( prevIterationView != null ) { curPosition = mFirstPosition + numChildren; curRightEdge = prevIterationView.getLeft() - itemSpacing; } else { // No children available! mFirstPosition = curPosition = mItemCount - 1; curRightEdge = getRight() - getLeft() - mPaddingRight; mShouldStopFling = true; } while ( curRightEdge > galleryLeft && curPosition < mItemCount ) { prevIterationView = makeAndAddView( curPosition, curPosition - mSelectedPosition, curRightEdge, false ); // Set state for next iteration curRightEdge = prevIterationView.getLeft() - itemSpacing; curPosition++; } } /** * Fill to gallery left ltr. */ private void fillToGalleryLeftLtr() { int itemSpacing = mSpacing; int galleryLeft = mPaddingLeft; // Set state for initial iteration View prevIterationView = getChildAt( 0 ); int curPosition; int curRightEdge; if ( prevIterationView != null ) { curPosition = mFirstPosition - 1; curRightEdge = prevIterationView.getLeft() - itemSpacing; } else { // No children available! curPosition = 0; curRightEdge = getRight() - getLeft() - mPaddingRight; mShouldStopFling = true; } while ( curRightEdge > galleryLeft /* && curPosition >= 0 */) { prevIterationView = makeAndAddView( curPosition, curPosition - mSelectedPosition, curRightEdge, false ); // Remember some state mFirstPosition = curPosition; // Set state for next iteration curRightEdge = prevIterationView.getLeft() - itemSpacing; curPosition--; } } /** * Fill to gallery right. */ private void fillToGalleryRight() { if ( mIsRtl ) { fillToGalleryRightRtl(); } else { fillToGalleryRightLtr(); } } /** * Fill to gallery right rtl. */ private void fillToGalleryRightRtl() { int itemSpacing = mSpacing; int galleryRight = getRight() - getLeft() - mPaddingRight; // Set state for initial iteration View prevIterationView = getChildAt( 0 ); int curPosition; int curLeftEdge; if ( prevIterationView != null ) { curPosition = mFirstPosition - 1; curLeftEdge = prevIterationView.getRight() + itemSpacing; } else { curPosition = 0; curLeftEdge = mPaddingLeft; mShouldStopFling = true; } while ( curLeftEdge < galleryRight && curPosition >= 0 ) { prevIterationView = makeAndAddView( curPosition, curPosition - mSelectedPosition, curLeftEdge, true ); // Remember some state mFirstPosition = curPosition; // Set state for next iteration curLeftEdge = prevIterationView.getRight() + itemSpacing; curPosition--; } } /** * Fill to gallery right ltr. */ private void fillToGalleryRightLtr() { int itemSpacing = mSpacing; int galleryRight = getRight() - getLeft() - mPaddingRight; int numChildren = getChildCount(); // Set state for initial iteration View prevIterationView = getChildAt( numChildren - 1 ); int curPosition; int curLeftEdge; if ( prevIterationView != null ) { curPosition = mFirstPosition + numChildren; curLeftEdge = prevIterationView.getRight() + itemSpacing; } else { mFirstPosition = curPosition = mItemCount - 1; curLeftEdge = mPaddingLeft; mShouldStopFling = true; } while ( curLeftEdge < galleryRight /* && curPosition < numItems */) { prevIterationView = makeAndAddView( curPosition, curPosition - mSelectedPosition, curLeftEdge, true ); // Set state for next iteration curLeftEdge = prevIterationView.getRight() + itemSpacing; curPosition++; } } /** * Obtain a view, either by pulling an existing view from the recycler or by getting a new one from the adapter. If we are * animating, make sure there is enough information in the view's layout parameters to animate from the old to new positions. * * @param position * Position in the gallery for the view to obtain * @param offset * Offset from the selected position * @param x * X-coordinate indicating where this view should be placed. This will either be the left or right edge of the view, * depending on the fromLeft parameter * @param fromLeft * Are we positioning views based on the left edge? (i.e., building from left to right)? * @return A view that has been added to the gallery */ private View makeAndAddView( int position, int offset, int x, boolean fromLeft ) { View child; int viewType = mAdapter.getItemViewType( position ); if ( !mDataChanged ) { child = mRecycleBin.get( viewType ).poll(); /* if ( valid ) { child = mRecycler.get( position ); } else { child = mRecyclerInvalidItems.get( position ); } */ if ( child != null ) { // Can reuse an existing view child = mAdapter.getView( position, child, this ); int childLeft = child.getLeft(); // Remember left and right edges of where views have been placed mRightMost = Math.max( mRightMost, childLeft + child.getMeasuredWidth() ); mLeftMost = Math.min( mLeftMost, childLeft ); // Position the view setUpChild( child, offset, x, fromLeft ); return child; } } // Nothing found in the recycler -- ask the adapter for a view child = mAdapter.getView( position, null, this ); // Position the view setUpChild( child, offset, x, fromLeft ); return child; } public void invalidateViews() { int count = getChildCount(); for ( int i = 0; i < count; i++ ) { View child = getChildAt( i ); mAdapter.getView( mFirstPosition + i, child, this ); } } /** * Helper for makeAndAddView to set the position of a view and fill out its layout parameters. * * @param child * The view to position * @param offset * Offset from the selected position * @param x * X-coordinate indicating where this view should be placed. This will either be the left or right edge of the view, * depending on the fromLeft parameter * @param fromLeft * Are we positioning views based on the left edge? (i.e., building from left to right)? */ private void setUpChild( View child, int offset, int x, boolean fromLeft ) { // Respect layout params that are already in the view. Otherwise // make some up... Gallery.LayoutParams lp = (Gallery.LayoutParams) child.getLayoutParams(); if ( lp == null ) { lp = (Gallery.LayoutParams) generateDefaultLayoutParams(); } addViewInLayout( child, fromLeft != mIsRtl ? -1 : 0, lp ); if ( mAutoSelectChild ) child.setSelected( offset == 0 ); // Get measure specs int childHeightSpec = ViewGroup.getChildMeasureSpec( mHeightMeasureSpec, mSpinnerPadding.top + mSpinnerPadding.bottom, lp.height ); int childWidthSpec = ViewGroup .getChildMeasureSpec( mWidthMeasureSpec, mSpinnerPadding.left + mSpinnerPadding.right, lp.width ); // Measure child child.measure( childWidthSpec, childHeightSpec ); int childLeft; int childRight; // Position vertically based on gravity setting int childTop = calculateTop( child, true ); int childBottom = childTop + child.getMeasuredHeight(); int width = child.getMeasuredWidth(); if ( fromLeft ) { childLeft = x; childRight = childLeft + width; } else { childLeft = x - width; childRight = x; } child.layout( childLeft, childTop, childRight, childBottom ); } /** * Figure out vertical placement based on mGravity. * * @param child * Child to place * @param duringLayout * the during layout * @return Where the top of the child should be */ private int calculateTop( View child, boolean duringLayout ) { int myHeight = duringLayout ? getMeasuredHeight() : getHeight(); int childHeight = duringLayout ? child.getMeasuredHeight() : child.getHeight(); int childTop = 0; switch ( mGravity ) { case Gravity.TOP: childTop = mSpinnerPadding.top; break; case Gravity.CENTER_VERTICAL: int availableSpace = myHeight - mSpinnerPadding.bottom - mSpinnerPadding.top - childHeight; childTop = mSpinnerPadding.top + ( availableSpace / 2 ); break; case Gravity.BOTTOM: childTop = myHeight - mSpinnerPadding.bottom - childHeight; break; } return childTop; } /* * (non-Javadoc) * * @see android.view.View#onTouchEvent(android.view.MotionEvent) */ @Override public boolean onTouchEvent( MotionEvent event ) { // Give everything to the gesture detector boolean retValue = mGestureDetector.onTouchEvent( event ); int action = event.getAction(); if ( action == MotionEvent.ACTION_UP ) { // Helper method for lifted finger onUp(); } else if ( action == MotionEvent.ACTION_CANCEL ) { onCancel(); } return retValue; } /* * (non-Javadoc) * * @see android.view.GestureDetector.OnGestureListener#onSingleTapUp(android.view.MotionEvent) */ @Override public boolean onSingleTapUp( MotionEvent e ) { if ( mDownTouchPosition >= 0 && mDownTouchPosition < mItemCount ) { // An item tap should make it selected, so scroll to this child. scrollToChild( mDownTouchPosition - mFirstPosition ); // Also pass the click so the client knows, if it wants to. if ( mShouldCallbackOnUnselectedItemClick || mDownTouchPosition == mSelectedPosition ) { performItemClick( mDownTouchView, mDownTouchPosition, mAdapter.getItemId( mDownTouchPosition ) ); } return true; } return false; } /* * (non-Javadoc) * * @see android.view.GestureDetector.OnGestureListener#onFling(android.view.MotionEvent, android.view.MotionEvent, float, float) */ @Override public boolean onFling( MotionEvent e1, MotionEvent e2, float velocityX, float velocityY ) { if ( !mShouldCallbackDuringFling ) { // We want to suppress selection changes // Remove any future code to set mSuppressSelectionChanged = false removeCallbacks( mDisableSuppressSelectionChangedRunnable ); // This will get reset once we scroll into slots if ( !mSuppressSelectionChanged ) mSuppressSelectionChanged = true; } // Fling the gallery! int initialVelocity = (int) -velocityX / 2; int initialX = initialVelocity < 0 ? Integer.MAX_VALUE : 0; mFlingRunnable.startUsingVelocity( initialX, initialVelocity ); return true; } /* * (non-Javadoc) * * @see android.view.GestureDetector.OnGestureListener#onScroll(android.view.MotionEvent, android.view.MotionEvent, float, float) */ @Override public boolean onScroll( MotionEvent e1, MotionEvent e2, float distanceX, float distanceY ) { /* * Now's a good time to tell our parent to stop intercepting our events! The user has moved more than the slop amount, since * GestureDetector ensures this before calling this method. Also, if a parent is more interested in this touch's events than * we are, it would have intercepted them by now (for example, we can assume when a Gallery is in the ListView, a vertical * scroll would not end up in this method since a ListView would have intercepted it by now). */ getParent().requestDisallowInterceptTouchEvent( true ); // As the user scrolls, we want to callback selection changes so related- // info on the screen is up-to-date with the gallery's selection if ( !mShouldCallbackDuringFling ) { if ( mIsFirstScroll ) { if ( !mSuppressSelectionChanged ) mSuppressSelectionChanged = true; postDelayed( mDisableSuppressSelectionChangedRunnable, SCROLL_TO_FLING_UNCERTAINTY_TIMEOUT ); if ( mItemsScrollListener != null ) { int selection = this.getSelectedItemPosition(); if ( selection >= 0 ) { View v = getSelectedView(); mItemsScrollListener.onScrollStarted( this, v, selection, getAdapter().getItemId( selection ) ); } } } } else { if ( mSuppressSelectionChanged ) mSuppressSelectionChanged = false; } // Track the motion if ( mIsFirstScroll ) { if ( distanceX > 0 ) distanceX -= mTouchSlop; else distanceX += mTouchSlop; } int delta = -1 * (int) distanceX; float limitedDelta = getOverScrollDelta( delta < 0, delta ); if ( limitedDelta != 0 ) { delta = (int) ( delta / Math.max( 1, Math.abs( limitedDelta / 10 ) ) ); } trackMotionScroll( -delta ); mIsFirstScroll = false; return true; } /** The is down. */ private boolean isDown; /* * (non-Javadoc) * * @see android.view.GestureDetector.OnGestureListener#onDown(android.view.MotionEvent) */ @Override public boolean onDown( MotionEvent e ) { isDown = true; // Kill any existing fling/scroll mFlingRunnable.stop( false ); // Get the item's view that was touched mDownTouchPosition = pointToPosition( (int) e.getX(), (int) e.getY() ); if ( mDownTouchPosition >= 0 && mDownTouchPosition < mItemCount ) { mDownTouchView = getChildAt( mDownTouchPosition - mFirstPosition ); mDownTouchView.setPressed( true ); } // Reset the multiple-scroll tracking state mIsFirstScroll = true; // Must return true to get matching events for this down event. return true; } /** * Called when a touch event's action is MotionEvent.ACTION_UP. */ void onUp() { isDown = false; if ( mFlingRunnable.isFinished() ) { scrollIntoSlots(); } else { if ( isOverScrolled() ) { scrollIntoSlots(); } } dispatchUnpress(); } /** * Called when a touch event's action is MotionEvent.ACTION_CANCEL. */ void onCancel() { onUp(); } /* * (non-Javadoc) * * @see android.view.GestureDetector.OnGestureListener#onLongPress(android.view.MotionEvent) */ @Override public void onLongPress( MotionEvent e ) { if ( mDownTouchPosition < 0 || mDownTouchPosition <= mItemCount ) { return; } performHapticFeedback( HapticFeedbackConstants.LONG_PRESS ); long id = getItemIdAtPosition( mDownTouchPosition ); dispatchLongPress( mDownTouchView, mDownTouchPosition, id ); } // Unused methods from GestureDetector.OnGestureListener below /* * (non-Javadoc) * * @see android.view.GestureDetector.OnGestureListener#onShowPress(android.view.MotionEvent) */ @Override public void onShowPress( MotionEvent e ) {} // Unused methods from GestureDetector.OnGestureListener above /** * Dispatch press. * * @param child * the child */ private void dispatchPress( View child ) { if ( child != null ) { child.setPressed( true ); } setPressed( true ); } /** * Dispatch unpress. */ private void dispatchUnpress() { for ( int i = getChildCount() - 1; i >= 0; i-- ) { getChildAt( i ).setPressed( false ); } setPressed( false ); } /* * (non-Javadoc) * * @see android.view.ViewGroup#dispatchSetSelected(boolean) */ @Override public void dispatchSetSelected( boolean selected ) { /* * We don't want to pass the selected state given from its parent to its children since this widget itself has a selected * state to give to its children. */ } /* * (non-Javadoc) * * @see android.view.ViewGroup#dispatchSetPressed(boolean) */ @Override protected void dispatchSetPressed( boolean pressed ) { // Show the pressed state on the selected child if ( mSelectedChild != null ) { mSelectedChild.setPressed( pressed ); } } /* * (non-Javadoc) * * @see android.view.View#getContextMenuInfo() */ @Override protected ContextMenuInfo getContextMenuInfo() { return mContextMenuInfo; } /* * (non-Javadoc) * * @see android.view.ViewGroup#showContextMenuForChild(android.view.View) */ @Override public boolean showContextMenuForChild( View originalView ) { final int longPressPosition = getPositionForView( originalView ); if ( longPressPosition < 0 ) { return false; } final long longPressId = mAdapter.getItemId( longPressPosition ); return dispatchLongPress( originalView, longPressPosition, longPressId ); } /* * (non-Javadoc) * * @see android.view.View#showContextMenu() */ @Override public boolean showContextMenu() { if ( isPressed() && mSelectedPosition >= 0 ) { int index = mSelectedPosition - mFirstPosition; View v = getChildAt( index ); return dispatchLongPress( v, mSelectedPosition, mSelectedRowId ); } return false; } /** * Dispatch long press. * * @param view * the view * @param position * the position * @param id * the id * @return true, if successful */ private boolean dispatchLongPress( View view, int position, long id ) { boolean handled = false; if ( mOnItemLongClickListener != null ) { handled = mOnItemLongClickListener.onItemLongClick( this, mDownTouchView, mDownTouchPosition, id ); } if ( !handled ) { mContextMenuInfo = new AdapterContextMenuInfo( view, position, id ); handled = super.showContextMenuForChild( this ); } if ( handled ) { performHapticFeedback( HapticFeedbackConstants.LONG_PRESS ); } return handled; } /** * Handles left, right, and clicking. * * @param keyCode * the key code * @param event * the event * @return true, if successful * @see android.view.View#onKeyDown */ @Override public boolean onKeyDown( int keyCode, KeyEvent event ) { switch ( keyCode ) { case KeyEvent.KEYCODE_DPAD_LEFT: if ( movePrevious() ) { playSoundEffect( SoundEffectConstants.NAVIGATION_LEFT ); } return true; case KeyEvent.KEYCODE_DPAD_RIGHT: if ( moveNext() ) { playSoundEffect( SoundEffectConstants.NAVIGATION_RIGHT ); } return true; case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_ENTER: mReceivedInvokeKeyDown = true; // fallthrough to default handling } return false; } @Override public boolean dispatchKeyEvent( KeyEvent event ) { boolean handled = event.dispatch( this, null, null ); return handled; } /* * (non-Javadoc) * * @see android.view.View#onKeyUp(int, android.view.KeyEvent) */ @Override public boolean onKeyUp( int keyCode, KeyEvent event ) { switch ( keyCode ) { case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_ENTER: { if ( mReceivedInvokeKeyDown ) { if ( mItemCount > 0 ) { dispatchPress( mSelectedChild ); postDelayed( new Runnable() { @Override public void run() { dispatchUnpress(); } }, ViewConfiguration.getPressedStateDuration() ); int selectedIndex = mSelectedPosition - mFirstPosition; performItemClick( getChildAt( selectedIndex ), mSelectedPosition, mAdapter.getItemId( mSelectedPosition ) ); } } // Clear the flag mReceivedInvokeKeyDown = false; return true; } } return false; } /** * Move previous. * * @return true, if successful */ boolean movePrevious() { if ( mItemCount > 0 && mSelectedPosition > 0 ) { scrollToChild( mSelectedPosition - mFirstPosition - 1 ); return true; } else { return false; } } /** * Move next. * * @return true, if successful */ boolean moveNext() { if ( mItemCount > 0 && mSelectedPosition < mItemCount - 1 ) { scrollToChild( mSelectedPosition - mFirstPosition + 1 ); return true; } else { return false; } } /** * Scroll to child. * * @param childPosition * the child position * @return true, if successful */ private boolean scrollToChild( int childPosition ) { View child = getChildAt( childPosition ); if ( child != null ) { if ( mItemsScrollListener != null ) { int selection = this.getSelectedItemPosition(); if ( selection >= 0 ) { View v = getSelectedView(); mItemsScrollListener.onScrollStarted( this, v, selection, getAdapter().getItemId( selection ) ); } } int distance = getCenterOfGallery() - getCenterOfView( child ); mFlingRunnable.startUsingDistance( 0, -distance ); return true; } return false; } /* * (non-Javadoc) * * @see com.aviary.android.feather.widget.AdapterView#setSelectedPositionInt(int) */ void setSelectedPositionInt( int position, boolean animate ) { super.setSelectedPositionInt( position ); updateSelectedItemMetadata( animate, false ); } /** * Update selected item metadata. */ private void updateSelectedItemMetadata( boolean animate, boolean changed ) { View oldSelectedChild = mSelectedChild; View child = mSelectedChild = getChildAt( mSelectedPosition - mFirstPosition ); if ( child == null ) { return; } if ( mAutoSelectChild ) child.setSelected( true ); if ( mItemsScrollListener != null ) { mItemsScrollListener.onScroll( this, child, mSelectedPosition, getAdapter().getItemId( mSelectedPosition ) ); } // fire vibration if ( mSelectedPosition != mLastMotionValue && animate ) { fireVibration(); } mLastMotionValue = mSelectedPosition; child.setFocusable( true ); if ( hasFocus() ) { child.requestFocus(); } // We unfocus the old child down here so the above hasFocus check // returns true if ( oldSelectedChild != null && oldSelectedChild != child ) { // Make sure its drawable state doesn't contain 'selected' if ( mAutoSelectChild ) oldSelectedChild.setSelected( false ); // Make sure it is not focusable anymore, since otherwise arrow keys // can make this one be focused oldSelectedChild.setFocusable( false ); if ( !animate && changed ) scrollCompleted(); } } /** * Fire vibration. */ private void fireVibration() { if ( mVibrationHandler != null ) { mVibrationHandler.sendEmptyMessage( MSG_VIBRATE ); } } /** * Describes how the child views are aligned. * * @param gravity * the new gravity * @attr ref android.R.styleable#Gallery_gravity */ public void setGravity( int gravity ) { if ( mGravity != gravity ) { mGravity = gravity; requestLayout(); } } /* * (non-Javadoc) * * @see android.view.ViewGroup#getChildDrawingOrder(int, int) */ @Override protected int getChildDrawingOrder( int childCount, int i ) { int selectedIndex = mSelectedPosition - mFirstPosition; // Just to be safe if ( selectedIndex < 0 ) return i; if ( i == childCount - 1 ) { // Draw the selected child last return selectedIndex; } else if ( i >= selectedIndex ) { // Move the children after the selected child earlier one return i + 1; } else { // Keep the children before the selected child the same return i; } } /* * (non-Javadoc) * * @see android.view.View#onFocusChanged(boolean, int, android.graphics.Rect) */ @Override protected void onFocusChanged( boolean gainFocus, int direction, Rect previouslyFocusedRect ) { super.onFocusChanged( gainFocus, direction, previouslyFocusedRect ); /* * The gallery shows focus by focusing the selected item. So, give focus to our selected item instead. We steal keys from our * selected item elsewhere. */ if ( gainFocus && mSelectedChild != null ) { mSelectedChild.requestFocus( direction ); if ( mAutoSelectChild ) mSelectedChild.setSelected( true ); } } /** The m scroll selection notifier. */ ScrollSelectionNotifier mScrollSelectionNotifier; /** * The Class ScrollSelectionNotifier. */ private class ScrollSelectionNotifier implements Runnable { /* * (non-Javadoc) * * @see java.lang.Runnable#run() */ @Override public void run() { if ( mDataChanged ) { if ( getAdapter() != null ) { post( this ); } } else { fireOnScrollCompleted(); } } } /** * Scroll completed. */ void scrollCompleted() { if ( mItemsScrollListener != null ) { if ( mInLayout || mBlockLayoutRequests ) { // If we are in a layout traversal, defer notification // by posting. This ensures that the view tree is // in a consistent state and is able to accomodate // new layout or invalidate requests. if ( mScrollSelectionNotifier == null ) { mScrollSelectionNotifier = new ScrollSelectionNotifier(); } post( mScrollSelectionNotifier ); } else { fireOnScrollCompleted(); } } } /** * Fire on scroll completed. */ private void fireOnScrollCompleted() { if ( mItemsScrollListener == null ) return; int selection = this.getSelectedItemPosition(); if ( selection >= 0 && selection < mItemCount ) { View v = getSelectedView(); mItemsScrollListener.onScrollFinished( this, v, selection, getAdapter().getItemId( selection ) ); } } /** * Gallery extends LayoutParams to provide a place to hold current Transformation information along with previous * position/transformation info. */ public static class LayoutParams extends ViewGroup.LayoutParams { /** * Instantiates a new layout params. * * @param c * the c * @param attrs * the attrs */ public LayoutParams( Context c, AttributeSet attrs ) { super( c, attrs ); } /** * Instantiates a new layout params. * * @param w * the w * @param h * the h */ public LayoutParams( int w, int h ) { super( w, h ); } /** * Instantiates a new layout params. * * @param source * the source */ public LayoutParams( ViewGroup.LayoutParams source ) { super( source ); } } @Override public int getMinX() { return 0; } @Override public int getMaxX() { return Integer.MAX_VALUE; } }