/* * HorizontalVariableListView.java * Handles horizontal variable widths and heights item scrolling * * @version 2.0 */ package com.external.HorizontalVariableListView.widget; import android.annotation.SuppressLint; import android.content.Context; import android.database.DataSetObserver; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.PorterDuff.Mode; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Build; import android.util.AttributeSet; import android.util.Log; import android.util.SparseBooleanArray; import android.view.*; import android.view.GestureDetector.OnGestureListener; import android.view.ViewTreeObserver.OnScrollChangedListener; import android.widget.AdapterView; import android.widget.ListAdapter; import com.external.HorizontalVariableListView.utils.DataSetObserverExtended; import com.external.HorizontalVariableListView.utils.ReflectionUtils; import com.external.HorizontalVariableListView.utils.ReflectionUtils.ReflectionException; import com.external.HorizontalVariableListView.widget.IFlingRunnable.FlingRunnableView; import com.insthub.O2OMobile.R; import java.lang.ref.WeakReference; import java.util.*; @SuppressLint("NewApi") public class HorizontalVariableListView extends HorizontalListView implements OnGestureListener, FlingRunnableView { public enum SelectionMode { Single, Multiple }; public interface OnItemClickedListener { /** * Callback method to be invoked when an item in this AdapterView has * been clicked. * <p> * Implementers can call getItemAtPosition(position) if they need to * access the data associated with the selected item. * * @param parent The AdapterView where the click happened. * @param view The view within the AdapterView that was clicked (this * will be a view provided by the adapter) * @param position The position of the view in the adapter. * @param id The row id of the item that was clicked. * @return if the implementation return false, then the selection will not be updated */ boolean onItemClick(AdapterView<?> parent, View view, int position, long id); } public static final int OVER_SCROLL_ALWAYS = 0; public static final int OVER_SCROLL_IF_CONTENT_SCROLLS = 1; public static final int OVER_SCROLL_NEVER = 2; protected static final String LOG_TAG = "horizontal-variable-list"; private static final float WIDTH_THRESHOLD = 1.1f; protected int mAlignMode = Gravity.CENTER; protected SparseBooleanArray mSelectedPositions = new SparseBooleanArray(); protected int mHeight = 0; protected int mPaddingTop = 0; protected int mPaddingBottom = 0; protected ListAdapter mAdapter; private int mAdapterItemCount; private int mLeftViewIndex = -1; private int mRightViewIndex = 0; private GestureDetector mGesture; private List<Queue<View>> mRecycleBin; private List<Integer> mChildWidths = new ArrayList<Integer>(); private List<Integer> mChildHeights = new ArrayList<Integer>(); private boolean mDataChanged = false; private IFlingRunnable mFlingRunnable; private boolean mForceLayout; private int mDragTolerance = 0; private boolean mDragScrollEnabled; protected EdgeGlow mEdgeGlowLeft, mEdgeGlowRight; private int mOverScrollMode = OVER_SCROLL_NEVER; private Matrix mEdgeMatrix = new Matrix(); private ScrollNotifier mScrollNotifier; private SelectionMode mChoiceMode = SelectionMode.Single; private OnItemSelectedListener mOnItemSelected; private OnItemClickedListener mOnItemClicked; private OnItemDragListener mItemDragListener; private OnScrollChangedListener mScrollListener; private OnScrollFinishedListener mScrollFinishedListener; private OnLayoutChangeListener mLayoutChangeListener; public void setOnItemDragListener( OnItemDragListener listener ) { mItemDragListener = listener; } public void setOnScrollListener( OnScrollChangedListener listener ) { mScrollListener = listener; } public void setOnLayoutChangeListener( OnLayoutChangeListener listener ) { mLayoutChangeListener = listener; } public void setOnScrollFinishedListener( OnScrollFinishedListener listener ) { mScrollFinishedListener = listener; } public OnItemDragListener getOnItemDragListener() { return mItemDragListener; } /** * Controls how selection is managed within the list.<br /> * Single or multiple selections are supported * * @see com.external.HorizontalVariableListView.widget.HorizontalVariableListView.SelectionMode * @param mode the selection mode */ public void setSelectionMode( SelectionMode mode ) { mChoiceMode = mode; } /** * Returns the current selection mode * @see com.external.HorizontalVariableListView.widget.HorizontalVariableListView.SelectionMode * @return */ public SelectionMode getChoiceMode() { return mChoiceMode; } /** * Instantiates a new horizontial fixed list view. * * @param context the context * @param attrs the attrs */ public HorizontalVariableListView ( Context context, AttributeSet attrs ) { super( context, attrs ); initView(); } public HorizontalVariableListView ( Context context, AttributeSet attrs, int defStyle ) { super( context, attrs, defStyle ); initView(); } private synchronized void initView() { if ( Build.VERSION.SDK_INT > 8 ) { try { mFlingRunnable = (IFlingRunnable) ReflectionUtils.newInstance( "it.sephiroth.android.library.widget.Fling9Runnable", new Class<?>[] { FlingRunnableView.class, int.class }, this, mAnimationDuration ); } catch ( ReflectionException e ) { mFlingRunnable = new Fling8Runnable( this, mAnimationDuration ); } } else { mFlingRunnable = new Fling8Runnable( this, mAnimationDuration ); } mLeftViewIndex = -1; mRightViewIndex = 0; mMaxX = Integer.MAX_VALUE; mMinX = 0; mRightEdge = 0; mLeftEdge = 0; mGesture = new GestureDetector( getContext(), mGestureListener ); mGesture.setIsLongpressEnabled( false ); setFocusable( true ); setFocusableInTouchMode( true ); ViewConfiguration configuration = ViewConfiguration.get( getContext() ); mTouchSlop = configuration.getScaledTouchSlop(); mDragTolerance = mTouchSlop; mOverscrollDistance = 10; mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); } public void setOverScrollMode( int mode ) { mOverScrollMode = mode; if ( mode != OVER_SCROLL_NEVER ) { if ( mEdgeGlowLeft == null ) { Drawable glow = getContext().getResources().getDrawable( R.drawable.overscroll_glow ); Drawable edge = getContext().getResources().getDrawable( R.drawable.overscroll_edge ); mEdgeGlowLeft = new EdgeGlow( edge, glow ); mEdgeGlowRight = new EdgeGlow( edge, glow ); mEdgeGlowLeft.setColorFilter( 0xFF33b5e5, Mode.MULTIPLY ); } } else { mEdgeGlowLeft = mEdgeGlowRight = null; } } public void setEdgeHeight( int value ) { mEdgesHeight = value; } public void setEdgeGravityY( int value ) { mEdgesGravityY = value; } @Override public void trackMotionScroll( int newX ) { scrollTo( newX, 0 ); mCurrentX = getScrollX(); removeNonVisibleItems( mCurrentX ); fillList( mCurrentX ); invalidate(); } @Override protected void dispatchDraw( Canvas canvas ) { super.dispatchDraw( canvas ); if ( getChildCount() > 0 ) { drawEdges( canvas ); } } /** * Draw glow edges. * * @param canvas * the canvas */ private void drawEdges( Canvas canvas ) { if ( mEdgeGlowLeft != null ) { if ( !mEdgeGlowLeft.isFinished() ) { final int restoreCount = canvas.save(); final int height = mEdgesHeight; mEdgeMatrix.reset(); mEdgeMatrix.postRotate( -90 ); mEdgeMatrix.postTranslate( 0, height ); if ( mEdgesGravityY == Gravity.BOTTOM ) { mEdgeMatrix.postTranslate( 0, getHeight() - height ); } canvas.concat( mEdgeMatrix ); mEdgeGlowLeft.setSize( height, height / 5 ); if ( mEdgeGlowLeft.draw( canvas ) ) { postInvalidate(); } canvas.restoreToCount( restoreCount ); } if ( !mEdgeGlowRight.isFinished() ) { final int restoreCount = canvas.save(); final int width = getWidth(); final int height = mEdgesHeight; mEdgeMatrix.reset(); mEdgeMatrix.postRotate( 90 ); mEdgeMatrix.postTranslate( mCurrentX + width, 0 ); if ( mEdgesGravityY == Gravity.BOTTOM ) { mEdgeMatrix.postTranslate( 0, getHeight() - height ); } canvas.concat( mEdgeMatrix ); mEdgeGlowRight.setSize( height, height / 5 ); if ( mEdgeGlowRight.draw( canvas ) ) { postInvalidate(); } canvas.restoreToCount( restoreCount ); } } } /** * Set if a vertical scroll movement will trigger a long click event * * @param value */ public void setDragScrollEnabled( boolean value ) { mDragScrollEnabled = value; } public boolean getDragScrollEnabled() { return mDragScrollEnabled; } public int getSelectedPosition() { if ( mSelectedPositions.size() > 0 ) return mSelectedPositions.keyAt( 0 ); return INVALID_POSITION; } public int[] getSelectedPositions() { int[] result; if ( mSelectedPositions.size() > 0 ) { // multiple if ( mChoiceMode == SelectionMode.Multiple ) { result = new int[mSelectedPositions.size()]; for ( int i = 0; i < mSelectedPositions.size(); i++ ) { result[i] = mSelectedPositions.keyAt( i ); } } else { // single result = new int[] { mSelectedPositions.keyAt( 0 ) }; } } else { result = new int[] { INVALID_POSITION }; } return result; } public void setSelectedPosition( int position, boolean fireEvent ) { if ( position == INVALID_POSITION ) { setSelectedItem( null, INVALID_POSITION, false, fireEvent ); } else { View child = getItemAt( position ); setSelectedItem( child, position, true, fireEvent ); } } public void setSelectedPositions( int[] positions, boolean fireEvent ) { if ( mChoiceMode == SelectionMode.Multiple ) { View child; int position; // first clear all current selection synchronized ( mSelectedPositions ) { for ( int i = 0; i < mSelectedPositions.size(); i++ ) { position = mSelectedPositions.keyAt( i ); child = getChildAt( position ); if ( null != child ) { child.setSelected( false ); } } mSelectedPositions.clear(); } if ( null != positions && positions.length > 0 ) { for ( int i = 0; i < positions.length - 1; i++ ) { position = positions[i]; child = getItemAt( position ); setSelectedItem( child, position, true, false ); } position = positions[positions.length - 1]; child = getItemAt( position ); setSelectedItem( child, position, true, fireEvent ); } } else { Log.w( LOG_TAG, "This method has no effect on single selection list" ); } } @Override public void setOnItemSelectedListener( OnItemSelectedListener listener ) { mOnItemSelected = listener; } /** * Toggle the item clicked listener. See the {@link com.external.HorizontalVariableListView.widget.HorizontalVariableListView.OnItemClickedListener} interface * @param listener */ public void setOnItemClickedListener( OnItemClickedListener listener ) { mOnItemClicked = listener; } private DataSetObserverExtended mDataObserverExtended = new DataSetObserverExtended() { @Override public void onAdded() { synchronized ( HorizontalVariableListView.this ) { mAdapterItemCount = mAdapter.getCount(); } Log.i( LOG_TAG, "onAdded: " + mAdapterItemCount ); mDataChanged = true; mMaxX = Integer.MAX_VALUE; requestLayout(); }; @Override public void onRemoved() { this.onChanged(); }; @Override public void onChanged() { mAdapterItemCount = mAdapter.getCount(); Log.i( LOG_TAG, "onChange: " + mAdapterItemCount ); reset(); }; @Override public void onInvalidated() { this.onChanged(); }; }; /** The m data observer. */ private DataSetObserver mDataObserver = new DataSetObserver() { @Override public void onChanged() { synchronized ( HorizontalVariableListView.this ) { mAdapterItemCount = mAdapter.getCount(); } Log.i( LOG_TAG, "onChanged(2): " + mAdapterItemCount ); invalidate(); reset(); } @Override public void onInvalidated() { mAdapterItemCount = mAdapter.getCount(); Log.i( LOG_TAG, "onInvalidated(2): " + mAdapterItemCount ); invalidate(); reset(); } }; public void requestFullLayout() { mForceLayout = true; invalidate(); requestLayout(); } /** The m height measure spec. */ private int mHeightMeasureSpec; /** The m width measure spec. */ private int mWidthMeasureSpec; /** The m a2_menu edge. */ private int mRightEdge, mLeftEdge; /* * (non-Javadoc) * * @see android.widget.AdapterView#getAdapter() */ @Override public ListAdapter getAdapter() { return mAdapter; } @Override public View getSelectedView() { return null; } @Override public void setAdapter( ListAdapter adapter ) { if ( mAdapter != null ) { if ( mAdapter instanceof BaseAdapterExtended ) { ( (BaseAdapterExtended) mAdapter ).unregisterDataSetObserverExtended( mDataObserverExtended ); } else { mAdapter.unregisterDataSetObserver( mDataObserver ); } emptyRecycler(); mAdapterItemCount = 0; } mAdapter = adapter; mChildWidths.clear(); mChildHeights.clear(); if ( mAdapter != null ) { mAdapterItemCount = mAdapter.getCount(); if ( mAdapter instanceof BaseAdapterExtended ) { ( (BaseAdapterExtended) mAdapter ).registerDataSetObserverExtended( mDataObserverExtended ); } else { mAdapter.registerDataSetObserver( mDataObserver ); } int total = mAdapter.getViewTypeCount(); mRecycleBin = Collections.synchronizedList( new ArrayList<Queue<View>>() ); for ( int i = 0; i < total; i++ ) { mRecycleBin.add( new LinkedList<View>() ); mChildWidths.add( -1 ); mChildHeights.add( -1 ); } } reset(); } private void emptyRecycler() { if ( null != mRecycleBin ) { while ( mRecycleBin.size() > 0 ) { Queue<View> recycler = mRecycleBin.remove( 0 ); recycler.clear(); } mRecycleBin.clear(); } } /** * Reset. */ private synchronized void reset() { mCurrentX = 0; initView(); removeAllViewsInLayout(); mForceLayout = true; requestLayout(); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); Log.d( LOG_TAG, "onDetachedFromWindow" ); removeCallbacks( mScrollNotifier ); emptyRecycler(); } /* * (non-Javadoc) * * @see android.widget.AdapterView#setSelection(int) */ @Override public void setSelection( int position ) {} /* * (non-Javadoc) * * @see android.view.View#onMeasure(int, int) */ @Override protected void onMeasure( int widthMeasureSpec, int heightMeasureSpec ) { super.onMeasure( widthMeasureSpec, heightMeasureSpec ); mHeightMeasureSpec = heightMeasureSpec; mWidthMeasureSpec = widthMeasureSpec; } /** * Adds the and measure child. * * @param child * the child * @param viewPos * the view pos */ private void addAndMeasureChild( final View child, int viewPos ) { LayoutParams params = child.getLayoutParams(); if ( params == null ) { params = new LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT ); } addViewInLayout( child, viewPos, params, false ); forceChildLayout( child, params ); } public void forceChildLayout( View child, LayoutParams params ) { int childHeightSpec = ViewGroup.getChildMeasureSpec( mHeightMeasureSpec, getPaddingTop() + getPaddingBottom(), params.height ); int childWidthSpec = ViewGroup.getChildMeasureSpec( mWidthMeasureSpec, getPaddingLeft() + getPaddingRight(), params.width ); child.measure( childWidthSpec, childHeightSpec ); } @SuppressWarnings ( "unused" ) @Override protected void onLayout( boolean changed, int left, int top, int right, int bottom ) { super.onLayout( changed, left, top, right, bottom ); if ( mAdapter == null ) { return; } if ( !changed && !mDataChanged ) { layoutChildren(); } mHeight = bottom - top; int trackValue; if ( changed ) { mPaddingTop = getPaddingTop(); mPaddingBottom = getPaddingBottom(); trackValue = mCurrentX = mOldX = 0; initView(); removeAllViewsInLayout(); } if ( mDataChanged ) { mSelectedPositions.clear(); trackValue = mCurrentX; mDataChanged = false; } if ( mForceLayout ) { mOldX = mCurrentX; trackValue = mOldX; initView(); removeAllViewsInLayout(); } if ( changed || mDataChanged || mForceLayout ) { trackMotionScroll( mOldX ); mForceLayout = false; } postNotifyLayoutChange( changed, left, top, right, bottom ); } public void layoutChildren() { int left, right; for ( int i = 0; i < getChildCount(); i++ ) { View child = getChildAt( i ); forceChildLayout( child, child.getLayoutParams() ); left = child.getLeft(); right = child.getRight(); int childHeight = child.getHeight(); layoutChild( child, left, right, childHeight ); // child.layout( a2_menu, top, right, top + childHeight ); } } /** * Fill list. * * @param positionX * the position x */ private void fillList( final int positionX ) { int edge = 0; View child = getChildAt( getChildCount() - 1 ); if ( child != null ) { edge = child.getRight(); } fillListRight( mCurrentX, edge ); edge = 0; child = getChildAt( 0 ); if ( child != null ) { edge = child.getLeft(); } fillListLeft( mCurrentX, edge ); } /** * Fill list a2_menu. * * @param positionX * the position x * @param leftEdge * the a2_menu edge */ private void fillListLeft( int positionX, int leftEdge ) { if ( mAdapter == null ) return; while ( ( leftEdge - positionX ) > mLeftEdge && mLeftViewIndex >= 0 ) { final boolean selected = getIsSelected( mLeftViewIndex ); int viewType = mAdapter.getItemViewType( mLeftViewIndex ); View child = mAdapter.getView( mLeftViewIndex, mRecycleBin.get( viewType ).poll(), this ); child.setSelected( selected ); addAndMeasureChild( child, 0 ); int childWidth = mChildWidths.get( viewType ); int childHeight = mChildHeights.get( viewType ); layoutChild( child, leftEdge - childWidth, leftEdge, childHeight ); leftEdge -= childWidth; mLeftViewIndex--; } } public View getItemAt( int position ) { return getChildAt( position - ( mLeftViewIndex + 1 ) ); } @Override public int getScreenPositionForView( View view ) { View listItem = view; try { View v; while ( !this.equals( ( v = (View) listItem.getParent() ) ) ) { listItem = v; } } catch ( ClassCastException e ) { // We made it up to the window without find this list view return INVALID_POSITION; } catch ( NullPointerException e ) { return INVALID_POSITION; } // Search the children for the list item final int childCount = getChildCount(); for ( int i = 0; i < childCount; i++ ) { if ( getChildAt( i ).equals( listItem ) ) { return i; } } // Child not found! return INVALID_POSITION; } @Override public int getPositionForView( View view ) { View listItem = view; try { View v; while ( !( v = (View) listItem.getParent() ).equals( this ) ) { listItem = v; } } catch ( ClassCastException e ) { // We made it up to the window without find this list view return INVALID_POSITION; } // Search the children for the list item final int childCount = getChildCount(); for ( int i = 0; i < childCount; i++ ) { if ( getChildAt( i ).equals( listItem ) ) { return mLeftViewIndex + i + 1; } } // Child not found! return INVALID_POSITION; } /** * Fill list right. * * @param positionX * the position x * @param rightEdge * the right edge */ private void fillListRight( int positionX, int rightEdge ) { boolean firstChild = getChildCount() == 0 || mDataChanged || mForceLayout; if ( mAdapter == null ) return; final int realWidth = getWidth(); int viewWidth = (int) ( (float) realWidth * WIDTH_THRESHOLD ); while ( ( rightEdge - positionX ) < viewWidth || firstChild ) { if ( mRightViewIndex >= mAdapterItemCount ) { break; } final boolean selected = getIsSelected( mRightViewIndex ); int viewType = mAdapter.getItemViewType( mRightViewIndex ); View child = mAdapter.getView( mRightViewIndex, mRecycleBin.get( viewType ).poll(), this ); child.setSelected( selected ); addAndMeasureChild( child, -1 ); int childWidth = mChildWidths.get( viewType ); int childHeight = mChildHeights.get( viewType ); if ( childWidth == -1 ) { childWidth = child.getMeasuredWidth(); childHeight = child.getMeasuredHeight(); mChildWidths.set( viewType, childWidth ); mChildHeights.set( viewType, childHeight ); } if ( firstChild ) { if ( mEdgesHeight == -1 ) { mEdgesHeight = childHeight; } mRightEdge = viewWidth; mLeftEdge = ( realWidth - viewWidth ); mMinX = 0; firstChild = false; } layoutChild( child, rightEdge, rightEdge + childWidth, childHeight ); rightEdge += childWidth; mRightViewIndex++; } if ( mRightViewIndex == mAdapterItemCount ) { // Log.i( LOG_TAG, "itemCount: " + mAdapterItemCount ); // Log.i( LOG_TAG, "rightEdge: " + rightEdge ); // Log.i( LOG_TAG, "realWidth: " + realWidth ); if ( rightEdge > realWidth ) { mMaxX = rightEdge - realWidth; } else { mMaxX = 0; } // Log.i( LOG_TAG, "maxX: " + mMaxX ); } } protected void layoutChild( View child, int left, int right, int childHeight ) { // Log.i( LOG_TAG, "layoutChild. height: " + mHeight + // ", child.height: " // + childHeight ); int top = mPaddingTop; if ( mAlignMode == Gravity.BOTTOM ) { top = top + ( mHeight - childHeight ); } else if ( mAlignMode == Gravity.CENTER ) { top = top + ( mHeight - childHeight ) / 2; } child.layout( left, top, right, top + childHeight ); } /** * Removes the non visible items. * * @param positionX * the position x */ private void removeNonVisibleItems( final int positionX ) { View child = getChildAt( 0 ); // remove to a2_menu... while ( child != null && child.getRight() - positionX <= mLeftEdge ) { if ( null != mAdapter ) { int position = getPositionForView( child ); int viewType = mAdapter.getItemViewType( position ); mRecycleBin.get( viewType ).offer( child ); } removeViewInLayout( child ); mLeftViewIndex++; child = getChildAt( 0 ); } // remove to right... child = getChildAt( getChildCount() - 1 ); while ( child != null && child.getLeft() - positionX >= mRightEdge ) { if ( null != mAdapter ) { int position = getPositionForView( child ); int viewType = mAdapter.getItemViewType( position ); mRecycleBin.get( viewType ).offer( child ); } removeViewInLayout( child ); mRightViewIndex--; child = getChildAt( getChildCount() - 1 ); } } private float mTestDragX, mTestDragY; private boolean mCanCheckDrag; private boolean mWasFlinging; private WeakReference<View> mOriginalDragItem; @Override public boolean onDown( MotionEvent event ) { return true; } @Override public boolean onScroll( MotionEvent e1, MotionEvent e2, float distanceX, float distanceY ) { return true; } @Override public boolean onFling( MotionEvent event0, MotionEvent event1, float velocityX, float velocityY ) { if ( mMaxX == 0 ) return false; mCanCheckDrag = false; mWasFlinging = true; mFlingRunnable.startUsingVelocity( mCurrentX, (int) -velocityX ); return true; } @Override public void onLongPress( MotionEvent e ) { if ( mWasFlinging ) return; OnItemLongClickListener listener = getOnItemLongClickListener(); if ( null != listener ) { if ( !mFlingRunnable.isFinished() ) return; int i = getChildAtPosition( e.getX(), e.getY() ); if ( i > -1 ) { View child = getChildAt( i ); fireLongPress( child, mLeftViewIndex + 1 + i, mAdapter.getItemId( mLeftViewIndex + 1 + i ) ); } } } private int getChildAtPosition( float x, float y ) { Rect viewRect = new Rect(); for ( int i = 0; i < getChildCount(); i++ ) { View child = getChildAt( i ); int left = child.getLeft(); int right = child.getRight(); int top = child.getTop(); int bottom = child.getBottom(); viewRect.set( left, top, right, bottom ); viewRect.offset( -mCurrentX, 0 ); if ( viewRect.contains( (int) x, (int) y ) ) { return i; } } return -1; } private boolean fireLongPress( View item, int position, long id ) { if ( getOnItemLongClickListener().onItemLongClick( HorizontalVariableListView.this, item, position, id ) ) { performHapticFeedback( HapticFeedbackConstants.LONG_PRESS ); return true; } return false; } private boolean fireItemDragStart( View item, int position, long id ) { mCanCheckDrag = false; mIsBeingDragged = false; if ( mItemDragListener.onItemStartDrag( HorizontalVariableListView.this, item, position, id ) ) { mIsDragging = true; performHapticFeedback( HapticFeedbackConstants.LONG_PRESS ); return true; } return false; } private void fireOnLayoutChangeListener( boolean changed, int left, int top, int right, int bottom ) { if( mLayoutChangeListener != null ) { mLayoutChangeListener.onLayoutChange( changed, left, top, right, bottom ); } } private void fireOnScrollChanged() { if ( mScrollListener != null ) { mScrollListener.onScrollChanged(); } } private void fireOnScrollFininshed() { if ( null != mScrollFinishedListener ) { mScrollFinishedListener.onScrollFinished( mCurrentX ); } } private void postScrollNotifier() { if ( mScrollListener != null ) { if ( mScrollNotifier == null ) { mScrollNotifier = new ScrollNotifier(); } post( mScrollNotifier ); } } private void postNotifyLayoutChange( final boolean changed, final int left, final int top, final int right, final int bottom ) { post( new Runnable() { @Override public void run() { fireOnLayoutChangeListener( changed, left, top, right, bottom ); } }); } private class ScrollNotifier implements Runnable { @Override public void run() { fireOnScrollChanged(); } } public void setIsDragging( boolean value ) { mIsDragging = value; } private int getItemIndex( View view ) { final int total = getChildCount(); for ( int i = 0; i < total; i++ ) { if ( view == getChildAt( i ) ) { return i; } } return -1; } /* * (non-Javadoc) * * @see * android.view.GestureDetector.OnGestureListener#onShowPress(android.view * .MotionEvent) */ @Override public void onShowPress( MotionEvent arg0 ) {} /* * (non-Javadoc) * * @see * android.view.GestureDetector.OnGestureListener#onSingleTapUp(android.view * .MotionEvent) */ @Override public boolean onSingleTapUp( MotionEvent arg0 ) { return false; } private boolean mIsDragging = false; private boolean mIsBeingDragged = false; private int mActivePointerId = -1; private int mLastMotionX; private float mLastMotionX2; private VelocityTracker mVelocityTracker; private static final int INVALID_POINTER = -1; private int mOverscrollDistance; private int mMinimumVelocity; private int mMaximumVelocity; private void initOrResetVelocityTracker() { if ( mVelocityTracker == null ) { mVelocityTracker = VelocityTracker.obtain(); } else { mVelocityTracker.clear(); } } private void initVelocityTrackerIfNotExists() { if ( mVelocityTracker == null ) { mVelocityTracker = VelocityTracker.obtain(); } } private void recycleVelocityTracker() { if ( mVelocityTracker != null ) { mVelocityTracker.recycle(); mVelocityTracker = null; } } @Override public void requestDisallowInterceptTouchEvent( boolean disallowIntercept ) { if ( disallowIntercept ) { recycleVelocityTracker(); } super.requestDisallowInterceptTouchEvent( disallowIntercept ); } @Override public boolean onInterceptTouchEvent( MotionEvent ev ) { getParent().requestDisallowInterceptTouchEvent( true ); if ( mIsDragging ) return false; final int action = ev.getAction(); mGesture.onTouchEvent( ev ); /* * Shortcut the most recurring case: the user is in the dragging state * and * he is moving his finger. We want to intercept this * motion. */ if ( action == MotionEvent.ACTION_MOVE ) { if ( mIsBeingDragged ) { return true; } } switch ( action & MotionEvent.ACTION_MASK ) { case MotionEvent.ACTION_MOVE: { /* * mIsBeingDragged == false, otherwise the shortcut would have * caught it. Check whether the user has moved far enough * from his original down touch. */ final int activePointerId = mActivePointerId; if ( activePointerId == INVALID_POINTER ) { // If we don't have a valid id, the touch down wasn't on // content. break; } final int pointerIndex = ev.findPointerIndex( activePointerId ); final int x = (int) ev.getX( pointerIndex ); final int y = (int) ev.getY( pointerIndex ); final int xDiff = Math.abs( x - mLastMotionX ); mLastMotionX2 = x; if ( checkDrag( x, y ) ) { return false; } if ( xDiff > mTouchSlop ) { mIsBeingDragged = true; mLastMotionX = x; initVelocityTrackerIfNotExists(); mVelocityTracker.addMovement( ev ); final ViewParent parent = getParent(); if ( parent != null ) { parent.requestDisallowInterceptTouchEvent( true ); } postScrollNotifier(); } break; } case MotionEvent.ACTION_DOWN: { final int x = (int) ev.getX(); final int y = (int) ev.getY(); mTestDragX = x; mTestDragY = y; /* * Remember location of down touch. ACTION_DOWN always refers to * pointer index 0. */ mLastMotionX = x; mLastMotionX2 = x; mActivePointerId = ev.getPointerId( 0 ); initOrResetVelocityTracker(); mVelocityTracker.addMovement( ev ); /* * If being flinged and user touches the screen, initiate drag; * otherwise don't. mScroller.isFinished should be false * when being flinged. */ mIsBeingDragged = !mFlingRunnable.isFinished(); mWasFlinging = !mFlingRunnable.isFinished(); mFlingRunnable.stop( false ); mCanCheckDrag = getDragScrollEnabled() && ( mItemDragListener != null ); if ( mCanCheckDrag ) { int i = getChildAtPosition( x, y ); if ( i > -1 ) { mOriginalDragItem = new WeakReference<View>( getChildAt( i ) ); } } break; } case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: /* Release the drag */ mIsBeingDragged = false; mActivePointerId = INVALID_POINTER; recycleVelocityTracker(); if ( mFlingRunnable.springBack( mCurrentX, 0, mMinX, mMaxX, 0, 0 ) ) { postInvalidate(); } mCanCheckDrag = false; break; case MotionEvent.ACTION_POINTER_UP: onSecondaryPointerUp( ev ); break; } return mIsBeingDragged; } @Override public boolean onTouchEvent( MotionEvent ev ) { initVelocityTrackerIfNotExists(); mVelocityTracker.addMovement( ev ); final int action = ev.getAction(); switch ( action & MotionEvent.ACTION_MASK ) { case MotionEvent.ACTION_DOWN: { // DOWN if ( getChildCount() == 0 ) { return false; } if ( ( mIsBeingDragged = !mFlingRunnable.isFinished() ) ) { final ViewParent parent = getParent(); if ( parent != null ) { parent.requestDisallowInterceptTouchEvent( true ); } } /* * If being flinged and user touches, stop the fling. isFinished * will be false if being flinged. */ if ( !mFlingRunnable.isFinished() ) { mFlingRunnable.stop( false ); } // Remember where the motion event started mTestDragX = ev.getX(); mTestDragY = ev.getY(); mLastMotionX2 = mLastMotionX = (int) ev.getX(); mActivePointerId = ev.getPointerId( 0 ); break; } case MotionEvent.ACTION_MOVE: { // MOVE final int activePointerIndex = ev.findPointerIndex( mActivePointerId ); final int x = (int) ev.getX( activePointerIndex ); final int y = (int) ev.getY( activePointerIndex ); int deltaX = mLastMotionX - x; if ( !mIsBeingDragged && Math.abs( deltaX ) > mTouchSlop ) { final ViewParent parent = getParent(); if ( parent != null ) { parent.requestDisallowInterceptTouchEvent( true ); } mIsBeingDragged = true; if ( deltaX > 0 ) { deltaX -= mTouchSlop; } else { deltaX += mTouchSlop; } postScrollNotifier(); } // first check if we can drag the item if ( checkDrag( x, y ) ) { return false; } if ( mIsBeingDragged ) { // Scroll to follow the motion event mLastMotionX = x; final float deltaX2 = mLastMotionX2 - x; final int oldX = getScrollX(); final int range = getScrollRange(); final int overscrollMode = mOverScrollMode; final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS || ( overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0 ); if ( overScrollingBy( deltaX, 0, mCurrentX, 0, range, 0, 0, mOverscrollDistance, true ) ) { mVelocityTracker.clear(); } if ( canOverscroll && mEdgeGlowLeft != null ) { final int pulledToX = oldX + deltaX; if ( pulledToX < mMinX ) { float overscroll = ( (float) -deltaX2 * 1.5f ) / getWidth(); mEdgeGlowLeft.onPull( overscroll ); if ( !mEdgeGlowRight.isFinished() ) { mEdgeGlowRight.onRelease(); } } else if ( pulledToX > mMaxX ) { float overscroll = ( (float) deltaX2 * 1.5f ) / getWidth(); mEdgeGlowRight.onPull( overscroll ); if ( !mEdgeGlowLeft.isFinished() ) { mEdgeGlowLeft.onRelease(); } } if ( mEdgeGlowLeft != null && ( !mEdgeGlowLeft.isFinished() || !mEdgeGlowRight.isFinished() ) ) { postInvalidate(); } } } break; } case MotionEvent.ACTION_UP: { if ( mIsBeingDragged ) { final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity( 1000, mMaximumVelocity ); final float velocityY = velocityTracker.getYVelocity(); final float velocityX = velocityTracker.getXVelocity(); if ( getChildCount() > 0 ) { if ( ( Math.abs( velocityX ) > mMinimumVelocity ) ) { onFling( ev, null, velocityX, velocityY ); } else { if ( mFlingRunnable.springBack( mCurrentX, 0, mMinX, mMaxX, 0, 0 ) ) { postInvalidate(); } } } mActivePointerId = INVALID_POINTER; endDrag(); mCanCheckDrag = false; if ( mFlingRunnable.isFinished() ) { scrollIntoSlots(); } } break; } case MotionEvent.ACTION_CANCEL: { if ( mIsBeingDragged && getChildCount() > 0 ) { if ( mFlingRunnable.springBack( mCurrentX, 0, mMinX, mMaxX, 0, 0 ) ) { postInvalidate(); } mActivePointerId = INVALID_POINTER; endDrag(); } break; } case MotionEvent.ACTION_POINTER_UP: { onSecondaryPointerUp( ev ); mLastMotionX2 = mLastMotionX = (int) ev.getX( ev.findPointerIndex( mActivePointerId ) ); mTestDragY = -1; mTestDragX = -1; break; } } return true; } private void onSecondaryPointerUp( MotionEvent ev ) { final int pointerIndex = ( ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK ) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; final int pointerId = ev.getPointerId( pointerIndex ); if ( pointerId == mActivePointerId ) { final int newPointerIndex = pointerIndex == 0 ? 1 : 0; mLastMotionX2 = mLastMotionX = (int) ev.getX( newPointerIndex ); mTestDragY = -1; mTestDragX = -1; mActivePointerId = ev.getPointerId( newPointerIndex ); if ( mVelocityTracker != null ) { mVelocityTracker.clear(); } } } /** * Check if the movement will fire a drag start event * * @param x * @param y * @return */ private boolean checkDrag( int x, int y ) { if ( mCanCheckDrag && !mIsDragging ) { if ( mTestDragX < 0 || mTestDragY < 0 ) return false; float dx = Math.abs( x - mTestDragX ); if ( dx > mDragTolerance ) { mTestDragY = -1; mCanCheckDrag = false; } else { float dy = Math.abs( y - mTestDragY ); if ( dy > ( (double) mDragTolerance * 1.5 ) ) { if ( mOriginalDragItem != null && mAdapter != null ) { View view = mOriginalDragItem.get(); int position = getItemIndex( view ); if ( null != view && position > -1 ) { getParent().requestDisallowInterceptTouchEvent( false ); if ( mItemDragListener != null ) { fireItemDragStart( view, mLeftViewIndex + 1 + position, mAdapter.getItemId( mLeftViewIndex + 1 + position ) ); return true; } } } mCanCheckDrag = false; } } } return false; } private void endDrag() { mIsBeingDragged = false; recycleVelocityTracker(); if ( mEdgeGlowLeft != null ) { mEdgeGlowLeft.onRelease(); mEdgeGlowRight.onRelease(); } } /** * Scroll the view with standard behavior for scrolling beyond the normal * content boundaries. Views that call this method should * override {@link #onOverScrolled(int, int, boolean, boolean)} to respond * to * the results of an over-scroll operation. * * Views can use this method to handle any touch or fling-based scrolling. * * @param deltaX * Change in X in pixels * @param deltaY * Change in Y in pixels * @param scrollX * Current X scroll value in pixels before applying deltaX * @param scrollY * Current Y scroll value in pixels before applying deltaY * @param scrollRangeX * Maximum content scroll range along the X axis * @param scrollRangeY * Maximum content scroll range along the Y axis * @param maxOverScrollX * Number of pixels to overscroll by in either direction along * the * X axis. * @param maxOverScrollY * Number of pixels to overscroll by in either direction along * the * Y axis. * @param isTouchEvent * true if this scroll operation is the result of a touch event. * @return true if scrolling was clamped to an over-scroll boundary along * either axis, false otherwise. */ protected boolean overScrollingBy( int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent ) { final int overScrollMode = mOverScrollMode; final boolean toLeft = deltaX > 0; final boolean overScrollHorizontal = overScrollMode == OVER_SCROLL_ALWAYS; int newScrollX = scrollX + deltaX; if ( !overScrollHorizontal ) { maxOverScrollX = 0; } // Clamp values if at the limits and record final int left = mMinX - maxOverScrollX; final int right = mMaxX == Integer.MAX_VALUE ? mMaxX : ( mMaxX + maxOverScrollX ); boolean clampedX = false; if ( newScrollX > right && toLeft ) { newScrollX = right; deltaX = mMaxX - scrollX; clampedX = true; } else if ( newScrollX < left && !toLeft ) { newScrollX = left; deltaX = mMinX - scrollX; clampedX = true; } onScrolling( newScrollX, deltaX, clampedX ); return clampedX; } public boolean onScrolling( int scrollX, int deltaX, boolean clampedX ) { if ( mAdapter == null ) return true; if ( !mFlingRunnable.isFinished() ) { mCurrentX = getScrollX(); if ( clampedX ) { mFlingRunnable.springBack( scrollX, 0, mMinX, mMaxX, 0, 0 ); } } else { trackMotionScroll( scrollX ); } return true; } @Override public void computeScroll() { if ( mFlingRunnable.computeScrollOffset() ) { int oldX = mCurrentX; int x = mFlingRunnable.getCurrX(); if ( oldX != x ) { final int range = getScrollRange(); final int overscrollMode = mOverScrollMode; final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS || ( overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0 ); overScrollingBy( x - oldX, 0, oldX, 0, range, 0, mOverscrollDistance, 0, false ); if ( canOverscroll && mEdgeGlowLeft != null ) { if ( x < 0 && oldX >= 0 ) { mEdgeGlowLeft.onAbsorb( (int) mFlingRunnable.getCurrVelocity() ); } else if ( x > range && oldX <= range ) { mEdgeGlowRight.onAbsorb( (int) mFlingRunnable.getCurrVelocity() ); } } } postInvalidate(); } } int getScrollRange() { if ( getChildCount() > 0 ) { return mMaxX - mMinX; } return 0; } /** The m animation duration. */ int mAnimationDuration = 400; /** The m child height. */ int mMaxX, mMinX; /** The m should stop fling. */ boolean mShouldStopFling; /** The m to a2_menu. */ boolean mToLeft; /** The m current x. */ int mCurrentX = 0; /** The m old x. */ int mOldX = 0; /** The m touch slop. */ int mTouchSlop; int mEdgesHeight = -1; int mEdgesGravityY = Gravity.CENTER; @Override public void scrollIntoSlots() { if ( !mFlingRunnable.isFinished() ) { return; } if ( mCurrentX > mMaxX || mCurrentX < mMinX ) { if ( mCurrentX > mMaxX ) { if ( mMaxX < 0 ) { mFlingRunnable.startUsingDistance( mCurrentX, mMinX - mCurrentX ); } else { mFlingRunnable.startUsingDistance( mCurrentX, mMaxX - mCurrentX ); } return; } else { mFlingRunnable.startUsingDistance( mCurrentX, mMinX - mCurrentX ); return; } } onFinishedMovement(); } public void smoothScrollTo( int targetX ) { mFlingRunnable.startUsingDistance( mCurrentX, targetX - mCurrentX ); } /** * On finished movement. */ protected void onFinishedMovement() { fireOnScrollFininshed(); } private void onItemClick( View child, int position ) { boolean clickValid = true; // always dispatch the item click listener if ( mOnItemClicked != null ) { playSoundEffect( SoundEffectConstants.CLICK ); clickValid = mOnItemClicked.onItemClick( this, child, position, mAdapter.getItemId( position ) ); } if ( clickValid ) { // now check the selected status if ( !getIsSelected( position ) ) { setSelectedItem( child, position, true, true ); } else { setSelectedItem( child, position, false, true ); } } } protected void setSelectedItem( final View newView, final int position, boolean selected, boolean fireEvent ) { if ( mChoiceMode == SelectionMode.Single ) { if ( mSelectedPositions.size() > 0 ) { int pos = mSelectedPositions.keyAt( 0 ); View child = getItemAt( pos ); if ( null != child ) { child.setSelected( false ); } } mSelectedPositions.clear(); } if ( selected ) { mSelectedPositions.put( position, true ); } else { mSelectedPositions.delete( position ); } if ( null != newView ) { newView.setSelected( selected ); } if ( fireEvent && mOnItemSelected != null ) { if ( mSelectedPositions.size() > 0 ) { mOnItemSelected.onItemSelected( this, newView, position, mAdapter.getItemId( position ) ); } else { mOnItemSelected.onNothingSelected( this ); } } } public boolean getIsSelected( int position ) { return mSelectedPositions.get( position, false ); } /** The m gesture listener. */ private OnGestureListener mGestureListener = new GestureDetector.SimpleOnGestureListener() { @Override public boolean onDoubleTap( MotionEvent e ) { return false; }; public boolean onSingleTapUp( MotionEvent e ) { return onItemClick( e ); }; @Override public boolean onDown( MotionEvent e ) { return false; }; @Override public boolean onFling( MotionEvent e1, MotionEvent e2, float velocityX, float velocityY ) { return false; }; @Override public void onLongPress( MotionEvent e ) { HorizontalVariableListView.this.onLongPress( e ); }; @Override public boolean onScroll( MotionEvent e1, MotionEvent e2, float distanceX, float distanceY ) { return false; }; @Override public void onShowPress( MotionEvent e ) {}; @Override public boolean onSingleTapConfirmed( MotionEvent e ) { return true; } private boolean onItemClick( MotionEvent ev ) { if ( !mFlingRunnable.isFinished() || mWasFlinging ) return false; Rect viewRect = new Rect(); for ( int i = 0; i < getChildCount(); i++ ) { View child = getChildAt( i ); int left = child.getLeft(); int right = child.getWidth(); int top = child.getTop(); int bottom = child.getBottom(); viewRect.set( left, top, left + right, bottom ); viewRect.offset( -mCurrentX, 0 ); if ( viewRect.contains( (int) ev.getX(), (int) ev.getY() ) ) { final int position = mLeftViewIndex + 1 + i; HorizontalVariableListView.this.onItemClick( child, position ); break; } } return true; } }; public View getChild( MotionEvent e ) { Rect viewRect = new Rect(); for ( int i = 0; i < getChildCount(); i++ ) { View child = getChildAt( i ); int left = child.getLeft(); int right = child.getRight(); int top = child.getTop(); int bottom = child.getBottom(); viewRect.set( left, top, right, bottom ); viewRect.offset( -mCurrentX, 0 ); if ( viewRect.contains( (int) e.getX(), (int) e.getY() ) ) { return child; } } return null; } @Override public int getMinX() { return mMinX; } @Override public int getMaxX() { return Integer.MAX_VALUE; } public void setDragTolerance( int value ) { mDragTolerance = value; } public void setGravity( int mode ) { mAlignMode = mode; } public int getGravity() { return mAlignMode; } }