/* * Copyright 2018 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 androidx.recyclerview.widget; import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX; import static androidx.core.view.ViewCompat.TYPE_NON_TOUCH; import static androidx.core.view.ViewCompat.TYPE_TOUCH; import android.animation.LayoutTransition; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.database.Observable; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.graphics.drawable.StateListDrawable; import android.os.Build; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.os.SystemClock; import android.util.AttributeSet; import android.util.Log; import android.util.SparseArray; import android.view.Display; import android.view.FocusFinder; import android.view.InputDevice; 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.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.animation.Interpolator; import android.widget.EdgeEffect; import android.widget.LinearLayout; import android.widget.OverScroller; import org.telegram.messenger.AndroidUtilities; import androidx.annotation.CallSuper; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.Px; import androidx.annotation.RestrictTo; import androidx.annotation.VisibleForTesting; import androidx.core.os.TraceCompat; import androidx.core.util.Preconditions; import androidx.core.view.AccessibilityDelegateCompat; import androidx.core.view.InputDeviceCompat; import androidx.core.view.MotionEventCompat; import androidx.core.view.NestedScrollingChild2; import androidx.core.view.NestedScrollingChild3; import androidx.core.view.NestedScrollingChildHelper; import androidx.core.view.ScrollingView; import androidx.core.view.ViewCompat; import androidx.core.view.ViewConfigurationCompat; import androidx.core.view.accessibility.AccessibilityEventCompat; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import androidx.core.widget.EdgeEffectCompat; import androidx.customview.view.AbsSavedState; import androidx.recyclerview.widget.RecyclerView.ItemAnimator.ItemHolderInfo; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; 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. * *
* RecyclerView introduces an additional level of abstraction between the {@link Adapter} and * {@link LayoutManager} to be able to detect data set changes in batches during a layout * calculation. This saves LayoutManager from tracking adapter changes to calculate animations. * It also helps with performance because all view bindings happen at the same time and unnecessary * bindings are avoided. *
* For this reason, there are two types of position
related methods in RecyclerView:
*
* These two positions are the same except the time between dispatching adapter.notify*
*
events and calculating the updated layout.
*
* Methods that return or receive *LayoutPosition*
use position as of the latest
* layout calculation (e.g. {@link ViewHolder#getLayoutPosition()},
* {@link #findViewHolderForLayoutPosition(int)}). These positions include all changes until the
* last layout calculation. You can rely on these positions to be consistent with what user is
* currently seeing on the screen. For example, if you have a list of items on the screen and user
* asks for the 5th element, you should use these methods as they'll match what user
* is seeing.
*
* The other set of position related methods are in the form of
* *AdapterPosition*
. (e.g. {@link ViewHolder#getAdapterPosition()},
* {@link #findViewHolderForAdapterPosition(int)}) You should use these methods when you need to
* work with up-to-date adapter positions even if they may not have been reflected to layout yet.
* For example, if you want to access the item in the adapter on a ViewHolder click, you should use
* {@link ViewHolder#getAdapterPosition()}. Beware that these methods may not be able to calculate
* adapter positions if {@link Adapter#notifyDataSetChanged()} has been called and new layout has
* not yet been calculated. For this reasons, you should carefully handle {@link #NO_POSITION} or
* null
results from these methods.
*
* When writing a {@link LayoutManager} you almost always want to use layout positions whereas when * writing an {@link Adapter}, you probably want to use adapter positions. *
*
*
* The best part of this approach is that it extends to any arbitrary changes - item updates, * moves, addition and removal can all be computed and handled the same way. Though you do have * to keep two copies of the list in memory while diffing, and must avoid mutating them, it's * possible to share unmodified elements between list versions. *
* There are three primary ways to do this for RecyclerView. We recommend you start with * {@link ListAdapter}, the higher-level API that builds in {@link List} diffing on a background * thread, with minimal code. {@link AsyncListDiffer} also provides this behavior, but without * defining an Adapter to subclass. If you want more control, {@link DiffUtil} is the lower-level * API you can use to compute the diffs yourself. Each approach allows you to specify how diffs * should be computed based on item data. *
*
*
* It is a bad practice for a developer to update the data in a scroll callback since it is
* potentially called during a layout.
*/
private int mDispatchScrollCounter = 0;
@NonNull
private EdgeEffectFactory mEdgeEffectFactory = new EdgeEffectFactory();
private EdgeEffect 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;
static final long FOREVER_NS = Long.MAX_VALUE;
// 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 int mTouchSlop;
private OnFlingListener mOnFlingListener;
private final int mMinFlingVelocity;
private final int mMaxFlingVelocity;
// This value is used when handling rotary encoder generic motion events.
private float mScaledHorizontalScrollFactor = Float.MIN_VALUE;
private float mScaledVerticalScrollFactor = Float.MIN_VALUE;
private boolean mPreserveFocusAfterLayout = true;
final ViewFlinger mViewFlinger = new ViewFlinger();
GapWorker mGapWorker;
GapWorker.LayoutPrefetchRegistryImpl mPrefetchRegistry =
ALLOW_THREAD_GAP_WORK ? new GapWorker.LayoutPrefetchRegistryImpl() : null;
final State mState = new State();
private OnScrollListener mScrollListener;
private List
* This is done because autofill's means of uniquely identifying views doesn't work out of the
* box with View recycling.
*/
@SuppressLint("InlinedApi")
private void initAutofill() {
if (ViewCompat.getImportantForAutofill(this) == View.IMPORTANT_FOR_AUTOFILL_AUTO) {
ViewCompat.setImportantForAutofill(this,
View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS);
}
}
/**
* Returns the accessibility delegate compatibility implementation used by the RecyclerView.
* @return An instance of AccessibilityDelegateCompat used by RecyclerView
*/
@Nullable
public RecyclerViewAccessibilityDelegate getCompatAccessibilityDelegate() {
return mAccessibilityDelegate;
}
/**
* Sets the accessibility delegate compatibility implementation used by RecyclerView.
* @param accessibilityDelegate The accessibility delegate to be used by RecyclerView.
*/
public void setAccessibilityDelegateCompat(
@Nullable RecyclerViewAccessibilityDelegate accessibilityDelegate) {
mAccessibilityDelegate = accessibilityDelegate;
ViewCompat.setAccessibilityDelegate(this, mAccessibilityDelegate);
}
@Override
public CharSequence getAccessibilityClassName() {
return "androidx.recyclerview.widget.RecyclerView";
}
/**
* Instantiate and set a LayoutManager, if specified in the attributes.
*/
private void createLayoutManager(Context context, String className, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
if (className != null) {
className = className.trim();
if (!className.isEmpty()) {
className = getFullClassName(context, className);
try {
ClassLoader classLoader;
if (isInEditMode()) {
// Stupid layoutlib cannot handle simple class loaders.
classLoader = this.getClass().getClassLoader();
} else {
classLoader = context.getClassLoader();
}
Class extends LayoutManager> layoutManagerClass =
Class.forName(className, false, classLoader)
.asSubclass(LayoutManager.class);
Constructor extends LayoutManager> constructor;
Object[] constructorArgs = null;
try {
constructor = layoutManagerClass
.getConstructor(LAYOUT_MANAGER_CONSTRUCTOR_SIGNATURE);
constructorArgs = new Object[]{context, attrs, defStyleAttr, defStyleRes};
} catch (NoSuchMethodException e) {
try {
constructor = layoutManagerClass.getConstructor();
} catch (NoSuchMethodException e1) {
e1.initCause(e);
throw new IllegalStateException(attrs.getPositionDescription()
+ ": Error creating LayoutManager " + className, e1);
}
}
constructor.setAccessible(true);
setLayoutManager(constructor.newInstance(constructorArgs));
} catch (ClassNotFoundException e) {
throw new IllegalStateException(attrs.getPositionDescription()
+ ": Unable to find LayoutManager " + className, e);
} catch (InvocationTargetException e) {
throw new IllegalStateException(attrs.getPositionDescription()
+ ": Could not instantiate the LayoutManager: " + className, e);
} catch (InstantiationException e) {
throw new IllegalStateException(attrs.getPositionDescription()
+ ": Could not instantiate the LayoutManager: " + className, e);
} catch (IllegalAccessException e) {
throw new IllegalStateException(attrs.getPositionDescription()
+ ": Cannot access non-public constructor " + className, e);
} catch (ClassCastException e) {
throw new IllegalStateException(attrs.getPositionDescription()
+ ": Class is not a LayoutManager " + className, e);
}
}
}
}
private String getFullClassName(Context context, String className) {
if (className.charAt(0) == '.') {
return context.getPackageName() + className;
}
if (className.contains(".")) {
return className;
}
return RecyclerView.class.getPackage().getName() + '.' + className;
}
private void initChildrenHelper() {
mChildHelper = new ChildHelper(new ChildHelper.Callback() {
@Override
public int getChildCount() {
return RecyclerView.this.getChildCount();
}
@Override
public void addView(View child, int index) {
if (VERBOSE_TRACING) {
TraceCompat.beginSection("RV addView");
}
RecyclerView.this.addView(child, index);
if (VERBOSE_TRACING) {
TraceCompat.endSection();
}
dispatchChildAttached(child);
}
@Override
public int indexOfChild(View view) {
return RecyclerView.this.indexOfChild(view);
}
@Override
public void removeViewAt(int index) {
final View child = RecyclerView.this.getChildAt(index);
if (child != null) {
dispatchChildDetached(child);
// Clear any android.view.animation.Animation that may prevent the item from
// detaching when being removed. If a child is re-added before the
// lazy detach occurs, it will receive invalid attach/detach sequencing.
child.clearAnimation();
}
if (VERBOSE_TRACING) {
TraceCompat.beginSection("RV removeViewAt");
}
RecyclerView.this.removeViewAt(index);
if (VERBOSE_TRACING) {
TraceCompat.endSection();
}
}
@Override
public View getChildAt(int offset) {
return RecyclerView.this.getChildAt(offset);
}
@Override
public void removeAllViews() {
final int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
dispatchChildDetached(child);
// Clear any android.view.animation.Animation that may prevent the item from
// detaching when being removed. If a child is re-added before the
// lazy detach occurs, it will receive invalid attach/detach sequencing.
child.clearAnimation();
}
RecyclerView.this.removeAllViews();
}
@Override
public ViewHolder getChildViewHolder(View view) {
return getChildViewHolderInt(view);
}
@Override
public void attachViewToParent(View child, int index,
ViewGroup.LayoutParams layoutParams) {
final ViewHolder vh = getChildViewHolderInt(child);
if (vh != null) {
if (!vh.isTmpDetached() && !vh.shouldIgnore()) {
throw new IllegalArgumentException("Called attach on a child which is not"
+ " detached: " + vh + exceptionLabel());
}
if (DEBUG) {
Log.d(TAG, "reAttach " + vh);
}
vh.clearTmpDetachFlag();
}
RecyclerView.this.attachViewToParent(child, index, layoutParams);
}
@Override
public void detachViewFromParent(int offset) {
final View view = getChildAt(offset);
if (view != null) {
final ViewHolder vh = getChildViewHolderInt(view);
if (vh != null) {
if (vh.isTmpDetached() && !vh.shouldIgnore()) {
throw new IllegalArgumentException("called detach on an already"
+ " detached child " + vh + exceptionLabel());
}
if (DEBUG) {
Log.d(TAG, "tmpDetach " + vh);
}
vh.addFlags(ViewHolder.FLAG_TMP_DETACHED);
}
}
RecyclerView.this.detachViewFromParent(offset);
}
@Override
public void onEnteredHiddenState(View child) {
final ViewHolder vh = getChildViewHolderInt(child);
if (vh != null) {
vh.onEnteredHiddenState(RecyclerView.this);
}
}
@Override
public void onLeftHiddenState(View child) {
final ViewHolder vh = getChildViewHolderInt(child);
if (vh != null) {
vh.onLeftHiddenState(RecyclerView.this);
}
}
});
}
void initAdapterManager() {
mAdapterHelper = new AdapterHelper(new AdapterHelper.Callback() {
@Override
public ViewHolder findViewHolder(int position) {
final ViewHolder vh = findViewHolderForPosition(position, true);
if (vh == null) {
return null;
}
// ensure it is not hidden because for adapter helper, the only thing matter is that
// LM thinks view is a child.
if (mChildHelper.isHidden(vh.itemView)) {
if (DEBUG) {
Log.d(TAG, "assuming view holder cannot be find because it is hidden");
}
return null;
}
return vh;
}
@Override
public void offsetPositionsForRemovingInvisible(int start, int count) {
offsetPositionRecordsForRemove(start, count, true);
mItemsAddedOrRemoved = true;
mState.mDeletedInvisibleItemCountSincePreviousLayout += count;
}
@Override
public void offsetPositionsForRemovingLaidOutOrNewView(
int positionStart, int itemCount) {
offsetPositionRecordsForRemove(positionStart, itemCount, false);
mItemsAddedOrRemoved = true;
}
@Override
public void markViewHoldersUpdated(int positionStart, int itemCount, Object payload) {
viewRangeUpdate(positionStart, itemCount, payload);
mItemsChanged = true;
}
@Override
public void onDispatchFirstPass(AdapterHelper.UpdateOp op) {
dispatchUpdate(op);
}
void dispatchUpdate(AdapterHelper.UpdateOp op) {
switch (op.cmd) {
case AdapterHelper.UpdateOp.ADD:
mLayout.onItemsAdded(RecyclerView.this, op.positionStart, op.itemCount);
break;
case AdapterHelper.UpdateOp.REMOVE:
mLayout.onItemsRemoved(RecyclerView.this, op.positionStart, op.itemCount);
break;
case AdapterHelper.UpdateOp.UPDATE:
mLayout.onItemsUpdated(RecyclerView.this, op.positionStart, op.itemCount,
op.payload);
break;
case AdapterHelper.UpdateOp.MOVE:
mLayout.onItemsMoved(RecyclerView.this, op.positionStart, op.itemCount, 1);
break;
}
}
@Override
public void onDispatchSecondPass(AdapterHelper.UpdateOp op) {
dispatchUpdate(op);
}
@Override
public void offsetPositionsForAdd(int positionStart, int itemCount) {
offsetPositionRecordsForInsert(positionStart, itemCount);
mItemsAddedOrRemoved = true;
}
@Override
public void offsetPositionsForMove(int from, int to) {
offsetPositionRecordsForMove(from, to);
// should we create mItemsMoved ?
mItemsAddedOrRemoved = true;
}
});
}
/**
* RecyclerView can perform several optimizations if it can know in advance that RecyclerView's
* size is not affected by the adapter contents. RecyclerView can still change its size based
* on other factors (e.g. its parent's size) but this size calculation cannot depend on the
* size of its children or contents of its adapter (except the number of items in the adapter).
*
* If your use of RecyclerView falls into this category, set this to {@code true}. It will allow
* RecyclerView to avoid invalidating the whole layout when its adapter contents change.
*
* @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;
}
@Override
public void setClipToPadding(boolean clipToPadding) {
if (clipToPadding != mClipToPadding) {
invalidateGlows();
}
mClipToPadding = clipToPadding;
super.setClipToPadding(clipToPadding);
if (mFirstLayoutComplete) {
requestLayout();
}
}
/**
* Returns whether this RecyclerView will clip its children to its padding, and resize (but
* not clip) any EdgeEffect to the padded region, if padding is present.
*
* By default, children are clipped to the padding of their parent
* RecyclerView. This clipping behavior is only enabled if padding is non-zero.
*
* @return true if this RecyclerView clips children to its padding and resizes (but doesn't
* clip) any EdgeEffect to the padded region, false otherwise.
*
* @attr name android:clipToPadding
*/
@Override
public boolean getClipToPadding() {
return mClipToPadding;
}
/**
* Configure the scrolling touch slop for a specific use case.
*
* Set up the RecyclerView's scrolling motion threshold based on common usages.
* Valid arguments are {@link #TOUCH_SLOP_DEFAULT} and {@link #TOUCH_SLOP_PAGING}.
*
* @param slopConstant One of the
* Note that it still calls onAdapterChanged callbacks.
*
* @param adapter The new adapter to set, or null to set no adapter.
* @param removeAndRecycleExistingViews If set to true, RecyclerView will recycle all existing
* Views. If adapters have stable ids and/or you want to
* animate the disappearing views, you may prefer to set
* this to false.
* @see #setAdapter(Adapter)
*/
public void swapAdapter(@Nullable Adapter adapter, boolean removeAndRecycleExistingViews) {
// bail out if layout is frozen
setLayoutFrozen(false);
setAdapterInternal(adapter, true, removeAndRecycleExistingViews);
processDataSetCompletelyChanged(true);
requestLayout();
}
/**
* Set a new adapter to provide child views on demand.
*
* When adapter is changed, all existing views are recycled back to the pool. If the pool has
* only one adapter, it will be cleared.
*
* @param adapter The new adapter to set, or null to set no adapter.
* @see #swapAdapter(Adapter, boolean)
*/
public void setAdapter(@Nullable Adapter adapter) {
// bail out if layout is frozen
setLayoutFrozen(false);
setAdapterInternal(adapter, false, true);
processDataSetCompletelyChanged(false);
requestLayout();
}
/**
* Removes and recycles all views - both those currently attached, and those in the Recycler.
*/
void removeAndRecycleViews() {
// 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);
}
// we should clear it here before adapters are swapped to ensure correct callbacks.
mRecycler.clear();
}
/**
* Replaces the current adapter with the new one and triggers listeners.
* @param adapter The new adapter
* @param compatibleWithPrevious If true, the new adapter is using the same View Holders and
* item types with the current adapter (helps us avoid cache
* invalidation).
* @param removeAndRecycleViews If true, we'll remove and recycle all existing views. If
* compatibleWithPrevious is false, this parameter is ignored.
*/
private void setAdapterInternal(@Nullable Adapter adapter, boolean compatibleWithPrevious,
boolean removeAndRecycleViews) {
if (mAdapter != null) {
mAdapter.unregisterAdapterDataObserver(mObserver);
mAdapter.onDetachedFromRecyclerView(this);
}
if (!compatibleWithPrevious || removeAndRecycleViews) {
removeAndRecycleViews();
}
mAdapterHelper.reset();
final Adapter oldAdapter = mAdapter;
mAdapter = adapter;
if (adapter != null) {
adapter.registerAdapterDataObserver(mObserver);
adapter.onAttachedToRecyclerView(this);
}
if (mLayout != null) {
mLayout.onAdapterChanged(oldAdapter, mAdapter);
}
mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);
mState.mStructureChanged = true;
}
/**
* Retrieves the previously set adapter or null if no adapter is set.
*
* @return The previously set adapter
* @see #setAdapter(Adapter)
*/
@Nullable
public Adapter getAdapter() {
return mAdapter;
}
/**
* Register a listener that will be notified whenever a child view is recycled.
*
* 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. Return the offset of the RecyclerView's text baseline from the its top
* boundary. If the LayoutManager of this RecyclerView does not support baseline alignment,
* this method returns -1. 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. 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. Several default strategies are provided for common uses such as lists and grids.
* If the {@link OnFlingListener} is set then it will receive
* calls to {@link #fling(int,int)} and will be able to intercept them.
*
* @param onFlingListener The {@link OnFlingListener} instance.
*/
public void setOnFlingListener(@Nullable OnFlingListener onFlingListener) {
mOnFlingListener = onFlingListener;
}
/**
* Get the current {@link OnFlingListener} from this {@link RecyclerView}.
*
* @return The {@link OnFlingListener} instance currently set (can be null).
*/
@Nullable
public OnFlingListener getOnFlingListener() {
return mOnFlingListener;
}
@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) {
if (!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
}
mPendingSavedState = (SavedState) state;
super.onRestoreInstanceState(mPendingSavedState.getSuperState());
if (mLayout != null && mPendingSavedState.mLayoutState != null) {
mLayout.onRestoreInstanceState(mPendingSavedState.mLayoutState);
}
}
/**
* Override to prevent freezing of any views created by the adapter.
*/
@Override
protected void dispatchSaveInstanceState(SparseArray 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. 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. 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. The given decoration will no longer impact the measurement and drawing of
* item views.
* See {@link ViewGroup#getChildDrawingOrder(int, int)} for details. Calling this method will
* always call {@link ViewGroup#setChildrenDrawingOrderEnabled(boolean)}. The parameter will be
* true if childDrawingOrderCallback is not null, false otherwise.
*
* Note that child drawing order may be overridden by View's elevation.
*
* @param childDrawingOrderCallback The ChildDrawingOrderCallback to be used by the drawing
* system.
*/
public void setChildDrawingOrderCallback(
@Nullable ChildDrawingOrderCallback childDrawingOrderCallback) {
if (childDrawingOrderCallback == mChildDrawingOrderCallback) {
return;
}
mChildDrawingOrderCallback = childDrawingOrderCallback;
setChildrenDrawingOrderEnabled(mChildDrawingOrderCallback != null);
}
/**
* Set a listener that will be notified of any changes in scroll state or position.
*
* @param listener Listener to set or null to clear
*
* @deprecated Use {@link #addOnScrollListener(OnScrollListener)} and
* {@link #removeOnScrollListener(OnScrollListener)}
*/
@Deprecated
public void setOnScrollListener(@Nullable OnScrollListener listener) {
mScrollListener = listener;
}
/**
* Add a listener that will be notified of any changes in scroll state or position.
*
* Components that add a listener should take care to remove it when finished.
* Other components that take ownership of a view may call {@link #clearOnScrollListeners()}
* to remove all attached listeners.
* To support smooth scrolling, you must override
* {@link LayoutManager#smoothScrollToPosition(RecyclerView, State, int)} and create a
* {@link SmoothScroller}.
*
* {@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) {
if (mLayoutSuppressed) {
return;
}
if (mLayout == null) {
Log.e(TAG, "Cannot smooth scroll without a LayoutManager set. "
+ "Call setLayoutManager with a non-null argument.");
return;
}
mLayout.smoothScrollToPosition(this, mState, position);
}
@Override
public void scrollTo(int x, int y) {
Log.w(TAG, "RecyclerView does not support scrolling to an absolute position. "
+ "Use scrollToPosition instead");
}
@Override
public void scrollBy(int x, int y) {
if (mLayout == null) {
Log.e(TAG, "Cannot scroll without a LayoutManager set. "
+ "Call setLayoutManager with a non-null argument.");
return;
}
if (mLayoutSuppressed) {
return;
}
final boolean canScrollHorizontal = mLayout.canScrollHorizontally();
final boolean canScrollVertical = mLayout.canScrollVertically();
if (canScrollHorizontal || canScrollVertical) {
scrollByInternal(canScrollHorizontal ? x : 0, canScrollVertical ? y : 0, null);
}
}
/**
* Scrolls the RV by 'dx' and 'dy' via calls to
* {@link LayoutManager#scrollHorizontallyBy(int, Recycler, State)} and
* {@link LayoutManager#scrollVerticallyBy(int, Recycler, State)}.
*
* Also sets how much of the scroll was actually consumed in 'consumed' parameter (indexes 0 and
* 1 for the x axis and y axis, respectively).
*
* This method should only be called in the context of an existing scroll operation such that
* any other necessary operations (such as a call to {@link #consumePendingUpdateOperations()})
* is already handled.
*/
void scrollStep(int dx, int dy, @Nullable int[] consumed) {
startInterceptRequestLayout();
onEnterLayoutOrScroll();
TraceCompat.beginSection(TRACE_SCROLL_TAG);
fillRemainingScrollValues(mState);
int consumedX = 0;
int consumedY = 0;
if (dx != 0) {
consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
}
if (dy != 0) {
consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
}
TraceCompat.endSection();
repositionShadowingViews();
onExitLayoutOrScroll();
stopInterceptRequestLayout(false);
if (consumed != null) {
consumed[0] = consumedX;
consumed[1] = consumedY;
}
}
/**
* Helper method reflect data changes to the state.
*
* Adapter changes during a scroll may trigger a crash because scroll assumes no data change
* but data actually changed.
*
* This method consumes all deferred changes to avoid that case.
*/
void consumePendingUpdateOperations() {
if (!mFirstLayoutComplete || mDataSetHasChangedAfterLayout) {
TraceCompat.beginSection(TRACE_ON_DATA_SET_CHANGE_LAYOUT_TAG);
dispatchLayout();
TraceCompat.endSection();
return;
}
if (!mAdapterHelper.hasPendingUpdates()) {
return;
}
// if it is only an item change (no add-remove-notifyDataSetChanged) we can check if any
// of the visible items is affected and if not, just ignore the change.
if (mAdapterHelper.hasAnyUpdateTypes(AdapterHelper.UpdateOp.UPDATE) && !mAdapterHelper
.hasAnyUpdateTypes(AdapterHelper.UpdateOp.ADD | AdapterHelper.UpdateOp.REMOVE
| AdapterHelper.UpdateOp.MOVE)) {
TraceCompat.beginSection(TRACE_HANDLE_ADAPTER_UPDATES_TAG);
startInterceptRequestLayout();
onEnterLayoutOrScroll();
mAdapterHelper.preProcess();
if (!mLayoutWasDefered) {
if (hasUpdatedView()) {
dispatchLayout();
} else {
// no need to layout, clean state
mAdapterHelper.consumePostponedUpdates();
}
}
stopInterceptRequestLayout(true);
onExitLayoutOrScroll();
TraceCompat.endSection();
} else if (mAdapterHelper.hasPendingUpdates()) {
TraceCompat.beginSection(TRACE_ON_DATA_SET_CHANGE_LAYOUT_TAG);
dispatchLayout();
TraceCompat.endSection();
}
}
/**
* @return True if an existing view holder needs to be updated
*/
private boolean hasUpdatedView() {
final int childCount = mChildHelper.getChildCount();
for (int i = 0; i < childCount; i++) {
final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
if (holder == null || holder.shouldIgnore()) {
continue;
}
if (holder.isUpdated()) {
return true;
}
}
return false;
}
/**
* Does not perform bounds checking. Used by internal methods that have already validated input.
*
* It also reports any unused scroll request to the related EdgeEffect.
*
* @param x The amount of horizontal scroll request
* @param y The amount of vertical scroll request
* @param ev The originating MotionEvent, or null if not from a touch event.
*
* @return Whether any scroll was consumed in either direction.
*/
boolean scrollByInternal(int x, int y, MotionEvent ev) {
int unconsumedX = 0;
int unconsumedY = 0;
int consumedX = 0;
int consumedY = 0;
consumePendingUpdateOperations();
if (mAdapter != null) {
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
scrollStep(x, y, mReusableIntPair);
consumedX = mReusableIntPair[0];
consumedY = mReusableIntPair[1];
unconsumedX = x - consumedX;
unconsumedY = y - consumedY;
}
if (!mItemDecorations.isEmpty()) {
invalidate();
}
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
TYPE_TOUCH, mReusableIntPair);
unconsumedX -= mReusableIntPair[0];
unconsumedY -= mReusableIntPair[1];
// Update the last touch co-ords, taking any scroll offset into account
mLastTouchX -= mScrollOffset[0];
mLastTouchY -= mScrollOffset[1];
if (ev != null) {
ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
}
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
if (ev != null && !MotionEventCompat.isFromSource(ev, InputDevice.SOURCE_MOUSE)) {
pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY);
}
considerReleasingGlowsOnScroll(x, y);
}
if (consumedX != 0 || consumedY != 0) {
dispatchOnScrolled(consumedX, consumedY);
}
if (!awakenScrollBars()) {
invalidate();
}
return consumedX != 0 || consumedY != 0;
}
/**
* 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.
* The range is expressed in arbitrary units that must be the same as the units used by
* {@link #computeHorizontalScrollRange()} and {@link #computeHorizontalScrollExtent()}. Default implementation returns 0. If you want to support scroll bars, override
* {@link RecyclerView.LayoutManager#computeHorizontalScrollOffset(RecyclerView.State)} in your
* LayoutManager. 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. The range is expressed in arbitrary units that must be the same as the units used by
* {@link #computeHorizontalScrollRange()} and {@link #computeHorizontalScrollOffset()}. Default implementation returns 0. If you want to support scroll bars, override
* {@link RecyclerView.LayoutManager#computeHorizontalScrollExtent(RecyclerView.State)} in your
* LayoutManager. Compute the horizontal range that the horizontal scrollbar represents. The range is expressed in arbitrary units that must be the same as the units used by
* {@link #computeHorizontalScrollExtent()} and {@link #computeHorizontalScrollOffset()}. Default implementation returns 0. If you want to support scroll bars, override
* {@link RecyclerView.LayoutManager#computeHorizontalScrollRange(RecyclerView.State)} in your
* LayoutManager. 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. The range is expressed in arbitrary units that must be the same as the units used by
* {@link #computeVerticalScrollRange()} and {@link #computeVerticalScrollExtent()}. Default implementation returns 0. If you want to support scroll bars, override
* {@link RecyclerView.LayoutManager#computeVerticalScrollOffset(RecyclerView.State)} in your
* LayoutManager. 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. The range is expressed in arbitrary units that must be the same as the units used by
* {@link #computeVerticalScrollRange()} and {@link #computeVerticalScrollOffset()}. Default implementation returns 0. If you want to support scroll bars, override
* {@link RecyclerView.LayoutManager#computeVerticalScrollExtent(RecyclerView.State)} in your
* LayoutManager. Compute the vertical range that the vertical scrollbar represents. The range is expressed in arbitrary units that must be the same as the units used by
* {@link #computeVerticalScrollExtent()} and {@link #computeVerticalScrollOffset()}. Default implementation returns 0. If you want to support scroll bars, override
* {@link RecyclerView.LayoutManager#computeVerticalScrollRange(RecyclerView.State)} in your
* LayoutManager.
* A call to this method must always be accompanied by a call to
* {@link #stopInterceptRequestLayout(boolean)} that follows the code that may trigger a
* child View to cause a call to {@link RecyclerView#requestLayout()}.
*
* @see #stopInterceptRequestLayout(boolean)
*/
void startInterceptRequestLayout() {
mInterceptRequestLayoutDepth++;
if (mInterceptRequestLayoutDepth == 1 && !mLayoutSuppressed) {
mLayoutWasDefered = false;
}
}
/**
* This method should be called after any code that may trigger a child view to cause a call to
* {@link RecyclerView#requestLayout()}.
*
* A call to this method must always be accompanied by a call to
* {@link #startInterceptRequestLayout()} that precedes the code that may trigger a child
* View to cause a call to {@link RecyclerView#requestLayout()}.
*
* @see #startInterceptRequestLayout()
*/
void stopInterceptRequestLayout(boolean performLayoutChildren) {
if (mInterceptRequestLayoutDepth < 1) {
//noinspection PointlessBooleanExpression
if (DEBUG) {
throw new IllegalStateException("stopInterceptRequestLayout was called more "
+ "times than startInterceptRequestLayout."
+ exceptionLabel());
}
mInterceptRequestLayoutDepth = 1;
}
if (!performLayoutChildren && !mLayoutSuppressed) {
// Reset the layout request eaten counter.
// This is necessary since eatRequest calls can be nested in which case the other
// call will override the inner one.
// for instance:
// eat layout for process adapter updates
// eat layout for dispatchLayout
// a bunch of req layout calls arrive
mLayoutWasDefered = false;
}
if (mInterceptRequestLayoutDepth == 1) {
// when layout is frozen we should delay dispatchLayout()
if (performLayoutChildren && mLayoutWasDefered && !mLayoutSuppressed
&& mLayout != null && mAdapter != null) {
dispatchLayout();
}
if (!mLayoutSuppressed) {
mLayoutWasDefered = false;
}
}
mInterceptRequestLayoutDepth--;
}
/**
* Tells this RecyclerView to suppress all layout and scroll calls until layout
* suppression is disabled with a later call to suppressLayout(false).
* When layout suppression is disabled, a requestLayout() call is sent
* if requestLayout() was attempted while layout was being suppressed.
*
* In addition to the layout suppression {@link #smoothScrollBy(int, int)},
* {@link #scrollBy(int, int)}, {@link #scrollToPosition(int)} and
* {@link #smoothScrollToPosition(int)} are dropped; TouchEvents and GenericMotionEvents are
* dropped; {@link LayoutManager#onFocusSearchFailed(View, int, Recycler, State)} will not be
* called.
*
*
*
* {@link #setAdapter(Adapter)} and {@link #swapAdapter(Adapter, boolean)} will automatically
* stop suppressing.
*
* Note: Running ItemAnimator is not stopped automatically, it's caller's
* responsibility to call ItemAnimator.end().
*
* @param suppress true to suppress layout and scroll, false to re-enable.
*/
public final void suppressLayout(boolean suppress) {
if (suppress != mLayoutSuppressed) {
assertNotInLayoutOrScroll("Do not suppressLayout in layout or scroll");
if (!suppress) {
mLayoutSuppressed = false;
if (mLayoutWasDefered && mLayout != null && mAdapter != null) {
requestLayout();
}
mLayoutWasDefered = false;
} else {
final long now = SystemClock.uptimeMillis();
MotionEvent cancelEvent = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
onTouchEvent(cancelEvent);
mLayoutSuppressed = true;
mIgnoreMotionEventTillDown = true;
stopScroll();
}
}
}
/**
* Returns whether layout and scroll calls on this container are currently being
* suppressed, due to an earlier call to {@link #suppressLayout(boolean)}.
*
* @return true if layout and scroll are currently suppressed, false otherwise.
*/
public final boolean isLayoutSuppressed() {
return mLayoutSuppressed;
}
/**
* Enable or disable layout and scroll. After
*
* {@link #setAdapter(Adapter)} and {@link #swapAdapter(Adapter, boolean)} will automatically
* stop frozen.
*
* Note: Running ItemAnimator is not stopped automatically, it's caller's
* responsibility to call ItemAnimator.end().
*
* @param frozen true to freeze layout and scroll, false to re-enable.
*
* @deprecated Use {@link #suppressLayout(boolean)}.
*/
@Deprecated
public void setLayoutFrozen(boolean frozen) {
suppressLayout(frozen);
}
/**
* @return true if layout and scroll are frozen
*
* @deprecated Use {@link #isLayoutSuppressed()}.
*/
@Deprecated
public boolean isLayoutFrozen() {
return isLayoutSuppressed();
}
/**
* @deprecated Use {@link #setItemAnimator(ItemAnimator)} ()}.
*/
@Deprecated
@Override
public void setLayoutTransition(LayoutTransition transition) {
if (transition == null) {
super.setLayoutTransition(null);
} else {
throw new IllegalArgumentException("Providing a LayoutTransition into RecyclerView is "
+ "not supported. Please use setItemAnimator() instead for animating changes "
+ "to the items in this RecyclerView");
}
}
/**
* 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(@Px int dx, @Px int dy) {
smoothScrollBy(dx, dy, null);
}
/**
* Animate a scroll by the given amount of pixels along either axis.
*
* @param dx Pixels to scroll horizontally
* @param dy Pixels to scroll vertically
* @param interpolator {@link Interpolator} to be used for scrolling. If it is
* {@code null}, RecyclerView is going to use the default interpolator.
*/
public void smoothScrollBy(@Px int dx, @Px int dy, @Nullable Interpolator interpolator) {
if (mLayout == null) {
Log.e(TAG, "Cannot smooth scroll without a LayoutManager set. "
+ "Call setLayoutManager with a non-null argument.");
return;
}
if (mLayoutSuppressed) {
return;
}
if (!mLayout.canScrollHorizontally()) {
dx = 0;
}
if (!mLayout.canScrollVertically()) {
dy = 0;
}
if (dx != 0 || dy != 0) {
mViewFlinger.smoothScrollBy(dx, dy, UNDEFINED_DURATION, interpolator);
}
}
/**
* 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 or
* LayoutManager does not support scrolling in the axis fling is issued.
*
* @see LayoutManager#canScrollVertically()
* @see LayoutManager#canScrollHorizontally()
*/
public boolean fling(int velocityX, int velocityY) {
if (mLayout == null) {
Log.e(TAG, "Cannot fling without a LayoutManager set. "
+ "Call setLayoutManager with a non-null argument.");
return false;
}
if (mLayoutSuppressed) {
return false;
}
final boolean canScrollHorizontal = mLayout.canScrollHorizontally();
final boolean canScrollVertical = mLayout.canScrollVertically();
if (!canScrollHorizontal || Math.abs(velocityX) < mMinFlingVelocity) {
velocityX = 0;
}
if (!canScrollVertical || Math.abs(velocityY) < mMinFlingVelocity) {
velocityY = 0;
}
if (velocityX == 0 && velocityY == 0) {
// If we don't have any velocity, return false
return false;
}
if (!dispatchNestedPreFling(velocityX, velocityY)) {
final boolean canScroll = canScrollHorizontal || canScrollVertical;
dispatchNestedFling(velocityX, velocityY, canScroll);
if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) {
return true;
}
if (canScroll) {
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontal) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertical) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
startNestedScroll(nestedScrollAxis, TYPE_NON_TOUCH);
velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
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() {
setScrollState(SCROLL_STATE_IDLE);
stopScrollersInternal();
}
/**
* Similar to {@link #stopScroll()} but does not set the state.
*/
private void stopScrollersInternal() {
mViewFlinger.stop();
if (mLayout != null) {
mLayout.stopSmoothScroller();
}
}
/**
* Returns the minimum velocity to start a fling.
*
* @return The minimum velocity to start a fling
*/
public int getMinFlingVelocity() {
return mMinFlingVelocity;
}
/**
* Returns the maximum fling velocity used by this RecyclerView.
*
* @return The maximum fling velocity used by this RecyclerView.
*/
public int getMaxFlingVelocity() {
return mMaxFlingVelocity;
}
/**
* Apply a pull to relevant overscroll glow effects
*/
private void pullGlows(float x, float overscrollX, float y, float overscrollY) {
boolean invalidate = false;
if (overscrollX < 0) {
ensureLeftGlow();
EdgeEffectCompat.onPull(mLeftGlow, -overscrollX / getWidth(), 1f - y / getHeight());
invalidate = true;
} else if (overscrollX > 0) {
ensureRightGlow();
EdgeEffectCompat.onPull(mRightGlow, overscrollX / getWidth(), y / getHeight());
invalidate = true;
}
if (overscrollY < 0) {
ensureTopGlow();
EdgeEffectCompat.onPull(mTopGlow, -overscrollY / getHeight(), x / getWidth());
invalidate = true;
} else if (overscrollY > 0) {
ensureBottomGlow();
EdgeEffectCompat.onPull(mBottomGlow, overscrollY / getHeight(), 1f - x / getWidth());
invalidate = true;
}
if (invalidate || overscrollX != 0 || overscrollY != 0) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
private void releaseGlows() {
boolean needsInvalidate = false;
if (mLeftGlow != null) {
mLeftGlow.onRelease();
needsInvalidate = mLeftGlow.isFinished();
}
if (mTopGlow != null) {
mTopGlow.onRelease();
needsInvalidate |= mTopGlow.isFinished();
}
if (mRightGlow != null) {
mRightGlow.onRelease();
needsInvalidate |= mRightGlow.isFinished();
}
if (mBottomGlow != null) {
mBottomGlow.onRelease();
needsInvalidate |= mBottomGlow.isFinished();
}
if (needsInvalidate) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
void considerReleasingGlowsOnScroll(int dx, int dy) {
boolean needsInvalidate = false;
if (mLeftGlow != null && !mLeftGlow.isFinished() && dx > 0) {
mLeftGlow.onRelease();
needsInvalidate = mLeftGlow.isFinished();
}
if (mRightGlow != null && !mRightGlow.isFinished() && dx < 0) {
mRightGlow.onRelease();
needsInvalidate |= mRightGlow.isFinished();
}
if (mTopGlow != null && !mTopGlow.isFinished() && dy > 0) {
mTopGlow.onRelease();
needsInvalidate |= mTopGlow.isFinished();
}
if (mBottomGlow != null && !mBottomGlow.isFinished() && dy < 0) {
mBottomGlow.onRelease();
needsInvalidate |= mBottomGlow.isFinished();
}
if (needsInvalidate) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
void absorbGlows(int velocityX, int velocityY) {
if (velocityX < 0) {
ensureLeftGlow();
if (mLeftGlow.isFinished()) {
mLeftGlow.onAbsorb(-velocityX);
}
} else if (velocityX > 0) {
ensureRightGlow();
if (mRightGlow.isFinished()) {
mRightGlow.onAbsorb(velocityX);
}
}
if (velocityY < 0) {
ensureTopGlow();
if (mTopGlow.isFinished()) {
mTopGlow.onAbsorb(-velocityY);
}
} else if (velocityY > 0) {
ensureBottomGlow();
if (mBottomGlow.isFinished()) {
mBottomGlow.onAbsorb(velocityY);
}
}
if (velocityX != 0 || velocityY != 0) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
void ensureLeftGlow() {
if (mLeftGlow != null) {
return;
}
mLeftGlow = mEdgeEffectFactory.createEdgeEffect(this, EdgeEffectFactory.DIRECTION_LEFT);
if (mClipToPadding) {
mLeftGlow.setSize(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(),
getMeasuredWidth() - getPaddingLeft() - getPaddingRight());
} else {
mLeftGlow.setSize(getMeasuredHeight(), getMeasuredWidth());
}
applyEdgeEffectColor(mLeftGlow);
}
void ensureRightGlow() {
if (mRightGlow != null) {
return;
}
mRightGlow = mEdgeEffectFactory.createEdgeEffect(this, EdgeEffectFactory.DIRECTION_RIGHT);
if (mClipToPadding) {
mRightGlow.setSize(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(),
getMeasuredWidth() - getPaddingLeft() - getPaddingRight());
} else {
mRightGlow.setSize(getMeasuredHeight(), getMeasuredWidth());
}
applyEdgeEffectColor(mRightGlow);
}
void ensureTopGlow() {
if (mTopGlow != null) {
return;
}
mTopGlow = mEdgeEffectFactory.createEdgeEffect(this, EdgeEffectFactory.DIRECTION_TOP);
if (mClipToPadding) {
mTopGlow.setSize(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
getMeasuredHeight() - getPaddingTop() - getPaddingBottom());
} else {
mTopGlow.setSize(getMeasuredWidth(), getMeasuredHeight());
}
applyEdgeEffectColor(mTopGlow);
}
void ensureBottomGlow() {
if (mBottomGlow != null) {
return;
}
mBottomGlow = mEdgeEffectFactory.createEdgeEffect(this, EdgeEffectFactory.DIRECTION_BOTTOM);
if (mClipToPadding) {
mBottomGlow.setSize(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
getMeasuredHeight() - getPaddingTop() - getPaddingBottom());
} else {
mBottomGlow.setSize(getMeasuredWidth(), getMeasuredHeight());
}
applyEdgeEffectColor(mBottomGlow);
}
void invalidateGlows() {
mLeftGlow = mRightGlow = mTopGlow = mBottomGlow = null;
}
/**
* Set a {@link EdgeEffectFactory} for this {@link RecyclerView}.
*
* When a new {@link EdgeEffectFactory} is set, any existing over-scroll effects are cleared
* and new effects are created as needed using
* {@link EdgeEffectFactory#createEdgeEffect(RecyclerView, int)}
*
* @param edgeEffectFactory The {@link EdgeEffectFactory} instance.
*/
public void setEdgeEffectFactory(@NonNull EdgeEffectFactory edgeEffectFactory) {
Preconditions.checkNotNull(edgeEffectFactory);
mEdgeEffectFactory = edgeEffectFactory;
invalidateGlows();
}
/**
* Retrieves the previously set {@link EdgeEffectFactory} or the default factory if nothing
* was set.
*
* @return The previously set {@link EdgeEffectFactory}
* @see #setEdgeEffectFactory(EdgeEffectFactory)
*/
@NonNull
public EdgeEffectFactory getEdgeEffectFactory() {
return mEdgeEffectFactory;
}
/**
* Since RecyclerView is a collection ViewGroup that includes virtual children (items that are
* in the Adapter but not visible in the UI), it employs a more involved focus search strategy
* that differs from other ViewGroups.
*
* It first does a focus search within the RecyclerView. If this search finds a View that is in
* the focus direction with respect to the currently focused View, RecyclerView returns that
* child as the next focus target. When it cannot find such child, it calls
* {@link LayoutManager#onFocusSearchFailed(View, int, Recycler, State)} to layout more Views
* in the focus search direction. If LayoutManager adds a View that matches the
* focus search criteria, it will be returned as the focus search result. Otherwise,
* RecyclerView will call parent to handle the focus search like a regular ViewGroup.
*
* When the direction is {@link View#FOCUS_FORWARD} or {@link View#FOCUS_BACKWARD}, a View that
* is not in the focus direction is still valid focus target which may not be the desired
* behavior if the Adapter has more children in the focus direction. To handle this case,
* RecyclerView converts the focus direction to an absolute direction and makes a preliminary
* focus search in that direction. If there are no Views to gain focus, it will call
* {@link LayoutManager#onFocusSearchFailed(View, int, Recycler, State)} before running a
* focus search with the original (relative) direction. This allows RecyclerView to provide
* better candidates to the focus search while still allowing the view system to take focus from
* the RecyclerView and give it to a more suitable child if such child exists.
*
* @param focused The view that currently has focus
* @param direction One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN},
* {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT}, {@link View#FOCUS_FORWARD},
* {@link View#FOCUS_BACKWARD} or 0 for not applicable.
*
* @return A new View that can be the next focus after the focused View
*/
@Override
public View focusSearch(View focused, int direction) {
View result = mLayout.onInterceptFocusSearch(focused, direction);
if (result != null) {
return result;
}
final boolean canRunFocusFailure = mAdapter != null && mLayout != null
&& !isComputingLayout() && !mLayoutSuppressed;
final FocusFinder ff = FocusFinder.getInstance();
if (canRunFocusFailure
&& (direction == View.FOCUS_FORWARD || direction == View.FOCUS_BACKWARD)) {
// convert direction to absolute direction and see if we have a view there and if not
// tell LayoutManager to add if it can.
boolean needsFocusFailureLayout = false;
if (mLayout.canScrollVertically()) {
final int absDir =
direction == View.FOCUS_FORWARD ? View.FOCUS_DOWN : View.FOCUS_UP;
final View found = ff.findNextFocus(this, focused, absDir);
needsFocusFailureLayout = found == null;
if (FORCE_ABS_FOCUS_SEARCH_DIRECTION) {
// Workaround for broken FOCUS_BACKWARD in API 15 and older devices.
direction = absDir;
}
}
if (!needsFocusFailureLayout && mLayout.canScrollHorizontally()) {
boolean rtl = mLayout.getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL;
final int absDir = (direction == View.FOCUS_FORWARD) ^ rtl
? View.FOCUS_RIGHT : View.FOCUS_LEFT;
final View found = ff.findNextFocus(this, focused, absDir);
needsFocusFailureLayout = found == null;
if (FORCE_ABS_FOCUS_SEARCH_DIRECTION) {
// Workaround for broken FOCUS_BACKWARD in API 15 and older devices.
direction = absDir;
}
}
if (needsFocusFailureLayout) {
consumePendingUpdateOperations();
final View focusedItemView = findContainingItemView(focused);
if (focusedItemView == null) {
// panic, focused view is not a child anymore, cannot call super.
return null;
}
startInterceptRequestLayout();
mLayout.onFocusSearchFailed(focused, direction, mRecycler, mState);
stopInterceptRequestLayout(false);
}
result = ff.findNextFocus(this, focused, direction);
} else {
result = ff.findNextFocus(this, focused, direction);
if (result == null && canRunFocusFailure) {
consumePendingUpdateOperations();
final View focusedItemView = findContainingItemView(focused);
if (focusedItemView == null) {
// panic, focused view is not a child anymore, cannot call super.
return null;
}
startInterceptRequestLayout();
result = mLayout.onFocusSearchFailed(focused, direction, mRecycler, mState);
stopInterceptRequestLayout(false);
}
}
if (result != null && !result.hasFocusable()) {
if (getFocusedChild() == null) {
// Scrolling to this unfocusable view is not meaningful since there is no currently
// focused view which RV needs to keep visible.
return super.focusSearch(focused, direction);
}
// If the next view returned by onFocusSearchFailed in layout manager has no focusable
// views, we still scroll to that view in order to make it visible on the screen.
// If it's focusable, framework already calls RV's requestChildFocus which handles
// bringing this newly focused item onto the screen.
requestChildOnScreen(result, null);
return focused;
}
return isPreferredNextFocus(focused, result, direction)
? result : super.focusSearch(focused, direction);
}
/**
* Checks if the new focus candidate is a good enough candidate such that RecyclerView will
* assign it as the next focus View instead of letting view hierarchy decide.
* A good candidate means a View that is aligned in the focus direction wrt the focused View
* and is not the RecyclerView itself.
* When this method returns false, RecyclerView will let the parent make the decision so the
* same View may still get the focus as a result of that search.
*/
private boolean isPreferredNextFocus(View focused, View next, int direction) {
if (next == null || next == this) {
return false;
}
// panic, result view is not a child anymore, maybe workaround b/37864393
if (findContainingItemView(next) == null) {
return false;
}
if (focused == null) {
return true;
}
// panic, focused view is not a child anymore, maybe workaround b/37864393
if (findContainingItemView(focused) == null) {
return true;
}
mTempRect.set(0, 0, focused.getWidth(), focused.getHeight());
mTempRect2.set(0, 0, next.getWidth(), next.getHeight());
offsetDescendantRectToMyCoords(focused, mTempRect);
offsetDescendantRectToMyCoords(next, mTempRect2);
final int rtl = mLayout.getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL ? -1 : 1;
int rightness = 0;
if ((mTempRect.left < mTempRect2.left
|| mTempRect.right <= mTempRect2.left)
&& mTempRect.right < mTempRect2.right) {
rightness = 1;
} else if ((mTempRect.right > mTempRect2.right
|| mTempRect.left >= mTempRect2.right)
&& mTempRect.left > mTempRect2.left) {
rightness = -1;
}
int downness = 0;
if ((mTempRect.top < mTempRect2.top
|| mTempRect.bottom <= mTempRect2.top)
&& mTempRect.bottom < mTempRect2.bottom) {
downness = 1;
} else if ((mTempRect.bottom > mTempRect2.bottom
|| mTempRect.top >= mTempRect2.bottom)
&& mTempRect.top > mTempRect2.top) {
downness = -1;
}
switch (direction) {
case View.FOCUS_LEFT:
return rightness < 0;
case View.FOCUS_RIGHT:
return rightness > 0;
case View.FOCUS_UP:
return downness < 0;
case View.FOCUS_DOWN:
return downness > 0;
case View.FOCUS_FORWARD:
return downness > 0 || (downness == 0 && rightness * rtl >= 0);
case View.FOCUS_BACKWARD:
return downness < 0 || (downness == 0 && rightness * rtl <= 0);
}
throw new IllegalArgumentException("Invalid direction: " + direction + exceptionLabel());
}
@Override
public void requestChildFocus(View child, View focused) {
if (!mLayout.onRequestChildFocus(this, mState, child, focused) && focused != null) {
requestChildOnScreen(child, focused);
}
super.requestChildFocus(child, focused);
}
/**
* Requests that the given child of the RecyclerView be positioned onto the screen. This method
* can be called for both unfocusable and focusable child views. For unfocusable child views,
* the {@param focused} parameter passed is null, whereas for a focusable child, this parameter
* indicates the actual descendant view within this child view that holds the focus.
* @param child The child view of this RecyclerView that wants to come onto the screen.
* @param focused The descendant view that actually has the focus if child is focusable, null
* otherwise.
*/
protected void requestChildOnScreen(@NonNull View child, @Nullable View focused) {
View rectView = (focused != null) ? focused : child;
mTempRect.set(0, 0, rectView.getWidth(), rectView.getHeight());
// get item decor offsets w/o refreshing. If they are invalid, there will be another
// layout pass to fix them, then it is LayoutManager's responsibility to keep focused
// View in viewport.
final ViewGroup.LayoutParams focusedLayoutParams = rectView.getLayoutParams();
if (focusedLayoutParams instanceof LayoutParams) {
// if focused child has item decors, use them. Otherwise, ignore.
final LayoutParams lp = (LayoutParams) focusedLayoutParams;
if (!lp.mInsetsDirty) {
final Rect insets = lp.mDecorInsets;
mTempRect.left -= insets.left;
mTempRect.right += insets.right;
mTempRect.top -= insets.top;
mTempRect.bottom += insets.bottom;
}
}
if (focused != null) {
offsetDescendantRectToMyCoords(focused, mTempRect);
offsetRectIntoDescendantCoords(child, mTempRect);
}
mLayout.requestChildRectangleOnScreen(this, child, mTempRect, !mFirstLayoutComplete,
(focused == null));
}
@Override
public boolean requestChildRectangleOnScreen(View child, Rect rect, boolean immediate) {
return mLayout.requestChildRectangleOnScreen(this, child, rect, immediate);
}
@Override
public void addFocusables(ArrayList 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. Calls {@link OnItemTouchListener#onInterceptTouchEvent(RecyclerView, MotionEvent)} on each
* of the registered {@link OnItemTouchListener}s, passing in the
* MotionEvent. If one returns true and the action is not ACTION_CANCEL, saves the intercepting
* OnItemTouchListener to be called for future {@link RecyclerView#onTouchEvent(MotionEvent)}
* and immediately returns true. If none want to intercept or the action is ACTION_CANCEL,
* returns false.
*
* @param e The MotionEvent
* @return true if an OnItemTouchListener is saved as intercepting.
*/
private boolean findInterceptingOnItemTouchListener(MotionEvent e) {
int action = e.getAction();
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) {
mInterceptingOnItemTouchListener = listener;
return true;
}
}
return false;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
if (mLayoutSuppressed) {
// When layout is suppressed, RV does not intercept the motion event.
// A child view e.g. a button may still get the click.
return false;
}
// Clear the active onInterceptTouchListener. None should be set at this time, and if one
// is, it's because some other code didn't follow the standard contract.
mInterceptingOnItemTouchListener = null;
if (findInterceptingOnItemTouchListener(e)) {
cancelScroll();
return true;
}
if (mLayout == null) {
return false;
}
final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
final boolean canScrollVertically = mLayout.canScrollVertically();
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(e);
final int action = e.getActionMasked();
final int actionIndex = e.getActionIndex();
switch (action) {
case MotionEvent.ACTION_DOWN:
if (mIgnoreMotionEventTillDown) {
mIgnoreMotionEventTillDown = false;
}
mScrollPointerId = e.getPointerId(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);
stopNestedScroll(TYPE_NON_TOUCH);
}
// Clear the nested offsets
mNestedOffsets[0] = mNestedOffsets[1] = 0;
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontally) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertically) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
break;
case MotionEvent.ACTION_POINTER_DOWN:
mScrollPointerId = e.getPointerId(actionIndex);
mInitialTouchX = mLastTouchX = (int) (e.getX(actionIndex) + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY(actionIndex) + 0.5f);
break;
case MotionEvent.ACTION_MOVE: {
final int index = e.findPointerIndex(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) (e.getX(index) + 0.5f);
final int y = (int) (e.getY(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 = x;
startScroll = true;
}
if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
mLastTouchY = y;
startScroll = true;
}
if (startScroll) {
setScrollState(SCROLL_STATE_DRAGGING);
}
}
} break;
case MotionEvent.ACTION_POINTER_UP: {
onPointerUp(e);
} break;
case MotionEvent.ACTION_UP: {
mVelocityTracker.clear();
stopNestedScroll(TYPE_TOUCH);
} break;
case MotionEvent.ACTION_CANCEL: {
cancelScroll();
}
}
return mScrollState == SCROLL_STATE_DRAGGING;
}
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
final int listenerCount = mOnItemTouchListeners.size();
for (int i = 0; i < listenerCount; i++) {
final OnItemTouchListener listener = mOnItemTouchListeners.get(i);
listener.onRequestDisallowInterceptTouchEvent(disallowIntercept);
}
super.requestDisallowInterceptTouchEvent(disallowIntercept);
}
@Override
public boolean onTouchEvent(MotionEvent e) {
if (mLayoutSuppressed || mIgnoreMotionEventTillDown) {
return false;
}
if (dispatchToOnItemTouchListeners(e)) {
cancelScroll();
return true;
}
if (mLayout == null) {
return false;
}
final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
final boolean canScrollVertically = mLayout.canScrollVertically();
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
boolean eventAddedToVelocityTracker = false;
final MotionEvent vtev = MotionEvent.obtain(e);
final int action = e.getActionMasked();
final int actionIndex = e.getActionIndex();
if (action == MotionEvent.ACTION_DOWN) {
mNestedOffsets[0] = mNestedOffsets[1] = 0;
}
vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);
switch (action) {
case MotionEvent.ACTION_DOWN: {
mScrollPointerId = e.getPointerId(0);
mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontally) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertically) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
} break;
case MotionEvent.ACTION_POINTER_DOWN: {
mScrollPointerId = e.getPointerId(actionIndex);
mInitialTouchX = mLastTouchX = (int) (e.getX(actionIndex) + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY(actionIndex) + 0.5f);
} break;
case MotionEvent.ACTION_MOVE: {
final int index = e.findPointerIndex(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) (e.getX(index) + 0.5f);
final int y = (int) (e.getY(index) + 0.5f);
int dx = mLastTouchX - x;
int dy = mLastTouchY - y;
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
if (dispatchNestedPreScroll(dx, dy, mReusableIntPair, mScrollOffset, TYPE_TOUCH)) {
dx -= mReusableIntPair[0];
dy -= mReusableIntPair[1];
vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
// Updated the nested offsets
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
}
if (mScrollState != SCROLL_STATE_DRAGGING) {
boolean startScroll = false;
if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
if (dx > 0) {
dx -= mTouchSlop;
} else {
dx += mTouchSlop;
}
startScroll = true;
}
if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
if (dy > 0) {
dy -= mTouchSlop;
} else {
dy += mTouchSlop;
}
startScroll = true;
}
if (startScroll) {
setScrollState(SCROLL_STATE_DRAGGING);
}
}
if (mScrollState == SCROLL_STATE_DRAGGING) {
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
vtev)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
if (mGapWorker != null && (dx != 0 || dy != 0)) {
mGapWorker.postFromTraversal(this, dx, dy);
}
}
} break;
case MotionEvent.ACTION_POINTER_UP: {
onPointerUp(e);
} break;
case MotionEvent.ACTION_UP: {
mVelocityTracker.addMovement(vtev);
eventAddedToVelocityTracker = true;
mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
final float xvel = canScrollHorizontally
? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
final float yvel = canScrollVertically
? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
setScrollState(SCROLL_STATE_IDLE);
}
resetScroll();
} break;
case MotionEvent.ACTION_CANCEL: {
cancelScroll();
} break;
}
if (!eventAddedToVelocityTracker) {
mVelocityTracker.addMovement(vtev);
}
vtev.recycle();
return true;
}
private void resetScroll() {
if (mVelocityTracker != null) {
mVelocityTracker.clear();
}
stopNestedScroll(TYPE_TOUCH);
releaseGlows();
}
private void cancelScroll() {
resetScroll();
setScrollState(SCROLL_STATE_IDLE);
}
private void onPointerUp(MotionEvent e) {
final int actionIndex = e.getActionIndex();
if (e.getPointerId(actionIndex) == mScrollPointerId) {
// Pick a new pointer to pick up the slack.
final int newIndex = actionIndex == 0 ? 1 : 0;
mScrollPointerId = e.getPointerId(newIndex);
mInitialTouchX = mLastTouchX = (int) (e.getX(newIndex) + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY(newIndex) + 0.5f);
}
}
@Override
public boolean onGenericMotionEvent(MotionEvent event) {
if (mLayout == null) {
return false;
}
if (mLayoutSuppressed) {
return false;
}
if (event.getAction() == MotionEvent.ACTION_SCROLL) {
final float vScroll, hScroll;
if ((event.getSource() & InputDeviceCompat.SOURCE_CLASS_POINTER) != 0) {
if (mLayout.canScrollVertically()) {
// Inverse the sign of the vertical scroll to align the scroll orientation
// with AbsListView.
vScroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL);
} else {
vScroll = 0f;
}
if (mLayout.canScrollHorizontally()) {
hScroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
} else {
hScroll = 0f;
}
} else if ((event.getSource() & InputDeviceCompat.SOURCE_ROTARY_ENCODER) != 0) {
final float axisScroll = event.getAxisValue(MotionEventCompat.AXIS_SCROLL);
if (mLayout.canScrollVertically()) {
// Invert the sign of the vertical scroll to align the scroll orientation
// with AbsListView.
vScroll = -axisScroll;
hScroll = 0f;
} else if (mLayout.canScrollHorizontally()) {
vScroll = 0f;
hScroll = axisScroll;
} else {
vScroll = 0f;
hScroll = 0f;
}
} else {
vScroll = 0f;
hScroll = 0f;
}
if (vScroll != 0 || hScroll != 0) {
scrollByInternal((int) (hScroll * mScaledHorizontalScrollFactor),
(int) (vScroll * mScaledVerticalScrollFactor), event);
}
}
return false;
}
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
if (mLayout == null) {
defaultOnMeasure(widthSpec, heightSpec);
return;
}
if (mLayout.isAutoMeasureEnabled()) {
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
/**
* This specific call should be considered deprecated and replaced with
* {@link #defaultOnMeasure(int, int)}. It can't actually be replaced as it could
* break existing third party code but all documentation directs developers to not
* override {@link LayoutManager#onMeasure(int, int)} when
* {@link LayoutManager#isAutoMeasureEnabled()} returns true.
*/
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
final boolean measureSpecModeIsExactly =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
if (measureSpecModeIsExactly || mAdapter == null) {
return;
}
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
// set dimensions in 2nd step. Pre-layout should happen with old dimensions for
// consistency
mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
dispatchLayoutStep2();
// now we can get the width and height from the children.
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
// if RecyclerView has non-exact width and height and if there is at least one child
// which also has non-exact width & height, we have to re-measure.
if (mLayout.shouldMeasureTwice()) {
mLayout.setMeasureSpecs(
MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
mState.mIsMeasuring = true;
dispatchLayoutStep2();
// now we can get the width and height from the children.
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
}
} else {
if (mHasFixedSize) {
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
return;
}
// custom onMeasure
if (mAdapterUpdateDuringMeasure) {
startInterceptRequestLayout();
onEnterLayoutOrScroll();
processAdapterUpdatesAndSetAnimationFlags();
onExitLayoutOrScroll();
if (mState.mRunPredictiveAnimations) {
mState.mInPreLayout = true;
} else {
// consume remaining updates to provide a consistent state with the layout pass.
mAdapterHelper.consumeUpdatesInOnePass();
mState.mInPreLayout = false;
}
mAdapterUpdateDuringMeasure = false;
stopInterceptRequestLayout(false);
} else if (mState.mRunPredictiveAnimations) {
// If mAdapterUpdateDuringMeasure is false and mRunPredictiveAnimations is true:
// this means there is already an onMeasure() call performed to handle the pending
// adapter change, two onMeasure() calls can happen if RV is a child of LinearLayout
// with layout_width=MATCH_PARENT. RV cannot call LM.onMeasure() second time
// because getViewForPosition() will crash when LM uses a child to measure.
setMeasuredDimension(getMeasuredWidth(), getMeasuredHeight());
return;
}
if (mAdapter != null) {
mState.mItemCount = mAdapter.getItemCount();
} else {
mState.mItemCount = 0;
}
startInterceptRequestLayout();
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
stopInterceptRequestLayout(false);
mState.mInPreLayout = false; // clear
}
}
/**
* An implementation of {@link View#onMeasure(int, int)} to fall back to in various scenarios
* where this RecyclerView is otherwise lacking better information.
*/
void defaultOnMeasure(int widthSpec, int heightSpec) {
// calling LayoutManager here is not pretty but that API is already public and it is better
// than creating another method since this is internal.
final int width = LayoutManager.chooseSize(widthSpec,
getPaddingLeft() + getPaddingRight(),
ViewCompat.getMinimumWidth(this));
final int height = LayoutManager.chooseSize(heightSpec,
getPaddingTop() + getPaddingBottom(),
ViewCompat.getMinimumHeight(this));
setMeasuredDimension(width, height);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (w != oldw || h != oldh) {
invalidateGlows();
// layout's w/h are updated during measure/layout steps.
}
}
/**
* 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(@Nullable ItemAnimator animator) {
if (mItemAnimator != null) {
mItemAnimator.endAnimations();
mItemAnimator.setListener(null);
}
mItemAnimator = animator;
if (mItemAnimator != null) {
mItemAnimator.setListener(mItemAnimatorListener);
}
}
void onEnterLayoutOrScroll() {
mLayoutOrScrollCounter++;
}
void onExitLayoutOrScroll() {
onExitLayoutOrScroll(true);
}
void onExitLayoutOrScroll(boolean enableChangeEvents) {
mLayoutOrScrollCounter--;
if (mLayoutOrScrollCounter < 1) {
if (DEBUG && mLayoutOrScrollCounter < 0) {
throw new IllegalStateException("layout or scroll counter cannot go below zero."
+ "Some calls are not matching" + exceptionLabel());
}
mLayoutOrScrollCounter = 0;
if (enableChangeEvents) {
dispatchContentChangedIfNecessary();
dispatchPendingImportantForAccessibilityChanges();
}
}
}
boolean isAccessibilityEnabled() {
return mAccessibilityManager != null && mAccessibilityManager.isEnabled();
}
private void dispatchContentChangedIfNecessary() {
final int flags = mEatenAccessibilityChangeFlags;
mEatenAccessibilityChangeFlags = 0;
if (flags != 0 && isAccessibilityEnabled()) {
final AccessibilityEvent event = AccessibilityEvent.obtain();
event.setEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
AccessibilityEventCompat.setContentChangeTypes(event, flags);
sendAccessibilityEventUnchecked(event);
}
}
/**
* Returns whether RecyclerView is currently computing a layout.
*
* If this method returns true, it means that RecyclerView is in a lockdown state and any
* attempt to update adapter contents will result in an exception because adapter contents
* cannot be changed while RecyclerView is trying to compute the layout.
*
* It is very unlikely that your code will be running during this state as it is
* called by the framework when a layout traversal happens or RecyclerView starts to scroll
* in response to system events (touch, accessibility etc).
*
* This case may happen if you have some custom logic to change adapter contents in
* response to a View callback (e.g. focus change callback) which might be triggered during a
* layout calculation. In these cases, you should just postpone the change using a Handler or a
* similar mechanism.
*
* @return
* This method may process only the pre-layout state of updates or all of them.
*/
private void processAdapterUpdatesAndSetAnimationFlags() {
if (mDataSetHasChangedAfterLayout) {
// Processing these items have no value since data set changed unexpectedly.
// Instead, we just reset it.
mAdapterHelper.reset();
if (mDispatchItemsChangedEvent) {
mLayout.onItemsChanged(this);
}
}
// simple animations are a subset of advanced animations (which will cause a
// pre-layout step)
// If layout supports predictive animations, pre-process to decide if we want to run them
if (predictiveItemAnimationsEnabled()) {
mAdapterHelper.preProcess();
} else {
mAdapterHelper.consumeUpdatesInOnePass();
}
boolean animationTypeSupported = mItemsAddedOrRemoved || mItemsChanged;
mState.mRunSimpleAnimations = mFirstLayoutComplete
&& mItemAnimator != null
&& (mDataSetHasChangedAfterLayout
|| animationTypeSupported
|| mLayout.mRequestedSimpleAnimations)
&& (!mDataSetHasChangedAfterLayout
|| mAdapter.hasStableIds());
mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations
&& animationTypeSupported
&& !mDataSetHasChangedAfterLayout
&& predictiveItemAnimationsEnabled();
}
/**
* 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 animated via
* {@link ItemAnimator#animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo)}
* DISAPPEARING views are animated via
* {@link ItemAnimator#animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)}
* APPEARING views are animated via
* {@link ItemAnimator#animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)}
* and changed views are animated via
* {@link ItemAnimator#animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)}.
*/
void dispatchLayout() {
if (mAdapter == null) {
Log.e(TAG, "No adapter attached; skipping layout");
// leave the state in START
return;
}
if (mLayout == null) {
Log.e(TAG, "No layout manager attached; skipping layout");
// leave the state in START
return;
}
mState.mIsMeasuring = false;
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
|| mLayout.getHeight() != getHeight()) {
// First 2 steps are done in onMeasure but looks like we have to run again due to
// changed size.
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else {
// always make sure we sync them (to ensure mode is exact)
mLayout.setExactMeasureSpecsFrom(this);
}
dispatchLayoutStep3();
}
private void saveFocusInfo() {
View child = null;
if (mPreserveFocusAfterLayout && hasFocus() && mAdapter != null) {
child = getFocusedChild();
}
final ViewHolder focusedVh = child == null ? null : findContainingViewHolder(child);
if (focusedVh == null) {
resetFocusInfo();
} else {
mState.mFocusedItemId = mAdapter.hasStableIds() ? focusedVh.getItemId() : NO_ID;
// mFocusedItemPosition should hold the current adapter position of the previously
// focused item. If the item is removed, we store the previous adapter position of the
// removed item.
mState.mFocusedItemPosition = mDataSetHasChangedAfterLayout ? NO_POSITION
: (focusedVh.isRemoved() ? focusedVh.mOldPosition
: focusedVh.getAdapterPosition());
mState.mFocusedSubChildId = getDeepestFocusedViewWithId(focusedVh.itemView);
}
}
private void resetFocusInfo() {
mState.mFocusedItemId = NO_ID;
mState.mFocusedItemPosition = NO_POSITION;
mState.mFocusedSubChildId = View.NO_ID;
}
/**
* Finds the best view candidate to request focus on using mFocusedItemPosition index of the
* previously focused item. It first traverses the adapter forward to find a focusable candidate
* and if no such candidate is found, it reverses the focus search direction for the items
* before the mFocusedItemPosition'th index;
* @return The best candidate to request focus on, or null if no such candidate exists. Null
* indicates all the existing adapter items are unfocusable.
*/
@Nullable
private View findNextViewToFocus() {
int startFocusSearchIndex = mState.mFocusedItemPosition != -1 ? mState.mFocusedItemPosition
: 0;
ViewHolder nextFocus;
final int itemCount = mState.getItemCount();
for (int i = startFocusSearchIndex; i < itemCount; i++) {
nextFocus = findViewHolderForAdapterPosition(i);
if (nextFocus == null) {
break;
}
if (nextFocus.itemView.hasFocusable()) {
return nextFocus.itemView;
}
}
final int limit = Math.min(itemCount, startFocusSearchIndex);
for (int i = limit - 1; i >= 0; i--) {
nextFocus = findViewHolderForAdapterPosition(i);
if (nextFocus == null) {
return null;
}
if (nextFocus.itemView.hasFocusable()) {
return nextFocus.itemView;
}
}
return null;
}
private void recoverFocusFromState() {
if (!mPreserveFocusAfterLayout || mAdapter == null || !hasFocus()
|| getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS
|| (getDescendantFocusability() == FOCUS_BEFORE_DESCENDANTS && isFocused())) {
// No-op if either of these cases happens:
// 1. RV has no focus, or 2. RV blocks focus to its children, or 3. RV takes focus
// before its children and is focused (i.e. it already stole the focus away from its
// descendants).
return;
}
// only recover focus if RV itself has the focus or the focused view is hidden
if (!isFocused()) {
final View focusedChild = getFocusedChild();
if (IGNORE_DETACHED_FOCUSED_CHILD
&& (focusedChild.getParent() == null || !focusedChild.hasFocus())) {
// Special handling of API 15-. A focused child can be invalid because mFocus is not
// cleared when the child is detached (mParent = null),
// This happens because clearFocus on API 15- does not invalidate mFocus of its
// parent when this child is detached.
// For API 16+, this is not an issue because requestFocus takes care of clearing the
// prior detached focused child. For API 15- the problem happens in 2 cases because
// clearChild does not call clearChildFocus on RV: 1. setFocusable(false) is called
// for the current focused item which calls clearChild or 2. when the prior focused
// child is removed, removeDetachedView called in layout step 3 which calls
// clearChild. We should ignore this invalid focused child in all our calculations
// for the next view to receive focus, and apply the focus recovery logic instead.
if (mChildHelper.getChildCount() == 0) {
// No children left. Request focus on the RV itself since one of its children
// was holding focus previously.
requestFocus();
return;
}
} else if (!mChildHelper.isHidden(focusedChild)) {
// If the currently focused child is hidden, apply the focus recovery logic.
// Otherwise return, i.e. the currently (unhidden) focused child is good enough :/.
return;
}
}
ViewHolder focusTarget = null;
// RV first attempts to locate the previously focused item to request focus on using
// mFocusedItemId. If such an item no longer exists, it then makes a best-effort attempt to
// find the next best candidate to request focus on based on mFocusedItemPosition.
if (mState.mFocusedItemId != NO_ID && mAdapter.hasStableIds()) {
focusTarget = findViewHolderForItemId(mState.mFocusedItemId);
}
View viewToFocus = null;
if (focusTarget == null || mChildHelper.isHidden(focusTarget.itemView)
|| !focusTarget.itemView.hasFocusable()) {
if (mChildHelper.getChildCount() > 0) {
// At this point, RV has focus and either of these conditions are true:
// 1. There's no previously focused item either because RV received focused before
// layout, or the previously focused item was removed, or RV doesn't have stable IDs
// 2. Previous focus child is hidden, or 3. Previous focused child is no longer
// focusable. In either of these cases, we make sure that RV still passes down the
// focus to one of its focusable children using a best-effort algorithm.
viewToFocus = findNextViewToFocus();
}
} else {
// looks like the focused item has been replaced with another view that represents the
// same item in the adapter. Request focus on that.
viewToFocus = focusTarget.itemView;
}
if (viewToFocus != null) {
if (mState.mFocusedSubChildId != NO_ID) {
View child = viewToFocus.findViewById(mState.mFocusedSubChildId);
if (child != null && child.isFocusable()) {
viewToFocus = child;
}
}
viewToFocus.requestFocus();
}
}
private int getDeepestFocusedViewWithId(View view) {
int lastKnownId = view.getId();
while (!view.isFocused() && view instanceof ViewGroup && view.hasFocus()) {
view = ((ViewGroup) view).getFocusedChild();
final int id = view.getId();
if (id != View.NO_ID) {
lastKnownId = view.getId();
}
}
return lastKnownId;
}
final void fillRemainingScrollValues(State state) {
if (getScrollState() == SCROLL_STATE_SETTLING) {
final OverScroller scroller = mViewFlinger.mOverScroller;
state.mRemainingScrollHorizontal = scroller.getFinalX() - scroller.getCurrX();
state.mRemainingScrollVertical = scroller.getFinalY() - scroller.getCurrY();
} else {
state.mRemainingScrollHorizontal = 0;
state.mRemainingScrollVertical = 0;
}
}
/**
* The first step of a layout where we;
* - process adapter updates
* - decide which animation should run
* - save information about current views
* - If necessary, run predictive layout and save its information
*/
private void dispatchLayoutStep1() {
mState.assertLayoutStep(State.STEP_START);
fillRemainingScrollValues(mState);
mState.mIsMeasuring = false;
startInterceptRequestLayout();
mViewInfoStore.clear();
onEnterLayoutOrScroll();
processAdapterUpdatesAndSetAnimationFlags();
saveFocusInfo();
mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged;
mItemsAddedOrRemoved = mItemsChanged = false;
mState.mInPreLayout = mState.mRunPredictiveAnimations;
mState.mItemCount = mAdapter.getItemCount();
findMinMaxChildLayoutPositions(mMinMaxLayoutPositions);
if (mState.mRunSimpleAnimations) {
// Step 0: Find out where all non-removed items are, pre-layout
int count = mChildHelper.getChildCount();
for (int i = 0; i < count; ++i) {
final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
if (holder.shouldIgnore() || (holder.isInvalid() && !mAdapter.hasStableIds())) {
continue;
}
final ItemHolderInfo animationInfo = mItemAnimator
.recordPreLayoutInformation(mState, holder,
ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
holder.getUnmodifiedPayloads());
mViewInfoStore.addToPreLayout(holder, animationInfo);
if (mState.mTrackOldChangeHolders && holder.isUpdated() && !holder.isRemoved()
&& !holder.shouldIgnore() && !holder.isInvalid()) {
long key = getChangedHolderKey(holder);
// This is NOT the only place where a ViewHolder is added to old change holders
// list. There is another case where:
// * A VH is currently hidden but not deleted
// * The hidden item is changed in the adapter
// * Layout manager decides to layout the item in the pre-Layout pass (step1)
// When this case is detected, RV will un-hide that view and add to the old
// change holders list.
mViewInfoStore.addToOldChangeHolders(key, holder);
}
}
}
if (mState.mRunPredictiveAnimations) {
// 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.
// Save old positions so that LayoutManager can run its mapping logic.
saveOldPositions();
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;
for (int i = 0; i < mChildHelper.getChildCount(); ++i) {
final View child = mChildHelper.getChildAt(i);
final ViewHolder viewHolder = getChildViewHolderInt(child);
if (viewHolder.shouldIgnore()) {
continue;
}
if (!mViewInfoStore.isInPreLayout(viewHolder)) {
int flags = ItemAnimator.buildAdapterChangeFlagsForAnimations(viewHolder);
boolean wasHidden = viewHolder
.hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
if (!wasHidden) {
flags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
}
final ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation(
mState, viewHolder, flags, viewHolder.getUnmodifiedPayloads());
if (wasHidden) {
recordAnimationInfoIfBouncedHiddenView(viewHolder, animationInfo);
} else {
mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);
}
}
}
// we don't process disappearing list because they may re-appear in post layout pass.
clearOldPositions();
} else {
clearOldPositions();
}
onExitLayoutOrScroll();
stopInterceptRequestLayout(false);
mState.mLayoutStep = State.STEP_LAYOUT;
}
/**
* The second layout step where we do the actual layout of the views for the final state.
* This step might be run multiple times if necessary (e.g. measure).
*/
private void dispatchLayoutStep2() {
startInterceptRequestLayout();
onEnterLayoutOrScroll();
mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
mAdapterHelper.consumeUpdatesInOnePass();
mState.mItemCount = mAdapter.getItemCount();
mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;
// 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
mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
mState.mLayoutStep = State.STEP_ANIMATIONS;
onExitLayoutOrScroll();
stopInterceptRequestLayout(false);
}
/**
* The final step of the layout where we save the information about views for animations,
* trigger animations and do any necessary cleanup.
*/
private void dispatchLayoutStep3() {
mState.assertLayoutStep(State.STEP_ANIMATIONS);
startInterceptRequestLayout();
onEnterLayoutOrScroll();
mState.mLayoutStep = State.STEP_START;
if (mState.mRunSimpleAnimations) {
// Step 3: Find out where things are now, and process change animations.
// traverse list in reverse because we may call animateChange in the loop which may
// remove the target view holder.
for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
if (holder.shouldIgnore()) {
continue;
}
long key = getChangedHolderKey(holder);
final ItemHolderInfo animationInfo = mItemAnimator
.recordPostLayoutInformation(mState, holder);
ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key);
if (oldChangeViewHolder != null && !oldChangeViewHolder.shouldIgnore()) {
// run a change animation
// If an Item is CHANGED but the updated version is disappearing, it creates
// a conflicting case.
// Since a view that is marked as disappearing is likely to be going out of
// bounds, we run a change animation. Both views will be cleaned automatically
// once their animations finish.
// On the other hand, if it is the same view holder instance, we run a
// disappearing animation instead because we are not going to rebind the updated
// VH unless it is enforced by the layout manager.
final boolean oldDisappearing = mViewInfoStore.isDisappearing(
oldChangeViewHolder);
final boolean newDisappearing = mViewInfoStore.isDisappearing(holder);
if (oldDisappearing && oldChangeViewHolder == holder) {
// run disappear animation instead of change
mViewInfoStore.addToPostLayout(holder, animationInfo);
} else {
final ItemHolderInfo preInfo = mViewInfoStore.popFromPreLayout(
oldChangeViewHolder);
// we add and remove so that any post info is merged.
mViewInfoStore.addToPostLayout(holder, animationInfo);
ItemHolderInfo postInfo = mViewInfoStore.popFromPostLayout(holder);
if (preInfo == null) {
handleMissingPreInfoForChangeError(key, holder, oldChangeViewHolder);
} else {
animateChange(oldChangeViewHolder, holder, preInfo, postInfo,
oldDisappearing, newDisappearing);
}
}
} else {
mViewInfoStore.addToPostLayout(holder, animationInfo);
}
}
// Step 4: Process view info lists and trigger animations
mViewInfoStore.process(mViewInfoProcessCallback);
}
mLayout.removeAndRecycleScrapInt(mRecycler);
mState.mPreviousLayoutItemCount = mState.mItemCount;
mDataSetHasChangedAfterLayout = false;
mDispatchItemsChangedEvent = false;
mState.mRunSimpleAnimations = false;
mState.mRunPredictiveAnimations = false;
mLayout.mRequestedSimpleAnimations = false;
if (mRecycler.mChangedScrap != null) {
mRecycler.mChangedScrap.clear();
}
if (mLayout.mPrefetchMaxObservedInInitialPrefetch) {
// Initial prefetch has expanded cache, so reset until next prefetch.
// This prevents initial prefetches from expanding the cache permanently.
mLayout.mPrefetchMaxCountObserved = 0;
mLayout.mPrefetchMaxObservedInInitialPrefetch = false;
mRecycler.updateViewCacheSize();
}
mLayout.onLayoutCompleted(mState);
onExitLayoutOrScroll();
stopInterceptRequestLayout(false);
mViewInfoStore.clear();
if (didChildRangeChange(mMinMaxLayoutPositions[0], mMinMaxLayoutPositions[1])) {
dispatchOnScrolled(0, 0);
}
recoverFocusFromState();
resetFocusInfo();
}
/**
* This handles the case where there is an unexpected VH missing in the pre-layout map.
*
* We might be able to detect the error in the application which will help the developer to
* resolve the issue.
*
* If it is not an expected error, we at least print an error to notify the developer and ignore
* the animation.
*
* https://code.google.com/p/android/issues/detail?id=193958
*
* @param key The change key
* @param holder Current ViewHolder
* @param oldChangeViewHolder Changed ViewHolder
*/
private void handleMissingPreInfoForChangeError(long key,
ViewHolder holder, ViewHolder oldChangeViewHolder) {
// check if two VH have the same key, if so, print that as an error
final int childCount = mChildHelper.getChildCount();
for (int i = 0; i < childCount; i++) {
View view = mChildHelper.getChildAt(i);
ViewHolder other = getChildViewHolderInt(view);
if (other == holder) {
continue;
}
final long otherKey = getChangedHolderKey(other);
if (otherKey == key) {
if (mAdapter != null && mAdapter.hasStableIds()) {
throw new IllegalStateException("Two different ViewHolders have the same stable"
+ " ID. Stable IDs in your adapter MUST BE unique and SHOULD NOT"
+ " change.\n ViewHolder 1:" + other + " \n View Holder 2:" + holder
+ exceptionLabel());
} else {
throw new IllegalStateException("Two different ViewHolders have the same change"
+ " ID. This might happen due to inconsistent Adapter update events or"
+ " if the LayoutManager lays out the same View multiple times."
+ "\n ViewHolder 1:" + other + " \n View Holder 2:" + holder
+ exceptionLabel());
}
}
}
// Very unlikely to happen but if it does, notify the developer.
Log.e(TAG, "Problem while matching changed view holders with the new"
+ "ones. The pre-layout information for the change holder " + oldChangeViewHolder
+ " cannot be found but it is necessary for " + holder + exceptionLabel());
}
/**
* Records the animation information for a view holder that was bounced from hidden list. It
* also clears the bounce back flag.
*/
void recordAnimationInfoIfBouncedHiddenView(ViewHolder viewHolder,
ItemHolderInfo animationInfo) {
// looks like this view bounced back from hidden list!
viewHolder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
if (mState.mTrackOldChangeHolders && viewHolder.isUpdated()
&& !viewHolder.isRemoved() && !viewHolder.shouldIgnore()) {
long key = getChangedHolderKey(viewHolder);
mViewInfoStore.addToOldChangeHolders(key, viewHolder);
}
mViewInfoStore.addToPreLayout(viewHolder, animationInfo);
}
private void findMinMaxChildLayoutPositions(int[] into) {
final int count = mChildHelper.getChildCount();
if (count == 0) {
into[0] = NO_POSITION;
into[1] = NO_POSITION;
return;
}
int minPositionPreLayout = Integer.MAX_VALUE;
int maxPositionPreLayout = Integer.MIN_VALUE;
for (int i = 0; i < count; ++i) {
final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
if (holder.shouldIgnore()) {
continue;
}
final int pos = holder.getLayoutPosition();
if (pos < minPositionPreLayout) {
minPositionPreLayout = pos;
}
if (pos > maxPositionPreLayout) {
maxPositionPreLayout = pos;
}
}
into[0] = minPositionPreLayout;
into[1] = maxPositionPreLayout;
}
private boolean didChildRangeChange(int minPositionPreLayout, int maxPositionPreLayout) {
findMinMaxChildLayoutPositions(mMinMaxLayoutPositions);
return mMinMaxLayoutPositions[0] != minPositionPreLayout
|| mMinMaxLayoutPositions[1] != maxPositionPreLayout;
}
@Override
protected void removeDetachedView(View child, boolean animate) {
ViewHolder vh = getChildViewHolderInt(child);
if (vh != null) {
if (vh.isTmpDetached()) {
vh.clearTmpDetachFlag();
} else if (!vh.shouldIgnore()) {
throw new IllegalArgumentException("Called removeDetachedView with a view which"
+ " is not flagged as tmp detached." + vh + exceptionLabel());
}
}
// Clear any android.view.animation.Animation that may prevent the item from
// detaching when being removed. If a child is re-added before the
// lazy detach occurs, it will receive invalid attach/detach sequencing.
child.clearAnimation();
dispatchChildDetached(child);
super.removeDetachedView(child, animate);
}
/**
* Returns a unique key to be used while handling change animations.
* It might be child's position or stable id depending on the adapter type.
*/
long getChangedHolderKey(ViewHolder holder) {
return mAdapter.hasStableIds() ? holder.getItemId() : holder.mPosition;
}
void animateAppearance(@NonNull ViewHolder itemHolder,
@Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) {
itemHolder.setIsRecyclable(false);
if (mItemAnimator.animateAppearance(itemHolder, preLayoutInfo, postLayoutInfo)) {
postAnimationRunner();
}
}
void animateDisappearance(@NonNull ViewHolder holder,
@NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) {
addAnimatingView(holder);
holder.setIsRecyclable(false);
if (mItemAnimator.animateDisappearance(holder, preLayoutInfo, postLayoutInfo)) {
postAnimationRunner();
}
}
private void animateChange(@NonNull ViewHolder oldHolder, @NonNull ViewHolder newHolder,
@NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo,
boolean oldHolderDisappearing, boolean newHolderDisappearing) {
oldHolder.setIsRecyclable(false);
if (oldHolderDisappearing) {
addAnimatingView(oldHolder);
}
if (oldHolder != newHolder) {
if (newHolderDisappearing) {
addAnimatingView(newHolder);
}
oldHolder.mShadowedHolder = newHolder;
// old holder should disappear after animation ends
addAnimatingView(oldHolder);
mRecycler.unscrapView(oldHolder);
newHolder.setIsRecyclable(false);
newHolder.mShadowingHolder = oldHolder;
}
if (mItemAnimator.animateChange(oldHolder, newHolder, preInfo, postInfo)) {
postAnimationRunner();
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
dispatchLayout();
TraceCompat.endSection();
mFirstLayoutComplete = true;
}
@Override
public void requestLayout() {
if (mInterceptRequestLayoutDepth == 0 && !mLayoutSuppressed) {
super.requestLayout();
} else {
mLayoutWasDefered = true;
}
}
void markItemDecorInsetsDirty() {
final int childCount = mChildHelper.getUnfilteredChildCount();
for (int i = 0; i < childCount; i++) {
final View child = mChildHelper.getUnfilteredChildAt(i);
((LayoutParams) child.getLayoutParams()).mInsetsDirty = true;
}
mRecycler.markItemDecorInsetsDirty();
}
@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, mState);
}
// TODO If padding is not 0 and clipChildrenToPadding is false, to draw glows properly, we
// need find children closest to edges. Not sure if it is worth the effort.
boolean needsInvalidate = false;
if (glowColor == null || glowColor != 0) {
if (mLeftGlow != null && !mLeftGlow.isFinished()) {
final int restore = c.save();
final int padding = mClipToPadding ? getPaddingBottom() : 0;
c.rotate(270);
c.translate(-getHeight() + padding, 0);
needsInvalidate = mLeftGlow != null && mLeftGlow.draw(c);
c.restoreToCount(restore);
}
if (mTopGlow != null && !mTopGlow.isFinished()) {
final int restore = c.save();
if (mClipToPadding) {
c.translate(getPaddingLeft(), getPaddingTop());
}
c.translate(0, topGlowOffset);
needsInvalidate |= mTopGlow != null && mTopGlow.draw(c);
c.restoreToCount(restore);
}
if (mRightGlow != null && !mRightGlow.isFinished()) {
final int restore = c.save();
final int width = getWidth();
final int padding = mClipToPadding ? getPaddingTop() : 0;
c.rotate(90);
c.translate(-padding, -width);
needsInvalidate |= mRightGlow != null && mRightGlow.draw(c);
c.restoreToCount(restore);
}
if (mBottomGlow != null && !mBottomGlow.isFinished()) {
final int restore = c.save();
c.rotate(180);
if (mClipToPadding) {
c.translate(-getWidth() + getPaddingRight(), -getHeight() + getPaddingBottom());
} else {
c.translate(-getWidth(), -getHeight() + bottomGlowOffset);
}
needsInvalidate |= mBottomGlow != null && mBottomGlow.draw(c);
c.restoreToCount(restore);
}
}
// If some views are animating, ItemDecorators are likely to move/change with them.
// Invalidate RecyclerView to re-draw decorators. This is still efficient because children's
// display lists are not invalidated.
if (!needsInvalidate && mItemAnimator != null && mItemDecorations.size() > 0
&& mItemAnimator.isRunning()) {
needsInvalidate = true;
}
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, mState);
}
}
@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" + exceptionLabel());
}
return mLayout.generateDefaultLayoutParams();
}
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
if (mLayout == null) {
throw new IllegalStateException("RecyclerView has no LayoutManager" + exceptionLabel());
}
return mLayout.generateLayoutParams(getContext(), attrs);
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
if (mLayout == null) {
throw new IllegalStateException("RecyclerView has no LayoutManager" + exceptionLabel());
}
return mLayout.generateLayoutParams(p);
}
/**
* Returns true if RecyclerView is currently running some animations.
*
* If you want to be notified when animations are finished, use
* {@link ItemAnimator#isRunning(ItemAnimator.ItemAnimatorFinishedListener)}.
*
* @return True if there are some item animations currently running or waiting to be started.
*/
public boolean isAnimating() {
return mItemAnimator != null && mItemAnimator.isRunning();
}
void saveOldPositions() {
final int childCount = mChildHelper.getUnfilteredChildCount();
for (int i = 0; i < childCount; i++) {
final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
if (DEBUG && holder.mPosition == -1 && !holder.isRemoved()) {
throw new IllegalStateException("view holder cannot have position -1 unless it"
+ " is removed" + exceptionLabel());
}
if (!holder.shouldIgnore()) {
holder.saveOldPosition();
}
}
}
void clearOldPositions() {
final int childCount = mChildHelper.getUnfilteredChildCount();
for (int i = 0; i < childCount; i++) {
final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
if (!holder.shouldIgnore()) {
holder.clearOldPosition();
}
}
mRecycler.clearOldPositions();
}
void offsetPositionRecordsForMove(int from, int to) {
final int childCount = mChildHelper.getUnfilteredChildCount();
final int start, end, inBetweenOffset;
if (from < to) {
start = from;
end = to;
inBetweenOffset = -1;
} else {
start = to;
end = from;
inBetweenOffset = 1;
}
for (int i = 0; i < childCount; i++) {
final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
if (holder == null || holder.mPosition < start || holder.mPosition > end) {
continue;
}
if (DEBUG) {
Log.d(TAG, "offsetPositionRecordsForMove attached child " + i + " holder "
+ holder);
}
if (holder.mPosition == from) {
holder.offsetPosition(to - from, false);
} else {
holder.offsetPosition(inBetweenOffset, false);
}
mState.mStructureChanged = true;
}
mRecycler.offsetPositionRecordsForMove(from, to);
requestLayout();
}
void offsetPositionRecordsForInsert(int positionStart, int itemCount) {
final int childCount = mChildHelper.getUnfilteredChildCount();
for (int i = 0; i < childCount; i++) {
final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
if (holder != null && !holder.shouldIgnore() && holder.mPosition >= positionStart) {
if (DEBUG) {
Log.d(TAG, "offsetPositionRecordsForInsert attached child " + i + " holder "
+ holder + " now at position " + (holder.mPosition + itemCount));
}
holder.offsetPosition(itemCount, false);
mState.mStructureChanged = true;
}
}
mRecycler.offsetPositionRecordsForInsert(positionStart, itemCount);
requestLayout();
}
void offsetPositionRecordsForRemove(int positionStart, int itemCount,
boolean applyToPreLayout) {
final int positionEnd = positionStart + itemCount;
final int childCount = mChildHelper.getUnfilteredChildCount();
for (int i = 0; i < childCount; i++) {
final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
if (holder != null && !holder.shouldIgnore()) {
if (holder.mPosition >= positionEnd) {
if (DEBUG) {
Log.d(TAG, "offsetPositionRecordsForRemove attached child " + i
+ " holder " + holder + " now at position "
+ (holder.mPosition - itemCount));
}
holder.offsetPosition(-itemCount, applyToPreLayout);
mState.mStructureChanged = true;
} else if (holder.mPosition >= positionStart) {
if (DEBUG) {
Log.d(TAG, "offsetPositionRecordsForRemove attached child " + i
+ " holder " + holder + " now REMOVED");
}
holder.flagRemovedAndOffsetPosition(positionStart - 1, -itemCount,
applyToPreLayout);
mState.mStructureChanged = true;
}
}
}
mRecycler.offsetPositionRecordsForRemove(positionStart, itemCount, applyToPreLayout);
requestLayout();
}
/**
* Rebind existing views for the given range, or create as needed.
*
* @param positionStart Adapter position to start at
* @param itemCount Number of views that must explicitly be rebound
*/
void viewRangeUpdate(int positionStart, int itemCount, Object payload) {
final int childCount = mChildHelper.getUnfilteredChildCount();
final int positionEnd = positionStart + itemCount;
for (int i = 0; i < childCount; i++) {
final View child = mChildHelper.getUnfilteredChildAt(i);
final ViewHolder holder = getChildViewHolderInt(child);
if (holder == null || holder.shouldIgnore()) {
continue;
}
if (holder.mPosition >= positionStart && holder.mPosition < positionEnd) {
// We re-bind these view holders after pre-processing is complete so that
// ViewHolders have their final positions assigned.
holder.addFlags(ViewHolder.FLAG_UPDATE);
holder.addChangePayload(payload);
// lp cannot be null since we get ViewHolder from it.
((LayoutParams) child.getLayoutParams()).mInsetsDirty = true;
}
}
mRecycler.viewRangeUpdate(positionStart, itemCount);
}
boolean canReuseUpdatedViewHolder(ViewHolder viewHolder) {
return mItemAnimator == null || mItemAnimator.canReuseUpdatedViewHolder(viewHolder,
viewHolder.getUnmodifiedPayloads());
}
/**
* Processes the fact that, as far as we can tell, the data set has completely changed.
*
*
* By default, this value is {@code true}.
*
* @return True if the RecyclerView will try to preserve focused Item after a layout if it loses
* focus.
*
* @see #setPreserveFocusAfterLayout(boolean)
*/
public boolean getPreserveFocusAfterLayout() {
return mPreserveFocusAfterLayout;
}
/**
* Set whether the RecyclerView should try to keep the same Item focused after a layout
* calculation or not.
*
* Usually, LayoutManagers keep focused views visible before and after layout but sometimes,
* views may lose focus during a layout calculation as their state changes or they are replaced
* with another view due to type change or animation. In these cases, RecyclerView can request
* focus on the new view automatically.
*
* @param preserveFocusAfterLayout Whether RecyclerView should preserve focused Item during a
* layout calculations. Defaults to true.
*
* @see #getPreserveFocusAfterLayout()
*/
public void setPreserveFocusAfterLayout(boolean preserveFocusAfterLayout) {
mPreserveFocusAfterLayout = preserveFocusAfterLayout;
}
/**
* Retrieve the {@link ViewHolder} for the given child view.
*
* @param child Child of this RecyclerView to query for its ViewHolder
* @return The child view's ViewHolder
*/
public ViewHolder getChildViewHolder(@NonNull View child) {
final ViewParent parent = child.getParent();
if (parent != null && parent != this) {
throw new IllegalArgumentException("View " + child + " is not a direct child of "
+ this);
}
return getChildViewHolderInt(child);
}
/**
* Traverses the ancestors of the given view and returns the item view that contains it and
* also a direct child of the RecyclerView. This returned view can be used to get the
* ViewHolder by calling {@link #getChildViewHolder(View)}.
*
* @param view The view that is a descendant of the RecyclerView.
*
* @return The direct child of the RecyclerView which contains the given view or null if the
* provided view is not a descendant of this RecyclerView.
*
* @see #getChildViewHolder(View)
* @see #findContainingViewHolder(View)
*/
@Nullable
public View findContainingItemView(@NonNull View view) {
ViewParent parent = view.getParent();
while (parent != null && parent != this && parent instanceof View) {
view = (View) parent;
parent = view.getParent();
}
return parent == this ? view : null;
}
/**
* Returns the ViewHolder that contains the given view.
*
* @param view The view that is a descendant of the RecyclerView.
*
* @return The ViewHolder that contains the given view or null if the provided view is not a
* descendant of this RecyclerView.
*/
@Nullable
public ViewHolder findContainingViewHolder(@NonNull View view) {
View itemView = findContainingItemView(view);
return itemView == null ? null : getChildViewHolder(itemView);
}
static ViewHolder getChildViewHolderInt(View child) {
if (child == null) {
return null;
}
return ((LayoutParams) child.getLayoutParams()).mViewHolder;
}
/**
* @deprecated use {@link #getChildAdapterPosition(View)} or
* {@link #getChildLayoutPosition(View)}.
*/
@Deprecated
public int getChildPosition(@NonNull View child) {
return getChildAdapterPosition(child);
}
/**
* Return the adapter position that the given child view corresponds to.
*
* @param child Child View to query
* @return Adapter position corresponding to the given view or {@link #NO_POSITION}
*/
public int getChildAdapterPosition(@NonNull View child) {
final ViewHolder holder = getChildViewHolderInt(child);
return holder != null ? holder.getAdapterPosition() : NO_POSITION;
}
/**
* Return the adapter position of the given child view as of the latest completed layout pass.
*
* This position may not be equal to Item's adapter position if there are pending changes
* in the adapter which have not been reflected to the layout yet.
*
* @param child Child View to query
* @return Adapter position of the given View as of last layout pass or {@link #NO_POSITION} if
* the View is representing a removed item.
*/
public int getChildLayoutPosition(@NonNull View child) {
final ViewHolder holder = getChildViewHolderInt(child);
return holder != null ? holder.getLayoutPosition() : NO_POSITION;
}
/**
* Return the stable item id that the given child view corresponds to.
*
* @param child Child View to query
* @return Item id corresponding to the given view or {@link #NO_ID}
*/
public long getChildItemId(@NonNull View child) {
if (mAdapter == null || !mAdapter.hasStableIds()) {
return NO_ID;
}
final ViewHolder holder = getChildViewHolderInt(child);
return holder != null ? holder.getItemId() : NO_ID;
}
/**
* @deprecated use {@link #findViewHolderForLayoutPosition(int)} or
* {@link #findViewHolderForAdapterPosition(int)}
*/
@Deprecated
@Nullable
public ViewHolder findViewHolderForPosition(int position) {
return findViewHolderForPosition(position, false);
}
/**
* Return the ViewHolder for the item in the given position of the data set as of the latest
* layout pass.
*
* This method checks only the children of RecyclerView. If the item at the given
*
* Note that when Adapter contents change, ViewHolder positions are not updated until the
* next layout calculation. If there are pending adapter updates, the return value of this
* method may not match your adapter contents. You can use
* #{@link ViewHolder#getAdapterPosition()} to get the current adapter position of a ViewHolder.
*
* When the ItemAnimator is running a change animation, there might be 2 ViewHolders
* with the same layout position representing the same Item. In this case, the updated
* ViewHolder will be returned.
*
* @param position The position of the item in the data set of the adapter
* @return The ViewHolder at
* This method checks only the children of RecyclerView. If the item at the given
*
* When the ItemAnimator is running a change animation, there might be 2 ViewHolders
* representing the same Item. In this case, the updated ViewHolder will be returned.
*
* @param position The position of the item in the data set of the adapter
* @return The ViewHolder at
* This method checks only the children of RecyclerView. If the item with the given
* Subclasses of RecyclerView may want to perform extra bookkeeping or modifications
* of child views as they become attached. This will be called before a
* {@link LayoutManager} measures or lays out the view and is a good time to perform these
* changes. Subclasses of RecyclerView may want to perform extra bookkeeping or modifications
* of child views as they become detached. This will be called as a
* {@link LayoutManager} fully detaches the child view from the parent and its window. This method will always be invoked before listeners. If a subclass needs to perform
* any additional upkeep or bookkeeping after scrolling but before listeners run,
* this is a good place to do so. This differs from {@link View#onScrollChanged(int, int, int, int)} in that it receives
* the distance scrolled in either direction within the adapter's data set instead of absolute
* scroll coordinates. Since RecyclerView cannot compute the absolute scroll position from
* any arbitrary point in the data set, This method will always be invoked before listeners, but after the LayoutManager
* responds to the scroll state change.
* If this method returns
* This method returns true if RecyclerView has not yet calculated the first layout after it is
* attached to the Window or the Adapter has been replaced.
*
* @return True if there are some adapter updates which are not yet reflected to layout or false
* if layout is up to date.
*/
public boolean hasPendingAdapterUpdates() {
return !mFirstLayoutComplete || mDataSetHasChangedAfterLayout
|| mAdapterHelper.hasPendingUpdates();
}
class ViewFlinger implements Runnable {
private int mLastFlingX;
private int mLastFlingY;
OverScroller mOverScroller;
Interpolator mInterpolator = sQuinticInterpolator;
// When set to true, postOnAnimation callbacks are delayed until the run method completes
private boolean mEatRunOnAnimationRequest = false;
// Tracks if postAnimationCallback should be re-attached when it is done
private boolean mReSchedulePostAnimationCallback = false;
ViewFlinger() {
mOverScroller = new OverScroller(getContext(), sQuinticInterpolator);
}
@Override
public void run() {
if (mLayout == null) {
stop();
return; // no layout, cannot scroll.
}
mReSchedulePostAnimationCallback = false;
mEatRunOnAnimationRequest = true;
consumePendingUpdateOperations();
// TODO(72745539): After reviewing the code, it seems to me we may actually want to
// update the reference to the OverScroller after onAnimation. It looks to me like
// it is possible that a new OverScroller could be created (due to a new Interpolator
// being used), when the current OverScroller knows it's done after
// scroller.computeScrollOffset() is called. If that happens, and we don't update the
// reference, it seems to me that we could prematurely stop the newly created scroller
// due to setScrollState(SCROLL_STATE_IDLE) being called below.
// Keep a local reference so that if it is changed during onAnimation method, it won't
// cause unexpected behaviors
final OverScroller scroller = mOverScroller;
if (scroller.computeScrollOffset()) {
final int x = scroller.getCurrX();
final int y = scroller.getCurrY();
int unconsumedX = x - mLastFlingX;
int unconsumedY = y - mLastFlingY;
mLastFlingX = x;
mLastFlingY = y;
int consumedX = 0;
int consumedY = 0;
// Nested Pre Scroll
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
if (dispatchNestedPreScroll(unconsumedX, unconsumedY, mReusableIntPair, null,
TYPE_NON_TOUCH)) {
unconsumedX -= mReusableIntPair[0];
unconsumedY -= mReusableIntPair[1];
}
// Based on movement, we may want to trigger the hiding of existing over scroll
// glows.
if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
considerReleasingGlowsOnScroll(unconsumedX, unconsumedY);
}
// Local Scroll
if (mAdapter != null) {
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
scrollStep(unconsumedX, unconsumedY, mReusableIntPair);
consumedX = mReusableIntPair[0];
consumedY = mReusableIntPair[1];
unconsumedX -= consumedX;
unconsumedY -= consumedY;
// If SmoothScroller exists, this ViewFlinger was started by it, so we must
// report back to SmoothScroller.
SmoothScroller smoothScroller = mLayout.mSmoothScroller;
if (smoothScroller != null && !smoothScroller.isPendingInitialRun()
&& smoothScroller.isRunning()) {
final int adapterSize = mState.getItemCount();
if (adapterSize == 0) {
smoothScroller.stop();
} else if (smoothScroller.getTargetPosition() >= adapterSize) {
smoothScroller.setTargetPosition(adapterSize - 1);
smoothScroller.onAnimation(consumedX, consumedY);
} else {
smoothScroller.onAnimation(consumedX, consumedY);
}
}
}
if (!mItemDecorations.isEmpty()) {
invalidate();
}
// Nested Post Scroll
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, null,
TYPE_NON_TOUCH, mReusableIntPair);
unconsumedX -= mReusableIntPair[0];
unconsumedY -= mReusableIntPair[1];
if (consumedX != 0 || consumedY != 0) {
dispatchOnScrolled(consumedX, consumedY);
}
if (!awakenScrollBars()) {
invalidate();
}
// We are done scrolling if scroller is finished, or for both the x and y dimension,
// we are done scrolling or we can't scroll further (we know we can't scroll further
// when we have unconsumed scroll distance). It's possible that we don't need
// to also check for scroller.isFinished() at all, but no harm in doing so in case
// of old bugs in Overscroller.
boolean scrollerFinishedX = scroller.getCurrX() == scroller.getFinalX();
boolean scrollerFinishedY = scroller.getCurrY() == scroller.getFinalY();
final boolean doneScrolling = scroller.isFinished()
|| ((scrollerFinishedX || unconsumedX != 0)
&& (scrollerFinishedY || unconsumedY != 0));
// Get the current smoothScroller. It may have changed by this point and we need to
// make sure we don't stop scrolling if it has changed and it's pending an initial
// run.
SmoothScroller smoothScroller = mLayout.mSmoothScroller;
boolean smoothScrollerPending =
smoothScroller != null && smoothScroller.isPendingInitialRun();
if (!smoothScrollerPending && doneScrolling) {
// If we are done scrolling and the layout's SmoothScroller is not pending,
// do the things we do at the end of a scroll and don't postOnAnimation.
if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
final int vel = (int) scroller.getCurrVelocity();
int velX = unconsumedX < 0 ? -vel : unconsumedX > 0 ? vel : 0;
int velY = unconsumedY < 0 ? -vel : unconsumedY > 0 ? vel : 0;
absorbGlows(velX, velY);
}
if (ALLOW_THREAD_GAP_WORK) {
mPrefetchRegistry.clearPrefetchPositions();
}
} else {
// Otherwise continue the scroll.
postOnAnimation();
if (mGapWorker != null) {
mGapWorker.postFromTraversal(RecyclerView.this, unconsumedX, unconsumedY);
}
}
}
SmoothScroller smoothScroller = mLayout.mSmoothScroller;
// call this after the onAnimation is complete not to have inconsistent callbacks etc.
if (smoothScroller != null && smoothScroller.isPendingInitialRun()) {
smoothScroller.onAnimation(0, 0);
}
mEatRunOnAnimationRequest = false;
if (mReSchedulePostAnimationCallback) {
internalPostOnAnimation();
} else {
setScrollState(SCROLL_STATE_IDLE);
stopNestedScroll(TYPE_NON_TOUCH);
}
}
void postOnAnimation() {
if (mEatRunOnAnimationRequest) {
mReSchedulePostAnimationCallback = true;
} else {
internalPostOnAnimation();
}
}
private void internalPostOnAnimation() {
removeCallbacks(this);
ViewCompat.postOnAnimation(RecyclerView.this, this);
}
public void fling(int velocityX, int velocityY) {
setScrollState(SCROLL_STATE_SETTLING);
mLastFlingX = mLastFlingY = 0;
// Because you can't define a custom interpolator for flinging, we should make sure we
// reset ourselves back to the teh default interpolator in case a different call
// changed our interpolator.
if (mInterpolator != sQuinticInterpolator) {
mInterpolator = sQuinticInterpolator;
mOverScroller = new OverScroller(getContext(), sQuinticInterpolator);
}
mOverScroller.fling(0, 0, velocityX, velocityY,
Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
postOnAnimation();
}
public void smoothScrollBy(int dx, int dy, int duration,
@Nullable Interpolator interpolator) {
// Handle cases where parameter values aren't defined.
if (duration == UNDEFINED_DURATION) {
duration = computeScrollDuration(dx, dy, 0, 0);
}
if (interpolator == null) {
interpolator = sQuinticInterpolator;
}
// If the Interpolator has changed, create a new OverScroller with the new
// interpolator.
if (mInterpolator != interpolator) {
mInterpolator = interpolator;
mOverScroller = new OverScroller(getContext(), interpolator);
}
// Reset the last fling information.
mLastFlingX = mLastFlingY = 0;
// Set to settling state and start scrolling.
setScrollState(SCROLL_STATE_SETTLING);
mOverScroller.startScroll(0, 0, dx, dy, duration);
if (Build.VERSION.SDK_INT < 23) {
// b/64931938 before API 23, startScroll() does not reset getCurX()/getCurY()
// to start values, which causes fillRemainingScrollValues() put in obsolete values
// for LayoutManager.onLayoutChildren().
mOverScroller.computeScrollOffset();
}
postOnAnimation();
}
private float distanceInfluenceForSnapDuration(float f) {
f -= 0.5f; // center the values about 0.
f *= 0.3f * (float) Math.PI / 2.0f;
return (float) Math.sin(f);
}
private int computeScrollDuration(int dx, int dy, int vx, int vy) {
final int absDx = Math.abs(dx);
final int absDy = Math.abs(dy);
final boolean horizontal = absDx > absDy;
final int velocity = (int) Math.sqrt(vx * vx + vy * vy);
final int delta = (int) Math.sqrt(dx * dx + dy * dy);
final int containerSize = horizontal ? getWidth() : getHeight();
final int halfContainerSize = containerSize / 2;
final float distanceRatio = Math.min(1.f, 1.f * delta / containerSize);
final float distance = halfContainerSize + halfContainerSize
* distanceInfluenceForSnapDuration(distanceRatio);
final int duration;
if (velocity > 0) {
duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
} else {
float absDelta = (float) (horizontal ? absDx : absDy);
duration = (int) (((absDelta / containerSize) + 1) * 300);
}
return Math.min(duration, MAX_SCROLL_DURATION);
}
public void stop() {
removeCallbacks(this);
mOverScroller.abortAnimation();
}
}
void repositionShadowingViews() {
// Fix up shadow views used by change animations
int count = mChildHelper.getChildCount();
for (int i = 0; i < count; i++) {
View view = mChildHelper.getChildAt(i);
ViewHolder holder = getChildViewHolder(view);
if (holder != null && holder.mShadowingHolder != null) {
View shadowingView = holder.mShadowingHolder.itemView;
int left = view.getLeft();
int top = view.getTop();
if (left != shadowingView.getLeft() || top != shadowingView.getTop()) {
shadowingView.layout(left, top,
left + shadowingView.getWidth(),
top + shadowingView.getHeight());
}
}
}
}
private class RecyclerViewDataObserver extends AdapterDataObserver {
RecyclerViewDataObserver() {
}
@Override
public void onChanged() {
assertNotInLayoutOrScroll(null);
mState.mStructureChanged = true;
processDataSetCompletelyChanged(true);
if (!mAdapterHelper.hasPendingUpdates()) {
requestLayout();
}
}
@Override
public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
assertNotInLayoutOrScroll(null);
if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)) {
triggerUpdateProcessor();
}
}
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
assertNotInLayoutOrScroll(null);
if (mAdapterHelper.onItemRangeInserted(positionStart, itemCount)) {
triggerUpdateProcessor();
}
}
@Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
assertNotInLayoutOrScroll(null);
if (mAdapterHelper.onItemRangeRemoved(positionStart, itemCount)) {
triggerUpdateProcessor();
}
}
@Override
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
assertNotInLayoutOrScroll(null);
if (mAdapterHelper.onItemRangeMoved(fromPosition, toPosition, itemCount)) {
triggerUpdateProcessor();
}
}
void triggerUpdateProcessor() {
if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
} else {
mAdapterUpdateDuringMeasure = true;
requestLayout();
}
}
}
/**
* EdgeEffectFactory lets you customize the over-scroll edge effect for RecyclerViews.
*
* @see RecyclerView#setEdgeEffectFactory(EdgeEffectFactory)
*/
public static class EdgeEffectFactory {
@Retention(RetentionPolicy.SOURCE)
@IntDef({DIRECTION_LEFT, DIRECTION_TOP, DIRECTION_RIGHT, DIRECTION_BOTTOM})
public @interface EdgeDirection {}
/**
* Direction constant for the left edge
*/
public static final int DIRECTION_LEFT = 0;
/**
* Direction constant for the top edge
*/
public static final int DIRECTION_TOP = 1;
/**
* Direction constant for the right edge
*/
public static final int DIRECTION_RIGHT = 2;
/**
* Direction constant for the bottom edge
*/
public static final int DIRECTION_BOTTOM = 3;
/**
* Create a new EdgeEffect for the provided direction.
*/
protected @NonNull EdgeEffect createEdgeEffect(@NonNull RecyclerView view,
@EdgeDirection int direction) {
return new EdgeEffect(view.getContext());
}
}
/**
* RecycledViewPool lets you share Views between multiple RecyclerViews.
*
* If you want to recycle views across RecyclerViews, create an instance of RecycledViewPool
* and use {@link RecyclerView#setRecycledViewPool(RecycledViewPool)}.
*
* RecyclerView automatically creates a pool for itself if you don't provide one.
*/
public static class RecycledViewPool {
private static final int DEFAULT_MAX_SCRAP = 20;
/**
* Tracks both pooled holders, as well as create/bind timing metadata for the given type.
*
* Note that this tracks running averages of create/bind time across all RecyclerViews
* (and, indirectly, Adapters) that use this pool.
*
* 1) This enables us to track average create and bind times across multiple adapters. Even
* though create (and especially bind) may behave differently for different Adapter
* subclasses, sharing the pool is a strong signal that they'll perform similarly, per type.
*
* 2) If {@link #willBindInTime(int, long, long)} returns false for one view, it will return
* false for all other views of its type for the same deadline. This prevents items
* constructed by {@link GapWorker} prefetch from being bound to a lower priority prefetch.
*/
static class ScrapData {
final ArrayList
* If the pool is already full for that ViewHolder's type, it will be immediately discarded.
*
* @param scrap ViewHolder to be added to the pool.
*/
public void putRecycledView(ViewHolder scrap) {
final int viewType = scrap.getItemViewType();
final ArrayList
* RecycledViewPool will clear its cache if it has only one adapter attached and the new
* adapter uses a different ViewHolder than the oldAdapter.
*
* @param oldAdapter The previous adapter instance. Will be detached.
* @param newAdapter The new adapter instance. Will be attached.
* @param compatibleWithPrevious True if both oldAdapter and newAdapter are using the same
* ViewHolder and view types.
*/
void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter,
boolean compatibleWithPrevious) {
if (oldAdapter != null) {
detach();
}
if (!compatibleWithPrevious && mAttachCount == 0) {
clear();
}
if (newAdapter != null) {
attach();
}
}
private ScrapData getScrapDataForType(int viewType) {
ScrapData scrapData = mScrap.get(viewType);
if (scrapData == null) {
scrapData = new ScrapData();
mScrap.put(viewType, scrapData);
}
return scrapData;
}
}
/**
* Utility method for finding an internal RecyclerView, if present
*/
@Nullable
static RecyclerView findNestedRecyclerView(@NonNull View view) {
if (!(view instanceof ViewGroup)) {
return null;
}
if (view instanceof RecyclerView) {
return (RecyclerView) view;
}
final ViewGroup parent = (ViewGroup) view;
final int count = parent.getChildCount();
for (int i = 0; i < count; i++) {
final View child = parent.getChildAt(i);
final RecyclerView descendant = findNestedRecyclerView(child);
if (descendant != null) {
return descendant;
}
}
return null;
}
/**
* Utility method for clearing holder's internal RecyclerView, if present
*/
static void clearNestedRecyclerViewIfNotNested(@NonNull ViewHolder holder) {
if (holder.mNestedRecyclerView != null) {
View item = holder.mNestedRecyclerView.get();
while (item != null) {
if (item == holder.itemView) {
return; // match found, don't need to clear
}
ViewParent parent = item.getParent();
if (parent instanceof View) {
item = (View) parent;
} else {
item = null;
}
}
holder.mNestedRecyclerView = null; // not nested
}
}
/**
* Time base for deadline-aware work scheduling. Overridable for testing.
*
* Will return 0 to avoid cost of System.nanoTime where deadline-aware work scheduling
* isn't relevant.
*/
long getNanoTime() {
if (ALLOW_THREAD_GAP_WORK) {
return System.nanoTime();
} else {
return 0;
}
}
/**
* A Recycler is responsible for managing scrapped or detached item views for reuse.
*
* A "scrapped" view is a view that is still attached to its parent RecyclerView but
* that has been marked for removal or reuse. Typical use of a Recycler by a {@link LayoutManager} will be to obtain views for
* an adapter's data set representing the data at a given position or item ID.
* If the view to be reused is considered "dirty" the adapter will be asked to rebind it.
* If not, the view can be quickly reused by the LayoutManager with no further work.
* Clean views that have not {@link android.view.View#isLayoutRequested() requested layout}
* may be repositioned by a LayoutManager without remeasurement.
* Checks whether a given view holder can be used for the provided position.
*
* @param holder ViewHolder
* @return true if ViewHolder matches the provided position, false otherwise
*/
boolean validateViewHolderForOffsetPosition(ViewHolder holder) {
// if it is a removed holder, nothing to verify since we cannot ask adapter anymore
// if it is not removed, verify the type and id.
if (holder.isRemoved()) {
if (DEBUG && !mState.isPreLayout()) {
throw new IllegalStateException("should not receive a removed view unless it"
+ " is pre layout" + exceptionLabel());
}
return mState.isPreLayout();
}
if (holder.mPosition < 0 || holder.mPosition >= mAdapter.getItemCount()) {
throw new IndexOutOfBoundsException("Inconsistency detected. Invalid view holder "
+ "adapter position" + holder + exceptionLabel());
}
if (!mState.isPreLayout()) {
// don't check type if it is pre-layout.
final int type = mAdapter.getItemViewType(holder.mPosition);
if (type != holder.getItemViewType()) {
return false;
}
}
if (mAdapter.hasStableIds()) {
return holder.getItemId() == mAdapter.getItemId(holder.mPosition);
}
return true;
}
/**
* Attempts to bind view, and account for relevant timing information. If
* deadlineNs != FOREVER_NS, this method may fail to bind, and return false.
*
* @param holder Holder to be bound.
* @param offsetPosition Position of item to be bound.
* @param position Pre-layout position of item to be bound.
* @param deadlineNs Time, relative to getNanoTime(), by which bind/create work should
* complete. If FOREVER_NS is passed, this method will not fail to
* bind the holder.
* @return
*/
private boolean tryBindViewHolderByDeadline(@NonNull ViewHolder holder, int offsetPosition,
int position, long deadlineNs) {
holder.mOwnerRecyclerView = RecyclerView.this;
final int viewType = holder.getItemViewType();
long startBindNs = getNanoTime();
if (deadlineNs != FOREVER_NS
&& !mRecyclerPool.willBindInTime(viewType, startBindNs, deadlineNs)) {
// abort - we have a deadline we can't meet
return false;
}
mAdapter.bindViewHolder(holder, offsetPosition);
long endBindNs = getNanoTime();
mRecyclerPool.factorInBindTime(holder.getItemViewType(), endBindNs - startBindNs);
attachAccessibilityDelegateOnBind(holder);
if (mState.isPreLayout()) {
holder.mPreLayoutPosition = position;
}
return true;
}
/**
* Binds the given View to the position. The View can be a View previously retrieved via
* {@link #getViewForPosition(int)} or created by
* {@link Adapter#onCreateViewHolder(ViewGroup, int)}.
*
* Generally, a LayoutManager should acquire its views via {@link #getViewForPosition(int)}
* and let the RecyclerView handle caching. This is a helper method for LayoutManager who
* wants to handle its own recycling logic.
*
* Note that, {@link #getViewForPosition(int)} already binds the View to the position so
* you don't need to call this method unless you want to bind this View to another position.
*
* @param view The view to update.
* @param position The position of the item to bind to this View.
*/
public void bindViewToPosition(@NonNull View view, int position) {
ViewHolder holder = getChildViewHolderInt(view);
if (holder == null) {
throw new IllegalArgumentException("The view does not have a ViewHolder. You cannot"
+ " pass arbitrary views to this method, they should be created by the "
+ "Adapter" + exceptionLabel());
}
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
+ "position " + position + "(offset:" + offsetPosition + ")."
+ "state:" + mState.getItemCount() + exceptionLabel());
}
tryBindViewHolderByDeadline(holder, offsetPosition, position, FOREVER_NS);
final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
final LayoutParams rvLayoutParams;
if (lp == null) {
rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
holder.itemView.setLayoutParams(rvLayoutParams);
} else if (!checkLayoutParams(lp)) {
rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
holder.itemView.setLayoutParams(rvLayoutParams);
} else {
rvLayoutParams = (LayoutParams) lp;
}
rvLayoutParams.mInsetsDirty = true;
rvLayoutParams.mViewHolder = holder;
rvLayoutParams.mPendingInvalidate = holder.itemView.getParent() == null;
}
/**
* RecyclerView provides artificial position range (item count) in pre-layout state and
* automatically maps these positions to {@link Adapter} positions when
* {@link #getViewForPosition(int)} or {@link #bindViewToPosition(View, int)} is called.
*
* Usually, LayoutManager does not need to worry about this. However, in some cases, your
* LayoutManager may need to call some custom component with item positions in which
* case you need the actual adapter position instead of the pre layout position. You
* can use this method to convert a pre-layout position to adapter (post layout) position.
*
* Note that if the provided position belongs to a deleted ViewHolder, this method will
* return -1.
*
* Calling this method in post-layout state returns the same value back.
*
* @param position The pre-layout position to convert. Must be greater or equal to 0 and
* less than {@link State#getItemCount()}.
*/
public int convertPreLayoutPositionToPostLayout(int position) {
if (position < 0 || position >= mState.getItemCount()) {
throw new IndexOutOfBoundsException("invalid position " + position + ". State "
+ "item count is " + mState.getItemCount() + exceptionLabel());
}
if (!mState.isPreLayout()) {
return position;
}
return mAdapterHelper.findPositionOffset(position);
}
/**
* Obtain a view initialized for the given position.
*
* This method should be used by {@link LayoutManager} implementations to obtain
* views to represent data from an {@link Adapter}.
*
* The Recycler may reuse a scrap or detached view from a shared pool if one is
* available for the correct view type. If the adapter has not indicated that the
* data at the given position has changed, the Recycler will attempt to hand back
* a scrap view that was previously initialized for that data without rebinding.
*
* @param position Position to obtain a view for
* @return A view representing the data at
* If a deadlineNs other than {@link #FOREVER_NS} is passed, this method early return
* rather than constructing or binding a ViewHolder if it doesn't think it has time.
* If a ViewHolder must be constructed and not enough time remains, null is returned. If a
* ViewHolder is aquired and must be bound but not enough time remains, an unbound holder is
* returned. Use {@link ViewHolder#isBound()} on the returned object to check for this.
*
* @param position Position of ViewHolder to be returned.
* @param dryRun True if the ViewHolder should not be removed from scrap/cache/
* @param deadlineNs Time, relative to getNanoTime(), by which bind/create work should
* complete. If FOREVER_NS is passed, this method will not fail to
* create/bind the holder if needed.
*
* @return ViewHolder for requested position
*/
@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
if (position < 0 || position >= mState.getItemCount()) {
throw new IndexOutOfBoundsException("Invalid item position " + position
+ "(" + position + "). Item count:" + mState.getItemCount()
+ exceptionLabel());
}
boolean fromScrapOrHiddenOrCache = false;
ViewHolder holder = null;
// 0) If there is a changed scrap, try to find from there
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
// 1) Find by position from scrap/hidden list/cache
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if (holder != null) {
if (!validateViewHolderForOffsetPosition(holder)) {
// recycle holder (and unscrap if relevant) since it can't be used
if (!dryRun) {
// we would like to recycle this but need to make sure it is not used by
// animation logic etc.
holder.addFlags(ViewHolder.FLAG_INVALID);
if (holder.isScrap()) {
removeDetachedView(holder.itemView, false);
holder.unScrap();
} else if (holder.wasReturnedFromScrap()) {
holder.clearReturnedFromScrapFlag();
}
recycleViewHolderInternal(holder);
}
holder = null;
} else {
fromScrapOrHiddenOrCache = true;
}
}
}
if (holder == null) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
+ "position " + position + "(offset:" + offsetPosition + ")."
+ "state:" + mState.getItemCount() + exceptionLabel());
}
final int type = mAdapter.getItemViewType(offsetPosition);
// 2) Find from scrap/cache via stable ids, if exists
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
if (holder != null) {
// update position
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
}
}
if (holder == null && mViewCacheExtension != null) {
// We are NOT sending the offsetPosition because LayoutManager does not
// know it.
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
if (holder == null) {
throw new IllegalArgumentException("getViewForPositionAndType returned"
+ " a view which does not have a ViewHolder"
+ exceptionLabel());
} else if (holder.shouldIgnore()) {
throw new IllegalArgumentException("getViewForPositionAndType returned"
+ " a view that is ignored. You must call stopIgnoring before"
+ " returning this view." + exceptionLabel());
}
}
}
if (holder == null) { // fallback to pool
if (DEBUG) {
Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
+ position + ") fetching from shared pool");
}
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
if (holder == null) {
long start = getNanoTime();
if (deadlineNs != FOREVER_NS
&& !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
// abort - we have a deadline we can't meet
return null;
}
holder = mAdapter.createViewHolder(RecyclerView.this, type);
if (ALLOW_THREAD_GAP_WORK) {
// only bother finding nested RV if prefetching
RecyclerView innerView = findNestedRecyclerView(holder.itemView);
if (innerView != null) {
holder.mNestedRecyclerView = new WeakReference<>(innerView);
}
}
long end = getNanoTime();
mRecyclerPool.factorInCreateTime(type, end - start);
if (DEBUG) {
Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder");
}
}
}
// This is very ugly but the only place we can grab this information
// before the View is rebound and returned to the LayoutManager for post layout ops.
// We don't need this in pre-layout since the VH is not updated by the LM.
if (fromScrapOrHiddenOrCache && !mState.isPreLayout() && holder
.hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) {
holder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
if (mState.mRunSimpleAnimations) {
int changeFlags = ItemAnimator
.buildAdapterChangeFlagsForAnimations(holder);
changeFlags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
final ItemHolderInfo info = mItemAnimator.recordPreLayoutInformation(mState,
holder, changeFlags, holder.getUnmodifiedPayloads());
recordAnimationInfoIfBouncedHiddenView(holder, info);
}
}
boolean bound = false;
if (mState.isPreLayout() && holder.isBound()) {
// do not update unless we absolutely have to.
holder.mPreLayoutPosition = position;
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
if (DEBUG && holder.isRemoved()) {
throw new IllegalStateException("Removed holder should be bound and it should"
+ " come here only in pre-layout. Holder: " + holder
+ exceptionLabel());
}
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
final LayoutParams rvLayoutParams;
if (lp == null) {
rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
holder.itemView.setLayoutParams(rvLayoutParams);
} else if (!checkLayoutParams(lp)) {
rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
holder.itemView.setLayoutParams(rvLayoutParams);
} else {
rvLayoutParams = (LayoutParams) lp;
}
rvLayoutParams.mViewHolder = holder;
rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
return holder;
}
private void attachAccessibilityDelegateOnBind(ViewHolder holder) {
if (isAccessibilityEnabled()) {
final View itemView = holder.itemView;
if (ViewCompat.getImportantForAccessibility(itemView)
== ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
ViewCompat.setImportantForAccessibility(itemView,
ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
}
AccessibilityDelegateCompat delegate =
ViewCompat.getAccessibilityDelegate(itemView);
if (delegate == null
|| delegate.getClass().equals(AccessibilityDelegateCompat.class)) {
holder.addFlags(ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE);
ViewCompat.setAccessibilityDelegate(itemView,
mAccessibilityDelegate.getItemDelegate());
}
}
}
private void invalidateDisplayListInt(ViewHolder holder) {
if (holder.itemView instanceof ViewGroup) {
invalidateDisplayListInt((ViewGroup) holder.itemView, false);
}
}
private void invalidateDisplayListInt(ViewGroup viewGroup, boolean invalidateThis) {
for (int i = viewGroup.getChildCount() - 1; i >= 0; i--) {
final View view = viewGroup.getChildAt(i);
if (view instanceof ViewGroup) {
invalidateDisplayListInt((ViewGroup) view, true);
}
}
if (!invalidateThis) {
return;
}
// we need to force it to become invisible
if (viewGroup.getVisibility() == View.INVISIBLE) {
viewGroup.setVisibility(View.VISIBLE);
viewGroup.setVisibility(View.INVISIBLE);
} else {
final int visibility = viewGroup.getVisibility();
viewGroup.setVisibility(View.INVISIBLE);
viewGroup.setVisibility(visibility);
}
}
/**
* Recycle a detached view. The specified view will be added to a pool of views
* for later rebinding and reuse.
*
* A view must be fully detached (removed from parent) before it may be recycled. If the
* View is scrapped, it will be removed from scrap list.
* A small exception to this rule is when the view does not have an animator reference
* but transient state is true (due to animations created outside ItemAnimator). In that
* case, adapter may choose to recycle it. From RecyclerView's perspective, the view is
* still recyclable since Adapter wants to do so.
*
* @param cachedViewIndex The index of the view in cached views list
*/
void recycleCachedViewAt(int cachedViewIndex) {
if (DEBUG) {
Log.d(TAG, "Recycling cached view at index " + cachedViewIndex);
}
ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
if (DEBUG) {
Log.d(TAG, "CachedViewHolder to be recycled: " + viewHolder);
}
addViewHolderToRecycledViewPool(viewHolder, true);
mCachedViews.remove(cachedViewIndex);
}
/**
* internal implementation checks if view is scrapped or attached and throws an exception
* if so.
* Public version un-scraps before calling recycle.
*/
void recycleViewHolderInternal(ViewHolder holder) {
if (holder.isScrap() || holder.itemView.getParent() != null) {
throw new IllegalArgumentException(
"Scrapped or attached views may not be recycled. isScrap:"
+ holder.isScrap() + " isAttached:"
+ (holder.itemView.getParent() != null) + exceptionLabel());
}
if (holder.isTmpDetached()) {
throw new IllegalArgumentException("Tmp detached view should be removed "
+ "from RecyclerView before it can be recycled: " + holder
+ exceptionLabel());
}
if (holder.shouldIgnore()) {
throw new IllegalArgumentException("Trying to recycle an ignored view holder. You"
+ " should first call stopIgnoringView(view) before calling recycle."
+ exceptionLabel());
}
final boolean transientStatePreventsRecycling = holder
.doesTransientStatePreventRecycling();
@SuppressWarnings("unchecked")
final boolean forceRecycle = mAdapter != null
&& transientStatePreventsRecycling
&& mAdapter.onFailedToRecycleView(holder);
boolean cached = false;
boolean recycled = false;
if (DEBUG && mCachedViews.contains(holder)) {
throw new IllegalArgumentException("cached view received recycle internal? "
+ holder + exceptionLabel());
}
if (forceRecycle || holder.isRecyclable()) {
if (mViewCacheMax > 0
&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_REMOVED
| ViewHolder.FLAG_UPDATE
| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
// Retire oldest cached view
int cachedViewSize = mCachedViews.size();
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
recycleCachedViewAt(0);
cachedViewSize--;
}
int targetCacheIndex = cachedViewSize;
if (ALLOW_THREAD_GAP_WORK
&& cachedViewSize > 0
&& !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
// when adding the view, skip past most recently prefetched views
int cacheIndex = cachedViewSize - 1;
while (cacheIndex >= 0) {
int cachedPos = mCachedViews.get(cacheIndex).mPosition;
if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
break;
}
cacheIndex--;
}
targetCacheIndex = cacheIndex + 1;
}
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
if (!cached) {
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
} else {
// NOTE: A view can fail to be recycled when it is scrolled off while an animation
// runs. In this case, the item is eventually recycled by
// ItemAnimatorRestoreListener#onAnimationFinished.
// TODO: consider cancelling an animation when an item is removed scrollBy,
// to return it to the pool faster
if (DEBUG) {
Log.d(TAG, "trying to recycle a non-recycleable holder. Hopefully, it will "
+ "re-visit here. We are still removing it from animation lists"
+ exceptionLabel());
}
}
// even if the holder is not removed, we still call this method so that it is removed
// from view holder lists.
mViewInfoStore.removeViewHolder(holder);
if (!cached && !recycled && transientStatePreventsRecycling) {
holder.mOwnerRecyclerView = null;
}
}
/**
* Prepares the ViewHolder to be removed/recycled, and inserts it into the RecycledViewPool.
*
* Pass false to dispatchRecycled for views that have not been bound.
*
* @param holder Holder to be added to the pool.
* @param dispatchRecycled True to dispatch View recycled callbacks.
*/
void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {
clearNestedRecyclerViewIfNotNested(holder);
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE)) {
holder.setFlags(0, ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE);
ViewCompat.setAccessibilityDelegate(holder.itemView, null);
}
if (dispatchRecycled) {
dispatchViewRecycled(holder);
}
holder.mOwnerRecyclerView = null;
getRecycledViewPool().putRecycledView(holder);
}
/**
* Used as a fast path for unscrapping and recycling a view during a bulk operation.
* The caller must call {@link #clearScrap()} when it's done to update the recycler's
* internal bookkeeping.
*/
void quickRecycleScrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
holder.mScrapContainer = null;
holder.mInChangeScrap = false;
holder.clearReturnedFromScrapFlag();
recycleViewHolderInternal(holder);
}
/**
* Mark an attached view as scrap.
*
* "Scrap" views are still attached to their parent RecyclerView but are eligible
* for rebinding and reuse. Requests for a view for a given position may return a
* reused or rebound scrap view instance. This view will no longer be eligible for reuse until re-scrapped or
* until it is explicitly removed and recycled.
* When {@link Recycler#getViewForPosition(int)} is called, Recycler checks attached scrap and
* first level cache to find a matching View. If it cannot find a suitable View, Recycler will
* call the {@link #getViewForPositionAndType(Recycler, int, int)} before checking
* {@link RecycledViewPool}.
*
* Note that, Recycler never sends Views to this method to be cached. It is developers
* responsibility to decide whether they want to keep their Views in this custom cache or let
* the default recycling policy handle it.
*/
public abstract static class ViewCacheExtension {
/**
* Returns a View that can be binded to the given Adapter position.
*
* This method should not create a new View. Instead, it is expected to return
* an already created View that can be re-used for the given type and position.
* If the View is marked as ignored, it should first call
* {@link LayoutManager#stopIgnoringView(View)} before returning the View.
*
* RecyclerView will re-bind the returned View to the position if necessary.
*
* @param recycler The Recycler that can be used to bind the View
* @param position The adapter position
* @param type The type of the View, defined by adapter
* @return A View that is bound to the given position or NULL if there is no View to re-use
* @see LayoutManager#ignoreView(View)
*/
@Nullable
public abstract View getViewForPositionAndType(@NonNull Recycler recycler, int position,
int type);
}
/**
* Base class for an Adapter
*
* Adapters provide a binding from an app-specific data set to views that are displayed
* within a {@link RecyclerView}.
* This new ViewHolder should be constructed with a new View that can represent the items
* of the given type. You can either create a new View manually or inflate it from an XML
* layout file.
*
* The new ViewHolder will be used to display items of the adapter using
* {@link #onBindViewHolder(ViewHolder, int, List)}. Since it will be re-used to display
* different items in the data set, it is a good idea to cache references to sub views of
* the View to avoid unnecessary {@link View#findViewById(int)} calls.
*
* @param parent The ViewGroup into which the new View will be added after it is bound to
* an adapter position.
* @param viewType The view type of the new View.
*
* @return A new ViewHolder that holds a View of the given view type.
* @see #getItemViewType(int)
* @see #onBindViewHolder(ViewHolder, int)
*/
@NonNull
public abstract VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType);
/**
* Called by RecyclerView to display the data at the specified position. This method should
* update the contents of the {@link ViewHolder#itemView} to reflect the item at the given
* position.
*
* Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method
* again if the position of the item changes in the data set unless the item itself is
* invalidated or the new position cannot be determined. For this reason, you should only
* use the
* Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method
* again if the position of the item changes in the data set unless the item itself is
* invalidated or the new position cannot be determined. For this reason, you should only
* use the
* Partial bind vs full bind:
*
* The payloads parameter is a merge list from {@link #notifyItemChanged(int, Object)} or
* {@link #notifyItemRangeChanged(int, int, Object)}. If the payloads list is not empty,
* the ViewHolder is currently bound to old data and Adapter may run an efficient partial
* update using the payload info. If the payload is empty, Adapter must run a full bind.
* Adapter should not assume that the payload passed in notify methods will be received by
* onBindViewHolder(). For example when the view is not attached to the screen, the
* payload in notifyItemChange() will be simply dropped.
*
* @param holder The ViewHolder which should be updated to represent the contents of the
* item at the given position in the data set.
* @param position The position of the item within the adapter's data set.
* @param payloads A non-null list of merged payloads. Can be empty list if requires full
* update.
*/
public void onBindViewHolder(@NonNull VH holder, int position,
@NonNull ListTOUCH_SLOP_
constants representing
* the intended usage of this RecyclerView
*/
public void setScrollingTouchSlop(int slopConstant) {
final ViewConfiguration vc = ViewConfiguration.get(getContext());
switch (slopConstant) {
default:
Log.w(TAG, "setScrollingTouchSlop(): bad argument constant "
+ slopConstant + "; using default value");
// fall-through
case TOUCH_SLOP_DEFAULT:
mTouchSlop = vc.getScaledTouchSlop();
break;
case TOUCH_SLOP_PAGING:
mTouchSlop = vc.getScaledPagingTouchSlop();
break;
}
}
/**
* Swaps the current adapter with the provided one. It is similar to
* {@link #setAdapter(Adapter)} but assumes existing adapter and the new adapter uses the same
* {@link ViewHolder} and does not clear the RecycledViewPool.
* suppressLayout(true)
does not prevent app from directly calling {@link
* LayoutManager#scrollToPosition(int)}, {@link LayoutManager#smoothScrollToPosition(
* RecyclerView, State, int)}.
* setLayoutFrozen(true)
is called,
* Layout requests will be postponed until setLayoutFrozen(false)
is called;
* child views are not updated when RecyclerView is frozen, {@link #smoothScrollBy(int, int)},
* {@link #scrollBy(int, int)}, {@link #scrollToPosition(int)} and
* {@link #smoothScrollToPosition(int)} are dropped; TouchEvents and GenericMotionEvents are
* dropped; {@link LayoutManager#onFocusSearchFailed(View, int, Recycler, State)} will not be
* called.
*
* setLayoutFrozen(true)
does not prevent app from directly calling {@link
* LayoutManager#scrollToPosition(int)}, {@link LayoutManager#smoothScrollToPosition(
* RecyclerView, State, int)}.
* true
if RecyclerView is currently computing a layout, false
* otherwise
*/
public boolean isComputingLayout() {
return mLayoutOrScrollCounter > 0;
}
/**
* Returns true if an accessibility event should not be dispatched now. This happens when an
* accessibility request arrives while RecyclerView does not have a stable state which is very
* hard to handle for a LayoutManager. Instead, this method records necessary information about
* the event and dispatches a window change event after the critical section is finished.
*
* @return True if the accessibility event should be postponed.
*/
boolean shouldDeferAccessibilityEvent(AccessibilityEvent event) {
if (isComputingLayout()) {
int type = 0;
if (event != null) {
type = AccessibilityEventCompat.getContentChangeTypes(event);
}
if (type == 0) {
type = AccessibilityEventCompat.CONTENT_CHANGE_TYPE_UNDEFINED;
}
mEatenAccessibilityChangeFlags |= type;
return true;
}
return false;
}
@Override
public void sendAccessibilityEventUnchecked(AccessibilityEvent event) {
if (shouldDeferAccessibilityEvent(event)) {
return;
}
super.sendAccessibilityEventUnchecked(event);
}
@Override
public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
onPopulateAccessibilityEvent(event);
return true;
}
/**
* 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.
*/
@Nullable
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.
*/
void postAnimationRunner() {
if (!mPostedAnimatorRunner && mIsAttached) {
ViewCompat.postOnAnimation(this, mItemAnimatorRunner);
mPostedAnimatorRunner = true;
}
}
private boolean predictiveItemAnimationsEnabled() {
return (mItemAnimator != null && mLayout.supportsPredictiveItemAnimations());
}
/**
* Consumes adapter updates and calculates which type of animations we want to run.
* Called in onMeasure and dispatchLayout.
*
*
*
* @param dispatchItemsChanged Whether to call
* {@link LayoutManager#onItemsChanged(RecyclerView)} during measure/layout.
*/
void processDataSetCompletelyChanged(boolean dispatchItemsChanged) {
mDispatchItemsChangedEvent |= dispatchItemsChanged;
mDataSetHasChangedAfterLayout = true;
markKnownViewsInvalid();
}
/**
* Mark all known views as invalid. Used in response to a, "the whole world might have changed"
* data change event.
*/
void markKnownViewsInvalid() {
final int childCount = mChildHelper.getUnfilteredChildCount();
for (int i = 0; i < childCount; i++) {
final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
if (holder != null && !holder.shouldIgnore()) {
holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID);
}
}
markItemDecorInsetsDirty();
mRecycler.markKnownViewsInvalid();
}
/**
* Invalidates all ItemDecorations. If RecyclerView has item decorations, calling this method
* will trigger a {@link #requestLayout()} call.
*/
public void invalidateItemDecorations() {
if (mItemDecorations.size() == 0) {
return;
}
if (mLayout != null) {
mLayout.assertNotInLayoutOrScroll("Cannot invalidate item decorations during a scroll"
+ " or layout");
}
markItemDecorInsetsDirty();
requestLayout();
}
/**
* Returns true if the RecyclerView should attempt to preserve currently focused Adapter Item's
* focus even if the View representing the Item is replaced during a layout calculation.
* position
is not laid out, it will not create a new one.
* position
or null if there is no such item
*/
@Nullable
public ViewHolder findViewHolderForLayoutPosition(int position) {
return findViewHolderForPosition(position, false);
}
/**
* Return the ViewHolder for the item in the given position of the data set. Unlike
* {@link #findViewHolderForLayoutPosition(int)} this method takes into account any pending
* adapter changes that may not be reflected to the layout yet. On the other hand, if
* {@link Adapter#notifyDataSetChanged()} has been called but the new layout has not been
* calculated yet, this method will return null
since the new positions of views
* are unknown until the layout is calculated.
* position
is not laid out, it will not create a new one.
* position
or null if there is no such item
*/
@Nullable
public ViewHolder findViewHolderForAdapterPosition(int position) {
if (mDataSetHasChangedAfterLayout) {
return null;
}
final int childCount = mChildHelper.getUnfilteredChildCount();
// hidden VHs are not preferred but if that is the only one we find, we rather return it
ViewHolder hidden = null;
for (int i = 0; i < childCount; i++) {
final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
if (holder != null && !holder.isRemoved()
&& getAdapterPositionFor(holder) == position) {
if (mChildHelper.isHidden(holder.itemView)) {
hidden = holder;
} else {
return holder;
}
}
}
return hidden;
}
@Nullable
ViewHolder findViewHolderForPosition(int position, boolean checkNewPosition) {
final int childCount = mChildHelper.getUnfilteredChildCount();
ViewHolder hidden = null;
for (int i = 0; i < childCount; i++) {
final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
if (holder != null && !holder.isRemoved()) {
if (checkNewPosition) {
if (holder.mPosition != position) {
continue;
}
} else if (holder.getLayoutPosition() != position) {
continue;
}
if (mChildHelper.isHidden(holder.itemView)) {
hidden = holder;
} else {
return holder;
}
}
}
// This method should not query cached views. It creates a problem during adapter updates
// when we are dealing with already laid out views. Also, for the public method, it is more
// reasonable to return null if position is not laid out.
return hidden;
}
/**
* Return the ViewHolder for the item with the given id. The RecyclerView must
* use an Adapter with {@link Adapter#setHasStableIds(boolean) stableIds} to
* return a non-null value.
* id
is not laid out, it will not create a new one.
*
* When the ItemAnimator is running a change animation, there might be 2 ViewHolders with the
* same id. In this case, the updated ViewHolder will be returned.
*
* @param id The id for the requested item
* @return The ViewHolder with the given id
or null if there is no such item
*/
public ViewHolder findViewHolderForItemId(long id) {
if (mAdapter == null || !mAdapter.hasStableIds()) {
return null;
}
final int childCount = mChildHelper.getUnfilteredChildCount();
ViewHolder hidden = null;
for (int i = 0; i < childCount; i++) {
final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
if (holder != null && !holder.isRemoved() && holder.getItemId() == id) {
if (mChildHelper.isHidden(holder.itemView)) {
hidden = holder;
} else {
return holder;
}
}
}
return hidden;
}
/**
* Find the topmost view under the given point.
*
* @param x Horizontal position in pixels to search
* @param y Vertical position in pixels to search
* @return The child view under (x, y) or null if no matching child is found
*/
@Nullable
public View findChildViewUnder(float x, float y) {
final int count = mChildHelper.getChildCount();
for (int i = count - 1; i >= 0; i--) {
final View child = mChildHelper.getChildAt(i);
final float translationX = child.getTranslationX();
final float translationY = child.getTranslationY();
if (x >= child.getLeft() + translationX
&& x <= child.getRight() + translationX
&& y >= child.getTop() + translationY
&& y <= child.getBottom() + translationY) {
return child;
}
}
return null;
}
@Override
public boolean drawChild(Canvas canvas, View child, long drawingTime) {
return super.drawChild(canvas, child, drawingTime);
}
/**
* Offset the bounds of all child views by dy
pixels.
* Useful for implementing simple scrolling in {@link LayoutManager LayoutManagers}.
*
* @param dy Vertical pixel offset to apply to the bounds of all child views
*/
public void offsetChildrenVertical(@Px int dy) {
final int childCount = mChildHelper.getChildCount();
for (int i = 0; i < childCount; i++) {
mChildHelper.getChildAt(i).offsetTopAndBottom(dy);
}
}
/**
* Called when an item view is attached to this RecyclerView.
*
* dx
pixels.
* Useful for implementing simple scrolling in {@link LayoutManager LayoutManagers}.
*
* @param dx Horizontal pixel offset to apply to the bounds of all child views
*/
public void offsetChildrenHorizontal(@Px int dx) {
final int childCount = mChildHelper.getChildCount();
for (int i = 0; i < childCount; i++) {
mChildHelper.getChildAt(i).offsetLeftAndRight(dx);
}
}
/**
* Returns the bounds of the view including its decoration and margins.
*
* @param view The view element to check
* @param outBounds A rect that will receive the bounds of the element including its
* decoration and margins.
*/
public void getDecoratedBoundsWithMargins(@NonNull View view, @NonNull Rect outBounds) {
getDecoratedBoundsWithMarginsInt(view, outBounds);
}
static void getDecoratedBoundsWithMarginsInt(View view, Rect outBounds) {
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
final Rect insets = lp.mDecorInsets;
outBounds.set(view.getLeft() - insets.left - lp.leftMargin,
view.getTop() - insets.top - lp.topMargin,
view.getRight() + insets.right + lp.rightMargin,
view.getBottom() + insets.bottom + lp.bottomMargin);
}
Rect getItemDecorInsetsForChild(View child) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (!lp.mInsetsDirty) {
return lp.mDecorInsets;
}
if (mState.isPreLayout() && (lp.isItemChanged() || lp.isViewInvalid())) {
// changed/invalid items should not be updated until they are rebound.
return lp.mDecorInsets;
}
final Rect insets = lp.mDecorInsets;
insets.set(0, 0, 0, 0);
final int decorCount = mItemDecorations.size();
for (int i = 0; i < decorCount; i++) {
mTempRect.set(0, 0, 0, 0);
mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
insets.left += mTempRect.left;
insets.top += mTempRect.top;
insets.right += mTempRect.right;
insets.bottom += mTempRect.bottom;
}
lp.mInsetsDirty = false;
return insets;
}
/**
* Called when the scroll position of this RecyclerView changes. Subclasses should use
* this method to respond to scrolling within the adapter's data set instead of an explicit
* listener.
*
* onScrollChanged
will always receive
* the current {@link View#getScrollX()} and {@link View#getScrollY()} values which
* do not correspond to the data set scroll position. However, some subclasses may choose
* to use these fields as special offsets.true
, it means that what user is currently seeing may not
* reflect them adapter contents (depending on what has changed).
* You may use this information to defer or cancel some operations.
* position
from adapter
*/
@NonNull
public View getViewForPosition(int position) {
return getViewForPosition(position, false);
}
View getViewForPosition(int position, boolean dryRun) {
return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
/**
* Attempts to get the ViewHolder for the given position, either from the Recycler scrap,
* cache, the RecycledViewPool, or creating it directly.
* position
parameter while acquiring the related data item inside
* this method and should not keep a copy of it. If you need the position of an item later
* on (e.g. in a click listener), use {@link ViewHolder#getAdapterPosition()} which will
* have the updated adapter position.
*
* Override {@link #onBindViewHolder(ViewHolder, int, List)} instead if Adapter can
* handle efficient partial bind.
*
* @param holder The ViewHolder which should be updated to represent the contents of the
* item at the given position in the data set.
* @param position The position of the item within the adapter's data set.
*/
public abstract void onBindViewHolder(@NonNull VH holder, int position);
/**
* Called by RecyclerView to display the data at the specified position. This method
* should update the contents of the {@link ViewHolder#itemView} to reflect the item at
* the given position.
* position
parameter while acquiring the related data item inside
* this method and should not keep a copy of it. If you need the position of an item later
* on (e.g. in a click listener), use {@link ViewHolder#getAdapterPosition()} which will
* have the updated adapter position.
*