mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-01 22:55:23 +00:00
6544 lines
220 KiB
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;
|
|
}
|
|
}
|
|
}
|