对于RecyclerView的使用,大家可以查看将替代ListView的RecyclerView 的使用详解(一),单单从代码结构来说RecyclerView确实比ListView优化了很多,也简化了我们编写代码量,但是有一个问题会导致开发者不会去用它,更比说替换ListView了,我不知道使用过RecyclerView的人有没有进一步查看,RecyclerView没有提供Item的点击事件,我们使用列表不仅仅为了显示数据,同时也可以能会交互,所以RecyclerView这个问题导致基本没有人用它,我清楚谷歌是怎么想的,不过RecyclerView也并没有把所有的路给堵死,需要我们写代码来实现Item的点击事件,我们都知道RecyclerView里面新加了ViewHolder这个静态抽象类,这个类里面有一个方法getPosition()可以返回当前ViewHolder实例的位置,实现onItemClick就是使用它来做的,下面有两种方法来实现:
第一种:不修改源码
这种方法不修改源码,问题是只能在RecyclerView.Adapter中实现ItemClick事件
public static class ViewHolder extends RecyclerView.ViewHolder {
public ViewHolder(View itemView) {
super(itemView);
itemView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Log.e("jwzhangjie", "当前点击的位置:"+getPosition());
}
});
}
}
这种方式直观上看起来不太好,不过也可以实现ItemClick事件。
第二种方法:修改RecyclerView源码
1、把在RecyClerView类里面定义OnItemClickListener接口
/**
* Interface definition for a callback to be invoked when an item in this
* RecyclerView.Adapter has been clicked.
*/
public interface OnItemClickListener {
/**
* Callback method to be invoked when an item in this RecyclerView.Adapter has
* been clicked.
* <p>
* Implementers can call getPosition(position) if they need
* to access the data associated with the selected item.
*
* @param view The view within the RecyclerView.Adapter that was clicked (this
* will be a view provided by the adapter)
* @param position The position of the view in the adapter.
*/
void onItemClick(View view, int position);
}
public static OnItemClickListener mOnItemClickListener = null;
/**
* Register a callback to be invoked when an item in this AdapterView has
* been clicked.
*
* @param listener The callback that will be invoked.
*/
public void setOnItemClickListener(OnItemClickListener listener) {
mOnItemClickListener = listener;
}
/**
* @return The callback to be invoked with an item in this AdapterView has
* been clicked, or null id no callback has been set.
*/
public final OnItemClickListener getOnItemClickListener() {
return mOnItemClickListener;
}
2、在RecyclerView中的抽象类ViewHolder中添加View的点击事件
public static abstract class ViewHolder implements OnClickListener{
public final View itemView;
int mPosition = NO_POSITION;
int mOldPosition = NO_POSITION;
long mItemId = NO_ID;
int mItemViewType = INVALID_TYPE;
/**
* This ViewHolder has been bound to a position; mPosition, mItemId and mItemViewType
* are all valid.
*/
static final int FLAG_BOUND = 1 << 0;
/**
* The data this ViewHolder's view reflects is stale and needs to be rebound
* by the adapter. mPosition and mItemId are consistent.
*/
static final int FLAG_UPDATE = 1 << 1;
/**
* This ViewHolder's data is invalid. The identity implied by mPosition and mItemId
* are not to be trusted and may no longer match the item view type.
* This ViewHolder must be fully rebound to different data.
*/
static final int FLAG_INVALID = 1 << 2;
/**
* This ViewHolder points at data that represents an item previously removed from the
* data set. Its view may still be used for things like outgoing animations.
*/
static final int FLAG_REMOVED = 1 << 3;
/**
* This ViewHolder should not be recycled. This flag is set via setIsRecyclable()
* and is intended to keep views around during animations.
*/
static final int FLAG_NOT_RECYCLABLE = 1 << 4;
private int mFlags;
private int mIsRecyclableCount = 0;
// If non-null, view is currently considered scrap and may be reused for other data by the
// scrap container.
private Recycler mScrapContainer = null;
@Override
public void onClick(View v) {
if (mOnItemClickListener != null) {
mOnItemClickListener.onItemClick(itemView, getPosition());
}
}
public ViewHolder(View itemView) {
if (itemView == null) {
throw new IllegalArgumentException("itemView may not be null");
}
this.itemView = itemView;
this.itemView.setOnClickListener(this);
}
void offsetPosition(int offset) {
if (mOldPosition == NO_POSITION) {
mOldPosition = mPosition;
}
mPosition += offset;
}
void clearOldPosition() {
mOldPosition = NO_POSITION;
}
public final int getPosition() {
return mOldPosition == NO_POSITION ? mPosition : mOldPosition;
}
public final long getItemId() {
return mItemId;
}
public final int getItemViewType() {
return mItemViewType;
}
boolean isScrap() {
return mScrapContainer != null;
}
void unScrap() {
mScrapContainer.unscrapView(this);
mScrapContainer = null;
}
void setScrapContainer(Recycler recycler) {
mScrapContainer = recycler;
}
boolean isInvalid() {
return (mFlags & FLAG_INVALID) != 0;
}
boolean needsUpdate() {
return (mFlags & FLAG_UPDATE) != 0;
}
boolean isBound() {
return (mFlags & FLAG_BOUND) != 0;
}
boolean isRemoved() {
return (mFlags & FLAG_REMOVED) != 0;
}
void setFlags(int flags, int mask) {
mFlags = (mFlags & ~mask) | (flags & mask);
}
void addFlags(int flags) {
mFlags |= flags;
}
void clearFlagsForSharedPool() {
mFlags = 0;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("ViewHolder{" +
Integer.toHexString(hashCode()) + " position=" + mPosition + " id=" + mItemId);
if (isScrap()) sb.append(" scrap");
if (isInvalid()) sb.append(" invalid");
if (!isBound()) sb.append(" unbound");
if (needsUpdate()) sb.append(" update");
if (isRemoved()) sb.append(" removed");
sb.append("}");
return sb.toString();
}
3、完成上面的步骤,就可以使用RecyclerView来完成itemClick事件了
cashAccountList.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(View view, int position) {
AppLog.e("position: "+position);
}
});
下面是完整的RecyclerView源码:
/*
* Copyright (C) 2013 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 android.support.v7.widget;
import android.content.Context;
import android.database.Observable;
import android.graphics.Canvas;
import android.graphics.PointF;
import android.graphics.Rect;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.Nullable;
import android.support.v4.util.ArrayMap;
import android.support.v4.util.Pools;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.VelocityTrackerCompat;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.EdgeEffectCompat;
import android.support.v4.widget.ScrollerCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseArray;
import android.util.SparseIntArray;
import android.view.FocusFinder;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.animation.Interpolator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* A flexible view for providing a limited window into a large data set.
*
* <h3>Glossary of terms:</h3>
*
* <ul>
* <li><em>Adapter:</em> A subclass of {@link Adapter} responsible for providing views
* that represent items in a data set.</li>
* <li><em>Position:</em> The position of a data item within an <em>Adapter</em>.</li>
* <li><em>Index:</em> The index of an attached child view as used in a call to
* {@link ViewGroup#getChildAt}. Contrast with <em>Position.</em></li>
* <li><em>Binding:</em> The process of preparing a child view to display data corresponding
* to a <em>position</em> within the adapter.</li>
* <li><em>Recycle (view):</em> A view previously used to display data for a specific adapter
* position may be placed in a cache for later reuse to display the same type of data again
* later. This can drastically improve performance by skipping initial layout inflation
* or construction.</li>
* <li><em>Scrap (view):</em> A child view that has entered into a temporarily detached
* state during layout. Scrap views may be reused without becoming fully detached
* from the parent RecyclerView, either unmodified if no rebinding is required or modified
* by the adapter if the view was considered <em>dirty</em>.</li>
* <li><em>Dirty (view):</em> A child view that must be rebound by the adapter before
* being displayed.</li>
* </ul>
*/
public class RecyclerView extends ViewGroup {
private static final String TAG = "RecyclerView";
private static final boolean DEBUG = false;
private static final boolean ENABLE_PREDICTIVE_ANIMATIONS = false;
private static final boolean DISPATCH_TEMP_DETACH = false;
public static final int HORIZONTAL = 0;
public static final int VERTICAL = 1;
public static final int NO_POSITION = -1;
public static final long NO_ID = -1;
public static final int INVALID_TYPE = -1;
private static final int MAX_SCROLL_DURATION = 2000;
private final RecyclerViewDataObserver mObserver = new RecyclerViewDataObserver();
private final Recycler mRecycler = new Recycler();
private SavedState mPendingSavedState;
/**
* Note: this Runnable is only ever posted if:
* 1) We've been through first layout
* 2) We know we have a fixed size (mHasFixedSize)
* 3) We're attached
*/
private final Runnable mUpdateChildViewsRunnable = new Runnable() {
public void run() {
if (mPendingUpdates.isEmpty()) {
return;
}
eatRequestLayout();
updateChildViews();
resumeRequestLayout(true);
}
};
private final Rect mTempRect = new Rect();
private final ArrayList<UpdateOp> mPendingUpdates = new ArrayList<UpdateOp>();
private final ArrayList<UpdateOp> mPendingLayoutUpdates = new ArrayList<UpdateOp>();
private Pools.Pool<UpdateOp> mUpdateOpPool = new Pools.SimplePool<UpdateOp>(UpdateOp.POOL_SIZE);
private Adapter mAdapter;
private LayoutManager mLayout;
private RecyclerListener mRecyclerListener;
private final ArrayList<ItemDecoration> mItemDecorations = new ArrayList<ItemDecoration>();
private final ArrayList<OnItemTouchListener> mOnItemTouchListeners =
new ArrayList<OnItemTouchListener>();
private OnItemTouchListener mActiveOnItemTouchListener;
private boolean mIsAttached;
private boolean mHasFixedSize;
private boolean mFirstLayoutComplete;
private boolean mEatRequestLayout;
private boolean mLayoutRequestEaten;
private boolean mAdapterUpdateDuringMeasure;
private final boolean mPostUpdatesOnAnimation;
private EdgeEffectCompat mLeftGlow, mTopGlow, mRightGlow, mBottomGlow;
ItemAnimator mItemAnimator = new DefaultItemAnimator();
private static final int INVALID_POINTER = -1;
/**
* The RecyclerView is not currently scrolling.
* @see #getScrollState()
*/
public static final int SCROLL_STATE_IDLE = 0;
/**
* The RecyclerView is currently being dragged by outside input such as user touch input.
* @see #getScrollState()
*/
public static final int SCROLL_STATE_DRAGGING = 1;
/**
* The RecyclerView is currently animating to a final position while not under
* outside control.
* @see #getScrollState()
*/
public static final int SCROLL_STATE_SETTLING = 2;
// Touch/scrolling handling
private int mScrollState = SCROLL_STATE_IDLE;
private int mScrollPointerId = INVALID_POINTER;
private VelocityTracker mVelocityTracker;
private int mInitialTouchX;
private int mInitialTouchY;
private int mLastTouchX;
private int mLastTouchY;
private final int mTouchSlop;
private final int mMinFlingVelocity;
private final int mMaxFlingVelocity;
private final ViewFlinger mViewFlinger = new ViewFlinger();
private final State mState = new State();
private OnScrollListener mScrollListener;
// For use in item animations
boolean mItemsAddedOrRemoved = false;
boolean mItemsChanged = false;
int mAnimatingViewIndex = -1;
int mNumAnimatingViews = 0;
boolean mInPreLayout = false;
private ItemAnimator.ItemAnimatorListener mItemAnimatorListener =
new ItemAnimatorRestoreListener();
private boolean mPostedAnimatorRunner = false;
private Runnable mItemAnimatorRunner = new Runnable() {
@Override
public void run() {
if (mItemAnimator != null) {
mItemAnimator.runPendingAnimations();
}
mPostedAnimatorRunner = false;
}
};
private static final Interpolator sQuinticInterpolator = new Interpolator() {
public float getInterpolation(float t) {
t -= 1.0f;
return t * t * t * t * t + 1.0f;
}
};
public RecyclerView(Context context) {
this(context, null);
}
public RecyclerView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public RecyclerView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
final int version = Build.VERSION.SDK_INT;
mPostUpdatesOnAnimation = version >= 16;
final ViewConfiguration vc = ViewConfiguration.get(context);
mTouchSlop = vc.getScaledTouchSlop();
mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
setWillNotDraw(ViewCompat.getOverScrollMode(this) == ViewCompat.OVER_SCROLL_NEVER);
mItemAnimator.setListener(mItemAnimatorListener);
}
/**
* RecyclerView can perform several optimizations if it can know in advance that changes in
* adapter content cannot change the size of the RecyclerView itself.
* If your use of RecyclerView falls into this category, set this to true.
*
* @param hasFixedSize true if adapter changes cannot affect the size of the RecyclerView.
*/
public void setHasFixedSize(boolean hasFixedSize) {
mHasFixedSize = hasFixedSize;
}
/**
* @return true if the app has specified that changes in adapter content cannot change
* the size of the RecyclerView itself.
*/
public boolean hasFixedSize() {
return mHasFixedSize;
}
/**
* Set a new adapter to provide child views on demand.
*
* @param adapter The new adapter to set, or null to set no adapter.
*/
public void setAdapter(Adapter adapter) {
if (mAdapter != null) {
mAdapter.unregisterAdapterDataObserver(mObserver);
}
// end all running animations
if (mItemAnimator != null) {
mItemAnimator.endAnimations();
}
// Since animations are ended, mLayout.children should be equal to recyclerView.children.
// This may not be true if item animator's end does not work as expected. (e.g. not release
// children instantly). It is safer to use mLayout's child count.
if (mLayout != null) {
mLayout.removeAndRecycleAllViews(mRecycler);
mLayout.removeAndRecycleScrapInt(mRecycler, true);
}
final Adapter oldAdapter = mAdapter;
mAdapter = adapter;
if (adapter != null) {
adapter.registerAdapterDataObserver(mObserver);
}
if (mLayout != null) {
mLayout.onAdapterChanged(oldAdapter, mAdapter);
}
mRecycler.onAdapterChanged(oldAdapter, mAdapter);
mState.mStructureChanged = true;
markKnownViewsInvalid();
requestLayout();
}
/**
* Retrieves the previously set adapter or null if no adapter is set.
*
* @return The previously set adapter
* @see #setAdapter(Adapter)
*/
public Adapter getAdapter() {
return mAdapter;
}
/**
* Register a listener that will be notified whenever a child view is recycled.
*
* <p>This listener will be called when a LayoutManager or the RecyclerView decides
* that a child view is no longer needed. If an application associates expensive
* or heavyweight data with item views, this may be a good place to release
* or free those resources.</p>
*
* @param listener Listener to register, or null to clear
*/
public void setRecyclerListener(RecyclerListener listener) {
mRecyclerListener = listener;
}
/**
* Set the {@link LayoutManager} that this RecyclerView will use.
*
* <p>In contrast to other adapter-backed views such as {@link android.widget.ListView}
* or {@link android.widget.GridView}, RecyclerView allows client code to provide custom
* layout arrangements for child views. These arrangements are controlled by the
* {@link LayoutManager}. A LayoutManager must be provided for RecyclerView to function.</p>
*
* <p>Several default strategies are provided for common uses such as lists and grids.</p>
*
* @param layout LayoutManager to use
*/
public void setLayoutManager(LayoutManager layout) {
if (layout == mLayout) {
return;
}
mRecycler.clear();
removeAllViews();
if (mLayout != null) {
if (mIsAttached) {
mLayout.onDetachedFromWindow(this);
}
mLayout.mRecyclerView = null;
}
mLayout = layout;
if (layout != null) {
if (layout.mRecyclerView != null) {
throw new IllegalArgumentException("LayoutManager " + layout +
" is already attached to a RecyclerView: " + layout.mRecyclerView);
}
layout.mRecyclerView = this;
if (mIsAttached) {
mLayout.onAttachedToWindow(this);
}
}
requestLayout();
}
@Override
protected Parcelable onSaveInstanceState() {
SavedState state = new SavedState(super.onSaveInstanceState());
if (mPendingSavedState != null) {
state.copyFrom(mPendingSavedState);
} else if (mLayout != null) {
state.mLayoutState = mLayout.onSaveInstanceState();
} else {
state.mLayoutState = null;
}
return state;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
mPendingSavedState = (SavedState) state;
super.onRestoreInstanceState(mPendingSavedState.getSuperState());
if (mLayout != null && mPendingSavedState.mLayoutState != null) {
mLayout.onRestoreInstanceState(mPendingSavedState.mLayoutState);
}
}
/**
* Adds a view to the animatingViews list.
* mAnimatingViews holds the child views that are currently being kept around
* purely for the purpose of being animated out of view. They are drawn as a regular
* part of the child list of the RecyclerView, but they are invisible to the LayoutManager
* as they are managed separately from the regular child views.
* @param view The view to be removed
*/
private void addAnimatingView(View view) {
boolean alreadyAdded = false;
if (mNumAnimatingViews > 0) {
for (int i = mAnimatingViewIndex; i < getChildCount(); ++i) {
if (getChildAt(i) == view) {
alreadyAdded = true;
break;
}
}
}
if (!alreadyAdded) {
if (mNumAnimatingViews == 0) {
mAnimatingViewIndex = getChildCount();
}
++mNumAnimatingViews;
addView(view);
}
mRecycler.unscrapView(getChildViewHolder(view));
}
/**
* Removes a view from the animatingViews list.
* @param view The view to be removed
* @see #addAnimatingView(View)
*/
private void removeAnimatingView(View view) {
if (mNumAnimatingViews > 0) {
for (int i = mAnimatingViewIndex; i < getChildCount(); ++i) {
if (getChildAt(i) == view) {
removeViewAt(i);
--mNumAnimatingViews;
if (mNumAnimatingViews == 0) {
mAnimatingViewIndex = -1;
}
mRecycler.recycleView(view);
return;
}
}
}
}
private View getAnimatingView(int position, int type) {
if (mNumAnimatingViews > 0) {
for (int i = mAnimatingViewIndex; i < getChildCount(); ++i) {
final View view = getChildAt(i);
ViewHolder holder = getChildViewHolder(view);
if (holder.getPosition() == position &&
( type == INVALID_TYPE || holder.getItemViewType() == type)) {
return view;
}
}
}
return null;
}
/**
* Return the {@link LayoutManager} currently responsible for
* layout policy for this RecyclerView.
*
* @return The currently bound LayoutManager
*/
public LayoutManager getLayoutManager() {
return mLayout;
}
/**
* Retrieve this RecyclerView's {@link RecycledViewPool}. This method will never return null;
* if no pool is set for this view a new one will be created. See
* {@link #setRecycledViewPool(RecycledViewPool) setRecycledViewPool} for more information.
*
* @return The pool used to store recycled item views for reuse.
* @see #setRecycledViewPool(RecycledViewPool)
*/
public RecycledViewPool getRecycledViewPool() {
return mRecycler.getRecycledViewPool();
}
/**
* Recycled view pools allow multiple RecyclerViews to share a common pool of scrap views.
* This can be useful if you have multiple RecyclerViews with adapters that use the same
* view types, for example if you have several data sets with the same kinds of item views
* displayed by a {@link android.support.v4.view.ViewPager ViewPager}.
*
* @param pool Pool to set. If this parameter is null a new pool will be created and used.
*/
public void setRecycledViewPool(RecycledViewPool pool) {
mRecycler.setRecycledViewPool(pool);
}
/**
* Set the number of offscreen views to retain before adding them to the potentially shared
* {@link #getRecycledViewPool() recycled view pool}.
*
* <p>The offscreen view cache stays aware of changes in the attached adapter, allowing
* a LayoutManager to reuse those views unmodified without needing to return to the adapter
* to rebind them.</p>
*
* @param size Number of views to cache offscreen before returning them to the general
* recycled view pool
*/
public void setItemViewCacheSize(int size) {
mRecycler.setViewCacheSize(size);
}
/**
* Return the current scrolling state of the RecyclerView.
*
* @return {@link #SCROLL_STATE_IDLE}, {@link #SCROLL_STATE_DRAGGING} or
* {@link #SCROLL_STATE_SETTLING}
*/
public int getScrollState() {
return mScrollState;
}
private void setScrollState(int state) {
if (state == mScrollState) {
return;
}
mScrollState = state;
if (state != SCROLL_STATE_SETTLING) {
stopScroll();
}
if (mScrollListener != null) {
mScrollListener.onScrollStateChanged(state);
}
}
/**
* Add an {@link ItemDecoration} to this RecyclerView. Item decorations can
* affect both measurement and drawing of individual item views.
*
* <p>Item decorations are ordered. Decorations placed earlier in the list will
* be run/queried/drawn first for their effects on item views. Padding added to views
* will be nested; a padding added by an earlier decoration will mean further
* item decorations in the list will be asked to draw/pad within the previous decoration's
* given area.</p>
*
* @param decor Decoration to add
* @param index Position in the decoration chain to insert this decoration at. If this value
* is negative the decoration will be added at the end.
*/
public void addItemDecoration(ItemDecoration decor, int index) {
if (mItemDecorations.isEmpty()) {
setWillNotDraw(false);
}
if (index < 0) {
mItemDecorations.add(decor);
} else {
mItemDecorations.add(index, decor);
}
markItemDecorInsetsDirty();
requestLayout();
}
/**
* Add an {@link ItemDecoration} to this RecyclerView. Item decorations can
* affect both measurement and drawing of individual item views.
*
* <p>Item decorations are ordered. Decorations placed earlier in the list will
* be run/queried/drawn first for their effects on item views. Padding added to views
* will be nested; a padding added by an earlier decoration will mean further
* item decorations in the list will be asked to draw/pad within the previous decoration's
* given area.</p>
*
* @param decor Decoration to add
*/
public void addItemDecoration(ItemDecoration decor) {
addItemDecoration(decor, -1);
}
/**
* Remove an {@link ItemDecoration} from this RecyclerView.
*
* <p>The given decoration will no longer impact the measurement and drawing of
* item views.</p>
*
* @param decor Decoration to remove
* @see #addItemDecoration(ItemDecoration)
*/
public void removeItemDecoration(ItemDecoration decor) {
mItemDecorations.remove(decor);
if (mItemDecorations.isEmpty()) {
setWillNotDraw(ViewCompat.getOverScrollMode(this) == ViewCompat.OVER_SCROLL_NEVER);
}
markItemDecorInsetsDirty();
requestLayout();
}
/**
* Set a listener that will be notified of any changes in scroll state or position.
*
* @param listener Listener to set or null to clear
*/
public void setOnScrollListener(OnScrollListener listener) {
mScrollListener = listener;
}
/**
* Convenience method to scroll to a certain position.
*
* RecyclerView does not implement scrolling logic, rather forwards the call to
* {@link android.support.v7.widget.RecyclerView.LayoutManager#scrollToPosition(int)}
* @param position Scroll to this adapter position
* @see android.support.v7.widget.RecyclerView.LayoutManager#scrollToPosition(int)
*/
public void scrollToPosition(int position) {
stopScroll();
mLayout.scrollToPosition(position);
awakenScrollBars();
}
/**
* Starts a smooth scroll to an adapter position.
* <p>
* To support smooth scrolling, you must override
* {@link LayoutManager#smoothScrollToPosition(RecyclerView, State, int)} and create a
* {@link SmoothScroller}.
* <p>
* {@link LayoutManager} is responsible for creating the actual scroll action. If you want to
* provide a custom smooth scroll logic, override
* {@link LayoutManager#smoothScrollToPosition(RecyclerView, State, int)} in your
* LayoutManager.
*
* @param position The adapter position to scroll to
* @see LayoutManager#smoothScrollToPosition(RecyclerView, State, int)
*/
public void smoothScrollToPosition(int position) {
mLayout.smoothScrollToPosition(this, mState, position);
}
@Override
public void scrollTo(int x, int y) {
throw new UnsupportedOperationException(
"RecyclerView does not support scrolling to an absolute position.");
}
@Override
public void scrollBy(int x, int y) {
if (mLayout == null) {
throw new IllegalStateException("Cannot scroll without a LayoutManager set. " +
"Call setLayoutManager with a non-null argument.");
}
final boolean canScrollHorizontal = mLayout.canScrollHorizontally();
final boolean canScrollVertical = mLayout.canScrollVertically();
if (canScrollHorizontal || canScrollVertical) {
scrollByInternal(canScrollHorizontal ? x : 0, canScrollVertical ? y : 0);
}
}
/**
* Helper method reflect data changes to the state.
* <p>
* Adapter changes during a scroll may trigger a crash because scroll assumes no data change
* but data actually changed.
* <p>
* This method consumes all deferred changes to avoid that case.
* <p>
* This also ends all pending animations. It will be changed once we can support
* animations during scroll.
*/
private void consumePendingUpdateOperations() {
if (mItemAnimator != null) {
mItemAnimator.endAnimations();
}
if (mPendingUpdates.size() > 0) {
mUpdateChildViewsRunnable.run();
}
}
/**
* Does not perform bounds checking. Used by internal methods that have already validated input.
*/
void scrollByInternal(int x, int y) {
int overscrollX = 0, overscrollY = 0;
consumePendingUpdateOperations();
if (mAdapter != null) {
eatRequestLayout();
if (x != 0) {
final int hresult = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
overscrollX = x - hresult;
}
if (y != 0) {
final int vresult = mLayout.scrollVerticallyBy(y, mRecycler, mState);
overscrollY = y - vresult;
}
resumeRequestLayout(false);
}
if (!mItemDecorations.isEmpty()) {
invalidate();
}
if (ViewCompat.getOverScrollMode(this) != ViewCompat.OVER_SCROLL_NEVER) {
pullGlows(overscrollX, overscrollY);
}
if (mScrollListener != null && (x != 0 || y != 0)) {
mScrollListener.onScrolled(x, y);
}
if (!awakenScrollBars()) {
invalidate();
}
}
/**
* <p>Compute the horizontal offset of the horizontal scrollbar's thumb within the horizontal
* range. This value is used to compute the length of the thumb within the scrollbar's track.
* </p>
*
* <p>The range is expressed in arbitrary units that must be the same as the units used by
* {@link #computeHorizontalScrollRange()} and {@link #computeHorizontalScrollExtent()}.</p>
*
* <p>Default implementation returns 0.</p>
*
* <p>If you want to support scroll bars, override
* {@link RecyclerView.LayoutManager#computeHorizontalScrollOffset(RecyclerView.State)} in your
* LayoutManager. </p>
*
* @return The horizontal offset of the scrollbar's thumb
* @see android.support.v7.widget.RecyclerView.LayoutManager#computeHorizontalScrollOffset
* (RecyclerView.Adapter)
*/
@Override
protected int computeHorizontalScrollOffset() {
return mLayout.canScrollHorizontally() ? mLayout.computeHorizontalScrollOffset(mState)
: 0;
}
/**
* <p>Compute the horizontal extent of the horizontal scrollbar's thumb within the
* horizontal range. This value is used to compute the length of the thumb within the
* scrollbar's track.</p>
*
* <p>The range is expressed in arbitrary units that must be the same as the units used by
* {@link #computeHorizontalScrollRange()} and {@link #computeHorizontalScrollOffset()}.</p>
*
* <p>Default implementation returns 0.</p>
*
* <p>If you want to support scroll bars, override
* {@link RecyclerView.LayoutManager#computeHorizontalScrollExtent(RecyclerView.State)} in your
* LayoutManager.</p>
*
* @return The horizontal extent of the scrollbar's thumb
* @see RecyclerView.LayoutManager#computeHorizontalScrollExtent(RecyclerView.State)
*/
@Override
protected int computeHorizontalScrollExtent() {
return mLayout.canScrollHorizontally() ? mLayout.computeHorizontalScrollExtent(mState) : 0;
}
/**
* <p>Compute the horizontal range that the horizontal scrollbar represents.</p>
*
* <p>The range is expressed in arbitrary units that must be the same as the units used by
* {@link #computeHorizontalScrollExtent()} and {@link #computeHorizontalScrollOffset()}.</p>
*
* <p>Default implementation returns 0.</p>
*
* <p>If you want to support scroll bars, override
* {@link RecyclerView.LayoutManager#computeHorizontalScrollRange(RecyclerView.State)} in your
* LayoutManager.</p>
*
* @return The total horizontal range represented by the vertical scrollbar
* @see RecyclerView.LayoutManager#computeHorizontalScrollRange(RecyclerView.State)
*/
@Override
protected int computeHorizontalScrollRange() {
return mLayout.canScrollHorizontally() ? mLayout.computeHorizontalScrollRange(mState) : 0;
}
/**
* <p>Compute the vertical offset of the vertical scrollbar's thumb within the vertical range.
* This value is used to compute the length of the thumb within the scrollbar's track. </p>
*
* <p>The range is expressed in arbitrary units that must be the same as the units used by
* {@link #computeVerticalScrollRange()} and {@link #computeVerticalScrollExtent()}.</p>
*
* <p>Default implementation returns 0.</p>
*
* <p>If you want to support scroll bars, override
* {@link RecyclerView.LayoutManager#computeVerticalScrollOffset(RecyclerView.State)} in your
* LayoutManager.</p>
*
* @return The vertical offset of the scrollbar's thumb
* @see android.support.v7.widget.RecyclerView.LayoutManager#computeVerticalScrollOffset
* (RecyclerView.Adapter)
*/
@Override
protected int computeVerticalScrollOffset() {
return mLayout.canScrollVertically() ? mLayout.computeVerticalScrollOffset(mState) : 0;
}
/**
* <p>Compute the vertical extent of the vertical scrollbar's thumb within the vertical range.
* This value is used to compute the length of the thumb within the scrollbar's track.</p>
*
* <p>The range is expressed in arbitrary units that must be the same as the units used by
* {@link #computeVerticalScrollRange()} and {@link #computeVerticalScrollOffset()}.</p>
*
* <p>Default implementation returns 0.</p>
*
* <p>If you want to support scroll bars, override
* {@link RecyclerView.LayoutManager#computeVerticalScrollExtent(RecyclerView.State)} in your
* LayoutManager.</p>
*
* @return The vertical extent of the scrollbar's thumb
* @see RecyclerView.LayoutManager#computeVerticalScrollExtent(RecyclerView.State)
*/
@Override
protected int computeVerticalScrollExtent() {
return mLayout.canScrollVertically() ? mLayout.computeVerticalScrollExtent(mState) : 0;
}
/**
* <p>Compute the vertical range that the vertical scrollbar represents.</p>
*
* <p>The range is expressed in arbitrary units that must be the same as the units used by
* {@link #computeVerticalScrollExtent()} and {@link #computeVerticalScrollOffset()}.</p>
*
* <p>Default implementation returns 0.</p>
*
* <p>If you want to support scroll bars, override
* {@link RecyclerView.LayoutManager#computeVerticalScrollRange(RecyclerView.State)} in your
* LayoutManager.</p>
*
* @return The total vertical range represented by the vertical scrollbar
* @see RecyclerView.LayoutManager#computeVerticalScrollRange(RecyclerView.State)
*/
@Override
protected int computeVerticalScrollRange() {
return mLayout.canScrollVertically() ? mLayout.computeVerticalScrollRange(mState) : 0;
}
void eatRequestLayout() {
if (!mEatRequestLayout) {
mEatRequestLayout = true;
mLayoutRequestEaten = false;
}
}
void resumeRequestLayout(boolean performLayoutChildren) {
if (mEatRequestLayout) {
if (performLayoutChildren && mLayoutRequestEaten &&
mLayout != null && mAdapter != null) {
dispatchLayout();
}
mEatRequestLayout = false;
mLayoutRequestEaten = false;
}
}
/**
* Animate a scroll by the given amount of pixels along either axis.
*
* @param dx Pixels to scroll horizontally
* @param dy Pixels to scroll vertically
*/
public void smoothScrollBy(int dx, int dy) {
if (dx != 0 || dy != 0) {
mViewFlinger.smoothScrollBy(dx, dy);
}
}
/**
* Begin a standard fling with an initial velocity along each axis in pixels per second.
* If the velocity given is below the system-defined minimum this method will return false
* and no fling will occur.
*
* @param velocityX Initial horizontal velocity in pixels per second
* @param velocityY Initial vertical velocity in pixels per second
* @return true if the fling was started, false if the velocity was too low to fling
*/
public boolean fling(int velocityX, int velocityY) {
if (Math.abs(velocityX) < mMinFlingVelocity) {
velocityX = 0;
}
if (Math.abs(velocityY) < mMinFlingVelocity) {
velocityY = 0;
}
velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
if (velocityX != 0 || velocityY != 0) {
mViewFlinger.fling(velocityX, velocityY);
return true;
}
return false;
}
/**
* Stop any current scroll in progress, such as one started by
* {@link #smoothScrollBy(int, int)}, {@link #fling(int, int)} or a touch-initiated fling.
*/
public void stopScroll() {
mViewFlinger.stop();
mLayout.stopSmoothScroller();
}
/**
* Apply a pull to relevant overscroll glow effects
*/
private void pullGlows(int overscrollX, int overscrollY) {
if (overscrollX < 0) {
if (mLeftGlow == null) {
mLeftGlow = new EdgeEffectCompat(getContext());
mLeftGlow.setSize(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(),
getMeasuredWidth() - getPaddingLeft() - getPaddingRight());
}
mLeftGlow.onPull(-overscrollX / (float) getWidth());
} else if (overscrollX > 0) {
if (mRightGlow == null) {
mRightGlow = new EdgeEffectCompat(getContext());
mRightGlow.setSize(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(),
getMeasuredWidth() - getPaddingLeft() - getPaddingRight());
}
mRightGlow.onPull(overscrollX / (float) getWidth());
}
if (overscrollY < 0) {
if (mTopGlow == null) {
mTopGlow = new EdgeEffectCompat(getContext());
mTopGlow.setSize(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
getMeasuredHeight() - getPaddingTop() - getPaddingBottom());
}
mTopGlow.onPull(-overscrollY / (float) getHeight());
} else if (overscrollY > 0) {
if (mBottomGlow == null) {
mBottomGlow = new EdgeEffectCompat(getContext());
mBottomGlow.setSize(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
getMeasuredHeight() - getPaddingTop() - getPaddingBottom());
}
mBottomGlow.onPull(overscrollY / (float) getHeight());
}
if (overscrollX != 0 || overscrollY != 0) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
private void releaseGlows() {
boolean needsInvalidate = false;
if (mLeftGlow != null) needsInvalidate = mLeftGlow.onRelease();
if (mTopGlow != null) needsInvalidate |= mTopGlow.onRelease();
if (mRightGlow != null) needsInvalidate |= mRightGlow.onRelease();
if (mBottomGlow != null) needsInvalidate |= mBottomGlow.onRelease();
if (needsInvalidate) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
void absorbGlows(int velocityX, int velocityY) {
if (velocityX < 0) {
if (mLeftGlow == null) {
mLeftGlow = new EdgeEffectCompat(getContext());
mLeftGlow.setSize(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(),
getMeasuredWidth() - getPaddingLeft() - getPaddingRight());
}
mLeftGlow.onAbsorb(-velocityX);
} else if (velocityX > 0) {
if (mRightGlow == null) {
mRightGlow = new EdgeEffectCompat(getContext());
mRightGlow.setSize(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(),
getMeasuredWidth() - getPaddingLeft() - getPaddingRight());
}
mRightGlow.onAbsorb(velocityX);
}
if (velocityY < 0) {
if (mTopGlow == null) {
mTopGlow = new EdgeEffectCompat(getContext());
mTopGlow.setSize(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
getMeasuredHeight() - getPaddingTop() - getPaddingBottom());
}
mTopGlow.onAbsorb(-velocityY);
} else if (velocityY > 0) {
if (mBottomGlow == null) {
mBottomGlow = new EdgeEffectCompat(getContext());
mBottomGlow.setSize(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
getMeasuredHeight() - getPaddingTop() - getPaddingBottom());
}
mBottomGlow.onAbsorb(velocityY);
}
if (velocityX != 0 || velocityY != 0) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
// Focus handling
@Override
public View focusSearch(View focused, int direction) {
View result = mLayout.onInterceptFocusSearch(focused, direction);
if (result != null) {
return result;
}
final FocusFinder ff = FocusFinder.getInstance();
result = ff.findNextFocus(this, focused, direction);
if (result == null && mAdapter != null) {
eatRequestLayout();
result = mLayout.onFocusSearchFailed(focused, direction, mRecycler, mState);
resumeRequestLayout(false);
}
return result != null ? result : super.focusSearch(focused, direction);
}
@Override
public void requestChildFocus(View child, View focused) {
if (!mLayout.onRequestChildFocus(this, child, focused)) {
mTempRect.set(0, 0, focused.getWidth(), focused.getHeight());
offsetDescendantRectToMyCoords(focused, mTempRect);
offsetRectIntoDescendantCoords(child, mTempRect);
requestChildRectangleOnScreen(child, mTempRect, !mFirstLayoutComplete);
}
super.requestChildFocus(child, focused);
}
@Override
public boolean requestChildRectangleOnScreen(View child, Rect rect, boolean immediate) {
return mLayout.requestChildRectangleOnScreen(this, child, rect, immediate);
}
@Override
public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
if (!mLayout.onAddFocusables(this, views, direction, focusableMode)) {
super.addFocusables(views, direction, focusableMode);
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mIsAttached = true;
mFirstLayoutComplete = false;
if (mLayout != null) {
mLayout.onAttachedToWindow(this);
}
mPostedAnimatorRunner = false;
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mFirstLayoutComplete = false;
stopScroll();
// TODO Mark what our target position was if relevant, then we can jump there
// on reattach.
mIsAttached = false;
if (mLayout != null) {
mLayout.onDetachedFromWindow(this);
}
removeCallbacks(mItemAnimatorRunner);
}
/**
* Add an {@link OnItemTouchListener} to intercept touch events before they are dispatched
* to child views or this view's standard scrolling behavior.
*
* <p>Client code may use listeners to implement item manipulation behavior. Once a listener
* returns true from
* {@link OnItemTouchListener#onInterceptTouchEvent(RecyclerView, MotionEvent)} its
* {@link OnItemTouchListener#onTouchEvent(RecyclerView, MotionEvent)} method will be called
* for each incoming MotionEvent until the end of the gesture.</p>
*
* @param listener Listener to add
*/
public void addOnItemTouchListener(OnItemTouchListener listener) {
mOnItemTouchListeners.add(listener);
}
/**
* Remove an {@link OnItemTouchListener}. It will no longer be able to intercept touch events.
*
* @param listener Listener to remove
*/
public void removeOnItemTouchListener(OnItemTouchListener listener) {
mOnItemTouchListeners.remove(listener);
if (mActiveOnItemTouchListener == listener) {
mActiveOnItemTouchListener = null;
}
}
private boolean dispatchOnItemTouchIntercept(MotionEvent e) {
final int action = e.getAction();
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_DOWN) {
mActiveOnItemTouchListener = null;
}
final int listenerCount = mOnItemTouchListeners.size();
for (int i = 0; i < listenerCount; i++) {
final OnItemTouchListener listener = mOnItemTouchListeners.get(i);
if (listener.onInterceptTouchEvent(this, e) && action != MotionEvent.ACTION_CANCEL) {
mActiveOnItemTouchListener = listener;
return true;
}
}
return false;
}
private boolean dispatchOnItemTouch(MotionEvent e) {
final int action = e.getAction();
if (mActiveOnItemTouchListener != null) {
if (action == MotionEvent.ACTION_DOWN) {
// Stale state from a previous gesture, we're starting a new one. Clear it.
mActiveOnItemTouchListener = null;
} else {
mActiveOnItemTouchListener.onTouchEvent(this, e);
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
// Clean up for the next gesture.
mActiveOnItemTouchListener = null;
}
return true;
}
}
// Listeners will have already received the ACTION_DOWN via dispatchOnItemTouchIntercept
// as called from onInterceptTouchEvent; skip it.
if (action != MotionEvent.ACTION_DOWN) {
final int listenerCount = mOnItemTouchListeners.size();
for (int i = 0; i < listenerCount; i++) {
final OnItemTouchListener listener = mOnItemTouchListeners.get(i);
if (listener.onInterceptTouchEvent(this, e)) {
mActiveOnItemTouchListener = listener;
return true;
}
}
}
return false;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
if (dispatchOnItemTouchIntercept(e)) {
cancelTouch();
return true;
}
final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
final boolean canScrollVertically = mLayout.canScrollVertically();
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(e);
final int action = MotionEventCompat.getActionMasked(e);
final int actionIndex = MotionEventCompat.getActionIndex(e);
switch (action) {
case MotionEvent.ACTION_DOWN:
mScrollPointerId = MotionEventCompat.getPointerId(e, 0);
mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
if (mScrollState == SCROLL_STATE_SETTLING) {
getParent().requestDisallowInterceptTouchEvent(true);
setScrollState(SCROLL_STATE_DRAGGING);
}
break;
case MotionEventCompat.ACTION_POINTER_DOWN:
mScrollPointerId = MotionEventCompat.getPointerId(e, actionIndex);
mInitialTouchX = mLastTouchX = (int) (MotionEventCompat.getX(e, actionIndex) + 0.5f);
mInitialTouchY = mLastTouchY = (int) (MotionEventCompat.getY(e, actionIndex) + 0.5f);
break;
case MotionEvent.ACTION_MOVE: {
final int index = MotionEventCompat.findPointerIndex(e, mScrollPointerId);
if (index < 0) {
Log.e(TAG, "Error processing scroll; pointer index for id " +
mScrollPointerId + " not found. Did any MotionEvents get skipped?");
return false;
}
final int x = (int) (MotionEventCompat.getX(e, index) + 0.5f);
final int y = (int) (MotionEventCompat.getY(e, index) + 0.5f);
if (mScrollState != SCROLL_STATE_DRAGGING) {
final int dx = x - mInitialTouchX;
final int dy = y - mInitialTouchY;
boolean startScroll = false;
if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
mLastTouchX = mInitialTouchX + mTouchSlop * (dx < 0 ? -1 : 1);
startScroll = true;
}
if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
mLastTouchY = mInitialTouchY + mTouchSlop * (dy < 0 ? -1 : 1);
startScroll = true;
}
if (startScroll) {
getParent().requestDisallowInterceptTouchEvent(true);
setScrollState(SCROLL_STATE_DRAGGING);
}
}
} break;
case MotionEventCompat.ACTION_POINTER_UP: {
onPointerUp(e);
} break;
case MotionEvent.ACTION_UP: {
mVelocityTracker.clear();
} break;
case MotionEvent.ACTION_CANCEL: {
cancelTouch();
}
}
return mScrollState == SCROLL_STATE_DRAGGING;
}
@Override
public boolean onTouchEvent(MotionEvent e) {
if (dispatchOnItemTouch(e)) {
cancelTouch();
return true;
}
final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
final boolean canScrollVertically = mLayout.canScrollVertically();
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(e);
final int action = MotionEventCompat.getActionMasked(e);
final int actionIndex = MotionEventCompat.getActionIndex(e);
switch (action) {
case MotionEvent.ACTION_DOWN: {
mScrollPointerId = MotionEventCompat.getPointerId(e, 0);
mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
} break;
case MotionEventCompat.ACTION_POINTER_DOWN: {
mScrollPointerId = MotionEventCompat.getPointerId(e, actionIndex);
mInitialTouchX = mLastTouchX = (int) (MotionEventCompat.getX(e, actionIndex) + 0.5f);
mInitialTouchY = mLastTouchY = (int) (MotionEventCompat.getY(e, actionIndex) + 0.5f);
} break;
case MotionEvent.ACTION_MOVE: {
final int index = MotionEventCompat.findPointerIndex(e, mScrollPointerId);
if (index < 0) {
Log.e(TAG, "Error processing scroll; pointer index for id " +
mScrollPointerId + " not found. Did any MotionEvents get skipped?");
return false;
}
final int x = (int) (MotionEventCompat.getX(e, index) + 0.5f);
final int y = (int) (MotionEventCompat.getY(e, index) + 0.5f);
if (mScrollState != SCROLL_STATE_DRAGGING) {
final int dx = x - mInitialTouchX;
final int dy = y - mInitialTouchY;
boolean startScroll = false;
if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
mLastTouchX = mInitialTouchX + mTouchSlop * (dx < 0 ? -1 : 1);
startScroll = true;
}
if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
mLastTouchY = mInitialTouchY + mTouchSlop * (dy < 0 ? -1 : 1);
startScroll = true;
}
if (startScroll) {
getParent().requestDisallowInterceptTouchEvent(true);
setScrollState(SCROLL_STATE_DRAGGING);
}
}
if (mScrollState == SCROLL_STATE_DRAGGING) {
final int dx = x - mLastTouchX;
final int dy = y - mLastTouchY;
scrollByInternal(canScrollHorizontally ? -dx : 0,
canScrollVertically ? -dy : 0);
}
mLastTouchX = x;
mLastTouchY = y;
} break;
case MotionEventCompat.ACTION_POINTER_UP: {
onPointerUp(e);
} break;
case MotionEvent.ACTION_UP: {
mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
final float xvel = canScrollHorizontally ?
-VelocityTrackerCompat.getXVelocity(mVelocityTracker, mScrollPointerId) : 0;
final float yvel = canScrollVertically ?
-VelocityTrackerCompat.getYVelocity(mVelocityTracker, mScrollPointerId) : 0;
if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
setScrollState(SCROLL_STATE_IDLE);
}
mVelocityTracker.clear();
releaseGlows();
} break;
case MotionEvent.ACTION_CANCEL: {
cancelTouch();
} break;
}
return true;
}
private void cancelTouch() {
mVelocityTracker.clear();
releaseGlows();
setScrollState(SCROLL_STATE_IDLE);
}
private void onPointerUp(MotionEvent e) {
final int actionIndex = MotionEventCompat.getActionIndex(e);
if (MotionEventCompat.getPointerId(e, actionIndex) == mScrollPointerId) {
// Pick a new pointer to pick up the slack.
final int newIndex = actionIndex == 0 ? 1 : 0;
mScrollPointerId = MotionEventCompat.getPointerId(e, newIndex);
mInitialTouchX = mLastTouchX = (int) (MotionEventCompat.getX(e, newIndex) + 0.5f);
mInitialTouchY = mLastTouchY = (int) (MotionEventCompat.getY(e, newIndex) + 0.5f);
}
}
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
if (mAdapterUpdateDuringMeasure) {
eatRequestLayout();
updateChildViews();
mAdapterUpdateDuringMeasure = false;
resumeRequestLayout(false);
}
if (mAdapter != null) {
mState.mItemCount = mAdapter.getItemCount();
}
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
final int widthSize = getMeasuredWidth();
final int heightSize = getMeasuredHeight();
if (mLeftGlow != null) mLeftGlow.setSize(heightSize, widthSize);
if (mTopGlow != null) mTopGlow.setSize(widthSize, heightSize);
if (mRightGlow != null) mRightGlow.setSize(heightSize, widthSize);
if (mBottomGlow != null) mBottomGlow.setSize(widthSize, heightSize);
}
/**
* Sets the {@link ItemAnimator} that will handle animations involving changes
* to the items in this RecyclerView. By default, RecyclerView instantiates and
* uses an instance of {@link DefaultItemAnimator}. Whether item animations are
* enabled for the RecyclerView depends on the ItemAnimator and whether
* the LayoutManager {@link LayoutManager#supportsPredictiveItemAnimations()
* supports item animations}.
*
* @param animator The ItemAnimator being set. If null, no animations will occur
* when changes occur to the items in this RecyclerView.
*/
public void setItemAnimator(ItemAnimator animator) {
if (mItemAnimator != null) {
mItemAnimator.setListener(null);
}
mItemAnimator = animator;
if (mItemAnimator != null) {
mItemAnimator.setListener(mItemAnimatorListener);
}
}
/**
* Gets the current ItemAnimator for this RecyclerView. A null return value
* indicates that there is no animator and that item changes will happen without
* any animations. By default, RecyclerView instantiates and
* uses an instance of {@link DefaultItemAnimator}.
*
* @return ItemAnimator The current ItemAnimator. If null, no animations will occur
* when changes occur to the items in this RecyclerView.
*/
public ItemAnimator getItemAnimator() {
return mItemAnimator;
}
/**
* Post a runnable to the next frame to run pending item animations. Only the first such
* request will be posted, governed by the mPostedAnimatorRunner flag.
*/
private void postAnimationRunner() {
if (!mPostedAnimatorRunner && mIsAttached) {
ViewCompat.postOnAnimation(this, mItemAnimatorRunner);
mPostedAnimatorRunner = true;
}
}
private boolean predictiveItemAnimationsEnabled() {
return (mItemAnimator != null && mLayout.supportsPredictiveItemAnimations());
}
/**
* Wrapper around layoutChildren() that handles animating changes caused by layout.
* Animations work on the assumption that there are five different kinds of items
* in play:
* PERSISTENT: items are visible before and after layout
* REMOVED: items were visible before layout and were removed by the app
* ADDED: items did not exist before layout and were added by the app
* DISAPPEARING: items exist in the data set before/after, but changed from
* visible to non-visible in the process of layout (they were moved off
* screen as a side-effect of other changes)
* APPEARING: items exist in the data set before/after, but changed from
* non-visible to visible in the process of layout (they were moved on
* screen as a side-effect of other changes)
* The overall approach figures out what items exist before/after layout and
* infers one of the five above states for each of the items. Then the animations
* are set up accordingly:
* PERSISTENT views are moved ({@link ItemAnimator#animateMove(ViewHolder, int, int, int, int)})
* REMOVED views are removed ({@link ItemAnimator#animateRemove(ViewHolder)})
* ADDED views are added ({@link ItemAnimator#animateAdd(ViewHolder)})
* DISAPPEARING views are moved off screen
* APPEARING views are moved on screen
*/
void dispatchLayout() {
if (mAdapter == null) {
Log.e(TAG, "No adapter attached; skipping layout");
return;
}
eatRequestLayout();
// simple animations are a subset of advanced animations (which will cause a
// prelayout step)
boolean animateChangesSimple = mItemAnimator != null && mItemsAddedOrRemoved
&& !mItemsChanged;
final boolean animateChangesAdvanced = ENABLE_PREDICTIVE_ANIMATIONS &&
animateChangesSimple && predictiveItemAnimationsEnabled();
mItemsAddedOrRemoved = mItemsChanged = false;
ArrayMap<View, Rect> appearingViewInitialBounds = null;
mState.mInPreLayout = animateChangesAdvanced;
mState.mItemCount = mAdapter.getItemCount();
if (animateChangesSimple) {
// Step 0: Find out where all non-removed items are, pre-layout
mState.mPreLayoutHolderMap.clear();
mState.mPostLayoutHolderMap.clear();
int count = getChildCount();
for (int i = 0; i < count; ++i) {
final ViewHolder holder = getChildViewHolderInt(getChildAt(i));
final View view = holder.itemView;
mState.mPreLayoutHolderMap.put(holder, new ItemHolderInfo(holder,
view.getLeft(), view.getTop(), view.getRight(), view.getBottom(),
holder.mPosition));
}
}
if (animateChangesAdvanced) {
// Step 1: run prelayout: This will use the old positions of items. The layout manager
// is expected to layout everything, even removed items (though not to add removed
// items back to the container). This gives the pre-layout position of APPEARING views
// which come into existence as part of the real layout.
mInPreLayout = true;
final boolean didStructureChange = mState.mStructureChanged;
mState.mStructureChanged = false;
// temporarily disable flag because we are asking for previous layout
mLayout.onLayoutChildren(mRecycler, mState);
mState.mStructureChanged = didStructureChange;
mInPreLayout = false;
appearingViewInitialBounds = new ArrayMap<View, Rect>();
for (int i = 0; i < getChildCount(); ++i) {
boolean found = false;
View child = getChildAt(i);
for (int j = 0; j < mState.mPreLayoutHolderMap.size(); ++j) {
ViewHolder holder = mState.mPreLayoutHolderMap.keyAt(j);
if (holder.itemView == child) {
found = true;
continue;
}
}
if (!found) {
appearingViewInitialBounds.put(child, new Rect(child.getLeft(), child.getTop(),
child.getRight(), child.getBottom()));
}
}
}
clearOldPositions();
dispatchLayoutUpdates();
mState.mItemCount = mAdapter.getItemCount();
// Step 2: Run layout
mState.mInPreLayout = false;
mLayout.onLayoutChildren(mRecycler, mState);
mState.mStructureChanged = false;
mPendingSavedState = null;
// onLayoutChildren may have caused client code to disable item animations; re-check
animateChangesSimple = animateChangesSimple && mItemAnimator != null;
if (animateChangesSimple) {
// Step 3: Find out where things are now, post-layout
int count = getChildCount();
for (int i = 0; i < count; ++i) {
ViewHolder holder = getChildViewHolderInt(getChildAt(i));
final View view = holder.itemView;
mState.mPostLayoutHolderMap.put(holder, new ItemHolderInfo(holder,
view.getLeft(), view.getTop(), view.getRight(), view.getBottom(),
holder.mPosition));
}
// Step 4: Animate DISAPPEARING and REMOVED items
int preLayoutCount = mState.mPreLayoutHolderMap.size();
for (int i = preLayoutCount - 1; i >= 0; i--) {
ViewHolder itemHolder = mState.mPreLayoutHolderMap.keyAt(i);
if (!mState.mPostLayoutHolderMap.containsKey(itemHolder)) {
ItemHolderInfo disappearingItem = mState.mPreLayoutHolderMap.valueAt(i);
mState.mPreLayoutHolderMap.removeAt(i);
View disappearingItemView = disappearingItem.holder.itemView;
removeDetachedView(disappearingItemView, false);
mRecycler.unscrapView(disappearingItem.holder);
animateDisappearance(disappearingItem);
}
}
// Step 5: Animate APPEARING and ADDED items
int postLayoutCount = mState.mPostLayoutHolderMap.size();
if (postLayoutCount > 0) {
for (int i = postLayoutCount - 1; i >= 0; i--) {
ViewHolder itemHolder = mState.mPostLayoutHolderMap.keyAt(i);
ItemHolderInfo info = mState.mPostLayoutHolderMap.valueAt(i);
if ((mState.mPreLayoutHolderMap.isEmpty() ||
!mState.mPreLayoutHolderMap.containsKey(itemHolder))) {
mState.mPostLayoutHolderMap.removeAt(i);
Rect initialBounds = (appearingViewInitialBounds != null) ?
appearingViewInitialBounds.get(itemHolder.itemView) : null;
animateAppearance(itemHolder, initialBounds,
info.left, info.top);
}
}
}
// Step 6: Animate PERSISTENT items
count = mState.mPostLayoutHolderMap.size();
for (int i = 0; i < count; ++i) {
ViewHolder postHolder = mState.mPostLayoutHolderMap.keyAt(i);
ItemHolderInfo postInfo = mState.mPostLayoutHolderMap.valueAt(i);
ItemHolderInfo preInfo = mState.mPreLayoutHolderMap.get(postHolder);
if (preInfo != null && postInfo != null) {
if (preInfo.left != postInfo.left || preInfo.top != postInfo.top) {
postHolder.setIsRecyclable(false);
if (DEBUG) {
Log.d(TAG, "PERSISTENT: " + postHolder +
" with view " + postHolder.itemView);
}
if (mItemAnimator.animateMove(postHolder,
preInfo.left, preInfo.top, postInfo.left, postInfo.top)) {
postAnimationRunner();
}
}
}
}
}
resumeRequestLayout(false);
mLayout.removeAndRecycleScrapInt(mRecycler, !animateChangesAdvanced);
mState.mPreviousLayoutItemCount = mState.mItemCount;
mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;
}
private void animateAppearance(ViewHolder itemHolder, Rect beforeBounds, int afterLeft,
int afterTop) {
View newItemView = itemHolder.itemView;
if (beforeBounds != null &&
(beforeBounds.left != afterLeft || beforeBounds.top != afterTop)) {
// slide items in if before/after locations differ
itemHolder.setIsRecyclable(false);
if (DEBUG) {
Log.d(TAG, "APPEARING: " + itemHolder + " with view " + newItemView);
}
if (mItemAnimator.animateMove(itemHolder,
beforeBounds.left, beforeBounds.top,
afterLeft, afterTop)) {
postAnimationRunner();
}
} else {
if (DEBUG) {
Log.d(TAG, "ADDED: " + itemHolder + " with view " + newItemView);
}
itemHolder.setIsRecyclable(false);
if (mItemAnimator.animateAdd(itemHolder)) {
postAnimationRunner();
}
}
}
private void animateDisappearance(ItemHolderInfo disappearingItem) {
View disappearingItemView = disappearingItem.holder.itemView;
addAnimatingView(disappearingItemView);
int oldLeft = disappearingItem.left;
int oldTop = disappearingItem.top;
int newLeft = disappearingItemView.getLeft();
int newTop = disappearingItemView.getTop();
if (oldLeft != newLeft || oldTop != newTop) {
disappearingItem.holder.setIsRecyclable(false);
disappearingItemView.layout(newLeft, newTop,
newLeft + disappearingItemView.getWidth(),
newTop + disappearingItemView.getHeight());
if (DEBUG) {
Log.d(TAG, "DISAPPEARING: " + disappearingItem.holder +
" with view " + disappearingItemView);
}
if (mItemAnimator.animateMove(disappearingItem.holder, oldLeft, oldTop,
newLeft, newTop)) {
postAnimationRunner();
}
} else {
if (DEBUG) {
Log.d(TAG, "REMOVED: " + disappearingItem.holder +
" with view " + disappearingItemView);
}
disappearingItem.holder.setIsRecyclable(false);
if (mItemAnimator.animateRemove(disappearingItem.holder)) {
postAnimationRunner();
}
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
eatRequestLayout();
dispatchLayout();
resumeRequestLayout(false);
mFirstLayoutComplete = true;
}
@Override
public void requestLayout() {
if (!mEatRequestLayout) {
super.requestLayout();
} else {
mLayoutRequestEaten = true;
}
}
void markItemDecorInsetsDirty() {
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
((LayoutParams) child.getLayoutParams()).mInsetsDirty = true;
}
}
@Override
public void draw(Canvas c) {
super.draw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDrawOver(c, this);
}
boolean needsInvalidate = false;
if (mLeftGlow != null && !mLeftGlow.isFinished()) {
final int restore = c.save();
c.rotate(270);
c.translate(-getHeight() + getPaddingTop(), 0);
needsInvalidate = mLeftGlow != null && mLeftGlow.draw(c);
c.restoreToCount(restore);
}
if (mTopGlow != null && !mTopGlow.isFinished()) {
c.translate(getPaddingLeft(), getPaddingTop());
needsInvalidate |= mTopGlow != null && mTopGlow.draw(c);
}
if (mRightGlow != null && !mRightGlow.isFinished()) {
final int restore = c.save();
final int width = getWidth();
c.rotate(90);
c.translate(-getPaddingTop(), -width);
needsInvalidate |= mRightGlow != null && mRightGlow.draw(c);
c.restoreToCount(restore);
}
if (mBottomGlow != null && !mBottomGlow.isFinished()) {
final int restore = c.save();
c.rotate(180);
c.translate(-getWidth() + getPaddingLeft(), -getHeight() + getPaddingTop());
needsInvalidate |= mBottomGlow != null && mBottomGlow.draw(c);
c.restoreToCount(restore);
}
if (needsInvalidate) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
@Override
public void onDraw(Canvas c) {
super.onDraw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDraw(c, this);
}
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams && mLayout.checkLayoutParams((LayoutParams) p);
}
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
if (mLayout == null) {
throw new IllegalStateException("RecyclerView has no LayoutManager");
}
return mLayout.generateDefaultLayoutParams();
}
@Override
public ViewGroup.LayoutParams generateLayoutParams(At