解决RecyclerView无法onItemClick问题的两种方法
内容摘要
对于RecyclerView的使用,大家可以查看将替代ListView的RecyclerView 的使用详解(一),单单从代码结构来说RecyclerView确实比ListView优化了很多,也简化了我们编写代码量,但是有
文章正文
对于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(AttributeSet attrs) { if (mLayout == null) { throw new IllegalStateException("RecyclerView has no LayoutManager"); } return mLayout.generateLayoutParams(getContext(), attrs); } @Override protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { if (mLayout == null) { throw new IllegalStateException("RecyclerView has no LayoutManager"); } return mLayout.generateLayoutParams(p); } private int findPositionOffset(int position) { int offset = 0; int count = mPendingLayoutUpdates.size(); for (int i = 0; i < count; ++i) { UpdateOp op = mPendingLayoutUpdates.get(i); if (op.positionStart <= position) { if (op.cmd == UpdateOp.REMOVE) { offset -= op.itemCount; } else if (op.cmd == UpdateOp.ADD) { off
代码注释