michael@0: /* michael@0: * Copyright (C) 2013 Lucas Rocha michael@0: * michael@0: * This code is based on bits and pieces of Android's AbsListView, michael@0: * Listview, and StaggeredGridView. michael@0: * michael@0: * Copyright (C) 2012 The Android Open Source Project michael@0: * michael@0: * Licensed under the Apache License, Version 2.0 (the "License"); michael@0: * you may not use this file except in compliance with the License. michael@0: * You may obtain a copy of the License at michael@0: * michael@0: * http://www.apache.org/licenses/LICENSE-2.0 michael@0: * michael@0: * Unless required by applicable law or agreed to in writing, software michael@0: * distributed under the License is distributed on an "AS IS" BASIS, michael@0: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. michael@0: * See the License for the specific language governing permissions and michael@0: * limitations under the License. michael@0: */ michael@0: michael@0: package org.mozilla.gecko.widget; michael@0: michael@0: import org.mozilla.gecko.R; michael@0: michael@0: import android.annotation.TargetApi; michael@0: import android.content.Context; michael@0: import android.content.res.TypedArray; michael@0: import android.database.DataSetObserver; michael@0: import android.graphics.Canvas; michael@0: import android.graphics.Rect; michael@0: import android.graphics.drawable.Drawable; michael@0: import android.graphics.drawable.TransitionDrawable; michael@0: import android.os.Build; michael@0: import android.os.Bundle; michael@0: import android.os.Parcel; michael@0: import android.os.Parcelable; michael@0: import android.os.SystemClock; michael@0: import android.support.v4.util.LongSparseArray; michael@0: import android.support.v4.util.SparseArrayCompat; michael@0: import android.support.v4.view.AccessibilityDelegateCompat; michael@0: import android.support.v4.view.KeyEventCompat; michael@0: import android.support.v4.view.MotionEventCompat; michael@0: import android.support.v4.view.VelocityTrackerCompat; michael@0: import android.support.v4.view.ViewCompat; michael@0: import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; michael@0: import android.support.v4.widget.EdgeEffectCompat; michael@0: import android.util.AttributeSet; michael@0: import android.util.Log; michael@0: import android.util.SparseBooleanArray; michael@0: import android.view.ContextMenu.ContextMenuInfo; michael@0: import android.view.FocusFinder; michael@0: import android.view.HapticFeedbackConstants; michael@0: import android.view.KeyEvent; michael@0: import android.view.MotionEvent; michael@0: import android.view.SoundEffectConstants; michael@0: import android.view.VelocityTracker; michael@0: import android.view.View; michael@0: import android.view.ViewConfiguration; michael@0: import android.view.ViewGroup; michael@0: import android.view.ViewParent; michael@0: import android.view.ViewTreeObserver; michael@0: import android.view.accessibility.AccessibilityEvent; michael@0: import android.view.accessibility.AccessibilityNodeInfo; michael@0: import android.widget.AdapterView; michael@0: import android.widget.Checkable; michael@0: import android.widget.ListAdapter; michael@0: import android.widget.Scroller; michael@0: michael@0: import java.util.ArrayList; michael@0: import java.util.List; michael@0: michael@0: /* michael@0: * Implementation Notes: michael@0: * michael@0: * Some terminology: michael@0: * michael@0: * index - index of the items that are currently visible michael@0: * position - index of the items in the cursor michael@0: * michael@0: * Given the bi-directional nature of this view, the source code michael@0: * usually names variables with 'start' to mean 'top' or 'left'; and michael@0: * 'end' to mean 'bottom' or 'right', depending on the current michael@0: * orientation of the widget. michael@0: */ michael@0: michael@0: /** michael@0: * A view that shows items in a vertical or horizontal scrolling list. michael@0: * The items come from the {@link ListAdapter} associated with this view. michael@0: */ michael@0: public class TwoWayView extends AdapterView implements michael@0: ViewTreeObserver.OnTouchModeChangeListener { michael@0: private static final String LOGTAG = "TwoWayView"; michael@0: michael@0: private static final int NO_POSITION = -1; michael@0: private static final int INVALID_POINTER = -1; michael@0: michael@0: public static final int[] STATE_NOTHING = new int[] { 0 }; michael@0: michael@0: private static final int TOUCH_MODE_REST = -1; michael@0: private static final int TOUCH_MODE_DOWN = 0; michael@0: private static final int TOUCH_MODE_TAP = 1; michael@0: private static final int TOUCH_MODE_DONE_WAITING = 2; michael@0: private static final int TOUCH_MODE_DRAGGING = 3; michael@0: private static final int TOUCH_MODE_FLINGING = 4; michael@0: private static final int TOUCH_MODE_OVERSCROLL = 5; michael@0: michael@0: private static final int TOUCH_MODE_UNKNOWN = -1; michael@0: private static final int TOUCH_MODE_ON = 0; michael@0: private static final int TOUCH_MODE_OFF = 1; michael@0: michael@0: private static final int LAYOUT_NORMAL = 0; michael@0: private static final int LAYOUT_FORCE_TOP = 1; michael@0: private static final int LAYOUT_SET_SELECTION = 2; michael@0: private static final int LAYOUT_FORCE_BOTTOM = 3; michael@0: private static final int LAYOUT_SPECIFIC = 4; michael@0: private static final int LAYOUT_SYNC = 5; michael@0: private static final int LAYOUT_MOVE_SELECTION = 6; michael@0: michael@0: private static final int SYNC_SELECTED_POSITION = 0; michael@0: private static final int SYNC_FIRST_POSITION = 1; michael@0: michael@0: private static final int SYNC_MAX_DURATION_MILLIS = 100; michael@0: michael@0: private static final int CHECK_POSITION_SEARCH_DISTANCE = 20; michael@0: michael@0: private static final float MAX_SCROLL_FACTOR = 0.33f; michael@0: michael@0: private static final int MIN_SCROLL_PREVIEW_PIXELS = 10; michael@0: michael@0: public static enum ChoiceMode { michael@0: NONE, michael@0: SINGLE, michael@0: MULTIPLE michael@0: } michael@0: michael@0: public static enum Orientation { michael@0: HORIZONTAL, michael@0: VERTICAL; michael@0: }; michael@0: michael@0: private ListAdapter mAdapter; michael@0: michael@0: private boolean mIsVertical; michael@0: michael@0: private int mItemMargin; michael@0: michael@0: private boolean mInLayout; michael@0: private boolean mBlockLayoutRequests; michael@0: michael@0: private boolean mIsAttached; michael@0: michael@0: private final RecycleBin mRecycler; michael@0: private AdapterDataSetObserver mDataSetObserver; michael@0: michael@0: private boolean mItemsCanFocus; michael@0: michael@0: final boolean[] mIsScrap = new boolean[1]; michael@0: michael@0: private boolean mDataChanged; michael@0: private int mItemCount; michael@0: private int mOldItemCount; michael@0: private boolean mHasStableIds; michael@0: private boolean mAreAllItemsSelectable; michael@0: michael@0: private int mFirstPosition; michael@0: private int mSpecificStart; michael@0: michael@0: private SavedState mPendingSync; michael@0: michael@0: private final int mTouchSlop; michael@0: private final int mMaximumVelocity; michael@0: private final int mFlingVelocity; michael@0: private float mLastTouchPos; michael@0: private float mTouchRemainderPos; michael@0: private int mActivePointerId; michael@0: michael@0: private final Rect mTempRect; michael@0: michael@0: private final ArrowScrollFocusResult mArrowScrollFocusResult; michael@0: michael@0: private Rect mTouchFrame; michael@0: private int mMotionPosition; michael@0: private CheckForTap mPendingCheckForTap; michael@0: private CheckForLongPress mPendingCheckForLongPress; michael@0: private CheckForKeyLongPress mPendingCheckForKeyLongPress; michael@0: private PerformClick mPerformClick; michael@0: private Runnable mTouchModeReset; michael@0: private int mResurrectToPosition; michael@0: michael@0: private boolean mIsChildViewEnabled; michael@0: michael@0: private boolean mDrawSelectorOnTop; michael@0: private Drawable mSelector; michael@0: private int mSelectorPosition; michael@0: private final Rect mSelectorRect; michael@0: michael@0: private int mOverScroll; michael@0: private final int mOverscrollDistance; michael@0: michael@0: private boolean mDesiredFocusableState; michael@0: private boolean mDesiredFocusableInTouchModeState; michael@0: michael@0: private SelectionNotifier mSelectionNotifier; michael@0: michael@0: private boolean mNeedSync; michael@0: private int mSyncMode; michael@0: private int mSyncPosition; michael@0: private long mSyncRowId; michael@0: private long mSyncHeight; michael@0: private int mSelectedStart; michael@0: michael@0: private int mNextSelectedPosition; michael@0: private long mNextSelectedRowId; michael@0: private int mSelectedPosition; michael@0: private long mSelectedRowId; michael@0: private int mOldSelectedPosition; michael@0: private long mOldSelectedRowId; michael@0: michael@0: private ChoiceMode mChoiceMode; michael@0: private int mCheckedItemCount; michael@0: private SparseBooleanArray mCheckStates; michael@0: LongSparseArray mCheckedIdStates; michael@0: michael@0: private ContextMenuInfo mContextMenuInfo; michael@0: michael@0: private int mLayoutMode; michael@0: private int mTouchMode; michael@0: private int mLastTouchMode; michael@0: private VelocityTracker mVelocityTracker; michael@0: private final Scroller mScroller; michael@0: michael@0: private EdgeEffectCompat mStartEdge; michael@0: private EdgeEffectCompat mEndEdge; michael@0: michael@0: private OnScrollListener mOnScrollListener; michael@0: private int mLastScrollState; michael@0: michael@0: private View mEmptyView; michael@0: michael@0: private ListItemAccessibilityDelegate mAccessibilityDelegate; michael@0: michael@0: private int mLastAccessibilityScrollEventFromIndex; michael@0: private int mLastAccessibilityScrollEventToIndex; michael@0: michael@0: public interface OnScrollListener { michael@0: michael@0: /** michael@0: * The view is not scrolling. Note navigating the list using the trackball counts as michael@0: * being in the idle state since these transitions are not animated. michael@0: */ michael@0: public static int SCROLL_STATE_IDLE = 0; michael@0: michael@0: /** michael@0: * The user is scrolling using touch, and their finger is still on the screen michael@0: */ michael@0: public static int SCROLL_STATE_TOUCH_SCROLL = 1; michael@0: michael@0: /** michael@0: * The user had previously been scrolling using touch and had performed a fling. The michael@0: * animation is now coasting to a stop michael@0: */ michael@0: public static int SCROLL_STATE_FLING = 2; michael@0: michael@0: /** michael@0: * Callback method to be invoked while the list view or grid view is being scrolled. If the michael@0: * view is being scrolled, this method will be called before the next frame of the scroll is michael@0: * rendered. In particular, it will be called before any calls to michael@0: * {@link Adapter#getView(int, View, ViewGroup)}. michael@0: * michael@0: * @param view The view whose scroll state is being reported michael@0: * michael@0: * @param scrollState The current scroll state. One of {@link #SCROLL_STATE_IDLE}, michael@0: * {@link #SCROLL_STATE_TOUCH_SCROLL} or {@link #SCROLL_STATE_IDLE}. michael@0: */ michael@0: public void onScrollStateChanged(TwoWayView view, int scrollState); michael@0: michael@0: /** michael@0: * Callback method to be invoked when the list or grid has been scrolled. This will be michael@0: * called after the scroll has completed michael@0: * @param view The view whose scroll state is being reported michael@0: * @param firstVisibleItem the index of the first visible cell (ignore if michael@0: * visibleItemCount == 0) michael@0: * @param visibleItemCount the number of visible cells michael@0: * @param totalItemCount the number of items in the list adaptor michael@0: */ michael@0: public void onScroll(TwoWayView view, int firstVisibleItem, int visibleItemCount, michael@0: int totalItemCount); michael@0: } michael@0: michael@0: /** michael@0: * A RecyclerListener is used to receive a notification whenever a View is placed michael@0: * inside the RecycleBin's scrap heap. This listener is used to free resources michael@0: * associated to Views placed in the RecycleBin. michael@0: * michael@0: * @see TwoWayView.RecycleBin michael@0: * @see TwoWayView#setRecyclerListener(TwoWayView.RecyclerListener) michael@0: */ michael@0: public static interface RecyclerListener { michael@0: /** michael@0: * Indicates that the specified View was moved into the recycler's scrap heap. michael@0: * The view is not displayed on screen any more and any expensive resource michael@0: * associated with the view should be discarded. michael@0: * michael@0: * @param view michael@0: */ michael@0: void onMovedToScrapHeap(View view); michael@0: } michael@0: michael@0: public TwoWayView(Context context) { michael@0: this(context, null); michael@0: } michael@0: michael@0: public TwoWayView(Context context, AttributeSet attrs) { michael@0: this(context, attrs, 0); michael@0: } michael@0: michael@0: public TwoWayView(Context context, AttributeSet attrs, int defStyle) { michael@0: super(context, attrs, defStyle); michael@0: michael@0: mNeedSync = false; michael@0: mVelocityTracker = null; michael@0: michael@0: mLayoutMode = LAYOUT_NORMAL; michael@0: mTouchMode = TOUCH_MODE_REST; michael@0: mLastTouchMode = TOUCH_MODE_UNKNOWN; michael@0: michael@0: mIsAttached = false; michael@0: michael@0: mContextMenuInfo = null; michael@0: michael@0: mOnScrollListener = null; michael@0: mLastScrollState = OnScrollListener.SCROLL_STATE_IDLE; michael@0: michael@0: final ViewConfiguration vc = ViewConfiguration.get(context); michael@0: mTouchSlop = vc.getScaledTouchSlop(); michael@0: mMaximumVelocity = vc.getScaledMaximumFlingVelocity(); michael@0: mFlingVelocity = vc.getScaledMinimumFlingVelocity(); michael@0: mOverscrollDistance = getScaledOverscrollDistance(vc); michael@0: michael@0: mOverScroll = 0; michael@0: michael@0: mScroller = new Scroller(context); michael@0: michael@0: mIsVertical = true; michael@0: michael@0: mItemsCanFocus = false; michael@0: michael@0: mTempRect = new Rect(); michael@0: michael@0: mArrowScrollFocusResult = new ArrowScrollFocusResult(); michael@0: michael@0: mSelectorPosition = INVALID_POSITION; michael@0: michael@0: mSelectorRect = new Rect(); michael@0: mSelectedStart = 0; michael@0: michael@0: mResurrectToPosition = INVALID_POSITION; michael@0: michael@0: mSelectedStart = 0; michael@0: mNextSelectedPosition = INVALID_POSITION; michael@0: mNextSelectedRowId = INVALID_ROW_ID; michael@0: mSelectedPosition = INVALID_POSITION; michael@0: mSelectedRowId = INVALID_ROW_ID; michael@0: mOldSelectedPosition = INVALID_POSITION; michael@0: mOldSelectedRowId = INVALID_ROW_ID; michael@0: michael@0: mChoiceMode = ChoiceMode.NONE; michael@0: mCheckedItemCount = 0; michael@0: mCheckedIdStates = null; michael@0: mCheckStates = null; michael@0: michael@0: mRecycler = new RecycleBin(); michael@0: mDataSetObserver = null; michael@0: michael@0: mAreAllItemsSelectable = true; michael@0: michael@0: mStartEdge = null; michael@0: mEndEdge = null; michael@0: michael@0: setClickable(true); michael@0: setFocusableInTouchMode(true); michael@0: setWillNotDraw(false); michael@0: setAlwaysDrawnWithCacheEnabled(false); michael@0: setWillNotDraw(false); michael@0: setClipToPadding(false); michael@0: michael@0: ViewCompat.setOverScrollMode(this, ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS); michael@0: michael@0: TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TwoWayView, defStyle, 0); michael@0: initializeScrollbars(a); michael@0: michael@0: mDrawSelectorOnTop = a.getBoolean( michael@0: R.styleable.TwoWayView_android_drawSelectorOnTop, false); michael@0: michael@0: Drawable d = a.getDrawable(R.styleable.TwoWayView_android_listSelector); michael@0: if (d != null) { michael@0: setSelector(d); michael@0: } michael@0: michael@0: int orientation = a.getInt(R.styleable.TwoWayView_android_orientation, -1); michael@0: if (orientation >= 0) { michael@0: setOrientation(Orientation.values()[orientation]); michael@0: } michael@0: michael@0: int choiceMode = a.getInt(R.styleable.TwoWayView_android_choiceMode, -1); michael@0: if (choiceMode >= 0) { michael@0: setChoiceMode(ChoiceMode.values()[choiceMode]); michael@0: } michael@0: michael@0: a.recycle(); michael@0: michael@0: updateScrollbarsDirection(); michael@0: } michael@0: michael@0: public void setOrientation(Orientation orientation) { michael@0: final boolean isVertical = (orientation.compareTo(Orientation.VERTICAL) == 0); michael@0: if (mIsVertical == isVertical) { michael@0: return; michael@0: } michael@0: michael@0: mIsVertical = isVertical; michael@0: michael@0: updateScrollbarsDirection(); michael@0: resetState(); michael@0: mRecycler.clear(); michael@0: michael@0: requestLayout(); michael@0: } michael@0: michael@0: public Orientation getOrientation() { michael@0: return (mIsVertical ? Orientation.VERTICAL : Orientation.HORIZONTAL); michael@0: } michael@0: michael@0: public void setItemMargin(int itemMargin) { michael@0: if (mItemMargin == itemMargin) { michael@0: return; michael@0: } michael@0: michael@0: mItemMargin = itemMargin; michael@0: requestLayout(); michael@0: } michael@0: michael@0: public int getItemMargin() { michael@0: return mItemMargin; michael@0: } michael@0: michael@0: /** michael@0: * Indicates that the views created by the ListAdapter can contain focusable michael@0: * items. michael@0: * michael@0: * @param itemsCanFocus true if items can get focus, false otherwise michael@0: */ michael@0: public void setItemsCanFocus(boolean itemsCanFocus) { michael@0: mItemsCanFocus = itemsCanFocus; michael@0: if (!itemsCanFocus) { michael@0: setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * @return Whether the views created by the ListAdapter can contain focusable michael@0: * items. michael@0: */ michael@0: public boolean getItemsCanFocus() { michael@0: return mItemsCanFocus; michael@0: } michael@0: michael@0: /** michael@0: * Set the listener that will receive notifications every time the list scrolls. michael@0: * michael@0: * @param l the scroll listener michael@0: */ michael@0: public void setOnScrollListener(OnScrollListener l) { michael@0: mOnScrollListener = l; michael@0: invokeOnItemScrollListener(); michael@0: } michael@0: michael@0: /** michael@0: * Sets the recycler listener to be notified whenever a View is set aside in michael@0: * the recycler for later reuse. This listener can be used to free resources michael@0: * associated to the View. michael@0: * michael@0: * @param listener The recycler listener to be notified of views set aside michael@0: * in the recycler. michael@0: * michael@0: * @see TwoWayView.RecycleBin michael@0: * @see TwoWayView.RecyclerListener michael@0: */ michael@0: public void setRecyclerListener(RecyclerListener l) { michael@0: mRecycler.mRecyclerListener = l; michael@0: } michael@0: michael@0: /** michael@0: * Controls whether the selection highlight drawable should be drawn on top of the item or michael@0: * behind it. michael@0: * michael@0: * @param onTop If true, the selector will be drawn on the item it is highlighting. The default michael@0: * is false. michael@0: * michael@0: * @attr ref android.R.styleable#AbsListView_drawSelectorOnTop michael@0: */ michael@0: public void setDrawSelectorOnTop(boolean drawSelectorOnTop) { michael@0: mDrawSelectorOnTop = drawSelectorOnTop; michael@0: } michael@0: michael@0: /** michael@0: * Set a Drawable that should be used to highlight the currently selected item. michael@0: * michael@0: * @param resID A Drawable resource to use as the selection highlight. michael@0: * michael@0: * @attr ref android.R.styleable#AbsListView_listSelector michael@0: */ michael@0: public void setSelector(int resID) { michael@0: setSelector(getResources().getDrawable(resID)); michael@0: } michael@0: michael@0: /** michael@0: * Set a Drawable that should be used to highlight the currently selected item. michael@0: * michael@0: * @param selector A Drawable to use as the selection highlight. michael@0: * michael@0: * @attr ref android.R.styleable#AbsListView_listSelector michael@0: */ michael@0: public void setSelector(Drawable selector) { michael@0: if (mSelector != null) { michael@0: mSelector.setCallback(null); michael@0: unscheduleDrawable(mSelector); michael@0: } michael@0: michael@0: mSelector = selector; michael@0: Rect padding = new Rect(); michael@0: selector.getPadding(padding); michael@0: michael@0: selector.setCallback(this); michael@0: updateSelectorState(); michael@0: } michael@0: michael@0: /** michael@0: * Returns the selector {@link android.graphics.drawable.Drawable} that is used to draw the michael@0: * selection in the list. michael@0: * michael@0: * @return the drawable used to display the selector michael@0: */ michael@0: public Drawable getSelector() { michael@0: return mSelector; michael@0: } michael@0: michael@0: /** michael@0: * {@inheritDoc} michael@0: */ michael@0: @Override michael@0: public int getSelectedItemPosition() { michael@0: return mNextSelectedPosition; michael@0: } michael@0: michael@0: /** michael@0: * {@inheritDoc} michael@0: */ michael@0: @Override michael@0: public long getSelectedItemId() { michael@0: return mNextSelectedRowId; michael@0: } michael@0: michael@0: /** michael@0: * Returns the number of items currently selected. This will only be valid michael@0: * if the choice mode is not {@link #CHOICE_MODE_NONE} (default). michael@0: * michael@0: *

To determine the specific items that are currently selected, use one of michael@0: * the getChecked* methods. michael@0: * michael@0: * @return The number of items currently selected michael@0: * michael@0: * @see #getCheckedItemPosition() michael@0: * @see #getCheckedItemPositions() michael@0: * @see #getCheckedItemIds() michael@0: */ michael@0: public int getCheckedItemCount() { michael@0: return mCheckedItemCount; michael@0: } michael@0: michael@0: /** michael@0: * Returns the checked state of the specified position. The result is only michael@0: * valid if the choice mode has been set to {@link #CHOICE_MODE_SINGLE} michael@0: * or {@link #CHOICE_MODE_MULTIPLE}. michael@0: * michael@0: * @param position The item whose checked state to return michael@0: * @return The item's checked state or false if choice mode michael@0: * is invalid michael@0: * michael@0: * @see #setChoiceMode(int) michael@0: */ michael@0: public boolean isItemChecked(int position) { michael@0: if (mChoiceMode.compareTo(ChoiceMode.NONE) == 0 && mCheckStates != null) { michael@0: return mCheckStates.get(position); michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: michael@0: /** michael@0: * Returns the currently checked item. The result is only valid if the choice michael@0: * mode has been set to {@link #CHOICE_MODE_SINGLE}. michael@0: * michael@0: * @return The position of the currently checked item or michael@0: * {@link #INVALID_POSITION} if nothing is selected michael@0: * michael@0: * @see #setChoiceMode(int) michael@0: */ michael@0: public int getCheckedItemPosition() { michael@0: if (mChoiceMode.compareTo(ChoiceMode.SINGLE) == 0 && michael@0: mCheckStates != null && mCheckStates.size() == 1) { michael@0: return mCheckStates.keyAt(0); michael@0: } michael@0: michael@0: return INVALID_POSITION; michael@0: } michael@0: michael@0: /** michael@0: * Returns the set of checked items in the list. The result is only valid if michael@0: * the choice mode has not been set to {@link #CHOICE_MODE_NONE}. michael@0: * michael@0: * @return A SparseBooleanArray which will return true for each call to michael@0: * get(int position) where position is a position in the list, michael@0: * or null if the choice mode is set to michael@0: * {@link #CHOICE_MODE_NONE}. michael@0: */ michael@0: public SparseBooleanArray getCheckedItemPositions() { michael@0: if (mChoiceMode.compareTo(ChoiceMode.NONE) != 0) { michael@0: return mCheckStates; michael@0: } michael@0: michael@0: return null; michael@0: } michael@0: michael@0: /** michael@0: * Returns the set of checked items ids. The result is only valid if the michael@0: * choice mode has not been set to {@link #CHOICE_MODE_NONE} and the adapter michael@0: * has stable IDs. ({@link ListAdapter#hasStableIds()} == {@code true}) michael@0: * michael@0: * @return A new array which contains the id of each checked item in the michael@0: * list. michael@0: */ michael@0: public long[] getCheckedItemIds() { michael@0: if (mChoiceMode.compareTo(ChoiceMode.NONE) == 0 || michael@0: mCheckedIdStates == null || mAdapter == null) { michael@0: return new long[0]; michael@0: } michael@0: michael@0: final LongSparseArray idStates = mCheckedIdStates; michael@0: final int count = idStates.size(); michael@0: final long[] ids = new long[count]; michael@0: michael@0: for (int i = 0; i < count; i++) { michael@0: ids[i] = idStates.keyAt(i); michael@0: } michael@0: michael@0: return ids; michael@0: } michael@0: michael@0: /** michael@0: * Sets the checked state of the specified position. The is only valid if michael@0: * the choice mode has been set to {@link #CHOICE_MODE_SINGLE} or michael@0: * {@link #CHOICE_MODE_MULTIPLE}. michael@0: * michael@0: * @param position The item whose checked state is to be checked michael@0: * @param value The new checked state for the item michael@0: */ michael@0: public void setItemChecked(int position, boolean value) { michael@0: if (mChoiceMode.compareTo(ChoiceMode.NONE) == 0) { michael@0: return; michael@0: } michael@0: michael@0: if (mChoiceMode.compareTo(ChoiceMode.MULTIPLE) == 0) { michael@0: boolean oldValue = mCheckStates.get(position); michael@0: mCheckStates.put(position, value); michael@0: michael@0: if (mCheckedIdStates != null && mAdapter.hasStableIds()) { michael@0: if (value) { michael@0: mCheckedIdStates.put(mAdapter.getItemId(position), position); michael@0: } else { michael@0: mCheckedIdStates.delete(mAdapter.getItemId(position)); michael@0: } michael@0: } michael@0: michael@0: if (oldValue != value) { michael@0: if (value) { michael@0: mCheckedItemCount++; michael@0: } else { michael@0: mCheckedItemCount--; michael@0: } michael@0: } michael@0: } else { michael@0: boolean updateIds = mCheckedIdStates != null && mAdapter.hasStableIds(); michael@0: michael@0: // Clear all values if we're checking something, or unchecking the currently michael@0: // selected item michael@0: if (value || isItemChecked(position)) { michael@0: mCheckStates.clear(); michael@0: michael@0: if (updateIds) { michael@0: mCheckedIdStates.clear(); michael@0: } michael@0: } michael@0: michael@0: // This may end up selecting the value we just cleared but this way michael@0: // we ensure length of mCheckStates is 1, a fact getCheckedItemPosition relies on michael@0: if (value) { michael@0: mCheckStates.put(position, true); michael@0: michael@0: if (updateIds) { michael@0: mCheckedIdStates.put(mAdapter.getItemId(position), position); michael@0: } michael@0: michael@0: mCheckedItemCount = 1; michael@0: } else if (mCheckStates.size() == 0 || !mCheckStates.valueAt(0)) { michael@0: mCheckedItemCount = 0; michael@0: } michael@0: } michael@0: michael@0: // Do not generate a data change while we are in the layout phase michael@0: if (!mInLayout && !mBlockLayoutRequests) { michael@0: mDataChanged = true; michael@0: rememberSyncState(); michael@0: requestLayout(); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Clear any choices previously set michael@0: */ michael@0: public void clearChoices() { michael@0: if (mCheckStates != null) { michael@0: mCheckStates.clear(); michael@0: } michael@0: michael@0: if (mCheckedIdStates != null) { michael@0: mCheckedIdStates.clear(); michael@0: } michael@0: michael@0: mCheckedItemCount = 0; michael@0: } michael@0: michael@0: /** michael@0: * @see #setChoiceMode(int) michael@0: * michael@0: * @return The current choice mode michael@0: */ michael@0: public ChoiceMode getChoiceMode() { michael@0: return mChoiceMode; michael@0: } michael@0: michael@0: /** michael@0: * Defines the choice behavior for the List. By default, Lists do not have any choice behavior michael@0: * ({@link #CHOICE_MODE_NONE}). By setting the choiceMode to {@link #CHOICE_MODE_SINGLE}, the michael@0: * List allows up to one item to be in a chosen state. By setting the choiceMode to michael@0: * {@link #CHOICE_MODE_MULTIPLE}, the list allows any number of items to be chosen. michael@0: * michael@0: * @param choiceMode One of {@link #CHOICE_MODE_NONE}, {@link #CHOICE_MODE_SINGLE}, or michael@0: * {@link #CHOICE_MODE_MULTIPLE} michael@0: */ michael@0: public void setChoiceMode(ChoiceMode choiceMode) { michael@0: mChoiceMode = choiceMode; michael@0: michael@0: if (mChoiceMode.compareTo(ChoiceMode.NONE) != 0) { michael@0: if (mCheckStates == null) { michael@0: mCheckStates = new SparseBooleanArray(); michael@0: } michael@0: michael@0: if (mCheckedIdStates == null && mAdapter != null && mAdapter.hasStableIds()) { michael@0: mCheckedIdStates = new LongSparseArray(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public ListAdapter getAdapter() { michael@0: return mAdapter; michael@0: } michael@0: michael@0: @Override michael@0: public void setAdapter(ListAdapter adapter) { michael@0: if (mAdapter != null && mDataSetObserver != null) { michael@0: mAdapter.unregisterDataSetObserver(mDataSetObserver); michael@0: } michael@0: michael@0: resetState(); michael@0: mRecycler.clear(); michael@0: michael@0: mAdapter = adapter; michael@0: mDataChanged = true; michael@0: michael@0: mOldSelectedPosition = INVALID_POSITION; michael@0: mOldSelectedRowId = INVALID_ROW_ID; michael@0: michael@0: if (mCheckStates != null) { michael@0: mCheckStates.clear(); michael@0: } michael@0: michael@0: if (mCheckedIdStates != null) { michael@0: mCheckedIdStates.clear(); michael@0: } michael@0: michael@0: if (mAdapter != null) { michael@0: mOldItemCount = mItemCount; michael@0: mItemCount = adapter.getCount(); michael@0: michael@0: mDataSetObserver = new AdapterDataSetObserver(); michael@0: mAdapter.registerDataSetObserver(mDataSetObserver); michael@0: michael@0: mRecycler.setViewTypeCount(adapter.getViewTypeCount()); michael@0: michael@0: mHasStableIds = adapter.hasStableIds(); michael@0: mAreAllItemsSelectable = adapter.areAllItemsEnabled(); michael@0: michael@0: if (mChoiceMode.compareTo(ChoiceMode.NONE) != 0 && mHasStableIds && michael@0: mCheckedIdStates == null) { michael@0: mCheckedIdStates = new LongSparseArray(); michael@0: } michael@0: michael@0: final int position = lookForSelectablePosition(0); michael@0: setSelectedPositionInt(position); michael@0: setNextSelectedPositionInt(position); michael@0: michael@0: if (mItemCount == 0) { michael@0: checkSelectionChanged(); michael@0: } michael@0: } else { michael@0: mItemCount = 0; michael@0: mHasStableIds = false; michael@0: mAreAllItemsSelectable = true; michael@0: michael@0: checkSelectionChanged(); michael@0: } michael@0: michael@0: checkFocus(); michael@0: requestLayout(); michael@0: } michael@0: michael@0: @Override michael@0: public int getFirstVisiblePosition() { michael@0: return mFirstPosition; michael@0: } michael@0: michael@0: @Override michael@0: public int getLastVisiblePosition() { michael@0: return mFirstPosition + getChildCount() - 1; michael@0: } michael@0: michael@0: @Override michael@0: public int getCount() { michael@0: return mItemCount; michael@0: } michael@0: michael@0: @Override michael@0: public int getPositionForView(View view) { michael@0: View child = view; michael@0: try { michael@0: View v; michael@0: while (!(v = (View) child.getParent()).equals(this)) { michael@0: child = v; michael@0: } michael@0: } catch (ClassCastException e) { michael@0: // We made it up to the window without find this list view michael@0: return INVALID_POSITION; michael@0: } michael@0: michael@0: // Search the children for the list item michael@0: final int childCount = getChildCount(); michael@0: for (int i = 0; i < childCount; i++) { michael@0: if (getChildAt(i).equals(child)) { michael@0: return mFirstPosition + i; michael@0: } michael@0: } michael@0: michael@0: // Child not found! michael@0: return INVALID_POSITION; michael@0: } michael@0: michael@0: @Override michael@0: public void getFocusedRect(Rect r) { michael@0: View view = getSelectedView(); michael@0: michael@0: if (view != null && view.getParent() == this) { michael@0: // The focused rectangle of the selected view offset into the michael@0: // coordinate space of this view. michael@0: view.getFocusedRect(r); michael@0: offsetDescendantRectToMyCoords(view, r); michael@0: } else { michael@0: super.getFocusedRect(r); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { michael@0: super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); michael@0: michael@0: if (gainFocus && mSelectedPosition < 0 && !isInTouchMode()) { michael@0: if (!mIsAttached && mAdapter != null) { michael@0: // Data may have changed while we were detached and it's valid michael@0: // to change focus while detached. Refresh so we don't die. michael@0: mDataChanged = true; michael@0: mOldItemCount = mItemCount; michael@0: mItemCount = mAdapter.getCount(); michael@0: } michael@0: michael@0: resurrectSelection(); michael@0: } michael@0: michael@0: final ListAdapter adapter = mAdapter; michael@0: int closetChildIndex = INVALID_POSITION; michael@0: int closestChildStart = 0; michael@0: michael@0: if (adapter != null && gainFocus && previouslyFocusedRect != null) { michael@0: previouslyFocusedRect.offset(getScrollX(), getScrollY()); michael@0: michael@0: // Don't cache the result of getChildCount or mFirstPosition here, michael@0: // it could change in layoutChildren. michael@0: if (adapter.getCount() < getChildCount() + mFirstPosition) { michael@0: mLayoutMode = LAYOUT_NORMAL; michael@0: layoutChildren(); michael@0: } michael@0: michael@0: // Figure out which item should be selected based on previously michael@0: // focused rect. michael@0: Rect otherRect = mTempRect; michael@0: int minDistance = Integer.MAX_VALUE; michael@0: final int childCount = getChildCount(); michael@0: final int firstPosition = mFirstPosition; michael@0: michael@0: for (int i = 0; i < childCount; i++) { michael@0: // Only consider selectable views michael@0: if (!adapter.isEnabled(firstPosition + i)) { michael@0: continue; michael@0: } michael@0: michael@0: View other = getChildAt(i); michael@0: other.getDrawingRect(otherRect); michael@0: offsetDescendantRectToMyCoords(other, otherRect); michael@0: int distance = getDistance(previouslyFocusedRect, otherRect, direction); michael@0: michael@0: if (distance < minDistance) { michael@0: minDistance = distance; michael@0: closetChildIndex = i; michael@0: closestChildStart = (mIsVertical ? other.getTop() : other.getLeft()); michael@0: } michael@0: } michael@0: } michael@0: michael@0: if (closetChildIndex >= 0) { michael@0: setSelectionFromOffset(closetChildIndex + mFirstPosition, closestChildStart); michael@0: } else { michael@0: requestLayout(); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: protected void onAttachedToWindow() { michael@0: super.onAttachedToWindow(); michael@0: michael@0: final ViewTreeObserver treeObserver = getViewTreeObserver(); michael@0: treeObserver.addOnTouchModeChangeListener(this); michael@0: michael@0: if (mAdapter != null && mDataSetObserver == null) { michael@0: mDataSetObserver = new AdapterDataSetObserver(); michael@0: mAdapter.registerDataSetObserver(mDataSetObserver); michael@0: michael@0: // Data may have changed while we were detached. Refresh. michael@0: mDataChanged = true; michael@0: mOldItemCount = mItemCount; michael@0: mItemCount = mAdapter.getCount(); michael@0: } michael@0: michael@0: mIsAttached = true; michael@0: } michael@0: michael@0: @Override michael@0: protected void onDetachedFromWindow() { michael@0: super.onDetachedFromWindow(); michael@0: michael@0: // Detach any view left in the scrap heap michael@0: mRecycler.clear(); michael@0: michael@0: final ViewTreeObserver treeObserver = getViewTreeObserver(); michael@0: treeObserver.removeOnTouchModeChangeListener(this); michael@0: michael@0: if (mAdapter != null) { michael@0: mAdapter.unregisterDataSetObserver(mDataSetObserver); michael@0: mDataSetObserver = null; michael@0: } michael@0: michael@0: if (mPerformClick != null) { michael@0: removeCallbacks(mPerformClick); michael@0: } michael@0: michael@0: if (mTouchModeReset != null) { michael@0: removeCallbacks(mTouchModeReset); michael@0: mTouchModeReset.run(); michael@0: } michael@0: michael@0: mIsAttached = false; michael@0: } michael@0: michael@0: @Override michael@0: public void onWindowFocusChanged(boolean hasWindowFocus) { michael@0: super.onWindowFocusChanged(hasWindowFocus); michael@0: michael@0: final int touchMode = isInTouchMode() ? TOUCH_MODE_ON : TOUCH_MODE_OFF; michael@0: michael@0: if (!hasWindowFocus) { michael@0: if (touchMode == TOUCH_MODE_OFF) { michael@0: // Remember the last selected element michael@0: mResurrectToPosition = mSelectedPosition; michael@0: } michael@0: } else { michael@0: // If we changed touch mode since the last time we had focus michael@0: if (touchMode != mLastTouchMode && mLastTouchMode != TOUCH_MODE_UNKNOWN) { michael@0: // If we come back in trackball mode, we bring the selection back michael@0: if (touchMode == TOUCH_MODE_OFF) { michael@0: // This will trigger a layout michael@0: resurrectSelection(); michael@0: michael@0: // If we come back in touch mode, then we want to hide the selector michael@0: } else { michael@0: hideSelector(); michael@0: mLayoutMode = LAYOUT_NORMAL; michael@0: layoutChildren(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: mLastTouchMode = touchMode; michael@0: } michael@0: michael@0: @Override michael@0: protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) { michael@0: boolean needsInvalidate = false; michael@0: michael@0: if (mIsVertical && mOverScroll != scrollY) { michael@0: onScrollChanged(getScrollX(), scrollY, getScrollX(), mOverScroll); michael@0: mOverScroll = scrollY; michael@0: needsInvalidate = true; michael@0: } else if (!mIsVertical && mOverScroll != scrollX) { michael@0: onScrollChanged(scrollX, getScrollY(), mOverScroll, getScrollY()); michael@0: mOverScroll = scrollX; michael@0: needsInvalidate = true; michael@0: } michael@0: michael@0: if (needsInvalidate) { michael@0: invalidate(); michael@0: awakenScrollbarsInternal(); michael@0: } michael@0: } michael@0: michael@0: @TargetApi(9) michael@0: private boolean overScrollByInternal(int deltaX, int deltaY, michael@0: int scrollX, int scrollY, michael@0: int scrollRangeX, int scrollRangeY, michael@0: int maxOverScrollX, int maxOverScrollY, michael@0: boolean isTouchEvent) { michael@0: if (Build.VERSION.SDK_INT < 9) { michael@0: return false; michael@0: } michael@0: michael@0: return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, michael@0: scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent); michael@0: } michael@0: michael@0: @Override michael@0: @TargetApi(9) michael@0: public void setOverScrollMode(int mode) { michael@0: if (Build.VERSION.SDK_INT < 9) { michael@0: return; michael@0: } michael@0: michael@0: if (mode != ViewCompat.OVER_SCROLL_NEVER) { michael@0: if (mStartEdge == null) { michael@0: Context context = getContext(); michael@0: michael@0: mStartEdge = new EdgeEffectCompat(context); michael@0: mEndEdge = new EdgeEffectCompat(context); michael@0: } michael@0: } else { michael@0: mStartEdge = null; michael@0: mEndEdge = null; michael@0: } michael@0: michael@0: super.setOverScrollMode(mode); michael@0: } michael@0: michael@0: public int pointToPosition(int x, int y) { michael@0: Rect frame = mTouchFrame; michael@0: if (frame == null) { michael@0: mTouchFrame = new Rect(); michael@0: frame = mTouchFrame; michael@0: } michael@0: michael@0: final int count = getChildCount(); michael@0: for (int i = count - 1; i >= 0; i--) { michael@0: final View child = getChildAt(i); michael@0: michael@0: if (child.getVisibility() == View.VISIBLE) { michael@0: child.getHitRect(frame); michael@0: michael@0: if (frame.contains(x, y)) { michael@0: return mFirstPosition + i; michael@0: } michael@0: } michael@0: } michael@0: return INVALID_POSITION; michael@0: } michael@0: michael@0: @Override michael@0: protected int computeVerticalScrollExtent() { michael@0: final int count = getChildCount(); michael@0: if (count == 0) { michael@0: return 0; michael@0: } michael@0: michael@0: int extent = count * 100; michael@0: michael@0: View child = getChildAt(0); michael@0: final int childTop = child.getTop(); michael@0: michael@0: int childHeight = child.getHeight(); michael@0: if (childHeight > 0) { michael@0: extent += (childTop * 100) / childHeight; michael@0: } michael@0: michael@0: child = getChildAt(count - 1); michael@0: final int childBottom = child.getBottom(); michael@0: michael@0: childHeight = child.getHeight(); michael@0: if (childHeight > 0) { michael@0: extent -= ((childBottom - getHeight()) * 100) / childHeight; michael@0: } michael@0: michael@0: return extent; michael@0: } michael@0: michael@0: @Override michael@0: protected int computeHorizontalScrollExtent() { michael@0: final int count = getChildCount(); michael@0: if (count == 0) { michael@0: return 0; michael@0: } michael@0: michael@0: int extent = count * 100; michael@0: michael@0: View child = getChildAt(0); michael@0: final int childLeft = child.getLeft(); michael@0: michael@0: int childWidth = child.getWidth(); michael@0: if (childWidth > 0) { michael@0: extent += (childLeft * 100) / childWidth; michael@0: } michael@0: michael@0: child = getChildAt(count - 1); michael@0: final int childRight = child.getRight(); michael@0: michael@0: childWidth = child.getWidth(); michael@0: if (childWidth > 0) { michael@0: extent -= ((childRight - getWidth()) * 100) / childWidth; michael@0: } michael@0: michael@0: return extent; michael@0: } michael@0: michael@0: @Override michael@0: protected int computeVerticalScrollOffset() { michael@0: final int firstPosition = mFirstPosition; michael@0: final int childCount = getChildCount(); michael@0: michael@0: if (firstPosition < 0 || childCount == 0) { michael@0: return 0; michael@0: } michael@0: michael@0: final View child = getChildAt(0); michael@0: final int childTop = child.getTop(); michael@0: michael@0: int childHeight = child.getHeight(); michael@0: if (childHeight > 0) { michael@0: return Math.max(firstPosition * 100 - (childTop * 100) / childHeight, 0); michael@0: } michael@0: michael@0: return 0; michael@0: } michael@0: michael@0: @Override michael@0: protected int computeHorizontalScrollOffset() { michael@0: final int firstPosition = mFirstPosition; michael@0: final int childCount = getChildCount(); michael@0: michael@0: if (firstPosition < 0 || childCount == 0) { michael@0: return 0; michael@0: } michael@0: michael@0: final View child = getChildAt(0); michael@0: final int childLeft = child.getLeft(); michael@0: michael@0: int childWidth = child.getWidth(); michael@0: if (childWidth > 0) { michael@0: return Math.max(firstPosition * 100 - (childLeft * 100) / childWidth, 0); michael@0: } michael@0: michael@0: return 0; michael@0: } michael@0: michael@0: @Override michael@0: protected int computeVerticalScrollRange() { michael@0: int result = Math.max(mItemCount * 100, 0); michael@0: michael@0: if (mIsVertical && mOverScroll != 0) { michael@0: // Compensate for overscroll michael@0: result += Math.abs((int) ((float) mOverScroll / getHeight() * mItemCount * 100)); michael@0: } michael@0: michael@0: return result; michael@0: } michael@0: michael@0: @Override michael@0: protected int computeHorizontalScrollRange() { michael@0: int result = Math.max(mItemCount * 100, 0); michael@0: michael@0: if (!mIsVertical && mOverScroll != 0) { michael@0: // Compensate for overscroll michael@0: result += Math.abs((int) ((float) mOverScroll / getWidth() * mItemCount * 100)); michael@0: } michael@0: michael@0: return result; michael@0: } michael@0: michael@0: @Override michael@0: public boolean showContextMenuForChild(View originalView) { michael@0: final int longPressPosition = getPositionForView(originalView); michael@0: if (longPressPosition >= 0) { michael@0: final long longPressId = mAdapter.getItemId(longPressPosition); michael@0: boolean handled = false; michael@0: michael@0: OnItemLongClickListener listener = getOnItemLongClickListener(); michael@0: if (listener != null) { michael@0: handled = listener.onItemLongClick(TwoWayView.this, originalView, michael@0: longPressPosition, longPressId); michael@0: } michael@0: michael@0: if (!handled) { michael@0: mContextMenuInfo = createContextMenuInfo( michael@0: getChildAt(longPressPosition - mFirstPosition), michael@0: longPressPosition, longPressId); michael@0: michael@0: handled = super.showContextMenuForChild(originalView); michael@0: } michael@0: michael@0: return handled; michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: michael@0: @Override michael@0: public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { michael@0: if (disallowIntercept) { michael@0: recycleVelocityTracker(); michael@0: } michael@0: michael@0: super.requestDisallowInterceptTouchEvent(disallowIntercept); michael@0: } michael@0: michael@0: @Override michael@0: public boolean onInterceptTouchEvent(MotionEvent ev) { michael@0: if (!mIsAttached) { michael@0: return false; michael@0: } michael@0: michael@0: final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; michael@0: switch (action) { michael@0: case MotionEvent.ACTION_DOWN: michael@0: initOrResetVelocityTracker(); michael@0: mVelocityTracker.addMovement(ev); michael@0: michael@0: mScroller.abortAnimation(); michael@0: michael@0: final float x = ev.getX(); michael@0: final float y = ev.getY(); michael@0: michael@0: mLastTouchPos = (mIsVertical ? y : x); michael@0: michael@0: final int motionPosition = findMotionRowOrColumn((int) mLastTouchPos); michael@0: michael@0: mActivePointerId = MotionEventCompat.getPointerId(ev, 0); michael@0: mTouchRemainderPos = 0; michael@0: michael@0: if (mTouchMode == TOUCH_MODE_FLINGING) { michael@0: return true; michael@0: } else if (motionPosition >= 0) { michael@0: mMotionPosition = motionPosition; michael@0: mTouchMode = TOUCH_MODE_DOWN; michael@0: } michael@0: michael@0: break; michael@0: michael@0: case MotionEvent.ACTION_MOVE: { michael@0: if (mTouchMode != TOUCH_MODE_DOWN) { michael@0: break; michael@0: } michael@0: michael@0: initVelocityTrackerIfNotExists(); michael@0: mVelocityTracker.addMovement(ev); michael@0: michael@0: final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId); michael@0: if (index < 0) { michael@0: Log.e(LOGTAG, "onInterceptTouchEvent could not find pointer with id " + michael@0: mActivePointerId + " - did TwoWayView receive an inconsistent " + michael@0: "event stream?"); michael@0: return false; michael@0: } michael@0: michael@0: final float pos; michael@0: if (mIsVertical) { michael@0: pos = MotionEventCompat.getY(ev, index); michael@0: } else { michael@0: pos = MotionEventCompat.getX(ev, index); michael@0: } michael@0: michael@0: final float diff = pos - mLastTouchPos + mTouchRemainderPos; michael@0: final int delta = (int) diff; michael@0: mTouchRemainderPos = diff - delta; michael@0: michael@0: if (maybeStartScrolling(delta)) { michael@0: return true; michael@0: } michael@0: michael@0: break; michael@0: } michael@0: michael@0: case MotionEvent.ACTION_CANCEL: michael@0: case MotionEvent.ACTION_UP: michael@0: mActivePointerId = INVALID_POINTER; michael@0: mTouchMode = TOUCH_MODE_REST; michael@0: recycleVelocityTracker(); michael@0: reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); michael@0: michael@0: break; michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: michael@0: @Override michael@0: public boolean onTouchEvent(MotionEvent ev) { michael@0: if (!isEnabled()) { michael@0: // A disabled view that is clickable still consumes the touch michael@0: // events, it just doesn't respond to them. michael@0: return isClickable() || isLongClickable(); michael@0: } michael@0: michael@0: if (!mIsAttached) { michael@0: return false; michael@0: } michael@0: michael@0: boolean needsInvalidate = false; michael@0: michael@0: initVelocityTrackerIfNotExists(); michael@0: mVelocityTracker.addMovement(ev); michael@0: michael@0: final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; michael@0: switch (action) { michael@0: case MotionEvent.ACTION_DOWN: { michael@0: if (mDataChanged) { michael@0: break; michael@0: } michael@0: michael@0: mVelocityTracker.clear(); michael@0: mScroller.abortAnimation(); michael@0: michael@0: final float x = ev.getX(); michael@0: final float y = ev.getY(); michael@0: michael@0: mLastTouchPos = (mIsVertical ? y : x); michael@0: michael@0: int motionPosition = pointToPosition((int) x, (int) y); michael@0: michael@0: mActivePointerId = MotionEventCompat.getPointerId(ev, 0); michael@0: mTouchRemainderPos = 0; michael@0: michael@0: if (mDataChanged) { michael@0: break; michael@0: } michael@0: michael@0: if (mTouchMode == TOUCH_MODE_FLINGING) { michael@0: mTouchMode = TOUCH_MODE_DRAGGING; michael@0: reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); michael@0: motionPosition = findMotionRowOrColumn((int) mLastTouchPos); michael@0: return true; michael@0: } else if (mMotionPosition >= 0 && mAdapter.isEnabled(mMotionPosition)) { michael@0: mTouchMode = TOUCH_MODE_DOWN; michael@0: triggerCheckForTap(); michael@0: } michael@0: michael@0: mMotionPosition = motionPosition; michael@0: michael@0: break; michael@0: } michael@0: michael@0: case MotionEvent.ACTION_MOVE: { michael@0: final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId); michael@0: if (index < 0) { michael@0: Log.e(LOGTAG, "onInterceptTouchEvent could not find pointer with id " + michael@0: mActivePointerId + " - did TwoWayView receive an inconsistent " + michael@0: "event stream?"); michael@0: return false; michael@0: } michael@0: michael@0: final float pos; michael@0: if (mIsVertical) { michael@0: pos = MotionEventCompat.getY(ev, index); michael@0: } else { michael@0: pos = MotionEventCompat.getX(ev, index); michael@0: } michael@0: michael@0: if (mDataChanged) { michael@0: // Re-sync everything if data has been changed michael@0: // since the scroll operation can query the adapter. michael@0: layoutChildren(); michael@0: } michael@0: michael@0: final float diff = pos - mLastTouchPos + mTouchRemainderPos; michael@0: final int delta = (int) diff; michael@0: mTouchRemainderPos = diff - delta; michael@0: michael@0: switch (mTouchMode) { michael@0: case TOUCH_MODE_DOWN: michael@0: case TOUCH_MODE_TAP: michael@0: case TOUCH_MODE_DONE_WAITING: michael@0: // Check if we have moved far enough that it looks more like a michael@0: // scroll than a tap michael@0: maybeStartScrolling(delta); michael@0: break; michael@0: michael@0: case TOUCH_MODE_DRAGGING: michael@0: case TOUCH_MODE_OVERSCROLL: michael@0: mLastTouchPos = pos; michael@0: maybeScroll(delta); michael@0: break; michael@0: } michael@0: michael@0: break; michael@0: } michael@0: michael@0: case MotionEvent.ACTION_CANCEL: michael@0: cancelCheckForTap(); michael@0: mTouchMode = TOUCH_MODE_REST; michael@0: reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); michael@0: michael@0: setPressed(false); michael@0: View motionView = this.getChildAt(mMotionPosition - mFirstPosition); michael@0: if (motionView != null) { michael@0: motionView.setPressed(false); michael@0: } michael@0: michael@0: if (mStartEdge != null && mEndEdge != null) { michael@0: needsInvalidate = mStartEdge.onRelease() | mEndEdge.onRelease(); michael@0: } michael@0: michael@0: recycleVelocityTracker(); michael@0: michael@0: break; michael@0: michael@0: case MotionEvent.ACTION_UP: { michael@0: switch (mTouchMode) { michael@0: case TOUCH_MODE_DOWN: michael@0: case TOUCH_MODE_TAP: michael@0: case TOUCH_MODE_DONE_WAITING: { michael@0: final int motionPosition = mMotionPosition; michael@0: final View child = getChildAt(motionPosition - mFirstPosition); michael@0: michael@0: final float x = ev.getX(); michael@0: final float y = ev.getY(); michael@0: michael@0: boolean inList = false; michael@0: if (mIsVertical) { michael@0: inList = x > getPaddingLeft() && x < getWidth() - getPaddingRight(); michael@0: } else { michael@0: inList = y > getPaddingTop() && y < getHeight() - getPaddingBottom(); michael@0: } michael@0: michael@0: if (child != null && !child.hasFocusable() && inList) { michael@0: if (mTouchMode != TOUCH_MODE_DOWN) { michael@0: child.setPressed(false); michael@0: } michael@0: michael@0: if (mPerformClick == null) { michael@0: mPerformClick = new PerformClick(); michael@0: } michael@0: michael@0: final PerformClick performClick = mPerformClick; michael@0: performClick.mClickMotionPosition = motionPosition; michael@0: performClick.rememberWindowAttachCount(); michael@0: michael@0: mResurrectToPosition = motionPosition; michael@0: michael@0: if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) { michael@0: if (mTouchMode == TOUCH_MODE_DOWN) { michael@0: cancelCheckForTap(); michael@0: } else { michael@0: cancelCheckForLongPress(); michael@0: } michael@0: michael@0: mLayoutMode = LAYOUT_NORMAL; michael@0: michael@0: if (!mDataChanged && mAdapter.isEnabled(motionPosition)) { michael@0: mTouchMode = TOUCH_MODE_TAP; michael@0: michael@0: setPressed(true); michael@0: positionSelector(mMotionPosition, child); michael@0: child.setPressed(true); michael@0: michael@0: if (mSelector != null) { michael@0: Drawable d = mSelector.getCurrent(); michael@0: if (d != null && d instanceof TransitionDrawable) { michael@0: ((TransitionDrawable) d).resetTransition(); michael@0: } michael@0: } michael@0: michael@0: if (mTouchModeReset != null) { michael@0: removeCallbacks(mTouchModeReset); michael@0: } michael@0: michael@0: mTouchModeReset = new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: mTouchMode = TOUCH_MODE_REST; michael@0: michael@0: setPressed(false); michael@0: child.setPressed(false); michael@0: michael@0: if (!mDataChanged) { michael@0: performClick.run(); michael@0: } michael@0: michael@0: mTouchModeReset = null; michael@0: } michael@0: }; michael@0: michael@0: postDelayed(mTouchModeReset, michael@0: ViewConfiguration.getPressedStateDuration()); michael@0: } else { michael@0: mTouchMode = TOUCH_MODE_REST; michael@0: updateSelectorState(); michael@0: } michael@0: } else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) { michael@0: performClick.run(); michael@0: } michael@0: } michael@0: michael@0: mTouchMode = TOUCH_MODE_REST; michael@0: updateSelectorState(); michael@0: michael@0: break; michael@0: } michael@0: michael@0: case TOUCH_MODE_DRAGGING: michael@0: if (contentFits()) { michael@0: mTouchMode = TOUCH_MODE_REST; michael@0: reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); michael@0: break; michael@0: } michael@0: michael@0: mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); michael@0: michael@0: final float velocity; michael@0: if (mIsVertical) { michael@0: velocity = VelocityTrackerCompat.getYVelocity(mVelocityTracker, michael@0: mActivePointerId); michael@0: } else { michael@0: velocity = VelocityTrackerCompat.getXVelocity(mVelocityTracker, michael@0: mActivePointerId); michael@0: } michael@0: michael@0: if (Math.abs(velocity) >= mFlingVelocity) { michael@0: mTouchMode = TOUCH_MODE_FLINGING; michael@0: reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING); michael@0: michael@0: mScroller.fling(0, 0, michael@0: (int) (mIsVertical ? 0 : velocity), michael@0: (int) (mIsVertical ? velocity : 0), michael@0: (mIsVertical ? 0 : Integer.MIN_VALUE), michael@0: (mIsVertical ? 0 : Integer.MAX_VALUE), michael@0: (mIsVertical ? Integer.MIN_VALUE : 0), michael@0: (mIsVertical ? Integer.MAX_VALUE : 0)); michael@0: michael@0: mLastTouchPos = 0; michael@0: needsInvalidate = true; michael@0: } else { michael@0: mTouchMode = TOUCH_MODE_REST; michael@0: reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); michael@0: } michael@0: michael@0: break; michael@0: michael@0: case TOUCH_MODE_OVERSCROLL: michael@0: mTouchMode = TOUCH_MODE_REST; michael@0: reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); michael@0: break; michael@0: } michael@0: michael@0: cancelCheckForTap(); michael@0: cancelCheckForLongPress(); michael@0: setPressed(false); michael@0: michael@0: if (mStartEdge != null && mEndEdge != null) { michael@0: needsInvalidate |= mStartEdge.onRelease() | mEndEdge.onRelease(); michael@0: } michael@0: michael@0: recycleVelocityTracker(); michael@0: michael@0: break; michael@0: } michael@0: } michael@0: michael@0: if (needsInvalidate) { michael@0: ViewCompat.postInvalidateOnAnimation(this); michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: michael@0: @Override michael@0: public void onTouchModeChanged(boolean isInTouchMode) { michael@0: if (isInTouchMode) { michael@0: // Get rid of the selection when we enter touch mode michael@0: hideSelector(); michael@0: michael@0: // Layout, but only if we already have done so previously. michael@0: // (Otherwise may clobber a LAYOUT_SYNC layout that was requested to restore michael@0: // state.) michael@0: if (getWidth() > 0 && getHeight() > 0 && getChildCount() > 0) { michael@0: layoutChildren(); michael@0: } michael@0: michael@0: updateSelectorState(); michael@0: } else { michael@0: final int touchMode = mTouchMode; michael@0: if (touchMode == TOUCH_MODE_OVERSCROLL) { michael@0: if (mOverScroll != 0) { michael@0: mOverScroll = 0; michael@0: finishEdgeGlows(); michael@0: invalidate(); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public boolean onKeyDown(int keyCode, KeyEvent event) { michael@0: return handleKeyEvent(keyCode, 1, event); michael@0: } michael@0: michael@0: @Override michael@0: public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { michael@0: return handleKeyEvent(keyCode, repeatCount, event); michael@0: } michael@0: michael@0: @Override michael@0: public boolean onKeyUp(int keyCode, KeyEvent event) { michael@0: return handleKeyEvent(keyCode, 1, event); michael@0: } michael@0: michael@0: @Override michael@0: public void sendAccessibilityEvent(int eventType) { michael@0: // Since this class calls onScrollChanged even if the mFirstPosition and the michael@0: // child count have not changed we will avoid sending duplicate accessibility michael@0: // events. michael@0: if (eventType == AccessibilityEvent.TYPE_VIEW_SCROLLED) { michael@0: final int firstVisiblePosition = getFirstVisiblePosition(); michael@0: final int lastVisiblePosition = getLastVisiblePosition(); michael@0: michael@0: if (mLastAccessibilityScrollEventFromIndex == firstVisiblePosition michael@0: && mLastAccessibilityScrollEventToIndex == lastVisiblePosition) { michael@0: return; michael@0: } else { michael@0: mLastAccessibilityScrollEventFromIndex = firstVisiblePosition; michael@0: mLastAccessibilityScrollEventToIndex = lastVisiblePosition; michael@0: } michael@0: } michael@0: michael@0: super.sendAccessibilityEvent(eventType); michael@0: } michael@0: michael@0: @Override michael@0: @TargetApi(14) michael@0: public void onInitializeAccessibilityEvent(AccessibilityEvent event) { michael@0: super.onInitializeAccessibilityEvent(event); michael@0: event.setClassName(TwoWayView.class.getName()); michael@0: } michael@0: michael@0: @Override michael@0: @TargetApi(14) michael@0: public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { michael@0: super.onInitializeAccessibilityNodeInfo(info); michael@0: info.setClassName(TwoWayView.class.getName()); michael@0: michael@0: AccessibilityNodeInfoCompat infoCompat = new AccessibilityNodeInfoCompat(info); michael@0: michael@0: if (isEnabled()) { michael@0: if (getFirstVisiblePosition() > 0) { michael@0: infoCompat.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); michael@0: } michael@0: michael@0: if (getLastVisiblePosition() < getCount() - 1) { michael@0: infoCompat.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); michael@0: } michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: @TargetApi(16) michael@0: public boolean performAccessibilityAction(int action, Bundle arguments) { michael@0: if (super.performAccessibilityAction(action, arguments)) { michael@0: return true; michael@0: } michael@0: michael@0: switch (action) { michael@0: case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: michael@0: if (isEnabled() && getLastVisiblePosition() < getCount() - 1) { michael@0: final int viewportSize; michael@0: if (mIsVertical) { michael@0: viewportSize = getHeight() - getPaddingTop() - getPaddingBottom(); michael@0: } else { michael@0: viewportSize = getWidth() - getPaddingLeft() - getPaddingRight(); michael@0: } michael@0: michael@0: // TODO: Use some form of smooth scroll instead michael@0: trackMotionScroll(viewportSize); michael@0: return true; michael@0: } michael@0: return false; michael@0: michael@0: case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: michael@0: if (isEnabled() && mFirstPosition > 0) { michael@0: final int viewportSize; michael@0: if (mIsVertical) { michael@0: viewportSize = getHeight() - getPaddingTop() - getPaddingBottom(); michael@0: } else { michael@0: viewportSize = getWidth() - getPaddingLeft() - getPaddingRight(); michael@0: } michael@0: michael@0: // TODO: Use some form of smooth scroll instead michael@0: trackMotionScroll(-viewportSize); michael@0: return true; michael@0: } michael@0: return false; michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: michael@0: /** michael@0: * Return true if child is an ancestor of parent, (or equal to the parent). michael@0: */ michael@0: private boolean isViewAncestorOf(View child, View parent) { michael@0: if (child == parent) { michael@0: return true; michael@0: } michael@0: michael@0: final ViewParent theParent = child.getParent(); michael@0: michael@0: return (theParent instanceof ViewGroup) && michael@0: isViewAncestorOf((View) theParent, parent); michael@0: } michael@0: michael@0: private void forceValidFocusDirection(int direction) { michael@0: if (mIsVertical && direction != View.FOCUS_UP && direction != View.FOCUS_DOWN) { michael@0: throw new IllegalArgumentException("Focus direction must be one of" michael@0: + " {View.FOCUS_UP, View.FOCUS_DOWN} for vertical orientation"); michael@0: } else if (!mIsVertical && direction != View.FOCUS_LEFT && direction != View.FOCUS_RIGHT) { michael@0: throw new IllegalArgumentException("Focus direction must be one of" michael@0: + " {View.FOCUS_LEFT, View.FOCUS_RIGHT} for vertical orientation"); michael@0: } michael@0: } michael@0: michael@0: private void forceValidInnerFocusDirection(int direction) { michael@0: if (mIsVertical && direction != View.FOCUS_LEFT && direction != View.FOCUS_RIGHT) { michael@0: throw new IllegalArgumentException("Direction must be one of" michael@0: + " {View.FOCUS_LEFT, View.FOCUS_RIGHT} for vertical orientation"); michael@0: } else if (!mIsVertical && direction != View.FOCUS_UP && direction != View.FOCUS_DOWN) { michael@0: throw new IllegalArgumentException("direction must be one of" michael@0: + " {View.FOCUS_UP, View.FOCUS_DOWN} for horizontal orientation"); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Scrolls up or down by the number of items currently present on screen. michael@0: * michael@0: * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or michael@0: * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the michael@0: * current view orientation. michael@0: * michael@0: * @return whether selection was moved michael@0: */ michael@0: boolean pageScroll(int direction) { michael@0: forceValidFocusDirection(direction); michael@0: michael@0: boolean forward = false; michael@0: int nextPage = -1; michael@0: michael@0: if (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT) { michael@0: nextPage = Math.max(0, mSelectedPosition - getChildCount() - 1); michael@0: } else if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) { michael@0: nextPage = Math.min(mItemCount - 1, mSelectedPosition + getChildCount() - 1); michael@0: forward = true; michael@0: } michael@0: michael@0: if (nextPage < 0) { michael@0: return false; michael@0: } michael@0: michael@0: final int position = lookForSelectablePosition(nextPage, forward); michael@0: if (position >= 0) { michael@0: mLayoutMode = LAYOUT_SPECIFIC; michael@0: mSpecificStart = (mIsVertical ? getPaddingTop() : getPaddingLeft()); michael@0: michael@0: if (forward && position > mItemCount - getChildCount()) { michael@0: mLayoutMode = LAYOUT_FORCE_BOTTOM; michael@0: } michael@0: michael@0: if (!forward && position < getChildCount()) { michael@0: mLayoutMode = LAYOUT_FORCE_TOP; michael@0: } michael@0: michael@0: setSelectionInt(position); michael@0: invokeOnItemScrollListener(); michael@0: michael@0: if (!awakenScrollbarsInternal()) { michael@0: invalidate(); michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: michael@0: /** michael@0: * Go to the last or first item if possible (not worrying about panning across or navigating michael@0: * within the internal focus of the currently selected item.) michael@0: * michael@0: * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or michael@0: * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the michael@0: * current view orientation. michael@0: * michael@0: * @return whether selection was moved michael@0: */ michael@0: boolean fullScroll(int direction) { michael@0: forceValidFocusDirection(direction); michael@0: michael@0: boolean moved = false; michael@0: if (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT) { michael@0: if (mSelectedPosition != 0) { michael@0: int position = lookForSelectablePosition(0, true); michael@0: if (position >= 0) { michael@0: mLayoutMode = LAYOUT_FORCE_TOP; michael@0: setSelectionInt(position); michael@0: invokeOnItemScrollListener(); michael@0: } michael@0: michael@0: moved = true; michael@0: } michael@0: } else if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) { michael@0: if (mSelectedPosition < mItemCount - 1) { michael@0: int position = lookForSelectablePosition(mItemCount - 1, true); michael@0: if (position >= 0) { michael@0: mLayoutMode = LAYOUT_FORCE_BOTTOM; michael@0: setSelectionInt(position); michael@0: invokeOnItemScrollListener(); michael@0: } michael@0: michael@0: moved = true; michael@0: } michael@0: } michael@0: michael@0: if (moved && !awakenScrollbarsInternal()) { michael@0: awakenScrollbarsInternal(); michael@0: invalidate(); michael@0: } michael@0: michael@0: return moved; michael@0: } michael@0: michael@0: /** michael@0: * To avoid horizontal/vertical focus searches changing the selected item, michael@0: * we manually focus search within the selected item (as applicable), and michael@0: * prevent focus from jumping to something within another item. michael@0: * michael@0: * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or michael@0: * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the michael@0: * current view orientation. michael@0: * michael@0: * @return Whether this consumes the key event. michael@0: */ michael@0: private boolean handleFocusWithinItem(int direction) { michael@0: forceValidInnerFocusDirection(direction); michael@0: michael@0: final int numChildren = getChildCount(); michael@0: michael@0: if (mItemsCanFocus && numChildren > 0 && mSelectedPosition != INVALID_POSITION) { michael@0: final View selectedView = getSelectedView(); michael@0: michael@0: if (selectedView != null && selectedView.hasFocus() && michael@0: selectedView instanceof ViewGroup) { michael@0: michael@0: final View currentFocus = selectedView.findFocus(); michael@0: final View nextFocus = FocusFinder.getInstance().findNextFocus( michael@0: (ViewGroup) selectedView, currentFocus, direction); michael@0: michael@0: if (nextFocus != null) { michael@0: // Do the math to get interesting rect in next focus' coordinates michael@0: currentFocus.getFocusedRect(mTempRect); michael@0: offsetDescendantRectToMyCoords(currentFocus, mTempRect); michael@0: offsetRectIntoDescendantCoords(nextFocus, mTempRect); michael@0: michael@0: if (nextFocus.requestFocus(direction, mTempRect)) { michael@0: return true; michael@0: } michael@0: } michael@0: michael@0: // We are blocking the key from being handled (by returning true) michael@0: // if the global result is going to be some other view within this michael@0: // list. This is to achieve the overall goal of having horizontal/vertical michael@0: // d-pad navigation remain in the current item depending on the current michael@0: // orientation in this view. michael@0: final View globalNextFocus = FocusFinder.getInstance().findNextFocus( michael@0: (ViewGroup) getRootView(), currentFocus, direction); michael@0: michael@0: if (globalNextFocus != null) { michael@0: return isViewAncestorOf(globalNextFocus, this); michael@0: } michael@0: } michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: michael@0: /** michael@0: * Scrolls to the next or previous item if possible. michael@0: * michael@0: * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or michael@0: * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the michael@0: * current view orientation. michael@0: * michael@0: * @return whether selection was moved michael@0: */ michael@0: private boolean arrowScroll(int direction) { michael@0: forceValidFocusDirection(direction); michael@0: michael@0: try { michael@0: mInLayout = true; michael@0: michael@0: final boolean handled = arrowScrollImpl(direction); michael@0: if (handled) { michael@0: playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); michael@0: } michael@0: michael@0: return handled; michael@0: } finally { michael@0: mInLayout = false; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * When selection changes, it is possible that the previously selected or the michael@0: * next selected item will change its size. If so, we need to offset some folks, michael@0: * and re-layout the items as appropriate. michael@0: * michael@0: * @param selectedView The currently selected view (before changing selection). michael@0: * should be null if there was no previous selection. michael@0: * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or michael@0: * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the michael@0: * current view orientation. michael@0: * @param newSelectedPosition The position of the next selection. michael@0: * @param newFocusAssigned whether new focus was assigned. This matters because michael@0: * when something has focus, we don't want to show selection (ugh). michael@0: */ michael@0: private void handleNewSelectionChange(View selectedView, int direction, int newSelectedPosition, michael@0: boolean newFocusAssigned) { michael@0: forceValidFocusDirection(direction); michael@0: michael@0: if (newSelectedPosition == INVALID_POSITION) { michael@0: throw new IllegalArgumentException("newSelectedPosition needs to be valid"); michael@0: } michael@0: michael@0: // Whether or not we are moving down/right or up/left, we want to preserve the michael@0: // top/left of whatever view is at the start: michael@0: // - moving down/right: the view that had selection michael@0: // - moving up/left: the view that is getting selection michael@0: final int selectedIndex = mSelectedPosition - mFirstPosition; michael@0: final int nextSelectedIndex = newSelectedPosition - mFirstPosition; michael@0: int startViewIndex, endViewIndex; michael@0: boolean topSelected = false; michael@0: View startView; michael@0: View endView; michael@0: michael@0: if (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT) { michael@0: startViewIndex = nextSelectedIndex; michael@0: endViewIndex = selectedIndex; michael@0: startView = getChildAt(startViewIndex); michael@0: endView = selectedView; michael@0: topSelected = true; michael@0: } else { michael@0: startViewIndex = selectedIndex; michael@0: endViewIndex = nextSelectedIndex; michael@0: startView = selectedView; michael@0: endView = getChildAt(endViewIndex); michael@0: } michael@0: michael@0: final int numChildren = getChildCount(); michael@0: michael@0: // start with top view: is it changing size? michael@0: if (startView != null) { michael@0: startView.setSelected(!newFocusAssigned && topSelected); michael@0: measureAndAdjustDown(startView, startViewIndex, numChildren); michael@0: } michael@0: michael@0: // is the bottom view changing size? michael@0: if (endView != null) { michael@0: endView.setSelected(!newFocusAssigned && !topSelected); michael@0: measureAndAdjustDown(endView, endViewIndex, numChildren); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Re-measure a child, and if its height changes, lay it out preserving its michael@0: * top, and adjust the children below it appropriately. michael@0: * michael@0: * @param child The child michael@0: * @param childIndex The view group index of the child. michael@0: * @param numChildren The number of children in the view group. michael@0: */ michael@0: private void measureAndAdjustDown(View child, int childIndex, int numChildren) { michael@0: int oldHeight = child.getHeight(); michael@0: measureChild(child); michael@0: michael@0: if (child.getMeasuredHeight() == oldHeight) { michael@0: return; michael@0: } michael@0: michael@0: // lay out the view, preserving its top michael@0: relayoutMeasuredChild(child); michael@0: michael@0: // adjust views below appropriately michael@0: final int heightDelta = child.getMeasuredHeight() - oldHeight; michael@0: for (int i = childIndex + 1; i < numChildren; i++) { michael@0: getChildAt(i).offsetTopAndBottom(heightDelta); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Do an arrow scroll based on focus searching. If a new view is michael@0: * given focus, return the selection delta and amount to scroll via michael@0: * an {@link ArrowScrollFocusResult}, otherwise, return null. michael@0: * michael@0: * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or michael@0: * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the michael@0: * current view orientation. michael@0: * michael@0: * @return The result if focus has changed, or null. michael@0: */ michael@0: private ArrowScrollFocusResult arrowScrollFocused(final int direction) { michael@0: forceValidFocusDirection(direction); michael@0: michael@0: final View selectedView = getSelectedView(); michael@0: final View newFocus; michael@0: final int searchPoint; michael@0: michael@0: if (selectedView != null && selectedView.hasFocus()) { michael@0: View oldFocus = selectedView.findFocus(); michael@0: newFocus = FocusFinder.getInstance().findNextFocus(this, oldFocus, direction); michael@0: } else { michael@0: if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) { michael@0: final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft()); michael@0: michael@0: final int selectedStart; michael@0: if (selectedView != null) { michael@0: selectedStart = (mIsVertical ? selectedView.getTop() : selectedView.getLeft()); michael@0: } else { michael@0: selectedStart = start; michael@0: } michael@0: michael@0: searchPoint = Math.max(selectedStart, start); michael@0: } else { michael@0: final int end = (mIsVertical ? getHeight() - getPaddingBottom() : michael@0: getWidth() - getPaddingRight()); michael@0: michael@0: final int selectedEnd; michael@0: if (selectedView != null) { michael@0: selectedEnd = (mIsVertical ? selectedView.getBottom() : selectedView.getRight()); michael@0: } else { michael@0: selectedEnd = end; michael@0: } michael@0: michael@0: searchPoint = Math.min(selectedEnd, end); michael@0: } michael@0: michael@0: final int x = (mIsVertical ? 0 : searchPoint); michael@0: final int y = (mIsVertical ? searchPoint : 0); michael@0: mTempRect.set(x, y, x, y); michael@0: michael@0: newFocus = FocusFinder.getInstance().findNextFocusFromRect(this, mTempRect, direction); michael@0: } michael@0: michael@0: if (newFocus != null) { michael@0: final int positionOfNewFocus = positionOfNewFocus(newFocus); michael@0: michael@0: // If the focus change is in a different new position, make sure michael@0: // we aren't jumping over another selectable position. michael@0: if (mSelectedPosition != INVALID_POSITION && positionOfNewFocus != mSelectedPosition) { michael@0: final int selectablePosition = lookForSelectablePositionOnScreen(direction); michael@0: michael@0: final boolean movingForward = michael@0: (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT); michael@0: final boolean movingBackward = michael@0: (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT); michael@0: michael@0: if (selectablePosition != INVALID_POSITION && michael@0: ((movingForward && selectablePosition < positionOfNewFocus) || michael@0: (movingBackward && selectablePosition > positionOfNewFocus))) { michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: int focusScroll = amountToScrollToNewFocus(direction, newFocus, positionOfNewFocus); michael@0: michael@0: final int maxScrollAmount = getMaxScrollAmount(); michael@0: if (focusScroll < maxScrollAmount) { michael@0: // Not moving too far, safe to give next view focus michael@0: newFocus.requestFocus(direction); michael@0: mArrowScrollFocusResult.populate(positionOfNewFocus, focusScroll); michael@0: return mArrowScrollFocusResult; michael@0: } else if (distanceToView(newFocus) < maxScrollAmount){ michael@0: // Case to consider: michael@0: // Too far to get entire next focusable on screen, but by going michael@0: // max scroll amount, we are getting it at least partially in view, michael@0: // so give it focus and scroll the max amount. michael@0: newFocus.requestFocus(direction); michael@0: mArrowScrollFocusResult.populate(positionOfNewFocus, maxScrollAmount); michael@0: return mArrowScrollFocusResult; michael@0: } michael@0: } michael@0: michael@0: return null; michael@0: } michael@0: michael@0: /** michael@0: * @return The maximum amount a list view will scroll in response to michael@0: * an arrow event. michael@0: */ michael@0: public int getMaxScrollAmount() { michael@0: return (int) (MAX_SCROLL_FACTOR * getHeight()); michael@0: } michael@0: michael@0: /** michael@0: * @return The amount to preview next items when arrow scrolling. michael@0: */ michael@0: private int getArrowScrollPreviewLength() { michael@0: // FIXME: TwoWayView has no fading edge support just yet but using it michael@0: // makes it convenient for defining the next item's previous length. michael@0: int fadingEdgeLength = michael@0: (mIsVertical ? getVerticalFadingEdgeLength() : getHorizontalFadingEdgeLength()); michael@0: michael@0: return mItemMargin + Math.max(MIN_SCROLL_PREVIEW_PIXELS, fadingEdgeLength); michael@0: } michael@0: michael@0: /** michael@0: * @param newFocus The view that would have focus. michael@0: * @return the position that contains newFocus michael@0: */ michael@0: private int positionOfNewFocus(View newFocus) { michael@0: final int numChildren = getChildCount(); michael@0: michael@0: for (int i = 0; i < numChildren; i++) { michael@0: final View child = getChildAt(i); michael@0: if (isViewAncestorOf(newFocus, child)) { michael@0: return mFirstPosition + i; michael@0: } michael@0: } michael@0: michael@0: throw new IllegalArgumentException("newFocus is not a child of any of the" michael@0: + " children of the list!"); michael@0: } michael@0: michael@0: /** michael@0: * Handle an arrow scroll going up or down. Take into account whether items are selectable, michael@0: * whether there are focusable items, etc. michael@0: * michael@0: * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or michael@0: * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the michael@0: * current view orientation. michael@0: * michael@0: * @return Whether any scrolling, selection or focus change occurred. michael@0: */ michael@0: private boolean arrowScrollImpl(int direction) { michael@0: forceValidFocusDirection(direction); michael@0: michael@0: if (getChildCount() <= 0) { michael@0: return false; michael@0: } michael@0: michael@0: View selectedView = getSelectedView(); michael@0: int selectedPos = mSelectedPosition; michael@0: michael@0: int nextSelectedPosition = lookForSelectablePositionOnScreen(direction); michael@0: int amountToScroll = amountToScroll(direction, nextSelectedPosition); michael@0: michael@0: // If we are moving focus, we may OVERRIDE the default behaviour michael@0: final ArrowScrollFocusResult focusResult = (mItemsCanFocus ? arrowScrollFocused(direction) : null); michael@0: if (focusResult != null) { michael@0: nextSelectedPosition = focusResult.getSelectedPosition(); michael@0: amountToScroll = focusResult.getAmountToScroll(); michael@0: } michael@0: michael@0: boolean needToRedraw = (focusResult != null); michael@0: if (nextSelectedPosition != INVALID_POSITION) { michael@0: handleNewSelectionChange(selectedView, direction, nextSelectedPosition, focusResult != null); michael@0: michael@0: setSelectedPositionInt(nextSelectedPosition); michael@0: setNextSelectedPositionInt(nextSelectedPosition); michael@0: michael@0: selectedView = getSelectedView(); michael@0: selectedPos = nextSelectedPosition; michael@0: michael@0: if (mItemsCanFocus && focusResult == null) { michael@0: // There was no new view found to take focus, make sure we michael@0: // don't leave focus with the old selection. michael@0: final View focused = getFocusedChild(); michael@0: if (focused != null) { michael@0: focused.clearFocus(); michael@0: } michael@0: } michael@0: michael@0: needToRedraw = true; michael@0: checkSelectionChanged(); michael@0: } michael@0: michael@0: if (amountToScroll > 0) { michael@0: trackMotionScroll(direction == View.FOCUS_UP || direction == View.FOCUS_LEFT ? michael@0: amountToScroll : -amountToScroll); michael@0: needToRedraw = true; michael@0: } michael@0: michael@0: // If we didn't find a new focusable, make sure any existing focused michael@0: // item that was panned off screen gives up focus. michael@0: if (mItemsCanFocus && focusResult == null && michael@0: selectedView != null && selectedView.hasFocus()) { michael@0: final View focused = selectedView.findFocus(); michael@0: if (!isViewAncestorOf(focused, this) || distanceToView(focused) > 0) { michael@0: focused.clearFocus(); michael@0: } michael@0: } michael@0: michael@0: // If the current selection is panned off, we need to remove the selection michael@0: if (nextSelectedPosition == INVALID_POSITION && selectedView != null michael@0: && !isViewAncestorOf(selectedView, this)) { michael@0: selectedView = null; michael@0: hideSelector(); michael@0: michael@0: // But we don't want to set the ressurect position (that would make subsequent michael@0: // unhandled key events bring back the item we just scrolled off) michael@0: mResurrectToPosition = INVALID_POSITION; michael@0: } michael@0: michael@0: if (needToRedraw) { michael@0: if (selectedView != null) { michael@0: positionSelector(selectedPos, selectedView); michael@0: mSelectedStart = selectedView.getTop(); michael@0: } michael@0: michael@0: if (!awakenScrollbarsInternal()) { michael@0: invalidate(); michael@0: } michael@0: michael@0: invokeOnItemScrollListener(); michael@0: return true; michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: michael@0: /** michael@0: * Determine how much we need to scroll in order to get the next selected view michael@0: * visible. The amount is capped at {@link #getMaxScrollAmount()}. michael@0: * michael@0: * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or michael@0: * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the michael@0: * current view orientation. michael@0: * @param nextSelectedPosition The position of the next selection, or michael@0: * {@link #INVALID_POSITION} if there is no next selectable position michael@0: * michael@0: * @return The amount to scroll. Note: this is always positive! Direction michael@0: * needs to be taken into account when actually scrolling. michael@0: */ michael@0: private int amountToScroll(int direction, int nextSelectedPosition) { michael@0: forceValidFocusDirection(direction); michael@0: michael@0: final int numChildren = getChildCount(); michael@0: michael@0: if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) { michael@0: final int end = (mIsVertical ? getHeight() - getPaddingBottom() : michael@0: getWidth() - getPaddingRight()); michael@0: michael@0: int indexToMakeVisible = numChildren - 1; michael@0: if (nextSelectedPosition != INVALID_POSITION) { michael@0: indexToMakeVisible = nextSelectedPosition - mFirstPosition; michael@0: } michael@0: michael@0: final int positionToMakeVisible = mFirstPosition + indexToMakeVisible; michael@0: final View viewToMakeVisible = getChildAt(indexToMakeVisible); michael@0: michael@0: int goalEnd = end; michael@0: if (positionToMakeVisible < mItemCount - 1) { michael@0: goalEnd -= getArrowScrollPreviewLength(); michael@0: } michael@0: michael@0: final int viewToMakeVisibleStart = michael@0: (mIsVertical ? viewToMakeVisible.getTop() : viewToMakeVisible.getLeft()); michael@0: final int viewToMakeVisibleEnd = michael@0: (mIsVertical ? viewToMakeVisible.getBottom() : viewToMakeVisible.getRight()); michael@0: michael@0: if (viewToMakeVisibleEnd <= goalEnd) { michael@0: // Target item is fully visible michael@0: return 0; michael@0: } michael@0: michael@0: if (nextSelectedPosition != INVALID_POSITION && michael@0: (goalEnd - viewToMakeVisibleStart) >= getMaxScrollAmount()) { michael@0: // Item already has enough of it visible, changing selection is good enough michael@0: return 0; michael@0: } michael@0: michael@0: int amountToScroll = (viewToMakeVisibleEnd - goalEnd); michael@0: michael@0: if (mFirstPosition + numChildren == mItemCount) { michael@0: final View lastChild = getChildAt(numChildren - 1); michael@0: final int lastChildEnd = (mIsVertical ? lastChild.getBottom() : lastChild.getRight()); michael@0: michael@0: // Last is last in list -> Make sure we don't scroll past it michael@0: final int max = lastChildEnd - end; michael@0: amountToScroll = Math.min(amountToScroll, max); michael@0: } michael@0: michael@0: return Math.min(amountToScroll, getMaxScrollAmount()); michael@0: } else { michael@0: final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft()); michael@0: michael@0: int indexToMakeVisible = 0; michael@0: if (nextSelectedPosition != INVALID_POSITION) { michael@0: indexToMakeVisible = nextSelectedPosition - mFirstPosition; michael@0: } michael@0: michael@0: final int positionToMakeVisible = mFirstPosition + indexToMakeVisible; michael@0: final View viewToMakeVisible = getChildAt(indexToMakeVisible); michael@0: michael@0: int goalStart = start; michael@0: if (positionToMakeVisible > 0) { michael@0: goalStart += getArrowScrollPreviewLength(); michael@0: } michael@0: michael@0: final int viewToMakeVisibleStart = michael@0: (mIsVertical ? viewToMakeVisible.getTop() : viewToMakeVisible.getLeft()); michael@0: final int viewToMakeVisibleEnd = michael@0: (mIsVertical ? viewToMakeVisible.getBottom() : viewToMakeVisible.getRight()); michael@0: michael@0: if (viewToMakeVisibleStart >= goalStart) { michael@0: // Item is fully visible michael@0: return 0; michael@0: } michael@0: michael@0: if (nextSelectedPosition != INVALID_POSITION && michael@0: (viewToMakeVisibleEnd - goalStart) >= getMaxScrollAmount()) { michael@0: // Item already has enough of it visible, changing selection is good enough michael@0: return 0; michael@0: } michael@0: michael@0: int amountToScroll = (goalStart - viewToMakeVisibleStart); michael@0: michael@0: if (mFirstPosition == 0) { michael@0: final View firstChild = getChildAt(0); michael@0: final int firstChildStart = (mIsVertical ? firstChild.getTop() : firstChild.getLeft()); michael@0: michael@0: // First is first in list -> make sure we don't scroll past it michael@0: final int max = start - firstChildStart; michael@0: amountToScroll = Math.min(amountToScroll, max); michael@0: } michael@0: michael@0: return Math.min(amountToScroll, getMaxScrollAmount()); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Determine how much we need to scroll in order to get newFocus in view. michael@0: * michael@0: * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or michael@0: * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the michael@0: * current view orientation. michael@0: * @param newFocus The view that would take focus. michael@0: * @param positionOfNewFocus The position of the list item containing newFocus michael@0: * michael@0: * @return The amount to scroll. Note: this is always positive! Direction michael@0: * needs to be taken into account when actually scrolling. michael@0: */ michael@0: private int amountToScrollToNewFocus(int direction, View newFocus, int positionOfNewFocus) { michael@0: forceValidFocusDirection(direction); michael@0: michael@0: int amountToScroll = 0; michael@0: michael@0: newFocus.getDrawingRect(mTempRect); michael@0: offsetDescendantRectToMyCoords(newFocus, mTempRect); michael@0: michael@0: if (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT) { michael@0: final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft()); michael@0: final int newFocusStart = (mIsVertical ? mTempRect.top : mTempRect.left); michael@0: michael@0: if (newFocusStart < start) { michael@0: amountToScroll = start - newFocusStart; michael@0: if (positionOfNewFocus > 0) { michael@0: amountToScroll += getArrowScrollPreviewLength(); michael@0: } michael@0: } michael@0: } else { michael@0: final int end = (mIsVertical ? getHeight() - getPaddingBottom() : michael@0: getWidth() - getPaddingRight()); michael@0: final int newFocusEnd = (mIsVertical ? mTempRect.bottom : mTempRect.right); michael@0: michael@0: if (newFocusEnd > end) { michael@0: amountToScroll = newFocusEnd - end; michael@0: if (positionOfNewFocus < mItemCount - 1) { michael@0: amountToScroll += getArrowScrollPreviewLength(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: return amountToScroll; michael@0: } michael@0: michael@0: /** michael@0: * Determine the distance to the nearest edge of a view in a particular michael@0: * direction. michael@0: * michael@0: * @param descendant A descendant of this list. michael@0: * @return The distance, or 0 if the nearest edge is already on screen. michael@0: */ michael@0: private int distanceToView(View descendant) { michael@0: descendant.getDrawingRect(mTempRect); michael@0: offsetDescendantRectToMyCoords(descendant, mTempRect); michael@0: michael@0: final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft()); michael@0: final int end = (mIsVertical ? getHeight() - getPaddingBottom() : michael@0: getWidth() - getPaddingRight()); michael@0: michael@0: final int viewStart = (mIsVertical ? mTempRect.top : mTempRect.left); michael@0: final int viewEnd = (mIsVertical ? mTempRect.bottom : mTempRect.right); michael@0: michael@0: int distance = 0; michael@0: if (viewEnd < start) { michael@0: distance = start - viewEnd; michael@0: } else if (viewStart > end) { michael@0: distance = viewStart - end; michael@0: } michael@0: michael@0: return distance; michael@0: } michael@0: michael@0: private boolean handleKeyScroll(KeyEvent event, int count, int direction) { michael@0: boolean handled = false; michael@0: michael@0: if (KeyEventCompat.hasNoModifiers(event)) { michael@0: handled = resurrectSelectionIfNeeded(); michael@0: if (!handled) { michael@0: while (count-- > 0) { michael@0: if (arrowScroll(direction)) { michael@0: handled = true; michael@0: } else { michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_ALT_ON)) { michael@0: handled = resurrectSelectionIfNeeded() || fullScroll(direction); michael@0: } michael@0: michael@0: return handled; michael@0: } michael@0: michael@0: private boolean handleKeyEvent(int keyCode, int count, KeyEvent event) { michael@0: if (mAdapter == null || !mIsAttached) { michael@0: return false; michael@0: } michael@0: michael@0: if (mDataChanged) { michael@0: layoutChildren(); michael@0: } michael@0: michael@0: boolean handled = false; michael@0: final int action = event.getAction(); michael@0: michael@0: if (action != KeyEvent.ACTION_UP) { michael@0: switch (keyCode) { michael@0: case KeyEvent.KEYCODE_DPAD_UP: michael@0: if (mIsVertical) { michael@0: handled = handleKeyScroll(event, count, View.FOCUS_UP); michael@0: } else if (KeyEventCompat.hasNoModifiers(event)) { michael@0: handled = handleFocusWithinItem(View.FOCUS_UP); michael@0: } michael@0: break; michael@0: michael@0: case KeyEvent.KEYCODE_DPAD_DOWN: { michael@0: if (mIsVertical) { michael@0: handled = handleKeyScroll(event, count, View.FOCUS_DOWN); michael@0: } else if (KeyEventCompat.hasNoModifiers(event)) { michael@0: handled = handleFocusWithinItem(View.FOCUS_DOWN); michael@0: } michael@0: break; michael@0: } michael@0: michael@0: case KeyEvent.KEYCODE_DPAD_LEFT: michael@0: if (!mIsVertical) { michael@0: handled = handleKeyScroll(event, count, View.FOCUS_LEFT); michael@0: } else if (KeyEventCompat.hasNoModifiers(event)) { michael@0: handled = handleFocusWithinItem(View.FOCUS_LEFT); michael@0: } michael@0: break; michael@0: michael@0: case KeyEvent.KEYCODE_DPAD_RIGHT: michael@0: if (!mIsVertical) { michael@0: handled = handleKeyScroll(event, count, View.FOCUS_RIGHT); michael@0: } else if (KeyEventCompat.hasNoModifiers(event)) { michael@0: handled = handleFocusWithinItem(View.FOCUS_RIGHT); michael@0: } michael@0: break; michael@0: michael@0: case KeyEvent.KEYCODE_DPAD_CENTER: michael@0: case KeyEvent.KEYCODE_ENTER: michael@0: if (KeyEventCompat.hasNoModifiers(event)) { michael@0: handled = resurrectSelectionIfNeeded(); michael@0: if (!handled michael@0: && event.getRepeatCount() == 0 && getChildCount() > 0) { michael@0: keyPressed(); michael@0: handled = true; michael@0: } michael@0: } michael@0: break; michael@0: michael@0: case KeyEvent.KEYCODE_SPACE: michael@0: if (KeyEventCompat.hasNoModifiers(event)) { michael@0: handled = resurrectSelectionIfNeeded() || michael@0: pageScroll(mIsVertical ? View.FOCUS_DOWN : View.FOCUS_RIGHT); michael@0: } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_SHIFT_ON)) { michael@0: handled = resurrectSelectionIfNeeded() || michael@0: fullScroll(mIsVertical ? View.FOCUS_UP : View.FOCUS_LEFT); michael@0: } michael@0: michael@0: handled = true; michael@0: break; michael@0: michael@0: case KeyEvent.KEYCODE_PAGE_UP: michael@0: if (KeyEventCompat.hasNoModifiers(event)) { michael@0: handled = resurrectSelectionIfNeeded() || michael@0: pageScroll(mIsVertical ? View.FOCUS_UP : View.FOCUS_LEFT); michael@0: } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_ALT_ON)) { michael@0: handled = resurrectSelectionIfNeeded() || michael@0: fullScroll(mIsVertical ? View.FOCUS_UP : View.FOCUS_LEFT); michael@0: } michael@0: break; michael@0: michael@0: case KeyEvent.KEYCODE_PAGE_DOWN: michael@0: if (KeyEventCompat.hasNoModifiers(event)) { michael@0: handled = resurrectSelectionIfNeeded() || michael@0: pageScroll(mIsVertical ? View.FOCUS_DOWN : View.FOCUS_RIGHT); michael@0: } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_ALT_ON)) { michael@0: handled = resurrectSelectionIfNeeded() || michael@0: fullScroll(mIsVertical ? View.FOCUS_DOWN : View.FOCUS_RIGHT); michael@0: } michael@0: break; michael@0: michael@0: case KeyEvent.KEYCODE_MOVE_HOME: michael@0: if (KeyEventCompat.hasNoModifiers(event)) { michael@0: handled = resurrectSelectionIfNeeded() || michael@0: fullScroll(mIsVertical ? View.FOCUS_UP : View.FOCUS_LEFT); michael@0: } michael@0: break; michael@0: michael@0: case KeyEvent.KEYCODE_MOVE_END: michael@0: if (KeyEventCompat.hasNoModifiers(event)) { michael@0: handled = resurrectSelectionIfNeeded() || michael@0: fullScroll(mIsVertical ? View.FOCUS_DOWN : View.FOCUS_RIGHT); michael@0: } michael@0: break; michael@0: } michael@0: } michael@0: michael@0: if (handled) { michael@0: return true; michael@0: } michael@0: michael@0: switch (action) { michael@0: case KeyEvent.ACTION_DOWN: michael@0: return super.onKeyDown(keyCode, event); michael@0: michael@0: case KeyEvent.ACTION_UP: michael@0: if (!isEnabled()) { michael@0: return true; michael@0: } michael@0: michael@0: if (isClickable() && isPressed() && michael@0: mSelectedPosition >= 0 && mAdapter != null && michael@0: mSelectedPosition < mAdapter.getCount()) { michael@0: michael@0: final View child = getChildAt(mSelectedPosition - mFirstPosition); michael@0: if (child != null) { michael@0: performItemClick(child, mSelectedPosition, mSelectedRowId); michael@0: child.setPressed(false); michael@0: } michael@0: michael@0: setPressed(false); michael@0: return true; michael@0: } michael@0: michael@0: return false; michael@0: michael@0: case KeyEvent.ACTION_MULTIPLE: michael@0: return super.onKeyMultiple(keyCode, count, event); michael@0: michael@0: default: michael@0: return false; michael@0: } michael@0: } michael@0: michael@0: private void initOrResetVelocityTracker() { michael@0: if (mVelocityTracker == null) { michael@0: mVelocityTracker = VelocityTracker.obtain(); michael@0: } else { michael@0: mVelocityTracker.clear(); michael@0: } michael@0: } michael@0: michael@0: private void initVelocityTrackerIfNotExists() { michael@0: if (mVelocityTracker == null) { michael@0: mVelocityTracker = VelocityTracker.obtain(); michael@0: } michael@0: } michael@0: michael@0: private void recycleVelocityTracker() { michael@0: if (mVelocityTracker != null) { michael@0: mVelocityTracker.recycle(); michael@0: mVelocityTracker = null; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Notify our scroll listener (if there is one) of a change in scroll state michael@0: */ michael@0: private void invokeOnItemScrollListener() { michael@0: if (mOnScrollListener != null) { michael@0: mOnScrollListener.onScroll(this, mFirstPosition, getChildCount(), mItemCount); michael@0: } michael@0: michael@0: // Dummy values, View's implementation does not use these. michael@0: onScrollChanged(0, 0, 0, 0); michael@0: } michael@0: michael@0: private void reportScrollStateChange(int newState) { michael@0: if (newState == mLastScrollState) { michael@0: return; michael@0: } michael@0: michael@0: if (mOnScrollListener != null) { michael@0: mLastScrollState = newState; michael@0: mOnScrollListener.onScrollStateChanged(this, newState); michael@0: } michael@0: } michael@0: michael@0: private boolean maybeStartScrolling(int delta) { michael@0: final boolean isOverScroll = (mOverScroll != 0); michael@0: if (Math.abs(delta) <= mTouchSlop && !isOverScroll) { michael@0: return false; michael@0: } michael@0: michael@0: if (isOverScroll) { michael@0: mTouchMode = TOUCH_MODE_OVERSCROLL; michael@0: } else { michael@0: mTouchMode = TOUCH_MODE_DRAGGING; michael@0: } michael@0: michael@0: // Time to start stealing events! Once we've stolen them, don't michael@0: // let anyone steal from us. michael@0: final ViewParent parent = getParent(); michael@0: if (parent != null) { michael@0: parent.requestDisallowInterceptTouchEvent(true); michael@0: } michael@0: michael@0: cancelCheckForLongPress(); michael@0: michael@0: setPressed(false); michael@0: View motionView = getChildAt(mMotionPosition - mFirstPosition); michael@0: if (motionView != null) { michael@0: motionView.setPressed(false); michael@0: } michael@0: michael@0: reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); michael@0: michael@0: return true; michael@0: } michael@0: michael@0: private void maybeScroll(int delta) { michael@0: if (mTouchMode == TOUCH_MODE_DRAGGING) { michael@0: handleDragChange(delta); michael@0: } else if (mTouchMode == TOUCH_MODE_OVERSCROLL) { michael@0: handleOverScrollChange(delta); michael@0: } michael@0: } michael@0: michael@0: private void handleDragChange(int delta) { michael@0: // Time to start stealing events! Once we've stolen them, don't michael@0: // let anyone steal from us. michael@0: final ViewParent parent = getParent(); michael@0: if (parent != null) { michael@0: parent.requestDisallowInterceptTouchEvent(true); michael@0: } michael@0: michael@0: final int motionIndex; michael@0: if (mMotionPosition >= 0) { michael@0: motionIndex = mMotionPosition - mFirstPosition; michael@0: } else { michael@0: // If we don't have a motion position that we can reliably track, michael@0: // pick something in the middle to make a best guess at things below. michael@0: motionIndex = getChildCount() / 2; michael@0: } michael@0: michael@0: int motionViewPrevStart = 0; michael@0: View motionView = this.getChildAt(motionIndex); michael@0: if (motionView != null) { michael@0: motionViewPrevStart = (mIsVertical ? motionView.getTop() : motionView.getLeft()); michael@0: } michael@0: michael@0: boolean atEdge = trackMotionScroll(delta); michael@0: michael@0: motionView = this.getChildAt(motionIndex); michael@0: if (motionView != null) { michael@0: final int motionViewRealStart = michael@0: (mIsVertical ? motionView.getTop() : motionView.getLeft()); michael@0: michael@0: if (atEdge) { michael@0: final int overscroll = -delta - (motionViewRealStart - motionViewPrevStart); michael@0: updateOverScrollState(delta, overscroll); michael@0: } michael@0: } michael@0: } michael@0: michael@0: private void updateOverScrollState(int delta, int overscroll) { michael@0: overScrollByInternal((mIsVertical ? 0 : overscroll), michael@0: (mIsVertical ? overscroll : 0), michael@0: (mIsVertical ? 0 : mOverScroll), michael@0: (mIsVertical ? mOverScroll : 0), michael@0: 0, 0, michael@0: (mIsVertical ? 0 : mOverscrollDistance), michael@0: (mIsVertical ? mOverscrollDistance : 0), michael@0: true); michael@0: michael@0: if (Math.abs(mOverscrollDistance) == Math.abs(mOverScroll)) { michael@0: // Break fling velocity if we impacted an edge michael@0: if (mVelocityTracker != null) { michael@0: mVelocityTracker.clear(); michael@0: } michael@0: } michael@0: michael@0: final int overscrollMode = ViewCompat.getOverScrollMode(this); michael@0: if (overscrollMode == ViewCompat.OVER_SCROLL_ALWAYS || michael@0: (overscrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && !contentFits())) { michael@0: mTouchMode = TOUCH_MODE_OVERSCROLL; michael@0: michael@0: float pull = (float) overscroll / (mIsVertical ? getHeight() : getWidth()); michael@0: if (delta > 0) { michael@0: mStartEdge.onPull(pull); michael@0: michael@0: if (!mEndEdge.isFinished()) { michael@0: mEndEdge.onRelease(); michael@0: } michael@0: } else if (delta < 0) { michael@0: mEndEdge.onPull(pull); michael@0: michael@0: if (!mStartEdge.isFinished()) { michael@0: mStartEdge.onRelease(); michael@0: } michael@0: } michael@0: michael@0: if (delta != 0) { michael@0: ViewCompat.postInvalidateOnAnimation(this); michael@0: } michael@0: } michael@0: } michael@0: michael@0: private void handleOverScrollChange(int delta) { michael@0: final int oldOverScroll = mOverScroll; michael@0: final int newOverScroll = oldOverScroll - delta; michael@0: michael@0: int overScrollDistance = -delta; michael@0: if ((newOverScroll < 0 && oldOverScroll >= 0) || michael@0: (newOverScroll > 0 && oldOverScroll <= 0)) { michael@0: overScrollDistance = -oldOverScroll; michael@0: delta += overScrollDistance; michael@0: } else { michael@0: delta = 0; michael@0: } michael@0: michael@0: if (overScrollDistance != 0) { michael@0: updateOverScrollState(delta, overScrollDistance); michael@0: } michael@0: michael@0: if (delta != 0) { michael@0: if (mOverScroll != 0) { michael@0: mOverScroll = 0; michael@0: ViewCompat.postInvalidateOnAnimation(this); michael@0: } michael@0: michael@0: trackMotionScroll(delta); michael@0: mTouchMode = TOUCH_MODE_DRAGGING; michael@0: michael@0: // We did not scroll the full amount. Treat this essentially like the michael@0: // start of a new touch scroll michael@0: mMotionPosition = findClosestMotionRowOrColumn((int) mLastTouchPos); michael@0: mTouchRemainderPos = 0; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * What is the distance between the source and destination rectangles given the direction of michael@0: * focus navigation between them? The direction basically helps figure out more quickly what is michael@0: * self evident by the relationship between the rects... michael@0: * michael@0: * @param source the source rectangle michael@0: * @param dest the destination rectangle michael@0: * @param direction the direction michael@0: * @return the distance between the rectangles michael@0: */ michael@0: private static int getDistance(Rect source, Rect dest, int direction) { michael@0: int sX, sY; // source x, y michael@0: int dX, dY; // dest x, y michael@0: michael@0: switch (direction) { michael@0: case View.FOCUS_RIGHT: michael@0: sX = source.right; michael@0: sY = source.top + source.height() / 2; michael@0: dX = dest.left; michael@0: dY = dest.top + dest.height() / 2; michael@0: break; michael@0: michael@0: case View.FOCUS_DOWN: michael@0: sX = source.left + source.width() / 2; michael@0: sY = source.bottom; michael@0: dX = dest.left + dest.width() / 2; michael@0: dY = dest.top; michael@0: break; michael@0: michael@0: case View.FOCUS_LEFT: michael@0: sX = source.left; michael@0: sY = source.top + source.height() / 2; michael@0: dX = dest.right; michael@0: dY = dest.top + dest.height() / 2; michael@0: break; michael@0: michael@0: case View.FOCUS_UP: michael@0: sX = source.left + source.width() / 2; michael@0: sY = source.top; michael@0: dX = dest.left + dest.width() / 2; michael@0: dY = dest.bottom; michael@0: break; michael@0: michael@0: case View.FOCUS_FORWARD: michael@0: case View.FOCUS_BACKWARD: michael@0: sX = source.right + source.width() / 2; michael@0: sY = source.top + source.height() / 2; michael@0: dX = dest.left + dest.width() / 2; michael@0: dY = dest.top + dest.height() / 2; michael@0: break; michael@0: michael@0: default: michael@0: throw new IllegalArgumentException("direction must be one of " michael@0: + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, " michael@0: + "FOCUS_FORWARD, FOCUS_BACKWARD}."); michael@0: } michael@0: michael@0: int deltaX = dX - sX; michael@0: int deltaY = dY - sY; michael@0: michael@0: return deltaY * deltaY + deltaX * deltaX; michael@0: } michael@0: michael@0: private int findMotionRowOrColumn(int motionPos) { michael@0: int childCount = getChildCount(); michael@0: if (childCount == 0) { michael@0: return INVALID_POSITION; michael@0: } michael@0: michael@0: for (int i = 0; i < childCount; i++) { michael@0: View v = getChildAt(i); michael@0: michael@0: if ((mIsVertical && motionPos <= v.getBottom()) || michael@0: (!mIsVertical && motionPos <= v.getRight())) { michael@0: return mFirstPosition + i; michael@0: } michael@0: } michael@0: michael@0: return INVALID_POSITION; michael@0: } michael@0: michael@0: private int findClosestMotionRowOrColumn(int motionPos) { michael@0: final int childCount = getChildCount(); michael@0: if (childCount == 0) { michael@0: return INVALID_POSITION; michael@0: } michael@0: michael@0: final int motionRow = findMotionRowOrColumn(motionPos); michael@0: if (motionRow != INVALID_POSITION) { michael@0: return motionRow; michael@0: } else { michael@0: return mFirstPosition + childCount - 1; michael@0: } michael@0: } michael@0: michael@0: @TargetApi(9) michael@0: private int getScaledOverscrollDistance(ViewConfiguration vc) { michael@0: if (Build.VERSION.SDK_INT < 9) { michael@0: return 0; michael@0: } michael@0: michael@0: return vc.getScaledOverscrollDistance(); michael@0: } michael@0: michael@0: private boolean contentFits() { michael@0: final int childCount = getChildCount(); michael@0: if (childCount == 0) { michael@0: return true; michael@0: } michael@0: michael@0: if (childCount != mItemCount) { michael@0: return false; michael@0: } michael@0: michael@0: View first = getChildAt(0); michael@0: View last = getChildAt(childCount - 1); michael@0: michael@0: if (mIsVertical) { michael@0: return first.getTop() >= getPaddingTop() && michael@0: last.getBottom() <= getHeight() - getPaddingBottom(); michael@0: } else { michael@0: return first.getLeft() >= getPaddingLeft() && michael@0: last.getRight() <= getWidth() - getPaddingRight(); michael@0: } michael@0: } michael@0: michael@0: private void updateScrollbarsDirection() { michael@0: setHorizontalScrollBarEnabled(!mIsVertical); michael@0: setVerticalScrollBarEnabled(mIsVertical); michael@0: } michael@0: michael@0: private void triggerCheckForTap() { michael@0: if (mPendingCheckForTap == null) { michael@0: mPendingCheckForTap = new CheckForTap(); michael@0: } michael@0: michael@0: postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); michael@0: } michael@0: michael@0: private void cancelCheckForTap() { michael@0: if (mPendingCheckForTap == null) { michael@0: return; michael@0: } michael@0: michael@0: removeCallbacks(mPendingCheckForTap); michael@0: } michael@0: michael@0: private void triggerCheckForLongPress() { michael@0: if (mPendingCheckForLongPress == null) { michael@0: mPendingCheckForLongPress = new CheckForLongPress(); michael@0: } michael@0: michael@0: mPendingCheckForLongPress.rememberWindowAttachCount(); michael@0: michael@0: postDelayed(mPendingCheckForLongPress, michael@0: ViewConfiguration.getLongPressTimeout()); michael@0: } michael@0: michael@0: private void cancelCheckForLongPress() { michael@0: if (mPendingCheckForLongPress == null) { michael@0: return; michael@0: } michael@0: michael@0: removeCallbacks(mPendingCheckForLongPress); michael@0: } michael@0: michael@0: boolean trackMotionScroll(int incrementalDelta) { michael@0: final int childCount = getChildCount(); michael@0: if (childCount == 0) { michael@0: return true; michael@0: } michael@0: michael@0: final View first = getChildAt(0); michael@0: final int firstStart = (mIsVertical ? first.getTop() : first.getLeft()); michael@0: michael@0: final View last = getChildAt(childCount - 1); michael@0: final int lastEnd = (mIsVertical ? last.getBottom() : last.getRight()); michael@0: michael@0: final int paddingTop = getPaddingTop(); michael@0: final int paddingBottom = getPaddingBottom(); michael@0: final int paddingLeft = getPaddingLeft(); michael@0: final int paddingRight = getPaddingRight(); michael@0: michael@0: final int paddingStart = (mIsVertical ? paddingTop : paddingLeft); michael@0: michael@0: final int spaceBefore = paddingStart - firstStart; michael@0: final int end = (mIsVertical ? getHeight() - paddingBottom : michael@0: getWidth() - paddingRight); michael@0: final int spaceAfter = lastEnd - end; michael@0: michael@0: final int size; michael@0: if (mIsVertical) { michael@0: size = getHeight() - paddingBottom - paddingTop; michael@0: } else { michael@0: size = getWidth() - paddingRight - paddingLeft; michael@0: } michael@0: michael@0: if (incrementalDelta < 0) { michael@0: incrementalDelta = Math.max(-(size - 1), incrementalDelta); michael@0: } else { michael@0: incrementalDelta = Math.min(size - 1, incrementalDelta); michael@0: } michael@0: michael@0: final int firstPosition = mFirstPosition; michael@0: michael@0: final boolean cannotScrollDown = (firstPosition == 0 && michael@0: firstStart >= paddingStart && incrementalDelta >= 0); michael@0: final boolean cannotScrollUp = (firstPosition + childCount == mItemCount && michael@0: lastEnd <= end && incrementalDelta <= 0); michael@0: michael@0: if (cannotScrollDown || cannotScrollUp) { michael@0: return incrementalDelta != 0; michael@0: } michael@0: michael@0: final boolean inTouchMode = isInTouchMode(); michael@0: if (inTouchMode) { michael@0: hideSelector(); michael@0: } michael@0: michael@0: int start = 0; michael@0: int count = 0; michael@0: michael@0: final boolean down = (incrementalDelta < 0); michael@0: if (down) { michael@0: int childrenStart = -incrementalDelta + paddingStart; michael@0: michael@0: for (int i = 0; i < childCount; i++) { michael@0: final View child = getChildAt(i); michael@0: final int childEnd = (mIsVertical ? child.getBottom() : child.getRight()); michael@0: michael@0: if (childEnd >= childrenStart) { michael@0: break; michael@0: } michael@0: michael@0: count++; michael@0: mRecycler.addScrapView(child, firstPosition + i); michael@0: } michael@0: } else { michael@0: int childrenEnd = end - incrementalDelta; michael@0: michael@0: for (int i = childCount - 1; i >= 0; i--) { michael@0: final View child = getChildAt(i); michael@0: final int childStart = (mIsVertical ? child.getTop() : child.getLeft()); michael@0: michael@0: if (childStart <= childrenEnd) { michael@0: break; michael@0: } michael@0: michael@0: start = i; michael@0: count++; michael@0: mRecycler.addScrapView(child, firstPosition + i); michael@0: } michael@0: } michael@0: michael@0: mBlockLayoutRequests = true; michael@0: michael@0: if (count > 0) { michael@0: detachViewsFromParent(start, count); michael@0: } michael@0: michael@0: // invalidate before moving the children to avoid unnecessary invalidate michael@0: // calls to bubble up from the children all the way to the top michael@0: if (!awakenScrollbarsInternal()) { michael@0: invalidate(); michael@0: } michael@0: michael@0: offsetChildren(incrementalDelta); michael@0: michael@0: if (down) { michael@0: mFirstPosition += count; michael@0: } michael@0: michael@0: final int absIncrementalDelta = Math.abs(incrementalDelta); michael@0: if (spaceBefore < absIncrementalDelta || spaceAfter < absIncrementalDelta) { michael@0: fillGap(down); michael@0: } michael@0: michael@0: if (!inTouchMode && mSelectedPosition != INVALID_POSITION) { michael@0: final int childIndex = mSelectedPosition - mFirstPosition; michael@0: if (childIndex >= 0 && childIndex < getChildCount()) { michael@0: positionSelector(mSelectedPosition, getChildAt(childIndex)); michael@0: } michael@0: } else if (mSelectorPosition != INVALID_POSITION) { michael@0: final int childIndex = mSelectorPosition - mFirstPosition; michael@0: if (childIndex >= 0 && childIndex < getChildCount()) { michael@0: positionSelector(INVALID_POSITION, getChildAt(childIndex)); michael@0: } michael@0: } else { michael@0: mSelectorRect.setEmpty(); michael@0: } michael@0: michael@0: mBlockLayoutRequests = false; michael@0: michael@0: invokeOnItemScrollListener(); michael@0: michael@0: return false; michael@0: } michael@0: michael@0: @TargetApi(14) michael@0: private final float getCurrVelocity() { michael@0: if (Build.VERSION.SDK_INT >= 14) { michael@0: return mScroller.getCurrVelocity(); michael@0: } michael@0: michael@0: return 0; michael@0: } michael@0: michael@0: @TargetApi(5) michael@0: private boolean awakenScrollbarsInternal() { michael@0: if (Build.VERSION.SDK_INT >= 5) { michael@0: return super.awakenScrollBars(); michael@0: } else { michael@0: return false; michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void computeScroll() { michael@0: if (!mScroller.computeScrollOffset()) { michael@0: return; michael@0: } michael@0: michael@0: final int pos; michael@0: if (mIsVertical) { michael@0: pos = mScroller.getCurrY(); michael@0: } else { michael@0: pos = mScroller.getCurrX(); michael@0: } michael@0: michael@0: final int diff = (int) (pos - mLastTouchPos); michael@0: mLastTouchPos = pos; michael@0: michael@0: final boolean stopped = trackMotionScroll(diff); michael@0: michael@0: if (!stopped && !mScroller.isFinished()) { michael@0: ViewCompat.postInvalidateOnAnimation(this); michael@0: } else { michael@0: if (stopped) { michael@0: final int overScrollMode = ViewCompat.getOverScrollMode(this); michael@0: if (overScrollMode != ViewCompat.OVER_SCROLL_NEVER) { michael@0: final EdgeEffectCompat edge = michael@0: (diff > 0 ? mStartEdge : mEndEdge); michael@0: michael@0: boolean needsInvalidate = michael@0: edge.onAbsorb(Math.abs((int) getCurrVelocity())); michael@0: michael@0: if (needsInvalidate) { michael@0: ViewCompat.postInvalidateOnAnimation(this); michael@0: } michael@0: } michael@0: michael@0: mScroller.abortAnimation(); michael@0: } michael@0: michael@0: mTouchMode = TOUCH_MODE_REST; michael@0: reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); michael@0: } michael@0: } michael@0: michael@0: private void finishEdgeGlows() { michael@0: if (mStartEdge != null) { michael@0: mStartEdge.finish(); michael@0: } michael@0: michael@0: if (mEndEdge != null) { michael@0: mEndEdge.finish(); michael@0: } michael@0: } michael@0: michael@0: private boolean drawStartEdge(Canvas canvas) { michael@0: if (mStartEdge.isFinished()) { michael@0: return false; michael@0: } michael@0: michael@0: if (mIsVertical) { michael@0: return mStartEdge.draw(canvas); michael@0: } michael@0: michael@0: final int restoreCount = canvas.save(); michael@0: final int height = getHeight() - getPaddingTop() - getPaddingBottom(); michael@0: michael@0: canvas.translate(0, height); michael@0: canvas.rotate(270); michael@0: michael@0: final boolean needsInvalidate = mStartEdge.draw(canvas); michael@0: canvas.restoreToCount(restoreCount); michael@0: return needsInvalidate; michael@0: } michael@0: michael@0: private boolean drawEndEdge(Canvas canvas) { michael@0: if (mEndEdge.isFinished()) { michael@0: return false; michael@0: } michael@0: michael@0: final int restoreCount = canvas.save(); michael@0: final int width = getWidth() - getPaddingLeft() - getPaddingRight(); michael@0: final int height = getHeight() - getPaddingTop() - getPaddingBottom(); michael@0: michael@0: if (mIsVertical) { michael@0: canvas.translate(-width, height); michael@0: canvas.rotate(180, width, 0); michael@0: } else { michael@0: canvas.translate(width, 0); michael@0: canvas.rotate(90); michael@0: } michael@0: michael@0: final boolean needsInvalidate = mEndEdge.draw(canvas); michael@0: canvas.restoreToCount(restoreCount); michael@0: return needsInvalidate; michael@0: } michael@0: michael@0: private void drawSelector(Canvas canvas) { michael@0: if (!mSelectorRect.isEmpty()) { michael@0: final Drawable selector = mSelector; michael@0: selector.setBounds(mSelectorRect); michael@0: selector.draw(canvas); michael@0: } michael@0: } michael@0: michael@0: private void useDefaultSelector() { michael@0: setSelector(getResources().getDrawable( michael@0: android.R.drawable.list_selector_background)); michael@0: } michael@0: michael@0: private boolean shouldShowSelector() { michael@0: return (hasFocus() && !isInTouchMode()) || touchModeDrawsInPressedState(); michael@0: } michael@0: michael@0: private void positionSelector(int position, View selected) { michael@0: if (position != INVALID_POSITION) { michael@0: mSelectorPosition = position; michael@0: } michael@0: michael@0: mSelectorRect.set(selected.getLeft(), selected.getTop(), selected.getRight(), michael@0: selected.getBottom()); michael@0: michael@0: final boolean isChildViewEnabled = mIsChildViewEnabled; michael@0: if (selected.isEnabled() != isChildViewEnabled) { michael@0: mIsChildViewEnabled = !isChildViewEnabled; michael@0: michael@0: if (getSelectedItemPosition() != INVALID_POSITION) { michael@0: refreshDrawableState(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: private void hideSelector() { michael@0: if (mSelectedPosition != INVALID_POSITION) { michael@0: if (mLayoutMode != LAYOUT_SPECIFIC) { michael@0: mResurrectToPosition = mSelectedPosition; michael@0: } michael@0: michael@0: if (mNextSelectedPosition >= 0 && mNextSelectedPosition != mSelectedPosition) { michael@0: mResurrectToPosition = mNextSelectedPosition; michael@0: } michael@0: michael@0: setSelectedPositionInt(INVALID_POSITION); michael@0: setNextSelectedPositionInt(INVALID_POSITION); michael@0: michael@0: mSelectedStart = 0; michael@0: } michael@0: } michael@0: michael@0: private void setSelectedPositionInt(int position) { michael@0: mSelectedPosition = position; michael@0: mSelectedRowId = getItemIdAtPosition(position); michael@0: } michael@0: michael@0: private void setSelectionInt(int position) { michael@0: setNextSelectedPositionInt(position); michael@0: boolean awakeScrollbars = false; michael@0: michael@0: final int selectedPosition = mSelectedPosition; michael@0: if (selectedPosition >= 0) { michael@0: if (position == selectedPosition - 1) { michael@0: awakeScrollbars = true; michael@0: } else if (position == selectedPosition + 1) { michael@0: awakeScrollbars = true; michael@0: } michael@0: } michael@0: michael@0: layoutChildren(); michael@0: michael@0: if (awakeScrollbars) { michael@0: awakenScrollbarsInternal(); michael@0: } michael@0: } michael@0: michael@0: private void setNextSelectedPositionInt(int position) { michael@0: mNextSelectedPosition = position; michael@0: mNextSelectedRowId = getItemIdAtPosition(position); michael@0: michael@0: // If we are trying to sync to the selection, update that too michael@0: if (mNeedSync && mSyncMode == SYNC_SELECTED_POSITION && position >= 0) { michael@0: mSyncPosition = position; michael@0: mSyncRowId = mNextSelectedRowId; michael@0: } michael@0: } michael@0: michael@0: private boolean touchModeDrawsInPressedState() { michael@0: switch (mTouchMode) { michael@0: case TOUCH_MODE_TAP: michael@0: case TOUCH_MODE_DONE_WAITING: michael@0: return true; michael@0: default: michael@0: return false; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Sets the selector state to "pressed" and posts a CheckForKeyLongPress to see if michael@0: * this is a long press. michael@0: */ michael@0: private void keyPressed() { michael@0: if (!isEnabled() || !isClickable()) { michael@0: return; michael@0: } michael@0: michael@0: final Drawable selector = mSelector; michael@0: final Rect selectorRect = mSelectorRect; michael@0: michael@0: if (selector != null && (isFocused() || touchModeDrawsInPressedState()) michael@0: && !selectorRect.isEmpty()) { michael@0: michael@0: final View child = getChildAt(mSelectedPosition - mFirstPosition); michael@0: michael@0: if (child != null) { michael@0: if (child.hasFocusable()) { michael@0: return; michael@0: } michael@0: michael@0: child.setPressed(true); michael@0: } michael@0: michael@0: setPressed(true); michael@0: michael@0: final boolean longClickable = isLongClickable(); michael@0: final Drawable d = selector.getCurrent(); michael@0: if (d != null && d instanceof TransitionDrawable) { michael@0: if (longClickable) { michael@0: ((TransitionDrawable) d).startTransition( michael@0: ViewConfiguration.getLongPressTimeout()); michael@0: } else { michael@0: ((TransitionDrawable) d).resetTransition(); michael@0: } michael@0: } michael@0: michael@0: if (longClickable && !mDataChanged) { michael@0: if (mPendingCheckForKeyLongPress == null) { michael@0: mPendingCheckForKeyLongPress = new CheckForKeyLongPress(); michael@0: } michael@0: michael@0: mPendingCheckForKeyLongPress.rememberWindowAttachCount(); michael@0: postDelayed(mPendingCheckForKeyLongPress, ViewConfiguration.getLongPressTimeout()); michael@0: } michael@0: } michael@0: } michael@0: michael@0: private void updateSelectorState() { michael@0: if (mSelector != null) { michael@0: if (shouldShowSelector()) { michael@0: mSelector.setState(getDrawableState()); michael@0: } else { michael@0: mSelector.setState(STATE_NOTHING); michael@0: } michael@0: } michael@0: } michael@0: michael@0: private void checkSelectionChanged() { michael@0: if ((mSelectedPosition != mOldSelectedPosition) || (mSelectedRowId != mOldSelectedRowId)) { michael@0: selectionChanged(); michael@0: mOldSelectedPosition = mSelectedPosition; michael@0: mOldSelectedRowId = mSelectedRowId; michael@0: } michael@0: } michael@0: michael@0: private void selectionChanged() { michael@0: OnItemSelectedListener listener = getOnItemSelectedListener(); michael@0: if (listener == null) { michael@0: return; michael@0: } michael@0: michael@0: if (mInLayout || mBlockLayoutRequests) { michael@0: // If we are in a layout traversal, defer notification michael@0: // by posting. This ensures that the view tree is michael@0: // in a consistent state and is able to accommodate michael@0: // new layout or invalidate requests. michael@0: if (mSelectionNotifier == null) { michael@0: mSelectionNotifier = new SelectionNotifier(); michael@0: } michael@0: michael@0: post(mSelectionNotifier); michael@0: } else { michael@0: fireOnSelected(); michael@0: performAccessibilityActionsOnSelected(); michael@0: } michael@0: } michael@0: michael@0: private void fireOnSelected() { michael@0: OnItemSelectedListener listener = getOnItemSelectedListener(); michael@0: if (listener == null) { michael@0: return; michael@0: } michael@0: michael@0: final int selection = getSelectedItemPosition(); michael@0: if (selection >= 0) { michael@0: View v = getSelectedView(); michael@0: listener.onItemSelected(this, v, selection, michael@0: mAdapter.getItemId(selection)); michael@0: } else { michael@0: listener.onNothingSelected(this); michael@0: } michael@0: } michael@0: michael@0: private void performAccessibilityActionsOnSelected() { michael@0: final int position = getSelectedItemPosition(); michael@0: if (position >= 0) { michael@0: // We fire selection events here not in View michael@0: sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); michael@0: } michael@0: } michael@0: michael@0: private int lookForSelectablePosition(int position) { michael@0: return lookForSelectablePosition(position, true); michael@0: } michael@0: michael@0: private int lookForSelectablePosition(int position, boolean lookDown) { michael@0: final ListAdapter adapter = mAdapter; michael@0: if (adapter == null || isInTouchMode()) { michael@0: return INVALID_POSITION; michael@0: } michael@0: michael@0: final int itemCount = mItemCount; michael@0: if (!mAreAllItemsSelectable) { michael@0: if (lookDown) { michael@0: position = Math.max(0, position); michael@0: while (position < itemCount && !adapter.isEnabled(position)) { michael@0: position++; michael@0: } michael@0: } else { michael@0: position = Math.min(position, itemCount - 1); michael@0: while (position >= 0 && !adapter.isEnabled(position)) { michael@0: position--; michael@0: } michael@0: } michael@0: michael@0: if (position < 0 || position >= itemCount) { michael@0: return INVALID_POSITION; michael@0: } michael@0: michael@0: return position; michael@0: } else { michael@0: if (position < 0 || position >= itemCount) { michael@0: return INVALID_POSITION; michael@0: } michael@0: michael@0: return position; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or michael@0: * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the michael@0: * current view orientation. michael@0: * michael@0: * @return The position of the next selectable position of the views that michael@0: * are currently visible, taking into account the fact that there might michael@0: * be no selection. Returns {@link #INVALID_POSITION} if there is no michael@0: * selectable view on screen in the given direction. michael@0: */ michael@0: private int lookForSelectablePositionOnScreen(int direction) { michael@0: forceValidFocusDirection(direction); michael@0: michael@0: final int firstPosition = mFirstPosition; michael@0: final ListAdapter adapter = getAdapter(); michael@0: michael@0: if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) { michael@0: int startPos = (mSelectedPosition != INVALID_POSITION ? michael@0: mSelectedPosition + 1 : firstPosition); michael@0: michael@0: if (startPos >= adapter.getCount()) { michael@0: return INVALID_POSITION; michael@0: } michael@0: michael@0: if (startPos < firstPosition) { michael@0: startPos = firstPosition; michael@0: } michael@0: michael@0: final int lastVisiblePos = getLastVisiblePosition(); michael@0: michael@0: for (int pos = startPos; pos <= lastVisiblePos; pos++) { michael@0: if (adapter.isEnabled(pos) michael@0: && getChildAt(pos - firstPosition).getVisibility() == View.VISIBLE) { michael@0: return pos; michael@0: } michael@0: } michael@0: } else { michael@0: final int last = firstPosition + getChildCount() - 1; michael@0: michael@0: int startPos = (mSelectedPosition != INVALID_POSITION) ? michael@0: mSelectedPosition - 1 : firstPosition + getChildCount() - 1; michael@0: michael@0: if (startPos < 0 || startPos >= adapter.getCount()) { michael@0: return INVALID_POSITION; michael@0: } michael@0: michael@0: if (startPos > last) { michael@0: startPos = last; michael@0: } michael@0: michael@0: for (int pos = startPos; pos >= firstPosition; pos--) { michael@0: if (adapter.isEnabled(pos) michael@0: && getChildAt(pos - firstPosition).getVisibility() == View.VISIBLE) { michael@0: return pos; michael@0: } michael@0: } michael@0: } michael@0: michael@0: return INVALID_POSITION; michael@0: } michael@0: michael@0: @Override michael@0: protected void drawableStateChanged() { michael@0: super.drawableStateChanged(); michael@0: updateSelectorState(); michael@0: } michael@0: michael@0: @Override michael@0: protected int[] onCreateDrawableState(int extraSpace) { michael@0: // If the child view is enabled then do the default behavior. michael@0: if (mIsChildViewEnabled) { michael@0: // Common case michael@0: return super.onCreateDrawableState(extraSpace); michael@0: } michael@0: michael@0: // The selector uses this View's drawable state. The selected child view michael@0: // is disabled, so we need to remove the enabled state from the drawable michael@0: // states. michael@0: final int enabledState = ENABLED_STATE_SET[0]; michael@0: michael@0: // If we don't have any extra space, it will return one of the static state arrays, michael@0: // and clearing the enabled state on those arrays is a bad thing! If we specify michael@0: // we need extra space, it will create+copy into a new array that safely mutable. michael@0: int[] state = super.onCreateDrawableState(extraSpace + 1); michael@0: int enabledPos = -1; michael@0: for (int i = state.length - 1; i >= 0; i--) { michael@0: if (state[i] == enabledState) { michael@0: enabledPos = i; michael@0: break; michael@0: } michael@0: } michael@0: michael@0: // Remove the enabled state michael@0: if (enabledPos >= 0) { michael@0: System.arraycopy(state, enabledPos + 1, state, enabledPos, michael@0: state.length - enabledPos - 1); michael@0: } michael@0: michael@0: return state; michael@0: } michael@0: michael@0: @Override michael@0: protected boolean canAnimate() { michael@0: return (super.canAnimate() && mItemCount > 0); michael@0: } michael@0: michael@0: @Override michael@0: protected void dispatchDraw(Canvas canvas) { michael@0: final boolean drawSelectorOnTop = mDrawSelectorOnTop; michael@0: if (!drawSelectorOnTop) { michael@0: drawSelector(canvas); michael@0: } michael@0: michael@0: super.dispatchDraw(canvas); michael@0: michael@0: if (drawSelectorOnTop) { michael@0: drawSelector(canvas); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void draw(Canvas canvas) { michael@0: super.draw(canvas); michael@0: michael@0: boolean needsInvalidate = false; michael@0: michael@0: if (mStartEdge != null) { michael@0: needsInvalidate |= drawStartEdge(canvas); michael@0: } michael@0: michael@0: if (mEndEdge != null) { michael@0: needsInvalidate |= drawEndEdge(canvas); michael@0: } michael@0: michael@0: if (needsInvalidate) { michael@0: ViewCompat.postInvalidateOnAnimation(this); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void requestLayout() { michael@0: if (!mInLayout && !mBlockLayoutRequests) { michael@0: super.requestLayout(); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public View getSelectedView() { michael@0: if (mItemCount > 0 && mSelectedPosition >= 0) { michael@0: return getChildAt(mSelectedPosition - mFirstPosition); michael@0: } else { michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void setSelection(int position) { michael@0: setSelectionFromOffset(position, 0); michael@0: } michael@0: michael@0: public void setSelectionFromOffset(int position, int offset) { michael@0: if (mAdapter == null) { michael@0: return; michael@0: } michael@0: michael@0: if (!isInTouchMode()) { michael@0: position = lookForSelectablePosition(position); michael@0: if (position >= 0) { michael@0: setNextSelectedPositionInt(position); michael@0: } michael@0: } else { michael@0: mResurrectToPosition = position; michael@0: } michael@0: michael@0: if (position >= 0) { michael@0: mLayoutMode = LAYOUT_SPECIFIC; michael@0: michael@0: if (mIsVertical) { michael@0: mSpecificStart = getPaddingTop() + offset; michael@0: } else { michael@0: mSpecificStart = getPaddingLeft() + offset; michael@0: } michael@0: michael@0: if (mNeedSync) { michael@0: mSyncPosition = position; michael@0: mSyncRowId = mAdapter.getItemId(position); michael@0: } michael@0: michael@0: requestLayout(); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public boolean dispatchKeyEvent(KeyEvent event) { michael@0: // Dispatch in the normal way michael@0: boolean handled = super.dispatchKeyEvent(event); michael@0: if (!handled) { michael@0: // If we didn't handle it... michael@0: final View focused = getFocusedChild(); michael@0: if (focused != null && event.getAction() == KeyEvent.ACTION_DOWN) { michael@0: // ... and our focused child didn't handle it michael@0: // ... give it to ourselves so we can scroll if necessary michael@0: handled = onKeyDown(event.getKeyCode(), event); michael@0: } michael@0: } michael@0: michael@0: return handled; michael@0: } michael@0: michael@0: @Override michael@0: protected void dispatchSetPressed(boolean pressed) { michael@0: // Don't dispatch setPressed to our children. We call setPressed on ourselves to michael@0: // get the selector in the right state, but we don't want to press each child. michael@0: } michael@0: michael@0: @Override michael@0: protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { michael@0: if (mSelector == null) { michael@0: useDefaultSelector(); michael@0: } michael@0: michael@0: int widthMode = MeasureSpec.getMode(widthMeasureSpec); michael@0: int heightMode = MeasureSpec.getMode(heightMeasureSpec); michael@0: int widthSize = MeasureSpec.getSize(widthMeasureSpec); michael@0: int heightSize = MeasureSpec.getSize(heightMeasureSpec); michael@0: michael@0: int childWidth = 0; michael@0: int childHeight = 0; michael@0: michael@0: mItemCount = (mAdapter == null ? 0 : mAdapter.getCount()); michael@0: if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED || michael@0: heightMode == MeasureSpec.UNSPECIFIED)) { michael@0: final View child = obtainView(0, mIsScrap); michael@0: michael@0: final int secondaryMeasureSpec = michael@0: (mIsVertical ? widthMeasureSpec : heightMeasureSpec); michael@0: michael@0: measureScrapChild(child, 0, secondaryMeasureSpec); michael@0: michael@0: childWidth = child.getMeasuredWidth(); michael@0: childHeight = child.getMeasuredHeight(); michael@0: michael@0: if (recycleOnMeasure()) { michael@0: mRecycler.addScrapView(child, -1); michael@0: } michael@0: } michael@0: michael@0: if (widthMode == MeasureSpec.UNSPECIFIED) { michael@0: widthSize = getPaddingLeft() + getPaddingRight() + childWidth; michael@0: if (mIsVertical) { michael@0: widthSize += getVerticalScrollbarWidth(); michael@0: } michael@0: } michael@0: michael@0: if (heightMode == MeasureSpec.UNSPECIFIED) { michael@0: heightSize = getPaddingTop() + getPaddingBottom() + childHeight; michael@0: if (!mIsVertical) { michael@0: heightSize += getHorizontalScrollbarHeight(); michael@0: } michael@0: } michael@0: michael@0: if (mIsVertical && heightMode == MeasureSpec.AT_MOST) { michael@0: heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1); michael@0: } michael@0: michael@0: if (!mIsVertical && widthMode == MeasureSpec.AT_MOST) { michael@0: widthSize = measureWidthOfChildren(heightMeasureSpec, 0, NO_POSITION, widthSize, -1); michael@0: } michael@0: michael@0: setMeasuredDimension(widthSize, heightSize); michael@0: } michael@0: michael@0: @Override michael@0: protected void onLayout(boolean changed, int l, int t, int r, int b) { michael@0: mInLayout = true; michael@0: michael@0: if (changed) { michael@0: final int childCount = getChildCount(); michael@0: for (int i = 0; i < childCount; i++) { michael@0: getChildAt(i).forceLayout(); michael@0: } michael@0: michael@0: mRecycler.markChildrenDirty(); michael@0: } michael@0: michael@0: layoutChildren(); michael@0: michael@0: mInLayout = false; michael@0: michael@0: final int width = r - l - getPaddingLeft() - getPaddingRight(); michael@0: final int height = b - t - getPaddingTop() - getPaddingBottom(); michael@0: michael@0: if (mStartEdge != null && mEndEdge != null) { michael@0: if (mIsVertical) { michael@0: mStartEdge.setSize(width, height); michael@0: mEndEdge.setSize(width, height); michael@0: } else { michael@0: mStartEdge.setSize(height, width); michael@0: mEndEdge.setSize(height, width); michael@0: } michael@0: } michael@0: } michael@0: michael@0: private void layoutChildren() { michael@0: if (getWidth() == 0 || getHeight() == 0) { michael@0: return; michael@0: } michael@0: michael@0: final boolean blockLayoutRequests = mBlockLayoutRequests; michael@0: if (!blockLayoutRequests) { michael@0: mBlockLayoutRequests = true; michael@0: } else { michael@0: return; michael@0: } michael@0: michael@0: try { michael@0: invalidate(); michael@0: michael@0: if (mAdapter == null) { michael@0: resetState(); michael@0: return; michael@0: } michael@0: michael@0: final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft()); michael@0: final int end = michael@0: (mIsVertical ? getHeight() - getPaddingBottom() : getWidth() - getPaddingRight()); michael@0: michael@0: int childCount = getChildCount(); michael@0: int index = 0; michael@0: int delta = 0; michael@0: michael@0: View focusLayoutRestoreView = null; michael@0: michael@0: View selected = null; michael@0: View oldSelected = null; michael@0: View newSelected = null; michael@0: View oldFirstChild = null; michael@0: michael@0: switch (mLayoutMode) { michael@0: case LAYOUT_SET_SELECTION: michael@0: index = mNextSelectedPosition - mFirstPosition; michael@0: if (index >= 0 && index < childCount) { michael@0: newSelected = getChildAt(index); michael@0: } michael@0: michael@0: break; michael@0: michael@0: case LAYOUT_FORCE_TOP: michael@0: case LAYOUT_FORCE_BOTTOM: michael@0: case LAYOUT_SPECIFIC: michael@0: case LAYOUT_SYNC: michael@0: break; michael@0: michael@0: case LAYOUT_MOVE_SELECTION: michael@0: default: michael@0: // Remember the previously selected view michael@0: index = mSelectedPosition - mFirstPosition; michael@0: if (index >= 0 && index < childCount) { michael@0: oldSelected = getChildAt(index); michael@0: } michael@0: michael@0: // Remember the previous first child michael@0: oldFirstChild = getChildAt(0); michael@0: michael@0: if (mNextSelectedPosition >= 0) { michael@0: delta = mNextSelectedPosition - mSelectedPosition; michael@0: } michael@0: michael@0: // Caution: newSelected might be null michael@0: newSelected = getChildAt(index + delta); michael@0: } michael@0: michael@0: final boolean dataChanged = mDataChanged; michael@0: if (dataChanged) { michael@0: handleDataChanged(); michael@0: } michael@0: michael@0: // Handle the empty set by removing all views that are visible michael@0: // and calling it a day michael@0: if (mItemCount == 0) { michael@0: resetState(); michael@0: return; michael@0: } else if (mItemCount != mAdapter.getCount()) { michael@0: throw new IllegalStateException("The content of the adapter has changed but " michael@0: + "TwoWayView did not receive a notification. Make sure the content of " michael@0: + "your adapter is not modified from a background thread, but only " michael@0: + "from the UI thread. [in TwoWayView(" + getId() + ", " + getClass() michael@0: + ") with Adapter(" + mAdapter.getClass() + ")]"); michael@0: } michael@0: michael@0: setSelectedPositionInt(mNextSelectedPosition); michael@0: michael@0: // Reset the focus restoration michael@0: View focusLayoutRestoreDirectChild = null; michael@0: michael@0: // Pull all children into the RecycleBin. michael@0: // These views will be reused if possible michael@0: final int firstPosition = mFirstPosition; michael@0: final RecycleBin recycleBin = mRecycler; michael@0: michael@0: if (dataChanged) { michael@0: for (int i = 0; i < childCount; i++) { michael@0: recycleBin.addScrapView(getChildAt(i), firstPosition + i); michael@0: } michael@0: } else { michael@0: recycleBin.fillActiveViews(childCount, firstPosition); michael@0: } michael@0: michael@0: // Take focus back to us temporarily to avoid the eventual michael@0: // call to clear focus when removing the focused child below michael@0: // from messing things up when ViewAncestor assigns focus back michael@0: // to someone else. michael@0: final View focusedChild = getFocusedChild(); michael@0: if (focusedChild != null) { michael@0: // We can remember the focused view to restore after relayout if the michael@0: // data hasn't changed, or if the focused position is a header or footer. michael@0: if (!dataChanged) { michael@0: focusLayoutRestoreDirectChild = focusedChild; michael@0: michael@0: // Remember the specific view that had focus michael@0: focusLayoutRestoreView = findFocus(); michael@0: if (focusLayoutRestoreView != null) { michael@0: // Tell it we are going to mess with it michael@0: focusLayoutRestoreView.onStartTemporaryDetach(); michael@0: } michael@0: } michael@0: michael@0: requestFocus(); michael@0: } michael@0: michael@0: // FIXME: We need a way to save current accessibility focus here michael@0: // so that it can be restored after we re-attach the children on each michael@0: // layout round. michael@0: michael@0: detachAllViewsFromParent(); michael@0: michael@0: switch (mLayoutMode) { michael@0: case LAYOUT_SET_SELECTION: michael@0: if (newSelected != null) { michael@0: final int newSelectedStart = michael@0: (mIsVertical ? newSelected.getTop() : newSelected.getLeft()); michael@0: michael@0: selected = fillFromSelection(newSelectedStart, start, end); michael@0: } else { michael@0: selected = fillFromMiddle(start, end); michael@0: } michael@0: michael@0: break; michael@0: michael@0: case LAYOUT_SYNC: michael@0: selected = fillSpecific(mSyncPosition, mSpecificStart); michael@0: break; michael@0: michael@0: case LAYOUT_FORCE_BOTTOM: michael@0: selected = fillBefore(mItemCount - 1, end); michael@0: adjustViewsStartOrEnd(); michael@0: break; michael@0: michael@0: case LAYOUT_FORCE_TOP: michael@0: mFirstPosition = 0; michael@0: selected = fillFromOffset(start); michael@0: adjustViewsStartOrEnd(); michael@0: break; michael@0: michael@0: case LAYOUT_SPECIFIC: michael@0: selected = fillSpecific(reconcileSelectedPosition(), mSpecificStart); michael@0: break; michael@0: michael@0: case LAYOUT_MOVE_SELECTION: michael@0: selected = moveSelection(oldSelected, newSelected, delta, start, end); michael@0: break; michael@0: michael@0: default: michael@0: if (childCount == 0) { michael@0: final int position = lookForSelectablePosition(0); michael@0: setSelectedPositionInt(position); michael@0: selected = fillFromOffset(start); michael@0: } else { michael@0: if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) { michael@0: int offset = start; michael@0: if (oldSelected != null) { michael@0: offset = (mIsVertical ? oldSelected.getTop() : oldSelected.getLeft()); michael@0: } michael@0: selected = fillSpecific(mSelectedPosition, offset); michael@0: } else if (mFirstPosition < mItemCount) { michael@0: int offset = start; michael@0: if (oldFirstChild != null) { michael@0: offset = (mIsVertical ? oldFirstChild.getTop() : oldFirstChild.getLeft()); michael@0: } michael@0: michael@0: selected = fillSpecific(mFirstPosition, offset); michael@0: } else { michael@0: selected = fillSpecific(0, start); michael@0: } michael@0: } michael@0: michael@0: break; michael@0: michael@0: } michael@0: michael@0: recycleBin.scrapActiveViews(); michael@0: michael@0: if (selected != null) { michael@0: if (mItemsCanFocus && hasFocus() && !selected.hasFocus()) { michael@0: final boolean focusWasTaken = (selected == focusLayoutRestoreDirectChild && michael@0: focusLayoutRestoreView != null && michael@0: focusLayoutRestoreView.requestFocus()) || selected.requestFocus(); michael@0: michael@0: if (!focusWasTaken) { michael@0: // Selected item didn't take focus, fine, but still want michael@0: // to make sure something else outside of the selected view michael@0: // has focus michael@0: final View focused = getFocusedChild(); michael@0: if (focused != null) { michael@0: focused.clearFocus(); michael@0: } michael@0: michael@0: positionSelector(INVALID_POSITION, selected); michael@0: } else { michael@0: selected.setSelected(false); michael@0: mSelectorRect.setEmpty(); michael@0: } michael@0: } else { michael@0: positionSelector(INVALID_POSITION, selected); michael@0: } michael@0: michael@0: mSelectedStart = (mIsVertical ? selected.getTop() : selected.getLeft()); michael@0: } else { michael@0: if (mTouchMode > TOUCH_MODE_DOWN && mTouchMode < TOUCH_MODE_DRAGGING) { michael@0: View child = getChildAt(mMotionPosition - mFirstPosition); michael@0: michael@0: if (child != null) { michael@0: positionSelector(mMotionPosition, child); michael@0: } michael@0: } else { michael@0: mSelectedStart = 0; michael@0: mSelectorRect.setEmpty(); michael@0: } michael@0: michael@0: // Even if there is not selected position, we may need to restore michael@0: // focus (i.e. something focusable in touch mode) michael@0: if (hasFocus() && focusLayoutRestoreView != null) { michael@0: focusLayoutRestoreView.requestFocus(); michael@0: } michael@0: } michael@0: michael@0: // Tell focus view we are done mucking with it, if it is still in michael@0: // our view hierarchy. michael@0: if (focusLayoutRestoreView != null michael@0: && focusLayoutRestoreView.getWindowToken() != null) { michael@0: focusLayoutRestoreView.onFinishTemporaryDetach(); michael@0: } michael@0: michael@0: mLayoutMode = LAYOUT_NORMAL; michael@0: mDataChanged = false; michael@0: mNeedSync = false; michael@0: michael@0: setNextSelectedPositionInt(mSelectedPosition); michael@0: if (mItemCount > 0) { michael@0: checkSelectionChanged(); michael@0: } michael@0: michael@0: invokeOnItemScrollListener(); michael@0: } finally { michael@0: if (!blockLayoutRequests) { michael@0: mBlockLayoutRequests = false; michael@0: mDataChanged = false; michael@0: } michael@0: } michael@0: } michael@0: michael@0: protected boolean recycleOnMeasure() { michael@0: return true; michael@0: } michael@0: michael@0: private void offsetChildren(int offset) { michael@0: final int childCount = getChildCount(); michael@0: michael@0: for (int i = 0; i < childCount; i++) { michael@0: final View child = getChildAt(i); michael@0: michael@0: if (mIsVertical) { michael@0: child.offsetTopAndBottom(offset); michael@0: } else { michael@0: child.offsetLeftAndRight(offset); michael@0: } michael@0: } michael@0: } michael@0: michael@0: private View moveSelection(View oldSelected, View newSelected, int delta, int start, michael@0: int end) { michael@0: final int selectedPosition = mSelectedPosition; michael@0: michael@0: final int oldSelectedStart = (mIsVertical ? oldSelected.getTop() : oldSelected.getLeft()); michael@0: final int oldSelectedEnd = (mIsVertical ? oldSelected.getBottom() : oldSelected.getRight()); michael@0: michael@0: View selected = null; michael@0: michael@0: if (delta > 0) { michael@0: /* michael@0: * Case 1: Scrolling down. michael@0: */ michael@0: michael@0: /* michael@0: * Before After michael@0: * | | | | michael@0: * +-------+ +-------+ michael@0: * | A | | A | michael@0: * | 1 | => +-------+ michael@0: * +-------+ | B | michael@0: * | B | | 2 | michael@0: * +-------+ +-------+ michael@0: * | | | | michael@0: * michael@0: * Try to keep the top of the previously selected item where it was. michael@0: * oldSelected = A michael@0: * selected = B michael@0: */ michael@0: michael@0: // Put oldSelected (A) where it belongs michael@0: oldSelected = makeAndAddView(selectedPosition - 1, oldSelectedStart, true, false); michael@0: michael@0: final int itemMargin = mItemMargin; michael@0: michael@0: // Now put the new selection (B) below that michael@0: selected = makeAndAddView(selectedPosition, oldSelectedEnd + itemMargin, true, true); michael@0: michael@0: final int selectedStart = (mIsVertical ? selected.getTop() : selected.getLeft()); michael@0: final int selectedEnd = (mIsVertical ? selected.getBottom() : selected.getRight()); michael@0: michael@0: // Some of the newly selected item extends below the bottom of the list michael@0: if (selectedEnd > end) { michael@0: // Find space available above the selection into which we can scroll upwards michael@0: final int spaceBefore = selectedStart - start; michael@0: michael@0: // Find space required to bring the bottom of the selected item fully into view michael@0: final int spaceAfter = selectedEnd - end; michael@0: michael@0: // Don't scroll more than half the size of the list michael@0: final int halfSpace = (end - start) / 2; michael@0: int offset = Math.min(spaceBefore, spaceAfter); michael@0: offset = Math.min(offset, halfSpace); michael@0: michael@0: if (mIsVertical) { michael@0: oldSelected.offsetTopAndBottom(-offset); michael@0: selected.offsetTopAndBottom(-offset); michael@0: } else { michael@0: oldSelected.offsetLeftAndRight(-offset); michael@0: selected.offsetLeftAndRight(-offset); michael@0: } michael@0: } michael@0: michael@0: // Fill in views before and after michael@0: fillBefore(mSelectedPosition - 2, selectedStart - itemMargin); michael@0: adjustViewsStartOrEnd(); michael@0: fillAfter(mSelectedPosition + 1, selectedEnd + itemMargin); michael@0: } else if (delta < 0) { michael@0: /* michael@0: * Case 2: Scrolling up. michael@0: */ michael@0: michael@0: /* michael@0: * Before After michael@0: * | | | | michael@0: * +-------+ +-------+ michael@0: * | A | | A | michael@0: * +-------+ => | 1 | michael@0: * | B | +-------+ michael@0: * | 2 | | B | michael@0: * +-------+ +-------+ michael@0: * | | | | michael@0: * michael@0: * Try to keep the top of the item about to become selected where it was. michael@0: * newSelected = A michael@0: * olSelected = B michael@0: */ michael@0: michael@0: if (newSelected != null) { michael@0: // Try to position the top of newSel (A) where it was before it was selected michael@0: final int newSelectedStart = (mIsVertical ? newSelected.getTop() : newSelected.getLeft()); michael@0: selected = makeAndAddView(selectedPosition, newSelectedStart, true, true); michael@0: } else { michael@0: // If (A) was not on screen and so did not have a view, position michael@0: // it above the oldSelected (B) michael@0: selected = makeAndAddView(selectedPosition, oldSelectedStart, false, true); michael@0: } michael@0: michael@0: final int selectedStart = (mIsVertical ? selected.getTop() : selected.getLeft()); michael@0: final int selectedEnd = (mIsVertical ? selected.getBottom() : selected.getRight()); michael@0: michael@0: // Some of the newly selected item extends above the top of the list michael@0: if (selectedStart < start) { michael@0: // Find space required to bring the top of the selected item fully into view michael@0: final int spaceBefore = start - selectedStart; michael@0: michael@0: // Find space available below the selection into which we can scroll downwards michael@0: final int spaceAfter = end - selectedEnd; michael@0: michael@0: // Don't scroll more than half the height of the list michael@0: final int halfSpace = (end - start) / 2; michael@0: int offset = Math.min(spaceBefore, spaceAfter); michael@0: offset = Math.min(offset, halfSpace); michael@0: michael@0: if (mIsVertical) { michael@0: selected.offsetTopAndBottom(offset); michael@0: } else { michael@0: selected.offsetLeftAndRight(offset); michael@0: } michael@0: } michael@0: michael@0: // Fill in views above and below michael@0: fillBeforeAndAfter(selected, selectedPosition); michael@0: } else { michael@0: /* michael@0: * Case 3: Staying still michael@0: */ michael@0: michael@0: selected = makeAndAddView(selectedPosition, oldSelectedStart, true, true); michael@0: michael@0: final int selectedStart = (mIsVertical ? selected.getTop() : selected.getLeft()); michael@0: final int selectedEnd = (mIsVertical ? selected.getBottom() : selected.getRight()); michael@0: michael@0: // We're staying still... michael@0: if (oldSelectedStart < start) { michael@0: // ... but the top of the old selection was off screen. michael@0: // (This can happen if the data changes size out from under us) michael@0: int newEnd = selectedEnd; michael@0: if (newEnd < start + 20) { michael@0: // Not enough visible -- bring it onscreen michael@0: if (mIsVertical) { michael@0: selected.offsetTopAndBottom(start - selectedStart); michael@0: } else { michael@0: selected.offsetLeftAndRight(start - selectedStart); michael@0: } michael@0: } michael@0: } michael@0: michael@0: // Fill in views above and below michael@0: fillBeforeAndAfter(selected, selectedPosition); michael@0: } michael@0: michael@0: return selected; michael@0: } michael@0: michael@0: void confirmCheckedPositionsById() { michael@0: // Clear out the positional check states, we'll rebuild it below from IDs. michael@0: mCheckStates.clear(); michael@0: michael@0: for (int checkedIndex = 0; checkedIndex < mCheckedIdStates.size(); checkedIndex++) { michael@0: final long id = mCheckedIdStates.keyAt(checkedIndex); michael@0: final int lastPos = mCheckedIdStates.valueAt(checkedIndex); michael@0: michael@0: final long lastPosId = mAdapter.getItemId(lastPos); michael@0: if (id != lastPosId) { michael@0: // Look around to see if the ID is nearby. If not, uncheck it. michael@0: final int start = Math.max(0, lastPos - CHECK_POSITION_SEARCH_DISTANCE); michael@0: final int end = Math.min(lastPos + CHECK_POSITION_SEARCH_DISTANCE, mItemCount); michael@0: boolean found = false; michael@0: michael@0: for (int searchPos = start; searchPos < end; searchPos++) { michael@0: final long searchId = mAdapter.getItemId(searchPos); michael@0: if (id == searchId) { michael@0: found = true; michael@0: mCheckStates.put(searchPos, true); michael@0: mCheckedIdStates.setValueAt(checkedIndex, searchPos); michael@0: break; michael@0: } michael@0: } michael@0: michael@0: if (!found) { michael@0: mCheckedIdStates.delete(id); michael@0: checkedIndex--; michael@0: mCheckedItemCount--; michael@0: } michael@0: } else { michael@0: mCheckStates.put(lastPos, true); michael@0: } michael@0: } michael@0: } michael@0: michael@0: private void handleDataChanged() { michael@0: if (mChoiceMode.compareTo(ChoiceMode.NONE) != 0 && mAdapter != null && mAdapter.hasStableIds()) { michael@0: confirmCheckedPositionsById(); michael@0: } michael@0: michael@0: mRecycler.clearTransientStateViews(); michael@0: michael@0: final int itemCount = mItemCount; michael@0: if (itemCount > 0) { michael@0: int newPos; michael@0: int selectablePos; michael@0: michael@0: // Find the row we are supposed to sync to michael@0: if (mNeedSync) { michael@0: // Update this first, since setNextSelectedPositionInt inspects it michael@0: mNeedSync = false; michael@0: mPendingSync = null; michael@0: michael@0: switch (mSyncMode) { michael@0: case SYNC_SELECTED_POSITION: michael@0: if (isInTouchMode()) { michael@0: // We saved our state when not in touch mode. (We know this because michael@0: // mSyncMode is SYNC_SELECTED_POSITION.) Now we are trying to michael@0: // restore in touch mode. Just leave mSyncPosition as it is (possibly michael@0: // adjusting if the available range changed) and return. michael@0: mLayoutMode = LAYOUT_SYNC; michael@0: mSyncPosition = Math.min(Math.max(0, mSyncPosition), itemCount - 1); michael@0: michael@0: return; michael@0: } else { michael@0: // See if we can find a position in the new data with the same michael@0: // id as the old selection. This will change mSyncPosition. michael@0: newPos = findSyncPosition(); michael@0: if (newPos >= 0) { michael@0: // Found it. Now verify that new selection is still selectable michael@0: selectablePos = lookForSelectablePosition(newPos, true); michael@0: if (selectablePos == newPos) { michael@0: // Same row id is selected michael@0: mSyncPosition = newPos; michael@0: michael@0: if (mSyncHeight == getHeight()) { michael@0: // If we are at the same height as when we saved state, try michael@0: // to restore the scroll position too. michael@0: mLayoutMode = LAYOUT_SYNC; michael@0: } else { michael@0: // We are not the same height as when the selection was saved, so michael@0: // don't try to restore the exact position michael@0: mLayoutMode = LAYOUT_SET_SELECTION; michael@0: } michael@0: michael@0: // Restore selection michael@0: setNextSelectedPositionInt(newPos); michael@0: return; michael@0: } michael@0: } michael@0: } michael@0: break; michael@0: michael@0: case SYNC_FIRST_POSITION: michael@0: // Leave mSyncPosition as it is -- just pin to available range michael@0: mLayoutMode = LAYOUT_SYNC; michael@0: mSyncPosition = Math.min(Math.max(0, mSyncPosition), itemCount - 1); michael@0: michael@0: return; michael@0: } michael@0: } michael@0: michael@0: if (!isInTouchMode()) { michael@0: // We couldn't find matching data -- try to use the same position michael@0: newPos = getSelectedItemPosition(); michael@0: michael@0: // Pin position to the available range michael@0: if (newPos >= itemCount) { michael@0: newPos = itemCount - 1; michael@0: } michael@0: if (newPos < 0) { michael@0: newPos = 0; michael@0: } michael@0: michael@0: // Make sure we select something selectable -- first look down michael@0: selectablePos = lookForSelectablePosition(newPos, true); michael@0: michael@0: if (selectablePos >= 0) { michael@0: setNextSelectedPositionInt(selectablePos); michael@0: return; michael@0: } else { michael@0: // Looking down didn't work -- try looking up michael@0: selectablePos = lookForSelectablePosition(newPos, false); michael@0: if (selectablePos >= 0) { michael@0: setNextSelectedPositionInt(selectablePos); michael@0: return; michael@0: } michael@0: } michael@0: } else { michael@0: // We already know where we want to resurrect the selection michael@0: if (mResurrectToPosition >= 0) { michael@0: return; michael@0: } michael@0: } michael@0: } michael@0: michael@0: // Nothing is selected. Give up and reset everything. michael@0: mLayoutMode = LAYOUT_FORCE_TOP; michael@0: mSelectedPosition = INVALID_POSITION; michael@0: mSelectedRowId = INVALID_ROW_ID; michael@0: mNextSelectedPosition = INVALID_POSITION; michael@0: mNextSelectedRowId = INVALID_ROW_ID; michael@0: mNeedSync = false; michael@0: mPendingSync = null; michael@0: mSelectorPosition = INVALID_POSITION; michael@0: michael@0: checkSelectionChanged(); michael@0: } michael@0: michael@0: private int reconcileSelectedPosition() { michael@0: int position = mSelectedPosition; michael@0: if (position < 0) { michael@0: position = mResurrectToPosition; michael@0: } michael@0: michael@0: position = Math.max(0, position); michael@0: position = Math.min(position, mItemCount - 1); michael@0: michael@0: return position; michael@0: } michael@0: michael@0: boolean resurrectSelection() { michael@0: final int childCount = getChildCount(); michael@0: if (childCount <= 0) { michael@0: return false; michael@0: } michael@0: michael@0: int selectedStart = 0; michael@0: int selectedPosition; michael@0: michael@0: final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft()); michael@0: final int end = michael@0: (mIsVertical ? getHeight() - getPaddingBottom() : getWidth() - getPaddingRight()); michael@0: michael@0: final int firstPosition = mFirstPosition; michael@0: final int toPosition = mResurrectToPosition; michael@0: boolean down = true; michael@0: michael@0: if (toPosition >= firstPosition && toPosition < firstPosition + childCount) { michael@0: selectedPosition = toPosition; michael@0: michael@0: final View selected = getChildAt(selectedPosition - mFirstPosition); michael@0: selectedStart = (mIsVertical ? selected.getTop() : selected.getLeft()); michael@0: } else if (toPosition < firstPosition) { michael@0: // Default to selecting whatever is first michael@0: selectedPosition = firstPosition; michael@0: michael@0: for (int i = 0; i < childCount; i++) { michael@0: final View child = getChildAt(i); michael@0: final int childStart = (mIsVertical ? child.getTop() : child.getLeft()); michael@0: michael@0: if (i == 0) { michael@0: // Remember the position of the first item michael@0: selectedStart = childStart; michael@0: } michael@0: michael@0: if (childStart >= start) { michael@0: // Found a view whose top is fully visible michael@0: selectedPosition = firstPosition + i; michael@0: selectedStart = childStart; michael@0: break; michael@0: } michael@0: } michael@0: } else { michael@0: selectedPosition = firstPosition + childCount - 1; michael@0: down = false; michael@0: michael@0: for (int i = childCount - 1; i >= 0; i--) { michael@0: final View child = getChildAt(i); michael@0: final int childStart = (mIsVertical ? child.getTop() : child.getLeft()); michael@0: final int childEnd = (mIsVertical ? child.getBottom() : child.getRight()); michael@0: michael@0: if (i == childCount - 1) { michael@0: selectedStart = childStart; michael@0: } michael@0: michael@0: if (childEnd <= end) { michael@0: selectedPosition = firstPosition + i; michael@0: selectedStart = childStart; michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: michael@0: mResurrectToPosition = INVALID_POSITION; michael@0: mTouchMode = TOUCH_MODE_REST; michael@0: reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); michael@0: michael@0: mSpecificStart = selectedStart; michael@0: michael@0: selectedPosition = lookForSelectablePosition(selectedPosition, down); michael@0: if (selectedPosition >= firstPosition && selectedPosition <= getLastVisiblePosition()) { michael@0: mLayoutMode = LAYOUT_SPECIFIC; michael@0: updateSelectorState(); michael@0: setSelectionInt(selectedPosition); michael@0: invokeOnItemScrollListener(); michael@0: } else { michael@0: selectedPosition = INVALID_POSITION; michael@0: } michael@0: michael@0: return selectedPosition >= 0; michael@0: } michael@0: michael@0: /** michael@0: * If there is a selection returns false. michael@0: * Otherwise resurrects the selection and returns true if resurrected. michael@0: */ michael@0: boolean resurrectSelectionIfNeeded() { michael@0: if (mSelectedPosition < 0 && resurrectSelection()) { michael@0: updateSelectorState(); michael@0: return true; michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: michael@0: private int getChildWidthMeasureSpec(LayoutParams lp) { michael@0: if (!mIsVertical && lp.width == LayoutParams.WRAP_CONTENT) { michael@0: return MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); michael@0: } else if (mIsVertical) { michael@0: final int maxWidth = getWidth() - getPaddingLeft() - getPaddingRight(); michael@0: return MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.EXACTLY); michael@0: } else { michael@0: return MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY); michael@0: } michael@0: } michael@0: michael@0: private int getChildHeightMeasureSpec(LayoutParams lp) { michael@0: if (mIsVertical && lp.height == LayoutParams.WRAP_CONTENT) { michael@0: return MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); michael@0: } else if (!mIsVertical) { michael@0: final int maxHeight = getHeight() - getPaddingTop() - getPaddingBottom(); michael@0: return MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.EXACTLY); michael@0: } else { michael@0: return MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY); michael@0: } michael@0: } michael@0: michael@0: private void measureChild(View child) { michael@0: measureChild(child, (LayoutParams) child.getLayoutParams()); michael@0: } michael@0: michael@0: private void measureChild(View child, LayoutParams lp) { michael@0: final int widthSpec = getChildWidthMeasureSpec(lp); michael@0: final int heightSpec = getChildHeightMeasureSpec(lp); michael@0: child.measure(widthSpec, heightSpec); michael@0: } michael@0: michael@0: private void relayoutMeasuredChild(View child) { michael@0: final int w = child.getMeasuredWidth(); michael@0: final int h = child.getMeasuredHeight(); michael@0: michael@0: final int childLeft = getPaddingLeft(); michael@0: final int childRight = childLeft + w; michael@0: final int childTop = child.getTop(); michael@0: final int childBottom = childTop + h; michael@0: michael@0: child.layout(childLeft, childTop, childRight, childBottom); michael@0: } michael@0: michael@0: private void measureScrapChild(View scrapChild, int position, int secondaryMeasureSpec) { michael@0: LayoutParams lp = (LayoutParams) scrapChild.getLayoutParams(); michael@0: if (lp == null) { michael@0: lp = generateDefaultLayoutParams(); michael@0: scrapChild.setLayoutParams(lp); michael@0: } michael@0: michael@0: lp.viewType = mAdapter.getItemViewType(position); michael@0: lp.forceAdd = true; michael@0: michael@0: final int widthMeasureSpec; michael@0: final int heightMeasureSpec; michael@0: if (mIsVertical) { michael@0: widthMeasureSpec = secondaryMeasureSpec; michael@0: heightMeasureSpec = getChildHeightMeasureSpec(lp); michael@0: } else { michael@0: widthMeasureSpec = getChildWidthMeasureSpec(lp); michael@0: heightMeasureSpec = secondaryMeasureSpec; michael@0: } michael@0: michael@0: scrapChild.measure(widthMeasureSpec, heightMeasureSpec); michael@0: } michael@0: michael@0: /** michael@0: * Measures the height of the given range of children (inclusive) and michael@0: * returns the height with this TwoWayView's padding and item margin heights michael@0: * included. If maxHeight is provided, the measuring will stop when the michael@0: * current height reaches maxHeight. michael@0: * michael@0: * @param widthMeasureSpec The width measure spec to be given to a child's michael@0: * {@link View#measure(int, int)}. michael@0: * @param startPosition The position of the first child to be shown. michael@0: * @param endPosition The (inclusive) position of the last child to be michael@0: * shown. Specify {@link #NO_POSITION} if the last child should be michael@0: * the last available child from the adapter. michael@0: * @param maxHeight The maximum height that will be returned (if all the michael@0: * children don't fit in this value, this value will be michael@0: * returned). michael@0: * @param disallowPartialChildPosition In general, whether the returned michael@0: * height should only contain entire children. This is more michael@0: * powerful--it is the first inclusive position at which partial michael@0: * children will not be allowed. Example: it looks nice to have michael@0: * at least 3 completely visible children, and in portrait this michael@0: * will most likely fit; but in landscape there could be times michael@0: * when even 2 children can not be completely shown, so a value michael@0: * of 2 (remember, inclusive) would be good (assuming michael@0: * startPosition is 0). michael@0: * @return The height of this TwoWayView with the given children. michael@0: */ michael@0: private int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition, michael@0: final int maxHeight, int disallowPartialChildPosition) { michael@0: michael@0: final int paddingTop = getPaddingTop(); michael@0: final int paddingBottom = getPaddingBottom(); michael@0: michael@0: final ListAdapter adapter = mAdapter; michael@0: if (adapter == null) { michael@0: return paddingTop + paddingBottom; michael@0: } michael@0: michael@0: // Include the padding of the list michael@0: int returnedHeight = paddingTop + paddingBottom; michael@0: final int itemMargin = mItemMargin; michael@0: michael@0: // The previous height value that was less than maxHeight and contained michael@0: // no partial children michael@0: int prevHeightWithoutPartialChild = 0; michael@0: int i; michael@0: View child; michael@0: michael@0: // mItemCount - 1 since endPosition parameter is inclusive michael@0: endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition; michael@0: final RecycleBin recycleBin = mRecycler; michael@0: final boolean shouldRecycle = recycleOnMeasure(); michael@0: final boolean[] isScrap = mIsScrap; michael@0: michael@0: for (i = startPosition; i <= endPosition; ++i) { michael@0: child = obtainView(i, isScrap); michael@0: michael@0: measureScrapChild(child, i, widthMeasureSpec); michael@0: michael@0: if (i > 0) { michael@0: // Count the item margin for all but one child michael@0: returnedHeight += itemMargin; michael@0: } michael@0: michael@0: // Recycle the view before we possibly return from the method michael@0: if (shouldRecycle) { michael@0: recycleBin.addScrapView(child, -1); michael@0: } michael@0: michael@0: returnedHeight += child.getMeasuredHeight(); michael@0: michael@0: if (returnedHeight >= maxHeight) { michael@0: // We went over, figure out which height to return. If returnedHeight > maxHeight, michael@0: // then the i'th position did not fit completely. michael@0: return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1) michael@0: && (i > disallowPartialChildPosition) // We've past the min pos michael@0: && (prevHeightWithoutPartialChild > 0) // We have a prev height michael@0: && (returnedHeight != maxHeight) // i'th child did not fit completely michael@0: ? prevHeightWithoutPartialChild michael@0: : maxHeight; michael@0: } michael@0: michael@0: if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) { michael@0: prevHeightWithoutPartialChild = returnedHeight; michael@0: } michael@0: } michael@0: michael@0: // At this point, we went through the range of children, and they each michael@0: // completely fit, so return the returnedHeight michael@0: return returnedHeight; michael@0: } michael@0: michael@0: /** michael@0: * Measures the width of the given range of children (inclusive) and michael@0: * returns the width with this TwoWayView's padding and item margin widths michael@0: * included. If maxWidth is provided, the measuring will stop when the michael@0: * current width reaches maxWidth. michael@0: * michael@0: * @param heightMeasureSpec The height measure spec to be given to a child's michael@0: * {@link View#measure(int, int)}. michael@0: * @param startPosition The position of the first child to be shown. michael@0: * @param endPosition The (inclusive) position of the last child to be michael@0: * shown. Specify {@link #NO_POSITION} if the last child should be michael@0: * the last available child from the adapter. michael@0: * @param maxWidth The maximum width that will be returned (if all the michael@0: * children don't fit in this value, this value will be michael@0: * returned). michael@0: * @param disallowPartialChildPosition In general, whether the returned michael@0: * width should only contain entire children. This is more michael@0: * powerful--it is the first inclusive position at which partial michael@0: * children will not be allowed. Example: it looks nice to have michael@0: * at least 3 completely visible children, and in portrait this michael@0: * will most likely fit; but in landscape there could be times michael@0: * when even 2 children can not be completely shown, so a value michael@0: * of 2 (remember, inclusive) would be good (assuming michael@0: * startPosition is 0). michael@0: * @return The width of this TwoWayView with the given children. michael@0: */ michael@0: private int measureWidthOfChildren(int heightMeasureSpec, int startPosition, int endPosition, michael@0: final int maxWidth, int disallowPartialChildPosition) { michael@0: michael@0: final int paddingLeft = getPaddingLeft(); michael@0: final int paddingRight = getPaddingRight(); michael@0: michael@0: final ListAdapter adapter = mAdapter; michael@0: if (adapter == null) { michael@0: return paddingLeft + paddingRight; michael@0: } michael@0: michael@0: // Include the padding of the list michael@0: int returnedWidth = paddingLeft + paddingRight; michael@0: final int itemMargin = mItemMargin; michael@0: michael@0: // The previous height value that was less than maxHeight and contained michael@0: // no partial children michael@0: int prevWidthWithoutPartialChild = 0; michael@0: int i; michael@0: View child; michael@0: michael@0: // mItemCount - 1 since endPosition parameter is inclusive michael@0: endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition; michael@0: final RecycleBin recycleBin = mRecycler; michael@0: final boolean shouldRecycle = recycleOnMeasure(); michael@0: final boolean[] isScrap = mIsScrap; michael@0: michael@0: for (i = startPosition; i <= endPosition; ++i) { michael@0: child = obtainView(i, isScrap); michael@0: michael@0: measureScrapChild(child, i, heightMeasureSpec); michael@0: michael@0: if (i > 0) { michael@0: // Count the item margin for all but one child michael@0: returnedWidth += itemMargin; michael@0: } michael@0: michael@0: // Recycle the view before we possibly return from the method michael@0: if (shouldRecycle) { michael@0: recycleBin.addScrapView(child, -1); michael@0: } michael@0: michael@0: returnedWidth += child.getMeasuredHeight(); michael@0: michael@0: if (returnedWidth >= maxWidth) { michael@0: // We went over, figure out which width to return. If returnedWidth > maxWidth, michael@0: // then the i'th position did not fit completely. michael@0: return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1) michael@0: && (i > disallowPartialChildPosition) // We've past the min pos michael@0: && (prevWidthWithoutPartialChild > 0) // We have a prev width michael@0: && (returnedWidth != maxWidth) // i'th child did not fit completely michael@0: ? prevWidthWithoutPartialChild michael@0: : maxWidth; michael@0: } michael@0: michael@0: if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) { michael@0: prevWidthWithoutPartialChild = returnedWidth; michael@0: } michael@0: } michael@0: michael@0: // At this point, we went through the range of children, and they each michael@0: // completely fit, so return the returnedWidth michael@0: return returnedWidth; michael@0: } michael@0: michael@0: private View makeAndAddView(int position, int offset, boolean flow, boolean selected) { michael@0: final int top; michael@0: final int left; michael@0: michael@0: if (mIsVertical) { michael@0: top = offset; michael@0: left = getPaddingLeft(); michael@0: } else { michael@0: top = getPaddingTop(); michael@0: left = offset; michael@0: } michael@0: michael@0: if (!mDataChanged) { michael@0: // Try to use an existing view for this position michael@0: final View activeChild = mRecycler.getActiveView(position); michael@0: if (activeChild != null) { michael@0: // Found it -- we're using an existing child michael@0: // This just needs to be positioned michael@0: setupChild(activeChild, position, top, left, flow, selected, true); michael@0: michael@0: return activeChild; michael@0: } michael@0: } michael@0: michael@0: // Make a new view for this position, or convert an unused view if possible michael@0: final View child = obtainView(position, mIsScrap); michael@0: michael@0: // This needs to be positioned and measured michael@0: setupChild(child, position, top, left, flow, selected, mIsScrap[0]); michael@0: michael@0: return child; michael@0: } michael@0: michael@0: @TargetApi(11) michael@0: private void setupChild(View child, int position, int top, int left, michael@0: boolean flow, boolean selected, boolean recycled) { michael@0: final boolean isSelected = selected && shouldShowSelector(); michael@0: final boolean updateChildSelected = isSelected != child.isSelected(); michael@0: final int touchMode = mTouchMode; michael@0: michael@0: final boolean isPressed = touchMode > TOUCH_MODE_DOWN && touchMode < TOUCH_MODE_DRAGGING && michael@0: mMotionPosition == position; michael@0: michael@0: final boolean updateChildPressed = isPressed != child.isPressed(); michael@0: final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested(); michael@0: michael@0: // Respect layout params that are already in the view. Otherwise make some up... michael@0: LayoutParams lp = (LayoutParams) child.getLayoutParams(); michael@0: if (lp == null) { michael@0: lp = generateDefaultLayoutParams(); michael@0: } michael@0: michael@0: lp.viewType = mAdapter.getItemViewType(position); michael@0: michael@0: if (recycled && !lp.forceAdd) { michael@0: attachViewToParent(child, (flow ? -1 : 0), lp); michael@0: } else { michael@0: lp.forceAdd = false; michael@0: addViewInLayout(child, (flow ? -1 : 0), lp, true); michael@0: } michael@0: michael@0: if (updateChildSelected) { michael@0: child.setSelected(isSelected); michael@0: } michael@0: michael@0: if (updateChildPressed) { michael@0: child.setPressed(isPressed); michael@0: } michael@0: michael@0: if (mChoiceMode.compareTo(ChoiceMode.NONE) != 0 && mCheckStates != null) { michael@0: if (child instanceof Checkable) { michael@0: ((Checkable) child).setChecked(mCheckStates.get(position)); michael@0: } else if (getContext().getApplicationInfo().targetSdkVersion michael@0: >= Build.VERSION_CODES.HONEYCOMB) { michael@0: child.setActivated(mCheckStates.get(position)); michael@0: } michael@0: } michael@0: michael@0: if (needToMeasure) { michael@0: measureChild(child, lp); michael@0: } else { michael@0: cleanupLayoutState(child); michael@0: } michael@0: michael@0: final int w = child.getMeasuredWidth(); michael@0: final int h = child.getMeasuredHeight(); michael@0: michael@0: final int childTop = (mIsVertical && !flow ? top - h : top); michael@0: final int childLeft = (!mIsVertical && !flow ? left - w : left); michael@0: michael@0: if (needToMeasure) { michael@0: final int childRight = childLeft + w; michael@0: final int childBottom = childTop + h; michael@0: michael@0: child.layout(childLeft, childTop, childRight, childBottom); michael@0: } else { michael@0: child.offsetLeftAndRight(childLeft - child.getLeft()); michael@0: child.offsetTopAndBottom(childTop - child.getTop()); michael@0: } michael@0: } michael@0: michael@0: void fillGap(boolean down) { michael@0: final int childCount = getChildCount(); michael@0: michael@0: if (down) { michael@0: final int paddingStart = (mIsVertical ? getPaddingTop() : getPaddingLeft()); michael@0: michael@0: final int lastEnd; michael@0: if (mIsVertical) { michael@0: lastEnd = getChildAt(childCount - 1).getBottom(); michael@0: } else { michael@0: lastEnd = getChildAt(childCount - 1).getRight(); michael@0: } michael@0: michael@0: final int offset = (childCount > 0 ? lastEnd + mItemMargin : paddingStart); michael@0: fillAfter(mFirstPosition + childCount, offset); michael@0: correctTooHigh(getChildCount()); michael@0: } else { michael@0: final int end; michael@0: final int firstStart; michael@0: michael@0: if (mIsVertical) { michael@0: end = getHeight() - getPaddingBottom(); michael@0: firstStart = getChildAt(0).getTop(); michael@0: } else { michael@0: end = getWidth() - getPaddingRight(); michael@0: firstStart = getChildAt(0).getLeft(); michael@0: } michael@0: michael@0: final int offset = (childCount > 0 ? firstStart - mItemMargin : end); michael@0: fillBefore(mFirstPosition - 1, offset); michael@0: correctTooLow(getChildCount()); michael@0: } michael@0: } michael@0: michael@0: private View fillBefore(int pos, int nextOffset) { michael@0: View selectedView = null; michael@0: michael@0: final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft()); michael@0: michael@0: while (nextOffset > start && pos >= 0) { michael@0: boolean isSelected = (pos == mSelectedPosition); michael@0: View child = makeAndAddView(pos, nextOffset, false, isSelected); michael@0: michael@0: if (mIsVertical) { michael@0: nextOffset = child.getTop() - mItemMargin; michael@0: } else { michael@0: nextOffset = child.getLeft() - mItemMargin; michael@0: } michael@0: michael@0: if (isSelected) { michael@0: selectedView = child; michael@0: } michael@0: michael@0: pos--; michael@0: } michael@0: michael@0: mFirstPosition = pos + 1; michael@0: michael@0: return selectedView; michael@0: } michael@0: michael@0: private View fillAfter(int pos, int nextOffset) { michael@0: View selectedView = null; michael@0: michael@0: final int end = michael@0: (mIsVertical ? getHeight() - getPaddingBottom() : getWidth() - getPaddingRight()); michael@0: michael@0: while (nextOffset < end && pos < mItemCount) { michael@0: boolean selected = (pos == mSelectedPosition); michael@0: michael@0: View child = makeAndAddView(pos, nextOffset, true, selected); michael@0: michael@0: if (mIsVertical) { michael@0: nextOffset = child.getBottom() + mItemMargin; michael@0: } else { michael@0: nextOffset = child.getRight() + mItemMargin; michael@0: } michael@0: michael@0: if (selected) { michael@0: selectedView = child; michael@0: } michael@0: michael@0: pos++; michael@0: } michael@0: michael@0: return selectedView; michael@0: } michael@0: michael@0: private View fillSpecific(int position, int offset) { michael@0: final boolean tempIsSelected = (position == mSelectedPosition); michael@0: View temp = makeAndAddView(position, offset, true, tempIsSelected); michael@0: michael@0: // Possibly changed again in fillBefore if we add rows above this one. michael@0: mFirstPosition = position; michael@0: michael@0: final int itemMargin = mItemMargin; michael@0: michael@0: final int offsetBefore; michael@0: if (mIsVertical) { michael@0: offsetBefore = temp.getTop() - itemMargin; michael@0: } else { michael@0: offsetBefore = temp.getLeft() - itemMargin; michael@0: } michael@0: final View before = fillBefore(position - 1, offsetBefore); michael@0: michael@0: // This will correct for the top of the first view not touching the top of the list michael@0: adjustViewsStartOrEnd(); michael@0: michael@0: final int offsetAfter; michael@0: if (mIsVertical) { michael@0: offsetAfter = temp.getBottom() + itemMargin; michael@0: } else { michael@0: offsetAfter = temp.getRight() + itemMargin; michael@0: } michael@0: final View after = fillAfter(position + 1, offsetAfter); michael@0: michael@0: final int childCount = getChildCount(); michael@0: if (childCount > 0) { michael@0: correctTooHigh(childCount); michael@0: } michael@0: michael@0: if (tempIsSelected) { michael@0: return temp; michael@0: } else if (before != null) { michael@0: return before; michael@0: } else { michael@0: return after; michael@0: } michael@0: } michael@0: michael@0: private View fillFromOffset(int nextOffset) { michael@0: mFirstPosition = Math.min(mFirstPosition, mSelectedPosition); michael@0: mFirstPosition = Math.min(mFirstPosition, mItemCount - 1); michael@0: michael@0: if (mFirstPosition < 0) { michael@0: mFirstPosition = 0; michael@0: } michael@0: michael@0: return fillAfter(mFirstPosition, nextOffset); michael@0: } michael@0: michael@0: private View fillFromMiddle(int start, int end) { michael@0: final int size = end - start; michael@0: int position = reconcileSelectedPosition(); michael@0: michael@0: View selected = makeAndAddView(position, start, true, true); michael@0: mFirstPosition = position; michael@0: michael@0: if (mIsVertical) { michael@0: int selectedHeight = selected.getMeasuredHeight(); michael@0: if (selectedHeight <= size) { michael@0: selected.offsetTopAndBottom((size - selectedHeight) / 2); michael@0: } michael@0: } else { michael@0: int selectedWidth = selected.getMeasuredWidth(); michael@0: if (selectedWidth <= size) { michael@0: selected.offsetLeftAndRight((size - selectedWidth) / 2); michael@0: } michael@0: } michael@0: michael@0: fillBeforeAndAfter(selected, position); michael@0: correctTooHigh(getChildCount()); michael@0: michael@0: return selected; michael@0: } michael@0: michael@0: private void fillBeforeAndAfter(View selected, int position) { michael@0: final int itemMargin = mItemMargin; michael@0: michael@0: final int offsetBefore; michael@0: if (mIsVertical) { michael@0: offsetBefore = selected.getTop() - itemMargin; michael@0: } else { michael@0: offsetBefore = selected.getLeft() - itemMargin; michael@0: } michael@0: michael@0: fillBefore(position - 1, offsetBefore); michael@0: michael@0: adjustViewsStartOrEnd(); michael@0: michael@0: final int offsetAfter; michael@0: if (mIsVertical) { michael@0: offsetAfter = selected.getBottom() + itemMargin; michael@0: } else { michael@0: offsetAfter = selected.getRight() + itemMargin; michael@0: } michael@0: michael@0: fillAfter(position + 1, offsetAfter); michael@0: } michael@0: michael@0: private View fillFromSelection(int selectedTop, int start, int end) { michael@0: final int selectedPosition = mSelectedPosition; michael@0: View selected; michael@0: michael@0: selected = makeAndAddView(selectedPosition, selectedTop, true, true); michael@0: michael@0: final int selectedStart = (mIsVertical ? selected.getTop() : selected.getLeft()); michael@0: final int selectedEnd = (mIsVertical ? selected.getBottom() : selected.getRight()); michael@0: michael@0: // Some of the newly selected item extends below the bottom of the list michael@0: if (selectedEnd > end) { michael@0: // Find space available above the selection into which we can scroll michael@0: // upwards michael@0: final int spaceAbove = selectedStart - start; michael@0: michael@0: // Find space required to bring the bottom of the selected item michael@0: // fully into view michael@0: final int spaceBelow = selectedEnd - end; michael@0: michael@0: final int offset = Math.min(spaceAbove, spaceBelow); michael@0: michael@0: // Now offset the selected item to get it into view michael@0: selected.offsetTopAndBottom(-offset); michael@0: } else if (selectedStart < start) { michael@0: // Find space required to bring the top of the selected item fully michael@0: // into view michael@0: final int spaceAbove = start - selectedStart; michael@0: michael@0: // Find space available below the selection into which we can scroll michael@0: // downwards michael@0: final int spaceBelow = end - selectedEnd; michael@0: michael@0: final int offset = Math.min(spaceAbove, spaceBelow); michael@0: michael@0: // Offset the selected item to get it into view michael@0: selected.offsetTopAndBottom(offset); michael@0: } michael@0: michael@0: // Fill in views above and below michael@0: fillBeforeAndAfter(selected, selectedPosition); michael@0: correctTooHigh(getChildCount()); michael@0: michael@0: return selected; michael@0: } michael@0: michael@0: private void correctTooHigh(int childCount) { michael@0: // First see if the last item is visible. If it is not, it is OK for the michael@0: // top of the list to be pushed up. michael@0: final int lastPosition = mFirstPosition + childCount - 1; michael@0: if (lastPosition != mItemCount - 1 || childCount == 0) { michael@0: return; michael@0: } michael@0: michael@0: // Get the last child ... michael@0: final View lastChild = getChildAt(childCount - 1); michael@0: michael@0: // ... and its end edge michael@0: final int lastEnd; michael@0: if (mIsVertical) { michael@0: lastEnd = lastChild.getBottom(); michael@0: } else { michael@0: lastEnd = lastChild.getRight(); michael@0: } michael@0: michael@0: // This is bottom of our drawable area michael@0: final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft()); michael@0: final int end = michael@0: (mIsVertical ? getHeight() - getPaddingBottom() : getWidth() - getPaddingRight()); michael@0: michael@0: // This is how far the end edge of the last view is from the end of the michael@0: // drawable area michael@0: int endOffset = end - lastEnd; michael@0: michael@0: View firstChild = getChildAt(0); michael@0: int firstStart = (mIsVertical ? firstChild.getTop() : firstChild.getLeft()); michael@0: michael@0: // Make sure we are 1) Too high, and 2) Either there are more rows above the michael@0: // first row or the first row is scrolled off the top of the drawable area michael@0: if (endOffset > 0 && (mFirstPosition > 0 || firstStart < start)) { michael@0: if (mFirstPosition == 0) { michael@0: // Don't pull the top too far down michael@0: endOffset = Math.min(endOffset, start - firstStart); michael@0: } michael@0: michael@0: // Move everything down michael@0: offsetChildren(endOffset); michael@0: michael@0: if (mFirstPosition > 0) { michael@0: firstStart = (mIsVertical ? firstChild.getTop() : firstChild.getLeft()); michael@0: michael@0: // Fill the gap that was opened above mFirstPosition with more rows, if michael@0: // possible michael@0: fillBefore(mFirstPosition - 1, firstStart - mItemMargin); michael@0: michael@0: // Close up the remaining gap michael@0: adjustViewsStartOrEnd(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: private void correctTooLow(int childCount) { michael@0: // First see if the first item is visible. If it is not, it is OK for the michael@0: // bottom of the list to be pushed down. michael@0: if (mFirstPosition != 0 || childCount == 0) { michael@0: return; michael@0: } michael@0: michael@0: final View first = getChildAt(0); michael@0: final int firstStart = (mIsVertical ? first.getTop() : first.getLeft()); michael@0: michael@0: final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft()); michael@0: michael@0: final int end; michael@0: if (mIsVertical) { michael@0: end = getHeight() - getPaddingBottom(); michael@0: } else { michael@0: end = getWidth() - getPaddingRight(); michael@0: } michael@0: michael@0: // This is how far the start edge of the first view is from the start of the michael@0: // drawable area michael@0: int startOffset = firstStart - start; michael@0: michael@0: View last = getChildAt(childCount - 1); michael@0: int lastEnd = (mIsVertical ? last.getBottom() : last.getRight()); michael@0: michael@0: int lastPosition = mFirstPosition + childCount - 1; michael@0: michael@0: // Make sure we are 1) Too low, and 2) Either there are more columns/rows below the michael@0: // last column/row or the last column/row is scrolled off the end of the michael@0: // drawable area michael@0: if (startOffset > 0) { michael@0: if (lastPosition < mItemCount - 1 || lastEnd > end) { michael@0: if (lastPosition == mItemCount - 1) { michael@0: // Don't pull the bottom too far up michael@0: startOffset = Math.min(startOffset, lastEnd - end); michael@0: } michael@0: michael@0: // Move everything up michael@0: offsetChildren(-startOffset); michael@0: michael@0: if (lastPosition < mItemCount - 1) { michael@0: lastEnd = (mIsVertical ? last.getBottom() : last.getRight()); michael@0: michael@0: // Fill the gap that was opened below the last position with more rows, if michael@0: // possible michael@0: fillAfter(lastPosition + 1, lastEnd + mItemMargin); michael@0: michael@0: // Close up the remaining gap michael@0: adjustViewsStartOrEnd(); michael@0: } michael@0: } else if (lastPosition == mItemCount - 1) { michael@0: adjustViewsStartOrEnd(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: private void adjustViewsStartOrEnd() { michael@0: if (getChildCount() == 0) { michael@0: return; michael@0: } michael@0: michael@0: final View firstChild = getChildAt(0); michael@0: michael@0: int delta; michael@0: if (mIsVertical) { michael@0: delta = firstChild.getTop() - getPaddingTop() - mItemMargin; michael@0: } else { michael@0: delta = firstChild.getLeft() - getPaddingLeft() - mItemMargin; michael@0: } michael@0: michael@0: if (delta < 0) { michael@0: // We only are looking to see if we are too low, not too high michael@0: delta = 0; michael@0: } michael@0: michael@0: if (delta != 0) { michael@0: offsetChildren(-delta); michael@0: } michael@0: } michael@0: michael@0: @TargetApi(14) michael@0: private SparseBooleanArray cloneCheckStates() { michael@0: if (mCheckStates == null) { michael@0: return null; michael@0: } michael@0: michael@0: SparseBooleanArray checkedStates; michael@0: michael@0: if (Build.VERSION.SDK_INT >= 14) { michael@0: checkedStates = mCheckStates.clone(); michael@0: } else { michael@0: checkedStates = new SparseBooleanArray(); michael@0: michael@0: for (int i = 0; i < mCheckStates.size(); i++) { michael@0: checkedStates.put(mCheckStates.keyAt(i), mCheckStates.valueAt(i)); michael@0: } michael@0: } michael@0: michael@0: return checkedStates; michael@0: } michael@0: michael@0: private int findSyncPosition() { michael@0: int itemCount = mItemCount; michael@0: michael@0: if (itemCount == 0) { michael@0: return INVALID_POSITION; michael@0: } michael@0: michael@0: final long idToMatch = mSyncRowId; michael@0: michael@0: // If there isn't a selection don't hunt for it michael@0: if (idToMatch == INVALID_ROW_ID) { michael@0: return INVALID_POSITION; michael@0: } michael@0: michael@0: // Pin seed to reasonable values michael@0: int seed = mSyncPosition; michael@0: seed = Math.max(0, seed); michael@0: seed = Math.min(itemCount - 1, seed); michael@0: michael@0: long endTime = SystemClock.uptimeMillis() + SYNC_MAX_DURATION_MILLIS; michael@0: michael@0: long rowId; michael@0: michael@0: // first position scanned so far michael@0: int first = seed; michael@0: michael@0: // last position scanned so far michael@0: int last = seed; michael@0: michael@0: // True if we should move down on the next iteration michael@0: boolean next = false; michael@0: michael@0: // True when we have looked at the first item in the data michael@0: boolean hitFirst; michael@0: michael@0: // True when we have looked at the last item in the data michael@0: boolean hitLast; michael@0: michael@0: // Get the item ID locally (instead of getItemIdAtPosition), so michael@0: // we need the adapter michael@0: final ListAdapter adapter = mAdapter; michael@0: if (adapter == null) { michael@0: return INVALID_POSITION; michael@0: } michael@0: michael@0: while (SystemClock.uptimeMillis() <= endTime) { michael@0: rowId = adapter.getItemId(seed); michael@0: if (rowId == idToMatch) { michael@0: // Found it! michael@0: return seed; michael@0: } michael@0: michael@0: hitLast = (last == itemCount - 1); michael@0: hitFirst = (first == 0); michael@0: michael@0: if (hitLast && hitFirst) { michael@0: // Looked at everything michael@0: break; michael@0: } michael@0: michael@0: if (hitFirst || (next && !hitLast)) { michael@0: // Either we hit the top, or we are trying to move down michael@0: last++; michael@0: seed = last; michael@0: michael@0: // Try going up next time michael@0: next = false; michael@0: } else if (hitLast || (!next && !hitFirst)) { michael@0: // Either we hit the bottom, or we are trying to move up michael@0: first--; michael@0: seed = first; michael@0: michael@0: // Try going down next time michael@0: next = true; michael@0: } michael@0: } michael@0: michael@0: return INVALID_POSITION; michael@0: } michael@0: michael@0: @TargetApi(16) michael@0: private View obtainView(int position, boolean[] isScrap) { michael@0: isScrap[0] = false; michael@0: michael@0: View scrapView = mRecycler.getTransientStateView(position); michael@0: if (scrapView != null) { michael@0: return scrapView; michael@0: } michael@0: michael@0: scrapView = mRecycler.getScrapView(position); michael@0: michael@0: final View child; michael@0: if (scrapView != null) { michael@0: child = mAdapter.getView(position, scrapView, this); michael@0: michael@0: if (child != scrapView) { michael@0: mRecycler.addScrapView(scrapView, position); michael@0: } else { michael@0: isScrap[0] = true; michael@0: } michael@0: } else { michael@0: child = mAdapter.getView(position, null, this); michael@0: } michael@0: michael@0: if (ViewCompat.getImportantForAccessibility(child) == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { michael@0: ViewCompat.setImportantForAccessibility(child, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); michael@0: } michael@0: michael@0: if (mHasStableIds) { michael@0: LayoutParams lp = (LayoutParams) child.getLayoutParams(); michael@0: michael@0: if (lp == null) { michael@0: lp = generateDefaultLayoutParams(); michael@0: } else if (!checkLayoutParams(lp)) { michael@0: lp = generateLayoutParams(lp); michael@0: } michael@0: michael@0: lp.id = mAdapter.getItemId(position); michael@0: michael@0: child.setLayoutParams(lp); michael@0: } michael@0: michael@0: if (mAccessibilityDelegate == null) { michael@0: mAccessibilityDelegate = new ListItemAccessibilityDelegate(); michael@0: } michael@0: michael@0: ViewCompat.setAccessibilityDelegate(child, mAccessibilityDelegate); michael@0: michael@0: return child; michael@0: } michael@0: michael@0: void resetState() { michael@0: removeAllViewsInLayout(); michael@0: michael@0: mSelectedStart = 0; michael@0: mFirstPosition = 0; michael@0: mDataChanged = false; michael@0: mNeedSync = false; michael@0: mPendingSync = null; michael@0: mOldSelectedPosition = INVALID_POSITION; michael@0: mOldSelectedRowId = INVALID_ROW_ID; michael@0: michael@0: mOverScroll = 0; michael@0: michael@0: setSelectedPositionInt(INVALID_POSITION); michael@0: setNextSelectedPositionInt(INVALID_POSITION); michael@0: michael@0: mSelectorPosition = INVALID_POSITION; michael@0: mSelectorRect.setEmpty(); michael@0: michael@0: invalidate(); michael@0: } michael@0: michael@0: private void rememberSyncState() { michael@0: if (getChildCount() == 0) { michael@0: return; michael@0: } michael@0: michael@0: mNeedSync = true; michael@0: michael@0: if (mSelectedPosition >= 0) { michael@0: View child = getChildAt(mSelectedPosition - mFirstPosition); michael@0: michael@0: mSyncRowId = mNextSelectedRowId; michael@0: mSyncPosition = mNextSelectedPosition; michael@0: michael@0: if (child != null) { michael@0: mSpecificStart = (mIsVertical ? child.getTop() : child.getLeft()); michael@0: } michael@0: michael@0: mSyncMode = SYNC_SELECTED_POSITION; michael@0: } else { michael@0: // Sync the based on the offset of the first view michael@0: View child = getChildAt(0); michael@0: ListAdapter adapter = getAdapter(); michael@0: michael@0: if (mFirstPosition >= 0 && mFirstPosition < adapter.getCount()) { michael@0: mSyncRowId = adapter.getItemId(mFirstPosition); michael@0: } else { michael@0: mSyncRowId = NO_ID; michael@0: } michael@0: michael@0: mSyncPosition = mFirstPosition; michael@0: michael@0: if (child != null) { michael@0: mSpecificStart = child.getTop(); michael@0: } michael@0: michael@0: mSyncMode = SYNC_FIRST_POSITION; michael@0: } michael@0: } michael@0: michael@0: private ContextMenuInfo createContextMenuInfo(View view, int position, long id) { michael@0: return new AdapterContextMenuInfo(view, position, id); michael@0: } michael@0: michael@0: @TargetApi(11) michael@0: private void updateOnScreenCheckedViews() { michael@0: final int firstPos = mFirstPosition; michael@0: final int count = getChildCount(); michael@0: michael@0: final boolean useActivated = getContext().getApplicationInfo().targetSdkVersion michael@0: >= Build.VERSION_CODES.HONEYCOMB; michael@0: michael@0: for (int i = 0; i < count; i++) { michael@0: final View child = getChildAt(i); michael@0: final int position = firstPos + i; michael@0: michael@0: if (child instanceof Checkable) { michael@0: ((Checkable) child).setChecked(mCheckStates.get(position)); michael@0: } else if (useActivated) { michael@0: child.setActivated(mCheckStates.get(position)); michael@0: } michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public boolean performItemClick(View view, int position, long id) { michael@0: boolean checkedStateChanged = false; michael@0: michael@0: if (mChoiceMode.compareTo(ChoiceMode.MULTIPLE) == 0) { michael@0: boolean checked = !mCheckStates.get(position, false); michael@0: mCheckStates.put(position, checked); michael@0: michael@0: if (mCheckedIdStates != null && mAdapter.hasStableIds()) { michael@0: if (checked) { michael@0: mCheckedIdStates.put(mAdapter.getItemId(position), position); michael@0: } else { michael@0: mCheckedIdStates.delete(mAdapter.getItemId(position)); michael@0: } michael@0: } michael@0: michael@0: if (checked) { michael@0: mCheckedItemCount++; michael@0: } else { michael@0: mCheckedItemCount--; michael@0: } michael@0: michael@0: checkedStateChanged = true; michael@0: } else if (mChoiceMode.compareTo(ChoiceMode.SINGLE) == 0) { michael@0: boolean checked = !mCheckStates.get(position, false); michael@0: if (checked) { michael@0: mCheckStates.clear(); michael@0: mCheckStates.put(position, true); michael@0: michael@0: if (mCheckedIdStates != null && mAdapter.hasStableIds()) { michael@0: mCheckedIdStates.clear(); michael@0: mCheckedIdStates.put(mAdapter.getItemId(position), position); michael@0: } michael@0: michael@0: mCheckedItemCount = 1; michael@0: } else if (mCheckStates.size() == 0 || !mCheckStates.valueAt(0)) { michael@0: mCheckedItemCount = 0; michael@0: } michael@0: michael@0: checkedStateChanged = true; michael@0: } michael@0: michael@0: if (checkedStateChanged) { michael@0: updateOnScreenCheckedViews(); michael@0: } michael@0: michael@0: return super.performItemClick(view, position, id); michael@0: } michael@0: michael@0: private boolean performLongPress(final View child, michael@0: final int longPressPosition, final long longPressId) { michael@0: // CHOICE_MODE_MULTIPLE_MODAL takes over long press. michael@0: boolean handled = false; michael@0: michael@0: OnItemLongClickListener listener = getOnItemLongClickListener(); michael@0: if (listener != null) { michael@0: handled = listener.onItemLongClick(TwoWayView.this, child, michael@0: longPressPosition, longPressId); michael@0: } michael@0: michael@0: if (!handled) { michael@0: mContextMenuInfo = createContextMenuInfo(child, longPressPosition, longPressId); michael@0: handled = super.showContextMenuForChild(TwoWayView.this); michael@0: } michael@0: michael@0: if (handled) { michael@0: performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); michael@0: } michael@0: michael@0: return handled; michael@0: } michael@0: michael@0: @Override michael@0: protected LayoutParams generateDefaultLayoutParams() { michael@0: if (mIsVertical) { michael@0: return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); michael@0: } else { michael@0: return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { michael@0: return new LayoutParams(lp); michael@0: } michael@0: michael@0: @Override michael@0: protected boolean checkLayoutParams(ViewGroup.LayoutParams lp) { michael@0: return lp instanceof LayoutParams; michael@0: } michael@0: michael@0: @Override michael@0: public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { michael@0: return new LayoutParams(getContext(), attrs); michael@0: } michael@0: michael@0: @Override michael@0: protected ContextMenuInfo getContextMenuInfo() { michael@0: return mContextMenuInfo; michael@0: } michael@0: michael@0: @Override michael@0: public Parcelable onSaveInstanceState() { michael@0: Parcelable superState = super.onSaveInstanceState(); michael@0: SavedState ss = new SavedState(superState); michael@0: michael@0: if (mPendingSync != null) { michael@0: ss.selectedId = mPendingSync.selectedId; michael@0: ss.firstId = mPendingSync.firstId; michael@0: ss.viewStart = mPendingSync.viewStart; michael@0: ss.position = mPendingSync.position; michael@0: ss.height = mPendingSync.height; michael@0: michael@0: return ss; michael@0: } michael@0: michael@0: boolean haveChildren = (getChildCount() > 0 && mItemCount > 0); michael@0: long selectedId = getSelectedItemId(); michael@0: ss.selectedId = selectedId; michael@0: ss.height = getHeight(); michael@0: michael@0: if (selectedId >= 0) { michael@0: ss.viewStart = mSelectedStart; michael@0: ss.position = getSelectedItemPosition(); michael@0: ss.firstId = INVALID_POSITION; michael@0: } else if (haveChildren && mFirstPosition > 0) { michael@0: // Remember the position of the first child. michael@0: // We only do this if we are not currently at the top of michael@0: // the list, for two reasons: michael@0: // michael@0: // (1) The list may be in the process of becoming empty, in michael@0: // which case mItemCount may not be 0, but if we try to michael@0: // ask for any information about position 0 we will crash. michael@0: // michael@0: // (2) Being "at the top" seems like a special case, anyway, michael@0: // and the user wouldn't expect to end up somewhere else when michael@0: // they revisit the list even if its content has changed. michael@0: michael@0: View child = getChildAt(0); michael@0: ss.viewStart = (mIsVertical ? child.getTop() : child.getLeft()); michael@0: michael@0: int firstPos = mFirstPosition; michael@0: if (firstPos >= mItemCount) { michael@0: firstPos = mItemCount - 1; michael@0: } michael@0: michael@0: ss.position = firstPos; michael@0: ss.firstId = mAdapter.getItemId(firstPos); michael@0: } else { michael@0: ss.viewStart = 0; michael@0: ss.firstId = INVALID_POSITION; michael@0: ss.position = 0; michael@0: } michael@0: michael@0: if (mCheckStates != null) { michael@0: ss.checkState = cloneCheckStates(); michael@0: } michael@0: michael@0: if (mCheckedIdStates != null) { michael@0: final LongSparseArray idState = new LongSparseArray(); michael@0: michael@0: final int count = mCheckedIdStates.size(); michael@0: for (int i = 0; i < count; i++) { michael@0: idState.put(mCheckedIdStates.keyAt(i), mCheckedIdStates.valueAt(i)); michael@0: } michael@0: michael@0: ss.checkIdState = idState; michael@0: } michael@0: michael@0: ss.checkedItemCount = mCheckedItemCount; michael@0: michael@0: return ss; michael@0: } michael@0: michael@0: @Override michael@0: public void onRestoreInstanceState(Parcelable state) { michael@0: SavedState ss = (SavedState) state; michael@0: super.onRestoreInstanceState(ss.getSuperState()); michael@0: michael@0: mDataChanged = true; michael@0: mSyncHeight = ss.height; michael@0: michael@0: if (ss.selectedId >= 0) { michael@0: mNeedSync = true; michael@0: mPendingSync = ss; michael@0: mSyncRowId = ss.selectedId; michael@0: mSyncPosition = ss.position; michael@0: mSpecificStart = ss.viewStart; michael@0: mSyncMode = SYNC_SELECTED_POSITION; michael@0: } else if (ss.firstId >= 0) { michael@0: setSelectedPositionInt(INVALID_POSITION); michael@0: michael@0: // Do this before setting mNeedSync since setNextSelectedPosition looks at mNeedSync michael@0: setNextSelectedPositionInt(INVALID_POSITION); michael@0: michael@0: mSelectorPosition = INVALID_POSITION; michael@0: mNeedSync = true; michael@0: mPendingSync = ss; michael@0: mSyncRowId = ss.firstId; michael@0: mSyncPosition = ss.position; michael@0: mSpecificStart = ss.viewStart; michael@0: mSyncMode = SYNC_FIRST_POSITION; michael@0: } michael@0: michael@0: if (ss.checkState != null) { michael@0: mCheckStates = ss.checkState; michael@0: } michael@0: michael@0: if (ss.checkIdState != null) { michael@0: mCheckedIdStates = ss.checkIdState; michael@0: } michael@0: michael@0: mCheckedItemCount = ss.checkedItemCount; michael@0: michael@0: requestLayout(); michael@0: } michael@0: michael@0: public static class LayoutParams extends ViewGroup.LayoutParams { michael@0: /** michael@0: * Type of this view as reported by the adapter michael@0: */ michael@0: int viewType; michael@0: michael@0: /** michael@0: * The stable ID of the item this view displays michael@0: */ michael@0: long id = -1; michael@0: michael@0: /** michael@0: * The position the view was removed from when pulled out of the michael@0: * scrap heap. michael@0: * @hide michael@0: */ michael@0: int scrappedFromPosition; michael@0: michael@0: /** michael@0: * When a TwoWayView is measured with an AT_MOST measure spec, it needs michael@0: * to obtain children views to measure itself. When doing so, the children michael@0: * are not attached to the window, but put in the recycler which assumes michael@0: * they've been attached before. Setting this flag will force the reused michael@0: * view to be attached to the window rather than just attached to the michael@0: * parent. michael@0: */ michael@0: boolean forceAdd; michael@0: michael@0: public LayoutParams(int width, int height) { michael@0: super(width, height); michael@0: michael@0: if (this.width == MATCH_PARENT) { michael@0: Log.w(LOGTAG, "Constructing LayoutParams with width FILL_PARENT " + michael@0: "does not make much sense as the view might change orientation. " + michael@0: "Falling back to WRAP_CONTENT"); michael@0: this.width = WRAP_CONTENT; michael@0: } michael@0: michael@0: if (this.height == MATCH_PARENT) { michael@0: Log.w(LOGTAG, "Constructing LayoutParams with height FILL_PARENT " + michael@0: "does not make much sense as the view might change orientation. " + michael@0: "Falling back to WRAP_CONTENT"); michael@0: this.height = WRAP_CONTENT; michael@0: } michael@0: } michael@0: michael@0: public LayoutParams(Context c, AttributeSet attrs) { michael@0: super(c, attrs); michael@0: michael@0: if (this.width == MATCH_PARENT) { michael@0: Log.w(LOGTAG, "Inflation setting LayoutParams width to MATCH_PARENT - " + michael@0: "does not make much sense as the view might change orientation. " + michael@0: "Falling back to WRAP_CONTENT"); michael@0: this.width = MATCH_PARENT; michael@0: } michael@0: michael@0: if (this.height == MATCH_PARENT) { michael@0: Log.w(LOGTAG, "Inflation setting LayoutParams height to MATCH_PARENT - " + michael@0: "does not make much sense as the view might change orientation. " + michael@0: "Falling back to WRAP_CONTENT"); michael@0: this.height = WRAP_CONTENT; michael@0: } michael@0: } michael@0: michael@0: public LayoutParams(ViewGroup.LayoutParams other) { michael@0: super(other); michael@0: michael@0: if (this.width == MATCH_PARENT) { michael@0: Log.w(LOGTAG, "Constructing LayoutParams with height MATCH_PARENT - " + michael@0: "does not make much sense as the view might change orientation. " + michael@0: "Falling back to WRAP_CONTENT"); michael@0: this.width = WRAP_CONTENT; michael@0: } michael@0: michael@0: if (this.height == MATCH_PARENT) { michael@0: Log.w(LOGTAG, "Constructing LayoutParams with height MATCH_PARENT - " + michael@0: "does not make much sense as the view might change orientation. " + michael@0: "Falling back to WRAP_CONTENT"); michael@0: this.height = WRAP_CONTENT; michael@0: } michael@0: } michael@0: } michael@0: michael@0: class RecycleBin { michael@0: private RecyclerListener mRecyclerListener; michael@0: private int mFirstActivePosition; michael@0: private View[] mActiveViews = new View[0]; michael@0: private ArrayList[] mScrapViews; michael@0: private int mViewTypeCount; michael@0: private ArrayList mCurrentScrap; michael@0: private SparseArrayCompat mTransientStateViews; michael@0: michael@0: public void setViewTypeCount(int viewTypeCount) { michael@0: if (viewTypeCount < 1) { michael@0: throw new IllegalArgumentException("Can't have a viewTypeCount < 1"); michael@0: } michael@0: michael@0: @SuppressWarnings({"unchecked", "rawtypes"}) michael@0: ArrayList[] scrapViews = new ArrayList[viewTypeCount]; michael@0: for (int i = 0; i < viewTypeCount; i++) { michael@0: scrapViews[i] = new ArrayList(); michael@0: } michael@0: michael@0: mViewTypeCount = viewTypeCount; michael@0: mCurrentScrap = scrapViews[0]; michael@0: mScrapViews = scrapViews; michael@0: } michael@0: michael@0: public void markChildrenDirty() { michael@0: if (mViewTypeCount == 1) { michael@0: final ArrayList scrap = mCurrentScrap; michael@0: final int scrapCount = scrap.size(); michael@0: michael@0: for (int i = 0; i < scrapCount; i++) { michael@0: scrap.get(i).forceLayout(); michael@0: } michael@0: } else { michael@0: final int typeCount = mViewTypeCount; michael@0: for (int i = 0; i < typeCount; i++) { michael@0: final ArrayList scrap = mScrapViews[i]; michael@0: final int scrapCount = scrap.size(); michael@0: michael@0: for (int j = 0; j < scrapCount; j++) { michael@0: scrap.get(j).forceLayout(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: if (mTransientStateViews != null) { michael@0: final int count = mTransientStateViews.size(); michael@0: for (int i = 0; i < count; i++) { michael@0: mTransientStateViews.valueAt(i).forceLayout(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: public boolean shouldRecycleViewType(int viewType) { michael@0: return viewType >= 0; michael@0: } michael@0: michael@0: void clear() { michael@0: if (mViewTypeCount == 1) { michael@0: final ArrayList scrap = mCurrentScrap; michael@0: final int scrapCount = scrap.size(); michael@0: michael@0: for (int i = 0; i < scrapCount; i++) { michael@0: removeDetachedView(scrap.remove(scrapCount - 1 - i), false); michael@0: } michael@0: } else { michael@0: final int typeCount = mViewTypeCount; michael@0: for (int i = 0; i < typeCount; i++) { michael@0: final ArrayList scrap = mScrapViews[i]; michael@0: final int scrapCount = scrap.size(); michael@0: michael@0: for (int j = 0; j < scrapCount; j++) { michael@0: removeDetachedView(scrap.remove(scrapCount - 1 - j), false); michael@0: } michael@0: } michael@0: } michael@0: michael@0: if (mTransientStateViews != null) { michael@0: mTransientStateViews.clear(); michael@0: } michael@0: } michael@0: michael@0: void fillActiveViews(int childCount, int firstActivePosition) { michael@0: if (mActiveViews.length < childCount) { michael@0: mActiveViews = new View[childCount]; michael@0: } michael@0: michael@0: mFirstActivePosition = firstActivePosition; michael@0: michael@0: final View[] activeViews = mActiveViews; michael@0: for (int i = 0; i < childCount; i++) { michael@0: View child = getChildAt(i); michael@0: michael@0: // Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in active views. michael@0: // However, we will NOT place them into scrap views. michael@0: activeViews[i] = child; michael@0: } michael@0: } michael@0: michael@0: View getActiveView(int position) { michael@0: final int index = position - mFirstActivePosition; michael@0: final View[] activeViews = mActiveViews; michael@0: michael@0: if (index >= 0 && index < activeViews.length) { michael@0: final View match = activeViews[index]; michael@0: activeViews[index] = null; michael@0: michael@0: return match; michael@0: } michael@0: michael@0: return null; michael@0: } michael@0: michael@0: View getTransientStateView(int position) { michael@0: if (mTransientStateViews == null) { michael@0: return null; michael@0: } michael@0: michael@0: final int index = mTransientStateViews.indexOfKey(position); michael@0: if (index < 0) { michael@0: return null; michael@0: } michael@0: michael@0: final View result = mTransientStateViews.valueAt(index); michael@0: mTransientStateViews.removeAt(index); michael@0: michael@0: return result; michael@0: } michael@0: michael@0: void clearTransientStateViews() { michael@0: if (mTransientStateViews != null) { michael@0: mTransientStateViews.clear(); michael@0: } michael@0: } michael@0: michael@0: View getScrapView(int position) { michael@0: if (mViewTypeCount == 1) { michael@0: return retrieveFromScrap(mCurrentScrap, position); michael@0: } else { michael@0: int whichScrap = mAdapter.getItemViewType(position); michael@0: if (whichScrap >= 0 && whichScrap < mScrapViews.length) { michael@0: return retrieveFromScrap(mScrapViews[whichScrap], position); michael@0: } michael@0: } michael@0: michael@0: return null; michael@0: } michael@0: michael@0: @TargetApi(14) michael@0: void addScrapView(View scrap, int position) { michael@0: LayoutParams lp = (LayoutParams) scrap.getLayoutParams(); michael@0: if (lp == null) { michael@0: return; michael@0: } michael@0: michael@0: lp.scrappedFromPosition = position; michael@0: michael@0: final int viewType = lp.viewType; michael@0: final boolean scrapHasTransientState = ViewCompat.hasTransientState(scrap); michael@0: michael@0: // Don't put views that should be ignored into the scrap heap michael@0: if (!shouldRecycleViewType(viewType) || scrapHasTransientState) { michael@0: if (scrapHasTransientState) { michael@0: if (mTransientStateViews == null) { michael@0: mTransientStateViews = new SparseArrayCompat(); michael@0: } michael@0: michael@0: mTransientStateViews.put(position, scrap); michael@0: } michael@0: michael@0: return; michael@0: } michael@0: michael@0: if (mViewTypeCount == 1) { michael@0: mCurrentScrap.add(scrap); michael@0: } else { michael@0: mScrapViews[viewType].add(scrap); michael@0: } michael@0: michael@0: // FIXME: Unfortunately, ViewCompat.setAccessibilityDelegate() doesn't accept michael@0: // null delegates. michael@0: if (Build.VERSION.SDK_INT >= 14) { michael@0: scrap.setAccessibilityDelegate(null); michael@0: } michael@0: michael@0: if (mRecyclerListener != null) { michael@0: mRecyclerListener.onMovedToScrapHeap(scrap); michael@0: } michael@0: } michael@0: michael@0: @TargetApi(14) michael@0: void scrapActiveViews() { michael@0: final View[] activeViews = mActiveViews; michael@0: final boolean multipleScraps = (mViewTypeCount > 1); michael@0: michael@0: ArrayList scrapViews = mCurrentScrap; michael@0: final int count = activeViews.length; michael@0: michael@0: for (int i = count - 1; i >= 0; i--) { michael@0: final View victim = activeViews[i]; michael@0: if (victim != null) { michael@0: final LayoutParams lp = (LayoutParams) victim.getLayoutParams(); michael@0: int whichScrap = lp.viewType; michael@0: michael@0: activeViews[i] = null; michael@0: michael@0: final boolean scrapHasTransientState = ViewCompat.hasTransientState(victim); michael@0: if (!shouldRecycleViewType(whichScrap) || scrapHasTransientState) { michael@0: if (scrapHasTransientState) { michael@0: removeDetachedView(victim, false); michael@0: michael@0: if (mTransientStateViews == null) { michael@0: mTransientStateViews = new SparseArrayCompat(); michael@0: } michael@0: michael@0: mTransientStateViews.put(mFirstActivePosition + i, victim); michael@0: } michael@0: michael@0: continue; michael@0: } michael@0: michael@0: if (multipleScraps) { michael@0: scrapViews = mScrapViews[whichScrap]; michael@0: } michael@0: michael@0: lp.scrappedFromPosition = mFirstActivePosition + i; michael@0: scrapViews.add(victim); michael@0: michael@0: // FIXME: Unfortunately, ViewCompat.setAccessibilityDelegate() doesn't accept michael@0: // null delegates. michael@0: if (Build.VERSION.SDK_INT >= 14) { michael@0: victim.setAccessibilityDelegate(null); michael@0: } michael@0: michael@0: if (mRecyclerListener != null) { michael@0: mRecyclerListener.onMovedToScrapHeap(victim); michael@0: } michael@0: } michael@0: } michael@0: michael@0: pruneScrapViews(); michael@0: } michael@0: michael@0: private void pruneScrapViews() { michael@0: final int maxViews = mActiveViews.length; michael@0: final int viewTypeCount = mViewTypeCount; michael@0: final ArrayList[] scrapViews = mScrapViews; michael@0: michael@0: for (int i = 0; i < viewTypeCount; ++i) { michael@0: final ArrayList scrapPile = scrapViews[i]; michael@0: int size = scrapPile.size(); michael@0: final int extras = size - maxViews; michael@0: michael@0: size--; michael@0: michael@0: for (int j = 0; j < extras; j++) { michael@0: removeDetachedView(scrapPile.remove(size--), false); michael@0: } michael@0: } michael@0: michael@0: if (mTransientStateViews != null) { michael@0: for (int i = 0; i < mTransientStateViews.size(); i++) { michael@0: final View v = mTransientStateViews.valueAt(i); michael@0: if (!ViewCompat.hasTransientState(v)) { michael@0: mTransientStateViews.removeAt(i); michael@0: i--; michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: void reclaimScrapViews(List views) { michael@0: if (mViewTypeCount == 1) { michael@0: views.addAll(mCurrentScrap); michael@0: } else { michael@0: final int viewTypeCount = mViewTypeCount; michael@0: final ArrayList[] scrapViews = mScrapViews; michael@0: michael@0: for (int i = 0; i < viewTypeCount; ++i) { michael@0: final ArrayList scrapPile = scrapViews[i]; michael@0: views.addAll(scrapPile); michael@0: } michael@0: } michael@0: } michael@0: michael@0: View retrieveFromScrap(ArrayList scrapViews, int position) { michael@0: int size = scrapViews.size(); michael@0: if (size <= 0) { michael@0: return null; michael@0: } michael@0: michael@0: for (int i = 0; i < size; i++) { michael@0: final View scrapView = scrapViews.get(i); michael@0: final LayoutParams lp = (LayoutParams) scrapView.getLayoutParams(); michael@0: michael@0: if (lp.scrappedFromPosition == position) { michael@0: scrapViews.remove(i); michael@0: return scrapView; michael@0: } michael@0: } michael@0: michael@0: return scrapViews.remove(size - 1); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void setEmptyView(View emptyView) { michael@0: super.setEmptyView(emptyView); michael@0: mEmptyView = emptyView; michael@0: updateEmptyStatus(); michael@0: } michael@0: michael@0: @Override michael@0: public void setFocusable(boolean focusable) { michael@0: final ListAdapter adapter = getAdapter(); michael@0: final boolean empty = (adapter == null || adapter.getCount() == 0); michael@0: michael@0: mDesiredFocusableState = focusable; michael@0: if (!focusable) { michael@0: mDesiredFocusableInTouchModeState = false; michael@0: } michael@0: michael@0: super.setFocusable(focusable && !empty); michael@0: } michael@0: michael@0: @Override michael@0: public void setFocusableInTouchMode(boolean focusable) { michael@0: final ListAdapter adapter = getAdapter(); michael@0: final boolean empty = (adapter == null || adapter.getCount() == 0); michael@0: michael@0: mDesiredFocusableInTouchModeState = focusable; michael@0: if (focusable) { michael@0: mDesiredFocusableState = true; michael@0: } michael@0: michael@0: super.setFocusableInTouchMode(focusable && !empty); michael@0: } michael@0: michael@0: private void checkFocus() { michael@0: final ListAdapter adapter = getAdapter(); michael@0: final boolean focusable = (adapter != null && adapter.getCount() > 0); michael@0: michael@0: // The order in which we set focusable in touch mode/focusable may matter michael@0: // for the client, see View.setFocusableInTouchMode() comments for more michael@0: // details michael@0: super.setFocusableInTouchMode(focusable && mDesiredFocusableInTouchModeState); michael@0: super.setFocusable(focusable && mDesiredFocusableState); michael@0: michael@0: if (mEmptyView != null) { michael@0: updateEmptyStatus(); michael@0: } michael@0: } michael@0: michael@0: private void updateEmptyStatus() { michael@0: final boolean isEmpty = (mAdapter == null || mAdapter.isEmpty()); michael@0: michael@0: if (isEmpty) { michael@0: if (mEmptyView != null) { michael@0: mEmptyView.setVisibility(View.VISIBLE); michael@0: setVisibility(View.GONE); michael@0: } else { michael@0: // If the caller just removed our empty view, make sure the list michael@0: // view is visible michael@0: setVisibility(View.VISIBLE); michael@0: } michael@0: michael@0: // We are now GONE, so pending layouts will not be dispatched. michael@0: // Force one here to make sure that the state of the list matches michael@0: // the state of the adapter. michael@0: if (mDataChanged) { michael@0: onLayout(false, getLeft(), getTop(), getRight(), getBottom()); michael@0: } michael@0: } else { michael@0: if (mEmptyView != null) { michael@0: mEmptyView.setVisibility(View.GONE); michael@0: } michael@0: michael@0: setVisibility(View.VISIBLE); michael@0: } michael@0: } michael@0: michael@0: private class AdapterDataSetObserver extends DataSetObserver { michael@0: private Parcelable mInstanceState = null; michael@0: michael@0: @Override michael@0: public void onChanged() { michael@0: mDataChanged = true; michael@0: mOldItemCount = mItemCount; michael@0: mItemCount = getAdapter().getCount(); michael@0: michael@0: // Detect the case where a cursor that was previously invalidated has michael@0: // been re-populated with new data. michael@0: if (TwoWayView.this.mHasStableIds && mInstanceState != null michael@0: && mOldItemCount == 0 && mItemCount > 0) { michael@0: TwoWayView.this.onRestoreInstanceState(mInstanceState); michael@0: mInstanceState = null; michael@0: } else { michael@0: rememberSyncState(); michael@0: } michael@0: michael@0: checkFocus(); michael@0: requestLayout(); michael@0: } michael@0: michael@0: @Override michael@0: public void onInvalidated() { michael@0: mDataChanged = true; michael@0: michael@0: if (TwoWayView.this.mHasStableIds) { michael@0: // Remember the current state for the case where our hosting activity is being michael@0: // stopped and later restarted michael@0: mInstanceState = TwoWayView.this.onSaveInstanceState(); michael@0: } michael@0: michael@0: // Data is invalid so we should reset our state michael@0: mOldItemCount = mItemCount; michael@0: mItemCount = 0; michael@0: michael@0: mSelectedPosition = INVALID_POSITION; michael@0: mSelectedRowId = INVALID_ROW_ID; michael@0: michael@0: mNextSelectedPosition = INVALID_POSITION; michael@0: mNextSelectedRowId = INVALID_ROW_ID; michael@0: michael@0: mNeedSync = false; michael@0: michael@0: checkFocus(); michael@0: requestLayout(); michael@0: } michael@0: } michael@0: michael@0: static class SavedState extends BaseSavedState { michael@0: long selectedId; michael@0: long firstId; michael@0: int viewStart; michael@0: int position; michael@0: int height; michael@0: int checkedItemCount; michael@0: SparseBooleanArray checkState; michael@0: LongSparseArray checkIdState; michael@0: michael@0: /** michael@0: * Constructor called from {@link TwoWayView#onSaveInstanceState()} michael@0: */ michael@0: SavedState(Parcelable superState) { michael@0: super(superState); michael@0: } michael@0: michael@0: /** michael@0: * Constructor called from {@link #CREATOR} michael@0: */ michael@0: private SavedState(Parcel in) { michael@0: super(in); michael@0: michael@0: selectedId = in.readLong(); michael@0: firstId = in.readLong(); michael@0: viewStart = in.readInt(); michael@0: position = in.readInt(); michael@0: height = in.readInt(); michael@0: michael@0: checkedItemCount = in.readInt(); michael@0: checkState = in.readSparseBooleanArray(); michael@0: michael@0: final int N = in.readInt(); michael@0: if (N > 0) { michael@0: checkIdState = new LongSparseArray(); michael@0: for (int i = 0; i < N; i++) { michael@0: final long key = in.readLong(); michael@0: final int value = in.readInt(); michael@0: checkIdState.put(key, value); michael@0: } michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void writeToParcel(Parcel out, int flags) { michael@0: super.writeToParcel(out, flags); michael@0: michael@0: out.writeLong(selectedId); michael@0: out.writeLong(firstId); michael@0: out.writeInt(viewStart); michael@0: out.writeInt(position); michael@0: out.writeInt(height); michael@0: michael@0: out.writeInt(checkedItemCount); michael@0: out.writeSparseBooleanArray(checkState); michael@0: michael@0: final int N = checkIdState != null ? checkIdState.size() : 0; michael@0: out.writeInt(N); michael@0: michael@0: for (int i = 0; i < N; i++) { michael@0: out.writeLong(checkIdState.keyAt(i)); michael@0: out.writeInt(checkIdState.valueAt(i)); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public String toString() { michael@0: return "TwoWayView.SavedState{" michael@0: + Integer.toHexString(System.identityHashCode(this)) michael@0: + " selectedId=" + selectedId michael@0: + " firstId=" + firstId michael@0: + " viewStart=" + viewStart michael@0: + " height=" + height michael@0: + " position=" + position michael@0: + " checkState=" + checkState + "}"; michael@0: } michael@0: michael@0: public static final Parcelable.Creator CREATOR michael@0: = new Parcelable.Creator() { michael@0: @Override michael@0: public SavedState createFromParcel(Parcel in) { michael@0: return new SavedState(in); michael@0: } michael@0: michael@0: @Override michael@0: public SavedState[] newArray(int size) { michael@0: return new SavedState[size]; michael@0: } michael@0: }; michael@0: } michael@0: michael@0: private class SelectionNotifier implements Runnable { michael@0: @Override michael@0: public void run() { michael@0: if (mDataChanged) { michael@0: // Data has changed between when this SelectionNotifier michael@0: // was posted and now. We need to wait until the AdapterView michael@0: // has been synched to the new data. michael@0: if (mAdapter != null) { michael@0: post(this); michael@0: } michael@0: } else { michael@0: fireOnSelected(); michael@0: performAccessibilityActionsOnSelected(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: private class WindowRunnnable { michael@0: private int mOriginalAttachCount; michael@0: michael@0: public void rememberWindowAttachCount() { michael@0: mOriginalAttachCount = getWindowAttachCount(); michael@0: } michael@0: michael@0: public boolean sameWindow() { michael@0: return hasWindowFocus() && getWindowAttachCount() == mOriginalAttachCount; michael@0: } michael@0: } michael@0: michael@0: private class PerformClick extends WindowRunnnable implements Runnable { michael@0: int mClickMotionPosition; michael@0: michael@0: @Override michael@0: public void run() { michael@0: if (mDataChanged) { michael@0: return; michael@0: } michael@0: michael@0: final ListAdapter adapter = mAdapter; michael@0: final int motionPosition = mClickMotionPosition; michael@0: michael@0: if (adapter != null && mItemCount > 0 && michael@0: motionPosition != INVALID_POSITION && michael@0: motionPosition < adapter.getCount() && sameWindow()) { michael@0: michael@0: final View child = getChildAt(motionPosition - mFirstPosition); michael@0: if (child != null) { michael@0: performItemClick(child, motionPosition, adapter.getItemId(motionPosition)); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: private final class CheckForTap implements Runnable { michael@0: @Override michael@0: public void run() { michael@0: if (mTouchMode != TOUCH_MODE_DOWN) { michael@0: return; michael@0: } michael@0: michael@0: mTouchMode = TOUCH_MODE_TAP; michael@0: michael@0: final View child = getChildAt(mMotionPosition - mFirstPosition); michael@0: if (child != null && !child.hasFocusable()) { michael@0: mLayoutMode = LAYOUT_NORMAL; michael@0: michael@0: if (!mDataChanged) { michael@0: setPressed(true); michael@0: child.setPressed(true); michael@0: michael@0: layoutChildren(); michael@0: positionSelector(mMotionPosition, child); michael@0: refreshDrawableState(); michael@0: michael@0: positionSelector(mMotionPosition, child); michael@0: refreshDrawableState(); michael@0: michael@0: final boolean longClickable = isLongClickable(); michael@0: michael@0: if (mSelector != null) { michael@0: Drawable d = mSelector.getCurrent(); michael@0: michael@0: if (d != null && d instanceof TransitionDrawable) { michael@0: if (longClickable) { michael@0: final int longPressTimeout = ViewConfiguration.getLongPressTimeout(); michael@0: ((TransitionDrawable) d).startTransition(longPressTimeout); michael@0: } else { michael@0: ((TransitionDrawable) d).resetTransition(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: if (longClickable) { michael@0: triggerCheckForLongPress(); michael@0: } else { michael@0: mTouchMode = TOUCH_MODE_DONE_WAITING; michael@0: } michael@0: } else { michael@0: mTouchMode = TOUCH_MODE_DONE_WAITING; michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: private class CheckForLongPress extends WindowRunnnable implements Runnable { michael@0: @Override michael@0: public void run() { michael@0: final int motionPosition = mMotionPosition; michael@0: final View child = getChildAt(motionPosition - mFirstPosition); michael@0: michael@0: if (child != null) { michael@0: final long longPressId = mAdapter.getItemId(mMotionPosition); michael@0: michael@0: boolean handled = false; michael@0: if (sameWindow() && !mDataChanged) { michael@0: handled = performLongPress(child, motionPosition, longPressId); michael@0: } michael@0: michael@0: if (handled) { michael@0: mTouchMode = TOUCH_MODE_REST; michael@0: setPressed(false); michael@0: child.setPressed(false); michael@0: } else { michael@0: mTouchMode = TOUCH_MODE_DONE_WAITING; michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: private class CheckForKeyLongPress extends WindowRunnnable implements Runnable { michael@0: public void run() { michael@0: if (!isPressed() || mSelectedPosition < 0) { michael@0: return; michael@0: } michael@0: michael@0: final int index = mSelectedPosition - mFirstPosition; michael@0: final View v = getChildAt(index); michael@0: michael@0: if (!mDataChanged) { michael@0: boolean handled = false; michael@0: michael@0: if (sameWindow()) { michael@0: handled = performLongPress(v, mSelectedPosition, mSelectedRowId); michael@0: } michael@0: michael@0: if (handled) { michael@0: setPressed(false); michael@0: v.setPressed(false); michael@0: } michael@0: } else { michael@0: setPressed(false); michael@0: michael@0: if (v != null) { michael@0: v.setPressed(false); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: private static class ArrowScrollFocusResult { michael@0: private int mSelectedPosition; michael@0: private int mAmountToScroll; michael@0: michael@0: /** michael@0: * How {@link TwoWayView#arrowScrollFocused} returns its values. michael@0: */ michael@0: void populate(int selectedPosition, int amountToScroll) { michael@0: mSelectedPosition = selectedPosition; michael@0: mAmountToScroll = amountToScroll; michael@0: } michael@0: michael@0: public int getSelectedPosition() { michael@0: return mSelectedPosition; michael@0: } michael@0: michael@0: public int getAmountToScroll() { michael@0: return mAmountToScroll; michael@0: } michael@0: } michael@0: michael@0: private class ListItemAccessibilityDelegate extends AccessibilityDelegateCompat { michael@0: @Override michael@0: public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { michael@0: super.onInitializeAccessibilityNodeInfo(host, info); michael@0: michael@0: final int position = getPositionForView(host); michael@0: final ListAdapter adapter = getAdapter(); michael@0: michael@0: // Cannot perform actions on invalid items michael@0: if (position == INVALID_POSITION || adapter == null) { michael@0: return; michael@0: } michael@0: michael@0: // Cannot perform actions on disabled items michael@0: if (!isEnabled() || !adapter.isEnabled(position)) { michael@0: return; michael@0: } michael@0: michael@0: if (position == getSelectedItemPosition()) { michael@0: info.setSelected(true); michael@0: info.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_SELECTION); michael@0: } else { michael@0: info.addAction(AccessibilityNodeInfoCompat.ACTION_SELECT); michael@0: } michael@0: michael@0: if (isClickable()) { michael@0: info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK); michael@0: info.setClickable(true); michael@0: } michael@0: michael@0: if (isLongClickable()) { michael@0: info.addAction(AccessibilityNodeInfoCompat.ACTION_LONG_CLICK); michael@0: info.setLongClickable(true); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public boolean performAccessibilityAction(View host, int action, Bundle arguments) { michael@0: if (super.performAccessibilityAction(host, action, arguments)) { michael@0: return true; michael@0: } michael@0: michael@0: final int position = getPositionForView(host); michael@0: final ListAdapter adapter = getAdapter(); michael@0: michael@0: // Cannot perform actions on invalid items michael@0: if (position == INVALID_POSITION || adapter == null) { michael@0: return false; michael@0: } michael@0: michael@0: // Cannot perform actions on disabled items michael@0: if (!isEnabled() || !adapter.isEnabled(position)) { michael@0: return false; michael@0: } michael@0: michael@0: final long id = getItemIdAtPosition(position); michael@0: michael@0: switch (action) { michael@0: case AccessibilityNodeInfoCompat.ACTION_CLEAR_SELECTION: michael@0: if (getSelectedItemPosition() == position) { michael@0: setSelection(INVALID_POSITION); michael@0: return true; michael@0: } michael@0: return false; michael@0: michael@0: case AccessibilityNodeInfoCompat.ACTION_SELECT: michael@0: if (getSelectedItemPosition() != position) { michael@0: setSelection(position); michael@0: return true; michael@0: } michael@0: return false; michael@0: michael@0: case AccessibilityNodeInfoCompat.ACTION_CLICK: michael@0: if (isClickable()) { michael@0: return performItemClick(host, position, id); michael@0: } michael@0: return false; michael@0: michael@0: case AccessibilityNodeInfoCompat.ACTION_LONG_CLICK: michael@0: if (isLongClickable()) { michael@0: return performLongPress(host, position, id); michael@0: } michael@0: return false; michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: } michael@0: }