gecko/mobile/android/base/widget/TwoWayView.java

6544 lines
220 KiB
Java

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