compile group: 'androidx.recyclerview', name: 'recyclerview', version: '1.4.0-beta01'
Artifact androidx.recyclerview:recyclerview:1.4.0-beta01 it located at Google repository (https://maven.google.com/)
To display updatable data in a RecyclerView, your adapter needs to signal inserts, moves, and
deletions to RecyclerView. You can build this yourself by manually calling
adapter.notify* methods when content changes, or you can use one of the easier solutions
RecyclerView provides:
If your RecyclerView is displaying a list that is re-fetched from scratch for each update (e.g.
from the network, or from a database),
can calculate the difference between
versions of the list. DiffUtil takes both lists as input and computes the difference,
which can be passed to RecyclerView to trigger minimal animations and updates to keep your UI
performant, and animations meaningful. This approach requires that each list is represented in
memory with immutable content, and relies on receiving updates as new instances of lists. This
approach is also ideal if your UI layer doesn't implement sorting, it just presents the data in
the order it's given.
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
ListAdapter, the higher-level API that builds in java.util.List
diffing on a background
thread, with minimal code. AsyncListDiffer also provides this behavior, but without
defining an Adapter to subclass. If you want more control, 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.
If your RecyclerView receives updates incrementally, e.g. item X is inserted, or item Y is
removed, you can use
to manage your list. You define how to order items,
and it will automatically trigger update signals that RecyclerView can use. SortedList works
if you only need to handle insert and remove events, and has the benefit that you only ever
need to have a single copy of the list in memory. It can also compute differences with
extends the diff-based approach to additionally support paged loading. It provides
the
class that operates as a self-loading list, provided a
source of data like a database, or paginated network API. It provides convenient list diffing
support out of the box, similar to ListAdapter and AsyncListDiffer. For more
information about the Paging library, see the
Constant that represents that a duration has not been defined.
The RecyclerView is not currently scrolling.
The RecyclerView is currently being dragged by outside input such as user touch input.
The RecyclerView is currently animating to a final position while not under
outside control.
Enable internal assertions about RecyclerView's state and throw exceptions if the
assertions are violated.
Enabling this may negatively affect performance and/or stability.
Enabling this may negatively affect performance and reduce the utility of logcat due to
high-volume logging. This generally should not be enabled in production
unless you have a specific reason for doing so.
Returns the accessibility delegate compatibility implementation used by the RecyclerView.
Sets the accessibility delegate compatibility implementation used by RecyclerView.
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 true. It will allow
RecyclerView to avoid invalidating the whole layout when its adapter contents change.
true if the app has specified that changes in adapter content cannot change
the size of the RecyclerView itself.
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.
true if this RecyclerView clips children to its padding and resizes (but doesn't
clip) any EdgeEffect to the padded region, false otherwise.
Note that it still calls onAdapterChanged callbacks.
When adapter is changed, all existing views are recycled back to the pool. If the pool has
only one adapter, it will be cleared.
Retrieves the previously set adapter or null if no adapter is set.
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.
The listeners will be called when a LayoutManager or the RecyclerView decides
that a child view is no longer needed. If an application associates data with
the item views being recycled, this may be a good place to release
or free those resources.
Removes the provided listener from RecyclerListener list.
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.
the offset of the baseline within the RecyclerView's bounds or -1
if baseline alignment is not supported
Register a listener that will be notified whenever a child view is attached to or detached
from RecyclerView.
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.
Removes the provided listener from child attached state listeners list.
In contrast to other adapter-backed views such as
or , RecyclerView allows client code to provide custom
layout arrangements for child views. These arrangements are controlled by the
RecyclerView.LayoutManager. A LayoutManager must be provided for RecyclerView to function.
Several default strategies are provided for common uses such as lists and grids.
Override to prevent freezing of any views created by the adapter.
Override to prevent thawing of any views created by the adapter.
The pool used to store recycled item views for reuse.
Recycled view pools allow multiple RecyclerViews to share a common pool of scrap views.
This can be useful if you have multiple RecyclerViews with adapters that use the same
view types, for example if you have several data sets with the same kinds of item views
displayed by a ViewPager.
Set the number of offscreen views to retain before adding them to the potentially shared
recycled view pool.
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.
Return the current scrolling state of the RecyclerView.
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.
number of ItemDecorations currently added added to this RecyclerView.
The given decoration will no longer impact the measurement and drawing of
item views.
Note that child drawing order may be overridden by View's elevation.
Set a listener that will be notified of any changes in scroll state or position.
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 RecyclerView.clearOnScrollListeners()
to remove all attached listeners.
Remove a listener that was notified of any changes in scroll state or position.
Remove all secondary listener that were notified of any changes in scroll state or position.
Compute the horizontal offset of the horizontal scrollbar's thumb within the horizontal
range. This value is used to compute the position of the thumb within the scrollbar's track.
Default implementation returns 0.
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.
Default implementation returns 0.
Compute the horizontal range that the horizontal scrollbar represents.
Default implementation returns 0.
Compute the vertical offset of the vertical scrollbar's thumb within the vertical range.
This value is used to compute the position of the thumb within the scrollbar's track.
Default implementation returns 0.
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.
Default implementation returns 0.
Compute the vertical range that the vertical scrollbar represents.
Default implementation returns 0.
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.
Note: Running ItemAnimator is not stopped automatically, it's caller's
responsibility to call ItemAnimator.end().
Returns whether layout and scroll calls on this container are currently being
suppressed, due to an earlier call to RecyclerView.suppressLayout(boolean).
true if layout and scroll are currently suppressed, false otherwise.
Note: Running ItemAnimator is not stopped automatically, it's caller's
responsibility to call ItemAnimator.end().
Animate a scroll by the given amount of pixels along either axis.
Animate a scroll by the given amount of pixels along either axis.
Smooth scrolls the RecyclerView by a given distance.
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.
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.
Returns the minimum velocity to start a fling.
Returns the maximum fling velocity used by this RecyclerView.
The maximum fling velocity used by this RecyclerView.
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
RecyclerView.LayoutManager 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.
Returns true if RecyclerView is attached to window.
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.
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 DefaultItemAnimator.
ItemAnimator The current ItemAnimator. If null, no animations will occur
when changes occur to the items in this RecyclerView.
True if there are some item animations currently running or waiting to be started.
Invalidates all ItemDecorations. If RecyclerView has item decorations, calling this method
will trigger a RecyclerView.requestLayout() call.
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.
By default, this value is true.
True if the RecyclerView will try to preserve focused Item after a layout if it loses
focus.
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.
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 RecyclerView.getChildViewHolder(View).
The direct child of the RecyclerView which contains the given view or null if the
provided view is not a descendant of this RecyclerView.
Returns the ViewHolder that contains the given view.
The ViewHolder that contains the given view or null if the provided view is not a
descendant of this RecyclerView.
Return the adapter position that the given child view corresponds to.
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.
Return the stable item id that the given child view corresponds to.
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
position
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 layout position representing the same Item. In this case, the updated
ViewHolder will be returned.
Return the ViewHolder for the item in the given position of the data set. Unlike
RecyclerView.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
RecyclerView.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.
This method checks only the children of RecyclerView. If the item at the given
position
is not laid out, it will not create a new one.
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.
Return the ViewHolder for the item with the given id. The RecyclerView must
use an Adapter with stableIds to
return a non-null value.
This method checks only the children of RecyclerView. If the item with the given
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.
Find the topmost view under the given point.
Subclasses of RecyclerView may want to perform extra bookkeeping or modifications
of child views as they become attached. This will be called before a
RecyclerView.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
RecyclerView.LayoutManager fully detaches the child view from the parent and its window.
Returns the bounds of the view including its decoration and margins.
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.
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.
Called when the scroll state of this RecyclerView changes. Subclasses should use this
method to respond to state changes instead of an explicit listener.
This method will always be invoked before listeners, but after the LayoutManager
responds to the scroll state change.
Returns whether there are pending adapter updates which are not yet applied to the layout.
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.
True if there are some adapter updates which are not yet reflected to layout or false
if layout is up to date.
/*
* 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;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
import static androidx.core.util.Preconditions.checkArgument;
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.hardware.SensorManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.SystemClock;
import android.os.Trace;
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.KeyEvent;
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 androidx.annotation.CallSuper;
import androidx.annotation.DoNotInline;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.annotation.RequiresApi;
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.DifferentialMotionFlingController;
import androidx.core.view.DifferentialMotionFlingTarget;
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.poolingcontainer.PoolingContainer;
import androidx.customview.poolingcontainer.PoolingContainerListener;
import androidx.customview.view.AbsSavedState;
import androidx.recyclerview.R;
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.IdentityHashMap;
import java.util.List;
import java.util.Set;
/**
* A flexible view for providing a limited window into a large data set.
*
* <h3>Glossary of terms:</h3>
*
* <ul>
* <li><em>Adapter:</em> A subclass of {@link Adapter} responsible for providing views
* that represent items in a data set.</li>
* <li><em>Position:</em> The position of a data item within an <em>Adapter</em>.</li>
* <li><em>Index:</em> The index of an attached child view as used in a call to
* {@link ViewGroup#getChildAt}. Contrast with <em>Position.</em></li>
* <li><em>Binding:</em> The process of preparing a child view to display data corresponding
* to a <em>position</em> within the adapter.</li>
* <li><em>Recycle (view):</em> A view previously used to display data for a specific adapter
* position may be placed in a cache for later reuse to display the same type of data again
* later. This can drastically improve performance by skipping initial layout inflation
* or construction.</li>
* <li><em>Scrap (view):</em> A child view that has entered into a temporarily detached
* state during layout. Scrap views may be reused without becoming fully detached
* from the parent RecyclerView, either unmodified if no rebinding is required or modified
* by the adapter if the view was considered <em>dirty</em>.</li>
* <li><em>Dirty (view):</em> A child view that must be rebound by the adapter before
* being displayed.</li>
* </ul>
*
* <h3>Positions in RecyclerView:</h3>
* <p>
* 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.
* <p>
* For this reason, there are two types of <code>position</code> related methods in RecyclerView:
* <ul>
* <li>layout position: Position of an item in the latest layout calculation. This is the
* position from the LayoutManager's perspective.</li>
* <li>adapter position: Position of an item in the adapter. This is the position from
* the Adapter's perspective.</li>
* </ul>
* <p>
* These two positions are the same except the time between dispatching <code>adapter.notify*
* </code> events and calculating the updated layout.
* <p>
* Methods that return or receive <code>*LayoutPosition*</code> 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 5<sup>th</sup> element, you should use these methods as they'll match what user
* is seeing.
* <p>
* The other set of position related methods are in the form of
* <code>*AdapterPosition*</code>. (e.g. {@link ViewHolder#getAbsoluteAdapterPosition()},
* {@link ViewHolder#getBindingAdapterPosition()},
* {@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#getBindingAdapterPosition()}. 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 <code>null</code> results from these methods.
* <p>
* 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.
* <p>
* <h3>Presenting Dynamic Data</h3>
* To display updatable data in a RecyclerView, your adapter needs to signal inserts, moves, and
* deletions to RecyclerView. You can build this yourself by manually calling
* {@code adapter.notify*} methods when content changes, or you can use one of the easier solutions
* RecyclerView provides:
* <p>
* <h4>List diffing with DiffUtil</h4>
* If your RecyclerView is displaying a list that is re-fetched from scratch for each update (e.g.
* from the network, or from a database), {@link DiffUtil} can calculate the difference between
* versions of the list. {@code DiffUtil} takes both lists as input and computes the difference,
* which can be passed to RecyclerView to trigger minimal animations and updates to keep your UI
* performant, and animations meaningful. This approach requires that each list is represented in
* memory with immutable content, and relies on receiving updates as new instances of lists. This
* approach is also ideal if your UI layer doesn't implement sorting, it just presents the data in
* the order it's given.
* <p>
* 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.
* <p>
* 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.
* <p>
* <h4>List mutation with SortedList</h4>
* If your RecyclerView receives updates incrementally, e.g. item X is inserted, or item Y is
* removed, you can use {@link SortedList} to manage your list. You define how to order items,
* and it will automatically trigger update signals that RecyclerView can use. SortedList works
* if you only need to handle insert and remove events, and has the benefit that you only ever
* need to have a single copy of the list in memory. It can also compute differences with
* {@link SortedList#replaceAll(Object[])}, but this method is more limited than the list diffing
* behavior above.
* <p>
* <h4>Paging Library</h4>
* The <a href="https://developer.android.com/topic/libraries/architecture/paging/">Paging
* library</a> extends the diff-based approach to additionally support paged loading. It provides
* the {@link androidx.paging.PagedList} class that operates as a self-loading list, provided a
* source of data like a database, or paginated network API. It provides convenient list diffing
* support out of the box, similar to {@code ListAdapter} and {@code AsyncListDiffer}. For more
* information about the Paging library, see the
* <a href="https://developer.android.com/topic/libraries/architecture/paging/">library
* documentation</a>.
*
* {@link androidx.recyclerview.R.attr#layoutManager}
*/
public class RecyclerView extends ViewGroup implements ScrollingView,
NestedScrollingChild2, NestedScrollingChild3 {
static final String TAG = "RecyclerView";
static boolean sDebugAssertionsEnabled = false;
static boolean sVerboseLoggingEnabled = false;
static final boolean VERBOSE_TRACING = false;
private static final int[] NESTED_SCROLLING_ATTRS =
{16843830 /* android.R.attr.nestedScrollingEnabled */};
/**
* The following are copied from OverScroller to determine how far a fling will go.
*/
private static final float SCROLL_FRICTION = 0.015f;
private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1)
private static final float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9));
private final float mPhysicalCoef;
/**
* On Kitkat and JB MR2, there is a bug which prevents DisplayList from being invalidated if
* a View is two levels deep(wrt to ViewHolder.itemView). DisplayList can be invalidated by
* setting View's visibility to INVISIBLE when View is detached. On Kitkat and JB MR2, Recycler
* recursively traverses itemView and invalidates display list for each ViewGroup that matches
* this criteria.
*/
static final boolean FORCE_INVALIDATE_DISPLAY_LIST = Build.VERSION.SDK_INT == 19
|| Build.VERSION.SDK_INT == 20;
/**
* On M+, an unspecified measure spec may include a hint which we can use. On older platforms,
* this value might be garbage. To save LayoutManagers from it, RecyclerView sets the size to
* 0 when mode is unspecified.
*/
static final boolean ALLOW_SIZE_IN_UNSPECIFIED_SPEC = Build.VERSION.SDK_INT >= 23;
/**
* On L+, with RenderThread, the UI thread has idle time after it has passed a frame off to
* RenderThread but before the next frame begins. We schedule prefetch work in this window.
*/
static final boolean ALLOW_THREAD_GAP_WORK = Build.VERSION.SDK_INT >= 21;
/**
* When flinging the stretch towards scrolling content, it should destretch quicker than the
* fling would normally do. The visual effect of flinging the stretch looks strange as little
* appears to happen at first and then when the stretch disappears, the content starts
* scrolling quickly.
*/
private static final float FLING_DESTRETCH_FACTOR = 4f;
/**
* A {@link android.content.pm.PackageManager} feature specifying if a device's rotary encoder
* has low resolution. Low resolution rotary encoders produce small number of
* {@link MotionEvent}s per a 360 degree rotation, meaning that each {@link MotionEvent} has
* large scroll values, which make {@link #scrollBy(int, int)} calls feel broken (due to the
* fact that each event produces large scrolls, and scrolling with large pixels causes a visible
* jump that does not feel smooth). As such, we will try adjusting our handling of generic
* motion caused by such low resolution rotary encoders differently to make the scrolling
* experience smooth.
*/
static final String LOW_RES_ROTARY_ENCODER_FEATURE = "android.hardware.rotaryencoder.lowres";
static final boolean DISPATCH_TEMP_DETACH = false;
@RestrictTo(LIBRARY_GROUP_PREFIX)
@IntDef({HORIZONTAL, VERTICAL})
@Retention(RetentionPolicy.SOURCE)
public @interface Orientation {
}
public static final int HORIZONTAL = LinearLayout.HORIZONTAL;
public static final int VERTICAL = LinearLayout.VERTICAL;
static final int DEFAULT_ORIENTATION = VERTICAL;
public static final int NO_POSITION = -1;
public static final long NO_ID = -1;
public static final int INVALID_TYPE = -1;
/**
* Constant for use with {@link #setScrollingTouchSlop(int)}. Indicates
* that the RecyclerView should use the standard touch slop for smooth,
* continuous scrolling.
*/
public static final int TOUCH_SLOP_DEFAULT = 0;
/**
* Constant for use with {@link #setScrollingTouchSlop(int)}. Indicates
* that the RecyclerView should use the standard touch slop for scrolling
* widgets that snap to a page or other coarse-grained barrier.
*/
public static final int TOUCH_SLOP_PAGING = 1;
/**
* Constant that represents that a duration has not been defined.
*/
public static final int UNDEFINED_DURATION = Integer.MIN_VALUE;
static final int MAX_SCROLL_DURATION = 2000;
/**
* RecyclerView is calculating a scroll.
* If there are too many of these in Systrace, some Views inside RecyclerView might be causing
* it. Try to avoid using EditText, focusable views or handle them with care.
*/
static final String TRACE_SCROLL_TAG = "RV Scroll";
/**
* OnLayout has been called by the View system.
* If this shows up too many times in Systrace, make sure the children of RecyclerView do not
* update themselves directly. This will cause a full re-layout but when it happens via the
* Adapter notifyItemChanged, RecyclerView can avoid full layout calculation.
*/
private static final String TRACE_ON_LAYOUT_TAG = "RV OnLayout";
/**
* NotifyDataSetChanged or equal has been called.
* If this is taking a long time, try sending granular notify adapter changes instead of just
* calling notifyDataSetChanged or setAdapter / swapAdapter. Adding stable ids to your adapter
* might help.
*/
private static final String TRACE_ON_DATA_SET_CHANGE_LAYOUT_TAG = "RV FullInvalidate";
/**
* RecyclerView is doing a layout for partial adapter updates (we know what has changed)
* If this is taking a long time, you may have dispatched too many Adapter updates causing too
* many Views being rebind. Make sure all are necessary and also prefer using notify*Range
* methods.
*/
private static final String TRACE_HANDLE_ADAPTER_UPDATES_TAG = "RV PartialInvalidate";
/**
* RecyclerView is attempting to pre-populate off screen views.
*/
static final String TRACE_PREFETCH_TAG = "RV Prefetch";
/**
* RecyclerView is creating a new View.
* If too many of these present in Systrace:
* - There might be a problem in Recycling (e.g. custom Animations that set transient state and
* prevent recycling or ItemAnimator not implementing the contract properly. ({@link
* > Adapter#onFailedToRecycleView(ViewHolder)})
*
* - There might be too many item view types.
* > Try merging them
*
* - There might be too many itemChange animations and not enough space in RecyclerPool.
* >Try increasing your pool size and item cache size.
*/
static final String TRACE_CREATE_VIEW_TAG = "RV CreateView";
private static final Class<?>[] LAYOUT_MANAGER_CONSTRUCTOR_SIGNATURE =
new Class<?>[]{Context.class, AttributeSet.class, int.class, int.class};
/**
* Enable internal assertions about RecyclerView's state and throw exceptions if the
* assertions are violated.
* <p>
* This is primarily intended to diagnose problems with RecyclerView, and
* <strong>should not be enabled in production</strong> unless you have a specific reason to
* do so.
* <p>
* Enabling this may negatively affect performance and/or stability.
*
* @param debugAssertionsEnabled true to enable assertions; false to disable them
*/
public static void setDebugAssertionsEnabled(boolean debugAssertionsEnabled) {
RecyclerView.sDebugAssertionsEnabled = debugAssertionsEnabled;
}
/**
* Enable verbose logging within RecyclerView itself.
* <p>
* Enabling this may negatively affect performance and reduce the utility of logcat due to
* high-volume logging. This generally <strong>should not be enabled in production</strong>
* unless you have a specific reason for doing so.
*
* @param verboseLoggingEnabled true to enable logging; false to disable it
*/
public static void setVerboseLoggingEnabled(boolean verboseLoggingEnabled) {
RecyclerView.sVerboseLoggingEnabled = verboseLoggingEnabled;
}
private final RecyclerViewDataObserver mObserver = new RecyclerViewDataObserver();
final Recycler mRecycler = new Recycler();
SavedState mPendingSavedState;
/**
* Handles adapter updates
*/
AdapterHelper mAdapterHelper;
/**
* Handles abstraction between LayoutManager children and RecyclerView children
*/
ChildHelper mChildHelper;
/**
* Keeps data about views to be used for animations
*/
final ViewInfoStore mViewInfoStore = new ViewInfoStore();
/**
* Prior to L, there is no way to query this variable which is why we override the setter and
* track it here.
*/
boolean mClipToPadding;
/**
* Note: this Runnable is only ever posted if:
* 1) We've been through first layout
* 2) We know we have a fixed size (mHasFixedSize)
* 3) We're attached
*/
final Runnable mUpdateChildViewsRunnable = new Runnable() {
@Override
public void run() {
if (!mFirstLayoutComplete || isLayoutRequested()) {
// a layout request will happen, we should not do layout here.
return;
}
if (!mIsAttached) {
requestLayout();
// if we are not attached yet, mark us as requiring layout and skip
return;
}
if (mLayoutSuppressed) {
mLayoutWasDefered = true;
return; //we'll process updates when ice age ends.
}
consumePendingUpdateOperations();
}
};
final Rect mTempRect = new Rect();
private final Rect mTempRect2 = new Rect();
final RectF mTempRectF = new RectF();
Adapter mAdapter;
@VisibleForTesting
LayoutManager mLayout;
// TODO: Remove this once setRecyclerListener has been removed.
RecyclerListener mRecyclerListener;
// default access to avoid the need for synthetic accessors for Recycler inner class.
final List<RecyclerListener> mRecyclerListeners = new ArrayList<>();
final ArrayList<ItemDecoration> mItemDecorations = new ArrayList<>();
private final ArrayList<OnItemTouchListener> mOnItemTouchListeners =
new ArrayList<>();
private OnItemTouchListener mInterceptingOnItemTouchListener;
boolean mIsAttached;
boolean mHasFixedSize;
boolean mEnableFastScroller;
@VisibleForTesting
boolean mFirstLayoutComplete;
/**
* The current depth of nested calls to {@link #startInterceptRequestLayout()} (number of
* calls to {@link #startInterceptRequestLayout()} - number of calls to
* {@link #stopInterceptRequestLayout(boolean)} . This is used to signal whether we
* should defer layout operations caused by layout requests from children of
* {@link RecyclerView}.
*/
private int mInterceptRequestLayoutDepth = 0;
/**
* True if a call to requestLayout was intercepted and prevented from executing like normal and
* we plan on continuing with normal execution later.
*/
boolean mLayoutWasDefered;
boolean mLayoutSuppressed;
private boolean mIgnoreMotionEventTillDown;
// binary OR of change events that were eaten during a layout or scroll.
private int mEatenAccessibilityChangeFlags;
boolean mAdapterUpdateDuringMeasure;
private final AccessibilityManager mAccessibilityManager;
private List<OnChildAttachStateChangeListener> mOnChildAttachStateListeners;
/**
* True after an event occurs that signals that the entire data set has changed. In that case,
* we cannot run any animations since we don't know what happened until layout.
*
* Attached items are invalid until next layout, at which point layout will animate/replace
* items as necessary, building up content from the (effectively) new adapter from scratch.
*
* Cached items must be discarded when setting this to true, so that the cache may be freely
* used by prefetching until the next layout occurs.
*
* @see #processDataSetCompletelyChanged(boolean)
*/
boolean mDataSetHasChangedAfterLayout = false;
/**
* True after the data set has completely changed and
* {@link LayoutManager#onItemsChanged(RecyclerView)} should be called during the subsequent
* measure/layout.
*
* @see #processDataSetCompletelyChanged(boolean)
*/
boolean mDispatchItemsChangedEvent = false;
/**
* This variable is incremented during a dispatchLayout and/or scroll.
* Some methods should not be called during these periods (e.g. adapter data change).
* Doing so will create hard to find bugs so we better check it and throw an exception.
*
* @see #assertInLayoutOrScroll(String)
* @see #assertNotInLayoutOrScroll(String)
*/
private int mLayoutOrScrollCounter = 0;
/**
* Similar to mLayoutOrScrollCounter but logs a warning instead of throwing an exception
* (for API compatibility).
* <p>
* 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 = sDefaultEdgeEffectFactory;
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.
float mScaledHorizontalScrollFactor = Float.MIN_VALUE;
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<OnScrollListener> mScrollListeners;
// For use in item animations
boolean mItemsAddedOrRemoved = false;
boolean mItemsChanged = false;
private ItemAnimator.ItemAnimatorListener mItemAnimatorListener =
new ItemAnimatorRestoreListener();
boolean mPostedAnimatorRunner = false;
RecyclerViewAccessibilityDelegate mAccessibilityDelegate;
private ChildDrawingOrderCallback mChildDrawingOrderCallback;
// simple array to keep min and max child position during a layout calculation
// preserved not to create a new one in each layout pass
private final int[] mMinMaxLayoutPositions = new int[2];
private NestedScrollingChildHelper mScrollingChildHelper;
private final int[] mScrollOffset = new int[2];
private final int[] mNestedOffsets = new int[2];
// Reusable int array to be passed to method calls that mutate it in order to "return" two ints.
final int[] mReusableIntPair = new int[2];
/**
* These are views that had their a11y importance changed during a layout. We defer these events
* until the end of the layout because a11y service may make sync calls back to the RV while
* the View's state is undefined.
*/
@VisibleForTesting
final List<ViewHolder> mPendingAccessibilityImportanceChange = new ArrayList<>();
private Runnable mItemAnimatorRunner = new Runnable() {
@Override
public void run() {
if (mItemAnimator != null) {
mItemAnimator.runPendingAnimations();
}
mPostedAnimatorRunner = false;
}
};
static final Interpolator sQuinticInterpolator = new Interpolator() {
@Override
public float getInterpolation(float t) {
t -= 1.0f;
return t * t * t * t * t + 1.0f;
}
};
static final StretchEdgeEffectFactory sDefaultEdgeEffectFactory =
new StretchEdgeEffectFactory();
// These fields are only used to track whether we need to layout and measure RV children in
// onLayout.
//
// We track this information because there is an optimized path such that when
// LayoutManager#isAutoMeasureEnabled() returns true and we are measured with
// MeasureSpec.EXACTLY in both dimensions, we skip measuring and layout children till the
// layout phase.
//
// However, there are times when we are first measured with something other than
// MeasureSpec.EXACTLY in both dimensions, in which case we measure and layout children during
// onMeasure. Then if we are measured again with EXACTLY, and we skip measurement, we will
// get laid out with a different size than we were last aware of being measured with. If
// that happens and we don't check for it, we may not remeasure children, which would be a bug.
//
// mLastAutoMeasureNonExactMeasureResult tracks our last known measurements in this case, and
// mLastAutoMeasureSkippedDueToExact tracks whether or not we skipped. So, whenever we
// layout, we can see if our last known measurement information is different from our actual
// laid out size, and if it is, only then do we remeasure and relayout children.
private boolean mLastAutoMeasureSkippedDueToExact;
private int mLastAutoMeasureNonExactMeasuredWidth = 0;
private int mLastAutoMeasureNonExactMeasuredHeight = 0;
/**
* Whether or not the device has {@link #LOW_RES_ROTARY_ENCODER_FEATURE}. Computed once and
* cached, since it's a static value that would not change on consecutive calls.
*/
@VisibleForTesting boolean mLowResRotaryEncoderFeature;
/**
* The callback to convert view info diffs into animations.
*/
private final ViewInfoStore.ProcessCallback mViewInfoProcessCallback =
new ViewInfoStore.ProcessCallback() {
@Override
public void processDisappeared(ViewHolder viewHolder, @NonNull ItemHolderInfo info,
@Nullable ItemHolderInfo postInfo) {
mRecycler.unscrapView(viewHolder);
animateDisappearance(viewHolder, info, postInfo);
}
@Override
public void processAppeared(ViewHolder viewHolder,
ItemHolderInfo preInfo, ItemHolderInfo info) {
animateAppearance(viewHolder, preInfo, info);
}
@Override
public void processPersistent(ViewHolder viewHolder,
@NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
viewHolder.setIsRecyclable(false);
if (mDataSetHasChangedAfterLayout) {
// since it was rebound, use change instead as we'll be mapping them from
// stable ids. If stable ids were false, we would not be running any
// animations
if (mItemAnimator.animateChange(viewHolder, viewHolder, preInfo,
postInfo)) {
postAnimationRunner();
}
} else if (mItemAnimator.animatePersistence(viewHolder, preInfo, postInfo)) {
postAnimationRunner();
}
}
@Override
public void unused(ViewHolder viewHolder) {
mLayout.removeAndRecycleView(viewHolder.itemView, mRecycler);
}
};
private final DifferentialMotionFlingTarget
mDifferentialMotionFlingTarget =
new DifferentialMotionFlingTarget() {
@Override
public boolean startDifferentialMotionFling(float velocity) {
int vx = 0;
int vy = 0;
if (mLayout.canScrollVertically()) {
vy = (int) velocity;
} else if (mLayout.canScrollHorizontally()) {
vx = (int) velocity;
}
if (vx == 0 && vy == 0) {
return false;
}
stopScroll();
// Fling with no threshold check, since the DifferentialMotionFlingHelper should
// have handled this already.
return flingNoThresholdCheck(vx, vy);
}
@Override
public void stopDifferentialMotionFling() {
stopScroll();
}
@Override
public float getScaledScrollFactor() {
if (mLayout.canScrollVertically()) {
return -mScaledVerticalScrollFactor;
}
if (mLayout.canScrollHorizontally()) {
return -mScaledHorizontalScrollFactor;
}
return 0;
}
};
@VisibleForTesting
DifferentialMotionFlingController mDifferentialMotionFlingController =
new DifferentialMotionFlingController(getContext(), mDifferentialMotionFlingTarget);
public RecyclerView(@NonNull Context context) {
this(context, null);
}
public RecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, R.attr.recyclerViewStyle);
}
public RecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setScrollContainer(true);
setFocusableInTouchMode(true);
final ViewConfiguration vc = ViewConfiguration.get(context);
mTouchSlop = vc.getScaledTouchSlop();
mScaledHorizontalScrollFactor =
ViewConfigurationCompat.getScaledHorizontalScrollFactor(vc, context);
mScaledVerticalScrollFactor =
ViewConfigurationCompat.getScaledVerticalScrollFactor(vc, context);
mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
final float ppi = context.getResources().getDisplayMetrics().density * 160.0f;
mPhysicalCoef = SensorManager.GRAVITY_EARTH // g (m/s^2)
* 39.37f // inch/meter
* ppi
* 0.84f; // look and feel tuning
setWillNotDraw(getOverScrollMode() == View.OVER_SCROLL_NEVER);
mItemAnimator.setListener(mItemAnimatorListener);
initAdapterManager();
initChildrenHelper();
initAutofill();
// If not explicitly specified this view is important for accessibility.
if (this.getImportantForAccessibility()
== View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
}
mAccessibilityManager = (AccessibilityManager) getContext()
.getSystemService(Context.ACCESSIBILITY_SERVICE);
setAccessibilityDelegateCompat(new RecyclerViewAccessibilityDelegate(this));
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RecyclerView,
defStyleAttr, 0);
ViewCompat.saveAttributeDataForStyleable(this, context, R.styleable.RecyclerView,
attrs, a, defStyleAttr, 0);
String layoutManagerName = a.getString(R.styleable.RecyclerView_layoutManager);
int descendantFocusability = a.getInt(
R.styleable.RecyclerView_android_descendantFocusability, -1);
if (descendantFocusability == -1) {
setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
}
mClipToPadding = a.getBoolean(R.styleable.RecyclerView_android_clipToPadding, true);
mEnableFastScroller = a.getBoolean(R.styleable.RecyclerView_fastScrollEnabled, false);
if (mEnableFastScroller) {
StateListDrawable verticalThumbDrawable = (StateListDrawable) a
.getDrawable(R.styleable.RecyclerView_fastScrollVerticalThumbDrawable);
Drawable verticalTrackDrawable = a
.getDrawable(R.styleable.RecyclerView_fastScrollVerticalTrackDrawable);
StateListDrawable horizontalThumbDrawable = (StateListDrawable) a
.getDrawable(R.styleable.RecyclerView_fastScrollHorizontalThumbDrawable);
Drawable horizontalTrackDrawable = a
.getDrawable(R.styleable.RecyclerView_fastScrollHorizontalTrackDrawable);
initFastScroller(verticalThumbDrawable, verticalTrackDrawable,
horizontalThumbDrawable, horizontalTrackDrawable);
}
a.recycle();
mLowResRotaryEncoderFeature =
context.getPackageManager().hasSystemFeature(LOW_RES_ROTARY_ENCODER_FEATURE);
// Create the layoutManager if specified.
createLayoutManager(context, layoutManagerName, attrs, defStyleAttr, 0);
boolean nestedScrollingEnabled = true;
if (Build.VERSION.SDK_INT >= 21) {
a = context.obtainStyledAttributes(attrs, NESTED_SCROLLING_ATTRS,
defStyleAttr, 0);
ViewCompat.saveAttributeDataForStyleable(this,
context, NESTED_SCROLLING_ATTRS, attrs, a, defStyleAttr, 0);
nestedScrollingEnabled = a.getBoolean(0, true);
a.recycle();
}
// Re-set whether nested scrolling is enabled so that it is set on all API levels
setNestedScrollingEnabled(nestedScrollingEnabled);
PoolingContainer.setPoolingContainer(this, true);
}
/**
* Label appended to all public exception strings, used to help find which RV in an app is
* hitting an exception.
*/
String exceptionLabel() {
return " " + super.toString()
+ ", adapter:" + mAdapter
+ ", layout:" + mLayout
+ ", context:" + getContext();
}
/**
* If not explicitly specified, this view and its children don't support autofill.
* <p>
* 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) {
Trace.beginSection("RV addView");
}
RecyclerView.this.addView(child, index);
if (VERBOSE_TRACING) {
Trace.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) {
Trace.beginSection("RV removeViewAt");
}
RecyclerView.this.removeViewAt(index);
if (VERBOSE_TRACING) {
Trace.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 (sVerboseLoggingEnabled) {
Log.d(TAG, "reAttach " + vh);
}
vh.clearTmpDetachFlag();
} else {
if (sDebugAssertionsEnabled) {
throw new IllegalArgumentException(
"No ViewHolder found for child: " + child + ", index: " + index
+ exceptionLabel());
}
}
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 (sVerboseLoggingEnabled) {
Log.d(TAG, "tmpDetach " + vh);
}
vh.addFlags(ViewHolder.FLAG_TMP_DETACHED);
}
} else {
if (sDebugAssertionsEnabled) {
throw new IllegalArgumentException(
"No view at offset " + offset + exceptionLabel());
}
}
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 (sVerboseLoggingEnabled) {
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).
* <p>
* 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.
* <p>
* 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 <code>TOUCH_SLOP_</code> 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.
* <p>
* 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.
* <p>
* 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.
*
* <p>This listener will be called when a LayoutManager or the RecyclerView decides
* that a child view is no longer needed. If an application associates expensive
* or heavyweight data with item views, this may be a good place to release
* or free those resources.</p>
*
* @param listener Listener to register, or null to clear
* @deprecated Use {@link #addRecyclerListener(RecyclerListener)} and
* {@link #removeRecyclerListener(RecyclerListener)}
*/
@Deprecated
public void setRecyclerListener(@Nullable RecyclerListener listener) {
mRecyclerListener = listener;
}
/**
* Register a listener that will be notified whenever a child view is recycled.
*
* <p>The listeners will be called when a LayoutManager or the RecyclerView decides
* that a child view is no longer needed. If an application associates data with
* the item views being recycled, this may be a good place to release
* or free those resources.</p>
*
* @param listener Listener to register.
*/
public void addRecyclerListener(@NonNull RecyclerListener listener) {
checkArgument(listener != null, "'listener' arg cannot "
+ "be null.");
mRecyclerListeners.add(listener);
}
/**
* Removes the provided listener from RecyclerListener list.
*
* @param listener Listener to unregister.
*/
public void removeRecyclerListener(@NonNull RecyclerListener listener) {
mRecyclerListeners.remove(listener);
}
/**
* <p>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.</p>
*
* @return the offset of the baseline within the RecyclerView's bounds or -1
* if baseline alignment is not supported
*/
@Override
public int getBaseline() {
if (mLayout != null) {
return mLayout.getBaseline();
} else {
return super.getBaseline();
}
}
/**
* Register a listener that will be notified whenever a child view is attached to or detached
* from RecyclerView.
*
* <p>This listener will be called when a LayoutManager or the RecyclerView decides
* that a child view is no longer needed. If an application associates expensive
* or heavyweight data with item views, this may be a good place to release
* or free those resources.</p>
*
* @param listener Listener to register
*/
public void addOnChildAttachStateChangeListener(
@NonNull OnChildAttachStateChangeListener listener) {
if (mOnChildAttachStateListeners == null) {
mOnChildAttachStateListeners = new ArrayList<>();
}
mOnChildAttachStateListeners.add(listener);
}
/**
* Removes the provided listener from child attached state listeners list.
*
* @param listener Listener to unregister
*/
public void removeOnChildAttachStateChangeListener(
@NonNull OnChildAttachStateChangeListener listener) {
if (mOnChildAttachStateListeners == null) {
return;
}
mOnChildAttachStateListeners.remove(listener);
}
/**
* Removes all listeners that were added via
* {@link #addOnChildAttachStateChangeListener(OnChildAttachStateChangeListener)}.
*/
public void clearOnChildAttachStateChangeListeners() {
if (mOnChildAttachStateListeners != null) {
mOnChildAttachStateListeners.clear();
}
}
/**
* Set the {@link LayoutManager} that this RecyclerView will use.
*
* <p>In contrast to other adapter-backed views such as {@link android.widget.ListView}
* or {@link android.widget.GridView}, RecyclerView allows client code to provide custom
* layout arrangements for child views. These arrangements are controlled by the
* {@link LayoutManager}. A LayoutManager must be provided for RecyclerView to function.</p>
*
* <p>Several default strategies are provided for common uses such as lists and grids.</p>
*
* @param layout LayoutManager to use
*/
public void setLayoutManager(@Nullable LayoutManager layout) {
if (layout == mLayout) {
return;
}
stopScroll();
// TODO We should do this switch a dispatchLayout pass and animate children. There is a good
// chance that LayoutManagers will re-use views.
if (mLayout != null) {
// end all running animations
if (mItemAnimator != null) {
mItemAnimator.endAnimations();
}
mLayout.removeAndRecycleAllViews(mRecycler);
mLayout.removeAndRecycleScrapInt(mRecycler);
mRecycler.clear();
if (mIsAttached) {
mLayout.dispatchDetachedFromWindow(this, mRecycler);
}
mLayout.setRecyclerView(null);
mLayout = null;
} else {
mRecycler.clear();
}
// this is just a defensive measure for faulty item animators.
mChildHelper.removeAllViewsUnfiltered();
mLayout = layout;
if (layout != null) {
if (layout.mRecyclerView != null) {
throw new IllegalArgumentException("LayoutManager " + layout
+ " is already attached to a RecyclerView:"
+ layout.mRecyclerView.exceptionLabel());
}
mLayout.setRecyclerView(this);
if (mIsAttached) {
mLayout.dispatchAttachedToWindow(this);
}
}
mRecycler.updateViewCacheSize();
requestLayout();
}
/**
* Set a {@link OnFlingListener} for this {@link RecyclerView}.
* <p>
* 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());
// Historically, some app developers have used onRestoreInstanceState(State) in ways it
// was never intended. For example, some devs have used it to manually set a state they
// updated themselves such that passing the state here would cause a LayoutManager to
// receive it and update its internal state accordingly, even if state was already
// previously restored. Therefore, it is necessary to always call requestLayout to retain
// the functionality even if it otherwise seems like a strange thing to do.
// ¯\_(ツ)_/¯
requestLayout();
}
/**
* Override to prevent freezing of any views created by the adapter.
*/
@Override
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
dispatchFreezeSelfOnly(container);
}
/**
* Override to prevent thawing of any views created by the adapter.
*/
@Override
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
dispatchThawSelfOnly(container);
}
/**
* Adds a view to the animatingViews list.
* mAnimatingViews holds the child views that are currently being kept around
* purely for the purpose of being animated out of view. They are drawn as a regular
* part of the child list of the RecyclerView, but they are invisible to the LayoutManager
* as they are managed separately from the regular child views.
*
* @param viewHolder The ViewHolder to be removed
*/
private void addAnimatingView(ViewHolder viewHolder) {
final View view = viewHolder.itemView;
final boolean alreadyParented = view.getParent() == this;
mRecycler.unscrapView(getChildViewHolder(view));
if (viewHolder.isTmpDetached()) {
// re-attach
mChildHelper.attachViewToParent(view, -1, view.getLayoutParams(), true);
} else if (!alreadyParented) {
mChildHelper.addView(view, true);
} else {
mChildHelper.hide(view);
}
}
/**
* Removes a view from the animatingViews list.
*
* @param view The view to be removed
* @return true if an animating view is removed
* @see #addAnimatingView(RecyclerView.ViewHolder)
*/
boolean removeAnimatingView(View view) {
startInterceptRequestLayout();
final boolean removed = mChildHelper.removeViewIfHidden(view);
if (removed) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
mRecycler.unscrapView(viewHolder);
mRecycler.recycleViewHolderInternal(viewHolder);
if (sVerboseLoggingEnabled) {
Log.d(TAG, "after removing animated view: " + view + ", " + this);
}
}
// only clear request eaten flag if we removed the view.
stopInterceptRequestLayout(!removed);
return removed;
}
/**
* Return the {@link LayoutManager} currently responsible for
* layout policy for this RecyclerView.
*
* @return The currently bound LayoutManager
*/
@Nullable
public LayoutManager getLayoutManager() {
return mLayout;
}
/**
* Retrieve this RecyclerView's {@link RecycledViewPool}. This method will never return null;
* if no pool is set for this view a new one will be created. See
* {@link #setRecycledViewPool(RecycledViewPool) setRecycledViewPool} for more information.
*
* @return The pool used to store recycled item views for reuse.
* @see #setRecycledViewPool(RecycledViewPool)
*/
@NonNull
public RecycledViewPool getRecycledViewPool() {
return mRecycler.getRecycledViewPool();
}
/**
* Recycled view pools allow multiple RecyclerViews to share a common pool of scrap views.
* This can be useful if you have multiple RecyclerViews with adapters that use the same
* view types, for example if you have several data sets with the same kinds of item views
* displayed by a {@link androidx.viewpager.widget.ViewPager}.
*
* @param pool Pool to set. If this parameter is null a new pool will be created and used.
*/
public void setRecycledViewPool(@Nullable RecycledViewPool pool) {
mRecycler.setRecycledViewPool(pool);
}
/**
* Sets a new {@link ViewCacheExtension} to be used by the Recycler.
*
* @param extension ViewCacheExtension to be used or null if you want to clear the existing one.
* @see ViewCacheExtension#getViewForPositionAndType(Recycler, int, int)
*/
public void setViewCacheExtension(@Nullable ViewCacheExtension extension) {
mRecycler.setViewCacheExtension(extension);
}
/**
* Set the number of offscreen views to retain before adding them to the potentially shared
* {@link #getRecycledViewPool() recycled view pool}.
*
* <p>The offscreen view cache stays aware of changes in the attached adapter, allowing
* a LayoutManager to reuse those views unmodified without needing to return to the adapter
* to rebind them.</p>
*
* @param size Number of views to cache offscreen before returning them to the general
* recycled view pool
*/
public void setItemViewCacheSize(int size) {
mRecycler.setViewCacheSize(size);
}
/**
* Return the current scrolling state of the RecyclerView.
*
* @return {@link #SCROLL_STATE_IDLE}, {@link #SCROLL_STATE_DRAGGING} or
* {@link #SCROLL_STATE_SETTLING}
*/
public int getScrollState() {
return mScrollState;
}
void setScrollState(int state) {
if (state == mScrollState) {
return;
}
if (sVerboseLoggingEnabled) {
Log.d(TAG, "setting scroll state to " + state + " from " + mScrollState,
new Exception());
}
mScrollState = state;
if (state != SCROLL_STATE_SETTLING) {
stopScrollersInternal();
}
dispatchOnScrollStateChanged(state);
}
/**
* Add an {@link ItemDecoration} to this RecyclerView. Item decorations can
* affect both measurement and drawing of individual item views.
*
* <p>Item decorations are ordered. Decorations placed earlier in the list will
* be run/queried/drawn first for their effects on item views. Padding added to views
* will be nested; a padding added by an earlier decoration will mean further
* item decorations in the list will be asked to draw/pad within the previous decoration's
* given area.</p>
*
* @param decor Decoration to add
* @param index Position in the decoration chain to insert this decoration at. If this value
* is negative the decoration will be added at the end.
*/
public void addItemDecoration(@NonNull ItemDecoration decor, int index) {
if (mLayout != null) {
mLayout.assertNotInLayoutOrScroll("Cannot add item decoration during a scroll or"
+ " layout");
}
if (mItemDecorations.isEmpty()) {
setWillNotDraw(false);
}
if (index < 0) {
mItemDecorations.add(decor);
} else {
mItemDecorations.add(index, decor);
}
markItemDecorInsetsDirty();
requestLayout();
}
/**
* Add an {@link ItemDecoration} to this RecyclerView. Item decorations can
* affect both measurement and drawing of individual item views.
*
* <p>Item decorations are ordered. Decorations placed earlier in the list will
* be run/queried/drawn first for their effects on item views. Padding added to views
* will be nested; a padding added by an earlier decoration will mean further
* item decorations in the list will be asked to draw/pad within the previous decoration's
* given area.</p>
*
* @param decor Decoration to add
*/
public void addItemDecoration(@NonNull ItemDecoration decor) {
addItemDecoration(decor, -1);
}
/**
* Returns an {@link ItemDecoration} previously added to this RecyclerView.
*
* @param index The index position of the desired ItemDecoration.
* @return the ItemDecoration at index position
* @throws IndexOutOfBoundsException on invalid index
*/
@NonNull
public ItemDecoration getItemDecorationAt(int index) {
final int size = getItemDecorationCount();
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException(index + " is an invalid index for size " + size);
}
return mItemDecorations.get(index);
}
/**
* Returns the number of {@link ItemDecoration} currently added to this RecyclerView.
*
* @return number of ItemDecorations currently added added to this RecyclerView.
*/
public int getItemDecorationCount() {
return mItemDecorations.size();
}
/**
* Removes the {@link ItemDecoration} associated with the supplied index position.
*
* @param index The index position of the ItemDecoration to be removed.
*/
public void removeItemDecorationAt(int index) {
final int size = getItemDecorationCount();
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException(index + " is an invalid index for size " + size);
}
removeItemDecoration(getItemDecorationAt(index));
}
/**
* Remove an {@link ItemDecoration} from this RecyclerView.
*
* <p>The given decoration will no longer impact the measurement and drawing of
* item views.</p>
*
* @param decor Decoration to remove
* @see #addItemDecoration(ItemDecoration)
*/
public void removeItemDecoration(@NonNull ItemDecoration decor) {
if (mLayout != null) {
mLayout.assertNotInLayoutOrScroll("Cannot remove item decoration during a scroll or"
+ " layout");
}
mItemDecorations.remove(decor);
if (mItemDecorations.isEmpty()) {
setWillNotDraw(getOverScrollMode() == View.OVER_SCROLL_NEVER);
}
markItemDecorInsetsDirty();
requestLayout();
}
/**
* Sets the {@link ChildDrawingOrderCallback} to be used for drawing children.
* <p>
* 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.
* <p>
* 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.
*
* <p>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.</p>
*
* @param listener listener to set
*/
public void addOnScrollListener(@NonNull OnScrollListener listener) {
if (mScrollListeners == null) {
mScrollListeners = new ArrayList<>();
}
mScrollListeners.add(listener);
}
/**
* Remove a listener that was notified of any changes in scroll state or position.
*
* @param listener listener to set or null to clear
*/
public void removeOnScrollListener(@NonNull OnScrollListener listener) {
if (mScrollListeners != null) {
mScrollListeners.remove(listener);
}
}
/**
* Remove all secondary listener that were notified of any changes in scroll state or position.
*/
public void clearOnScrollListeners() {
if (mScrollListeners != null) {
mScrollListeners.clear();
}
}
/**
* Convenience method to scroll to a certain position.
*
* RecyclerView does not implement scrolling logic, rather forwards the call to
* {@link RecyclerView.LayoutManager#scrollToPosition(int)}
*
* @param position Scroll to this adapter position
* @see RecyclerView.LayoutManager#scrollToPosition(int)
*/
public void scrollToPosition(int position) {
if (mLayoutSuppressed) {
return;
}
stopScroll();
if (mLayout == null) {
Log.e(TAG, "Cannot scroll to position a LayoutManager set. "
+ "Call setLayoutManager with a non-null argument.");
return;
}
mLayout.scrollToPosition(position);
awakenScrollBars();
}
void jumpToPositionForSmoothScroller(int position) {
if (mLayout == null) {
return;
}
// If we are jumping to a position, we are in fact scrolling the contents of the RV, so
// we should be sure that we are in the settling state.
setScrollState(SCROLL_STATE_SETTLING);
mLayout.scrollToPosition(position);
awakenScrollBars();
}
/**
* Starts a smooth scroll to an adapter position.
* <p>
* To support smooth scrolling, you must override
* {@link LayoutManager#smoothScrollToPosition(RecyclerView, State, int)} and create a
* {@link SmoothScroller}.
* <p>
* {@link LayoutManager} is responsible for creating the actual scroll action. If you want to
* provide a custom smooth scroll logic, override
* {@link LayoutManager#smoothScrollToPosition(RecyclerView, State, int)} in your
* LayoutManager.
*
* @param position The adapter position to scroll to
* @see LayoutManager#smoothScrollToPosition(RecyclerView, State, int)
*/
public void smoothScrollToPosition(int position) {
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 boolean dispatchKeyEvent(@Nullable KeyEvent event) {
// Let child to dispatch first, then handle ours if child didn't do it.
if (super.dispatchKeyEvent(event)) {
return true;
}
LayoutManager layoutManager = getLayoutManager();
// If there is no layout manager, then there is nothing to handle key events for.
if (layoutManager == null) {
return false;
}
if (layoutManager.canScrollVertically()) {
final int keyCode = event.getKeyCode();
switch (keyCode) {
case KeyEvent.KEYCODE_PAGE_DOWN:
case KeyEvent.KEYCODE_PAGE_UP:
int height = getMeasuredHeight();
if (keyCode == KeyEvent.KEYCODE_PAGE_DOWN) {
smoothScrollBy(0, height, null, UNDEFINED_DURATION);
} else {
smoothScrollBy(0, -height, null, UNDEFINED_DURATION);
}
return true;
case KeyEvent.KEYCODE_MOVE_HOME:
case KeyEvent.KEYCODE_MOVE_END:
final boolean isReversed = layoutManager.isLayoutReversed();
final int targetOffset;
if (keyCode == KeyEvent.KEYCODE_MOVE_HOME) {
targetOffset = isReversed ? getAdapter().getItemCount() : 0;
} else {
targetOffset = isReversed ? 0 : getAdapter().getItemCount();
}
smoothScrollToPosition(targetOffset);
return true;
}
} else if (layoutManager.canScrollHorizontally()) {
final int keyCode = event.getKeyCode();
switch (keyCode) {
case KeyEvent.KEYCODE_PAGE_DOWN:
case KeyEvent.KEYCODE_PAGE_UP:
int width = getMeasuredWidth();
if (keyCode == KeyEvent.KEYCODE_PAGE_DOWN) {
smoothScrollBy(width, 0, null, UNDEFINED_DURATION);
} else {
smoothScrollBy(-width, 0, null, UNDEFINED_DURATION);
}
return true;
case KeyEvent.KEYCODE_MOVE_HOME:
case KeyEvent.KEYCODE_MOVE_END:
final boolean isReversed = layoutManager.isLayoutReversed();
final int targetOffset;
if (keyCode == KeyEvent.KEYCODE_MOVE_HOME) {
targetOffset = isReversed ? getAdapter().getItemCount() : 0;
} else {
targetOffset = isReversed ? 0 : getAdapter().getItemCount();
}
smoothScrollToPosition(targetOffset);
return true;
}
}
return false;
}
@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,
TYPE_TOUCH);
}
}
/**
* Same as {@link RecyclerView#scrollBy(int, int)}, but also participates in nested scrolling.
* @param x The amount of horizontal scroll requested
* @param y The amount of vertical scroll requested
* @see androidx.core.view.NestedScrollingChild
*/
public void nestedScrollBy(int x, int y) {
nestedScrollByInternal(x, y, null, TYPE_NON_TOUCH);
}
/**
* Similar to {@link RecyclerView#scrollByInternal(int, int, MotionEvent, int)}, but fully
* participates in nested scrolling "end to end", meaning that it will start nested scrolling,
* participate in nested scrolling, and then end nested scrolling all within one call.
* @param x The amount of horizontal scroll requested.
* @param y The amount of vertical scroll requested.
* @param motionEvent The originating MotionEvent if any.
* @param type The type of nested scrolling to engage in (TYPE_TOUCH or TYPE_NON_TOUCH).
*/
@SuppressWarnings("SameParameterValue")
private void nestedScrollByInternal(int x, int y, @Nullable MotionEvent motionEvent, int type) {
if (mLayout == null) {
Log.e(TAG, "Cannot scroll without a LayoutManager set. "
+ "Call setLayoutManager with a non-null argument.");
return;
}
if (mLayoutSuppressed) {
return;
}
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
final boolean canScrollHorizontal = mLayout.canScrollHorizontally();
final boolean canScrollVertical = mLayout.canScrollVertically();
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontal) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertical) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
// If there is no MotionEvent, treat it as center-aligned edge effect:
float verticalDisplacement = motionEvent == null ? getHeight() / 2f : motionEvent.getY();
float horizontalDisplacement = motionEvent == null ? getWidth() / 2f : motionEvent.getX();
x -= releaseHorizontalGlow(x, verticalDisplacement);
y -= releaseVerticalGlow(y, horizontalDisplacement);
startNestedScroll(nestedScrollAxis, type);
if (dispatchNestedPreScroll(
canScrollHorizontal ? x : 0,
canScrollVertical ? y : 0,
mReusableIntPair, mScrollOffset, type
)) {
x -= mReusableIntPair[0];
y -= mReusableIntPair[1];
}
scrollByInternal(
canScrollHorizontal ? x : 0,
canScrollVertical ? y : 0,
motionEvent, type);
if (mGapWorker != null && (x != 0 || y != 0)) {
mGapWorker.postFromTraversal(this, x, y);
}
stopNestedScroll(type);
}
/**
* 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();
Trace.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);
}
Trace.endSection();
repositionShadowingViews();
onExitLayoutOrScroll();
stopInterceptRequestLayout(false);
if (consumed != null) {
consumed[0] = consumedX;
consumed[1] = consumedY;
}
}
/**
* Helper method reflect data changes to the state.
* <p>
* Adapter changes during a scroll may trigger a crash because scroll assumes no data change
* but data actually changed.
* <p>
* This method consumes all deferred changes to avoid that case.
*/
void consumePendingUpdateOperations() {
if (!mFirstLayoutComplete || mDataSetHasChangedAfterLayout) {
Trace.beginSection(TRACE_ON_DATA_SET_CHANGE_LAYOUT_TAG);
dispatchLayout();
Trace.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)) {
Trace.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();
Trace.endSection();
} else if (mAdapterHelper.hasPendingUpdates()) {
Trace.beginSection(TRACE_ON_DATA_SET_CHANGE_LAYOUT_TAG);
dispatchLayout();
Trace.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.
* <p>
* 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.
* @param type NestedScrollType, TOUCH or NON_TOUCH.
* @return Whether any scroll was consumed in either direction.
*/
boolean scrollByInternal(int x, int y, MotionEvent ev, int type) {
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, mReusableIntPair);
unconsumedX -= mReusableIntPair[0];
unconsumedY -= mReusableIntPair[1];
boolean consumedNestedScroll = mReusableIntPair[0] != 0 || mReusableIntPair[1] != 0;
// Update the last touch co-ords, taking any scroll offset into account
mLastTouchX -= mScrollOffset[0];
mLastTouchY -= 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);
// For rotary encoders, we release stretch EdgeEffects after they are pulled, to
// avoid the effects being stuck pulled.
if (Build.VERSION.SDK_INT >= 31
&& MotionEventCompat.isFromSource(ev, InputDevice.SOURCE_ROTARY_ENCODER)) {
releaseGlows();
}
}
considerReleasingGlowsOnScroll(x, y);
}
if (consumedX != 0 || consumedY != 0) {
dispatchOnScrolled(consumedX, consumedY);
}
if (!awakenScrollBars()) {
invalidate();
}
return consumedNestedScroll || consumedX != 0 || consumedY != 0;
}
/**
* If either of the horizontal edge glows are currently active, this consumes part or all of
* deltaX on the edge glow.
*
* @param deltaX The pointer motion, in pixels, in the horizontal direction, positive
* for moving down and negative for moving up.
* @param y The vertical position of the pointer.
* @return The amount of <code>deltaX</code> that has been consumed by the
* edge glow.
*/
private int releaseHorizontalGlow(int deltaX, float y) {
// First allow releasing existing overscroll effect:
float consumed = 0;
float displacement = y / getHeight();
float pullDistance = (float) deltaX / getWidth();
if (mLeftGlow != null && EdgeEffectCompat.getDistance(mLeftGlow) != 0) {
if (canScrollHorizontally(-1)) {
mLeftGlow.onRelease();
} else {
consumed = -EdgeEffectCompat.onPullDistance(mLeftGlow, -pullDistance,
1 - displacement);
if (EdgeEffectCompat.getDistance(mLeftGlow) == 0) {
mLeftGlow.onRelease();
}
}
invalidate();
} else if (mRightGlow != null && EdgeEffectCompat.getDistance(mRightGlow) != 0) {
if (canScrollHorizontally(1)) {
mRightGlow.onRelease();
} else {
consumed = EdgeEffectCompat.onPullDistance(mRightGlow, pullDistance, displacement);
if (EdgeEffectCompat.getDistance(mRightGlow) == 0) {
mRightGlow.onRelease();
}
}
invalidate();
}
return Math.round(consumed * getWidth());
}
/**
* If either of the vertical edge glows are currently active, this consumes part or all of
* deltaY on the edge glow.
*
* @param deltaY The pointer motion, in pixels, in the vertical direction, positive
* for moving down and negative for moving up.
* @param x The vertical position of the pointer.
* @return The amount of <code>deltaY</code> that has been consumed by the
* edge glow.
*/
private int releaseVerticalGlow(int deltaY, float x) {
// First allow releasing existing overscroll effect:
float consumed = 0;
float displacement = x / getWidth();
float pullDistance = (float) deltaY / getHeight();
if (mTopGlow != null && EdgeEffectCompat.getDistance(mTopGlow) != 0) {
if (canScrollVertically(-1)) {
mTopGlow.onRelease();
} else {
consumed = -EdgeEffectCompat.onPullDistance(mTopGlow, -pullDistance, displacement);
if (EdgeEffectCompat.getDistance(mTopGlow) == 0) {
mTopGlow.onRelease();
}
}
invalidate();
} else if (mBottomGlow != null && EdgeEffectCompat.getDistance(mBottomGlow) != 0) {
if (canScrollVertically(1)) {
mBottomGlow.onRelease();
} else {
consumed = EdgeEffectCompat.onPullDistance(mBottomGlow, pullDistance,
1 - displacement);
if (EdgeEffectCompat.getDistance(mBottomGlow) == 0) {
mBottomGlow.onRelease();
}
}
invalidate();
}
return Math.round(consumed * getHeight());
}
/**
* <p>Compute the horizontal offset of the horizontal scrollbar's thumb within the horizontal
* range. This value is used to compute the position of the thumb within the scrollbar's track.
* </p>
*
* <p>The range is expressed in arbitrary units that must be the same as the units used by
* {@link #computeHorizontalScrollRange()} and {@link #computeHorizontalScrollExtent()}.</p>
*
* <p>Default implementation returns 0.</p>
*
* <p>If you want to support scroll bars, override
* {@link RecyclerView.LayoutManager#computeHorizontalScrollOffset(RecyclerView.State)} in your
* LayoutManager. </p>
*
* @return The horizontal offset of the scrollbar's thumb
* @see RecyclerView.LayoutManager#computeHorizontalScrollOffset
* (RecyclerView.State)
*/
@Override
public int computeHorizontalScrollOffset() {
if (mLayout == null) {
return 0;
}
return mLayout.canScrollHorizontally() ? mLayout.computeHorizontalScrollOffset(mState) : 0;
}
/**
* <p>Compute the horizontal extent of the horizontal scrollbar's thumb within the
* horizontal range. This value is used to compute the length of the thumb within the
* scrollbar's track.</p>
*
* <p>The range is expressed in arbitrary units that must be the same as the units used by
* {@link #computeHorizontalScrollRange()} and {@link #computeHorizontalScrollOffset()}.</p>
*
* <p>Default implementation returns 0.</p>
*
* <p>If you want to support scroll bars, override
* {@link RecyclerView.LayoutManager#computeHorizontalScrollExtent(RecyclerView.State)} in your
* LayoutManager.</p>
*
* @return The horizontal extent of the scrollbar's thumb
* @see RecyclerView.LayoutManager#computeHorizontalScrollExtent(RecyclerView.State)
*/
@Override
public int computeHorizontalScrollExtent() {
if (mLayout == null) {
return 0;
}
return mLayout.canScrollHorizontally() ? mLayout.computeHorizontalScrollExtent(mState) : 0;
}
/**
* <p>Compute the horizontal range that the horizontal scrollbar represents.</p>
*
* <p>The range is expressed in arbitrary units that must be the same as the units used by
* {@link #computeHorizontalScrollExtent()} and {@link #computeHorizontalScrollOffset()}.</p>
*
* <p>Default implementation returns 0.</p>
*
* <p>If you want to support scroll bars, override
* {@link RecyclerView.LayoutManager#computeHorizontalScrollRange(RecyclerView.State)} in your
* LayoutManager.</p>
*
* @return The total horizontal range represented by the horizontal scrollbar
* @see RecyclerView.LayoutManager#computeHorizontalScrollRange(RecyclerView.State)
*/
@Override
public int computeHorizontalScrollRange() {
if (mLayout == null) {
return 0;
}
return mLayout.canScrollHorizontally() ? mLayout.computeHorizontalScrollRange(mState) : 0;
}
/**
* <p>Compute the vertical offset of the vertical scrollbar's thumb within the vertical range.
* This value is used to compute the position of the thumb within the scrollbar's track. </p>
*
* <p>The range is expressed in arbitrary units that must be the same as the units used by
* {@link #computeVerticalScrollRange()} and {@link #computeVerticalScrollExtent()}.</p>
*
* <p>Default implementation returns 0.</p>
*
* <p>If you want to support scroll bars, override
* {@link RecyclerView.LayoutManager#computeVerticalScrollOffset(RecyclerView.State)} in your
* LayoutManager.</p>
*
* @return The vertical offset of the scrollbar's thumb
* @see RecyclerView.LayoutManager#computeVerticalScrollOffset
* (RecyclerView.State)
*/
@Override
public int computeVerticalScrollOffset() {
if (mLayout == null) {
return 0;
}
return mLayout.canScrollVertically() ? mLayout.computeVerticalScrollOffset(mState) : 0;
}
/**
* <p>Compute the vertical extent of the vertical scrollbar's thumb within the vertical range.
* This value is used to compute the length of the thumb within the scrollbar's track.</p>
*
* <p>The range is expressed in arbitrary units that must be the same as the units used by
* {@link #computeVerticalScrollRange()} and {@link #computeVerticalScrollOffset()}.</p>
*
* <p>Default implementation returns 0.</p>
*
* <p>If you want to support scroll bars, override
* {@link RecyclerView.LayoutManager#computeVerticalScrollExtent(RecyclerView.State)} in your
* LayoutManager.</p>
*
* @return The vertical extent of the scrollbar's thumb
* @see RecyclerView.LayoutManager#computeVerticalScrollExtent(RecyclerView.State)
*/
@Override
public int computeVerticalScrollExtent() {
if (mLayout == null) {
return 0;
}
return mLayout.canScrollVertically() ? mLayout.computeVerticalScrollExtent(mState) : 0;
}
/**
* <p>Compute the vertical range that the vertical scrollbar represents.</p>
*
* <p>The range is expressed in arbitrary units that must be the same as the units used by
* {@link #computeVerticalScrollExtent()} and {@link #computeVerticalScrollOffset()}.</p>
*
* <p>Default implementation returns 0.</p>
*
* <p>If you want to support scroll bars, override
* {@link RecyclerView.LayoutManager#computeVerticalScrollRange(RecyclerView.State)} in your
* LayoutManager.</p>
*
* @return The total vertical range represented by the vertical scrollbar
* @see RecyclerView.LayoutManager#computeVerticalScrollRange(RecyclerView.State)
*/
@Override
public int computeVerticalScrollRange() {
if (mLayout == null) {
return 0;
}
return mLayout.canScrollVertically() ? mLayout.computeVerticalScrollRange(mState) : 0;
}
/**
* This method should be called before any code that may trigger a child view to cause a call to
* {@link RecyclerView#requestLayout()}. Doing so enables {@link RecyclerView} to avoid
* reacting to additional redundant calls to {@link #requestLayout()}.
* <p>
* 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()}.
* <p>
* 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 (sDebugAssertionsEnabled) {
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.
* <p>
* 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.
*
* <p>
* <code>suppressLayout(true)</code> does not prevent app from directly calling {@link
* LayoutManager#scrollToPosition(int)}, {@link LayoutManager#smoothScrollToPosition(
*RecyclerView, State, int)}.
* <p>
* {@link #setAdapter(Adapter)} and {@link #swapAdapter(Adapter, boolean)} will automatically
* stop suppressing.
* <p>
* 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.
*/
@Override
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.
*/
@Override
public final boolean isLayoutSuppressed() {
return mLayoutSuppressed;
}
/**
* Enable or disable layout and scroll. After <code>setLayoutFrozen(true)</code> is called,
* Layout requests will be postponed until <code>setLayoutFrozen(false)</code> 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.
*
* <p>
* <code>setLayoutFrozen(true)</code> does not prevent app from directly calling {@link
* LayoutManager#scrollToPosition(int)}, {@link LayoutManager#smoothScrollToPosition(
*RecyclerView, State, int)}.
* <p>
* {@link #setAdapter(Adapter)} and {@link #swapAdapter(Adapter, boolean)} will automatically
* stop frozen.
* <p>
* 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 will use an internal default interpolator.
*/
public void smoothScrollBy(@Px int dx, @Px int dy, @Nullable Interpolator interpolator) {
smoothScrollBy(dx, dy, interpolator, UNDEFINED_DURATION);
}
/**
* Smooth scrolls the RecyclerView by a given distance.
*
* @param dx x distance in pixels.
* @param dy y distance in pixels.
* @param interpolator {@link Interpolator} to be used for scrolling. If it is {@code null},
* RecyclerView will use an internal default interpolator.
* @param duration Duration of the animation in milliseconds. Set to
* {@link #UNDEFINED_DURATION}
* to have the duration be automatically calculated based on an internally
* defined standard initial velocity. A duration less than 1 (that does not
* equal UNDEFINED_DURATION), will result in a call to
* {@link #scrollBy(int, int)}.
*/
public void smoothScrollBy(@Px int dx, @Px int dy, @Nullable Interpolator interpolator,
int duration) {
smoothScrollBy(dx, dy, interpolator, duration, false);
}
/**
* Internal smooth scroll by implementation that currently has some tricky logic related to it's
* parameters.
* <ul>
* <li>For scrolling to occur, on either dimension, dx or dy must not be equal to 0 and the
* {@link LayoutManager} must support scrolling in a direction for which the value is not 0.
* <li>For smooth scrolling to occur, scrolling must occur and the duration must be equal to
* {@link #UNDEFINED_DURATION} or greater than 0.
* <li>For scrolling to occur with nested scrolling, smooth scrolling must occur and
* {@code withNestedScrolling} must be {@code true}. This could be updated, but it would
* require that {@link #scrollBy(int, int)} be implemented such that it too can handle nested
* scrolling.
* </ul>
*
* @param dx x distance in pixels.
* @param dy y distance in pixels.
* @param interpolator {@link Interpolator} to be used for scrolling. If it is {@code
* null},
* RecyclerView will use an internal default interpolator.
* @param duration Duration of the animation in milliseconds. Set to
* {@link #UNDEFINED_DURATION}
* to have the duration be automatically calculated based on an
* internally
* defined standard initial velocity. A duration less than 1 (that
* does not
* equal UNDEFINED_DURATION), will result in a call to
* {@link #scrollBy(int, int)}.
* @param withNestedScrolling True to perform the smooth scroll with nested scrolling. If
* {@code duration} is less than 0 and not equal to
* {@link #UNDEFINED_DURATION}, smooth scrolling will not occur and
* thus no nested scrolling will occur.
*/
// Should be considered private. Not private to avoid synthetic accessor.
void smoothScrollBy(@Px int dx, @Px int dy, @Nullable Interpolator interpolator,
int duration, boolean withNestedScrolling) {
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) {
boolean durationSuggestsAnimation = duration == UNDEFINED_DURATION || duration > 0;
if (durationSuggestsAnimation) {
if (withNestedScrolling) {
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (dx != 0) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (dy != 0) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
startNestedScroll(nestedScrollAxis, TYPE_NON_TOUCH);
}
mViewFlinger.smoothScrollBy(dx, dy, duration, interpolator);
} else {
scrollBy(dx, dy);
}
}
}
/**
* Begin a standard fling with an initial velocity along each axis in pixels per second.
* If the velocity given is below the system-defined minimum this method will return false
* and no fling will occur.
*
* @param velocityX Initial horizontal velocity in pixels per second
* @param velocityY Initial vertical velocity in pixels per second
* @return true if the fling was started, false if the velocity was too low to fling 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) {
return fling(velocityX, velocityY, mMinFlingVelocity, mMaxFlingVelocity);
}
/** Flings without checking fling velocity thresholds. */
boolean flingNoThresholdCheck(int velocityX, int velocityY) {
return fling(velocityX, velocityY, 0, Integer.MAX_VALUE);
}
private boolean fling(int velocityX, int velocityY, int minFlingVelocity,
int maxFlingVelocity) {
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) < minFlingVelocity) {
velocityX = 0;
}
if (!canScrollVertical || Math.abs(velocityY) < minFlingVelocity) {
velocityY = 0;
}
if (velocityX == 0 && velocityY == 0) {
// If we don't have any velocity, return false
return false;
}
// Flinging while the edge effect is active should affect the edge effect,
// not scrolling.
int flingX = 0;
int flingY = 0;
if (velocityX != 0) {
if (mLeftGlow != null && EdgeEffectCompat.getDistance(mLeftGlow) != 0) {
if (shouldAbsorb(mLeftGlow, -velocityX, getWidth())) {
mLeftGlow.onAbsorb(-velocityX);
} else {
flingX = velocityX;
}
velocityX = 0;
} else if (mRightGlow != null && EdgeEffectCompat.getDistance(mRightGlow) != 0) {
if (shouldAbsorb(mRightGlow, velocityX, getWidth())) {
mRightGlow.onAbsorb(velocityX);
} else {
flingX = velocityX;
}
velocityX = 0;
}
}
if (velocityY != 0) {
if (mTopGlow != null && EdgeEffectCompat.getDistance(mTopGlow) != 0) {
if (shouldAbsorb(mTopGlow, -velocityY, getHeight())) {
mTopGlow.onAbsorb(-velocityY);
} else {
flingY = velocityY;
}
velocityY = 0;
} else if (mBottomGlow != null && EdgeEffectCompat.getDistance(mBottomGlow) != 0) {
if (shouldAbsorb(mBottomGlow, velocityY, getHeight())) {
mBottomGlow.onAbsorb(velocityY);
} else {
flingY = velocityY;
}
velocityY = 0;
}
}
if (flingX != 0 || flingY != 0) {
flingX = Math.max(-maxFlingVelocity, Math.min(flingX, maxFlingVelocity));
flingY = Math.max(-maxFlingVelocity, Math.min(flingY, maxFlingVelocity));
startNestedScrollForType(TYPE_NON_TOUCH);
mViewFlinger.fling(flingX, flingY);
}
if (velocityX == 0 && velocityY == 0) {
return flingX != 0 || flingY != 0;
}
if (!dispatchNestedPreFling(velocityX, velocityY)) {
final boolean canScroll = canScrollHorizontal || canScrollVertical;
dispatchNestedFling(velocityX, velocityY, canScroll);
if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) {
return true;
}
if (canScroll) {
startNestedScrollForType(TYPE_NON_TOUCH);
velocityX = Math.max(-maxFlingVelocity, Math.min(velocityX, maxFlingVelocity));
velocityY = Math.max(-maxFlingVelocity, Math.min(velocityY, maxFlingVelocity));
mViewFlinger.fling(velocityX, velocityY);
return true;
}
}
return false;
}
private void startNestedScrollForType(int type) {
final boolean canScrollHorizontal = mLayout.canScrollHorizontally();
final boolean canScrollVertical = mLayout.canScrollVertically();
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontal) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertical) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
startNestedScroll(nestedScrollAxis, type);
}
/**
* Returns true if edgeEffect should call onAbsorb() with veclocity or false if it should
* animate with a fling. It will animate with a fling if the velocity will remove the
* EdgeEffect through its normal operation.
*
* @param edgeEffect The EdgeEffect that might absorb the velocity.
* @param velocity The velocity of the fling motion
* @param size The width or height of the RecyclerView, depending on the edge that the
* EdgeEffect is on.
* @return true if the velocity should be absorbed or false if it should be flung.
*/
private boolean shouldAbsorb(@NonNull EdgeEffect edgeEffect, int velocity, int size) {
if (velocity > 0) {
return true;
}
float distance = EdgeEffectCompat.getDistance(edgeEffect) * size;
// This is flinging without the spring, so let's see if it will fling past the overscroll
float flingDistance = getSplineFlingDistance(-velocity);
return flingDistance < distance;
}
/**
* If mLeftGlow or mRightGlow is currently active and the motion will remove some of the
* stretch, this will consume any of unconsumedX that the glow can. If the motion would
* increase the stretch, or the EdgeEffect isn't a stretch, then nothing will be consumed.
*
* @param unconsumedX The horizontal delta that might be consumed by the horizontal EdgeEffects
* @return The remaining unconsumed delta after the edge effects have consumed.
*/
int consumeFlingInHorizontalStretch(int unconsumedX) {
return consumeFlingInStretch(unconsumedX, mLeftGlow, mRightGlow, getWidth());
}
/**
* If mTopGlow or mBottomGlow is currently active and the motion will remove some of the
* stretch, this will consume any of unconsumedY that the glow can. If the motion would
* increase the stretch, or the EdgeEffect isn't a stretch, then nothing will be consumed.
*
* @param unconsumedY The vertical delta that might be consumed by the vertical EdgeEffects
* @return The remaining unconsumed delta after the edge effects have consumed.
*/
int consumeFlingInVerticalStretch(int unconsumedY) {
return consumeFlingInStretch(unconsumedY, mTopGlow, mBottomGlow, getHeight());
}
/**
* Used by consumeFlingInHorizontalStretch() and consumeFlinInVerticalStretch() for
* consuming deltas from EdgeEffects
* @param unconsumed The unconsumed delta that the EdgeEffets may consume
* @param startGlow The start (top or left) EdgeEffect
* @param endGlow The end (bottom or right) EdgeEffect
* @param size The width or height of the container, depending on whether this is for
* horizontal or vertical EdgeEffects
* @return The unconsumed delta after the EdgeEffects have had an opportunity to consume.
*/
private int consumeFlingInStretch(
int unconsumed,
EdgeEffect startGlow,
EdgeEffect endGlow,
int size
) {
if (unconsumed > 0 && startGlow != null && EdgeEffectCompat.getDistance(startGlow) != 0f) {
float deltaDistance = -unconsumed * FLING_DESTRETCH_FACTOR / size;
int consumed = Math.round(-size / FLING_DESTRETCH_FACTOR
* EdgeEffectCompat.onPullDistance(startGlow, deltaDistance, 0.5f));
if (consumed != unconsumed) {
startGlow.finish();
}
return unconsumed - consumed;
}
if (unconsumed < 0 && endGlow != null && EdgeEffectCompat.getDistance(endGlow) != 0f) {
float deltaDistance = unconsumed * FLING_DESTRETCH_FACTOR / size;
int consumed = Math.round(size / FLING_DESTRETCH_FACTOR
* EdgeEffectCompat.onPullDistance(endGlow, deltaDistance, 0.5f));
if (consumed != unconsumed) {
endGlow.finish();
}
return unconsumed - consumed;
}
return unconsumed;
}
/**
* 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.onPullDistance(mLeftGlow, -overscrollX / getWidth(),
1f - y / getHeight());
invalidate = true;
} else if (overscrollX > 0) {
ensureRightGlow();
EdgeEffectCompat.onPullDistance(mRightGlow, overscrollX / getWidth(), y / getHeight());
invalidate = true;
}
if (overscrollY < 0) {
ensureTopGlow();
EdgeEffectCompat.onPullDistance(mTopGlow, -overscrollY / getHeight(), x / getWidth());
invalidate = true;
} else if (overscrollY > 0) {
ensureBottomGlow();
EdgeEffectCompat.onPullDistance(mBottomGlow, overscrollY / getHeight(),
1f - x / getWidth());
invalidate = true;
}
if (invalidate || overscrollX != 0 || overscrollY != 0) {
postInvalidateOnAnimation();
}
}
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) {
postInvalidateOnAnimation();
}
}
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) {
postInvalidateOnAnimation();
}
}
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) {
postInvalidateOnAnimation();
}
}
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());
}
}
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());
}
}
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());
}
}
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());
}
}
void invalidateGlows() {
mLeftGlow = mRightGlow = mTopGlow = mBottomGlow = null;
}
/**
* Set a {@link EdgeEffectFactory} for this {@link RecyclerView}.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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 (!needsFocusFailureLayout && mLayout.canScrollHorizontally()) {
boolean rtl = mLayout.getLayoutDirection() == View.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 (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 || next == focused) {
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() == View.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.
*/
private 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<View> views, int direction, int focusableMode) {
if (mLayout == null || !mLayout.onAddFocusables(this, views, direction, focusableMode)) {
super.addFocusables(views, direction, focusableMode);
}
}
@Override
protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
if (isComputingLayout()) {
// if we are in the middle of a layout calculation, don't let any child take focus.
// RV will handle it after layout calculation is finished.
return false;
}
return super.onRequestFocusInDescendants(direction, previouslyFocusedRect);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mLayoutOrScrollCounter = 0;
mIsAttached = true;
mFirstLayoutComplete = mFirstLayoutComplete && !isLayoutRequested();
mRecycler.onAttachedToWindow();
if (mLayout != null) {
mLayout.dispatchAttachedToWindow(this);
}
mPostedAnimatorRunner = false;
if (ALLOW_THREAD_GAP_WORK) {
// Register with gap worker
mGapWorker = GapWorker.sGapWorker.get();
if (mGapWorker == null) {
mGapWorker = new GapWorker();
// break 60 fps assumption if data from display appears valid
// NOTE: we only do this query once, statically, because it's very expensive (> 1ms)
Display display = ViewCompat.getDisplay(this);
float refreshRate = 60.0f;
if (!isInEditMode() && display != null) {
float displayRefreshRate = display.getRefreshRate();
if (displayRefreshRate >= 30.0f) {
refreshRate = displayRefreshRate;
}
}
mGapWorker.mFrameIntervalNs = (long) (1000000000 / refreshRate);
GapWorker.sGapWorker.set(mGapWorker);
}
mGapWorker.add(this);
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mItemAnimator != null) {
mItemAnimator.endAnimations();
}
stopScroll();
mIsAttached = false;
if (mLayout != null) {
mLayout.dispatchDetachedFromWindow(this, mRecycler);
}
mPendingAccessibilityImportanceChange.clear();
removeCallbacks(mItemAnimatorRunner);
mViewInfoStore.onDetach();
mRecycler.onDetachedFromWindow();
PoolingContainer.callPoolingContainerOnReleaseForChildren(this);
if (ALLOW_THREAD_GAP_WORK && mGapWorker != null) {
// Unregister with gap worker
mGapWorker.remove(this);
mGapWorker = null;
}
}
/**
* Returns true if RecyclerView is attached to window.
*/
@Override
public boolean isAttachedToWindow() {
return mIsAttached;
}
/**
* Checks if RecyclerView is in the middle of a layout or scroll and throws an
* {@link IllegalStateException} if it <b>is not</b>.
*
* @param message The message for the exception. Can be null.
* @see #assertNotInLayoutOrScroll(String)
*/
void assertInLayoutOrScroll(String message) {
if (!isComputingLayout()) {
if (message == null) {
throw new IllegalStateException("Cannot call this method unless RecyclerView is "
+ "computing a layout or scrolling" + exceptionLabel());
}
throw new IllegalStateException(message + exceptionLabel());
}
}
/**
* Checks if RecyclerView is in the middle of a layout or scroll and throws an
* {@link IllegalStateException} if it <b>is</b>.
*
* @param message The message for the exception. Can be null.
* @see #assertInLayoutOrScroll(String)
*/
void assertNotInLayoutOrScroll(String message) {
if (isComputingLayout()) {
if (message == null) {
throw new IllegalStateException("Cannot call this method while RecyclerView is "
+ "computing a layout or scrolling" + exceptionLabel());
}
throw new IllegalStateException(message);
}
if (mDispatchScrollCounter > 0) {
Log.w(TAG, "Cannot call this method in a scroll callback. Scroll callbacks might"
+ "be run during a measure & layout pass where you cannot change the"
+ "RecyclerView data. Any method call that might change the structure"
+ "of the RecyclerView or the adapter contents should be postponed to"
+ "the next frame.",
new IllegalStateException("" + exceptionLabel()));
}
}
/**
* Add an {@link OnItemTouchListener} to intercept touch events before they are dispatched
* to child views or this view's standard scrolling behavior.
*
* <p>Client code may use listeners to implement item manipulation behavior. Once a listener
* returns true from
* {@link OnItemTouchListener#onInterceptTouchEvent(RecyclerView, MotionEvent)} its
* {@link OnItemTouchListener#onTouchEvent(RecyclerView, MotionEvent)} method will be called
* for each incoming MotionEvent until the end of the gesture.</p>
*
* @param listener Listener to add
* @see SimpleOnItemTouchListener
*/
public void addOnItemTouchListener(@NonNull OnItemTouchListener listener) {
mOnItemTouchListeners.add(listener);
}
/**
* Remove an {@link OnItemTouchListener}. It will no longer be able to intercept touch events.
*
* @param listener Listener to remove
*/
public void removeOnItemTouchListener(@NonNull OnItemTouchListener listener) {
mOnItemTouchListeners.remove(listener);
if (mInterceptingOnItemTouchListener == listener) {
mInterceptingOnItemTouchListener = null;
}
}
/**
* Dispatches the motion event to the intercepting OnItemTouchListener or provides opportunity
* for OnItemTouchListeners to intercept.
*
* @param e The MotionEvent
* @return True if handled by an intercepting OnItemTouchListener.
*/
private boolean dispatchToOnItemTouchListeners(MotionEvent e) {
// OnItemTouchListeners should receive calls to their methods in the same pattern that
// ViewGroups do. That pattern is a bit confusing, which in turn makes the below code a
// bit confusing. Here are rules for the pattern:
//
// 1. A single MotionEvent should not be passed to either OnInterceptTouchEvent or
// OnTouchEvent twice.
// 2. ACTION_DOWN MotionEvents may be passed to both onInterceptTouchEvent and
// onTouchEvent.
// 3. All other MotionEvents should be passed to either onInterceptTouchEvent or
// onTouchEvent, not both.
// Side Note: We don't currently perfectly mimic how MotionEvents work in the view system.
// If we were to do so, for every MotionEvent, any OnItemTouchListener that is before the
// intercepting OnItemTouchEvent should still have a chance to intercept, and if it does,
// the previously intercepting OnItemTouchEvent should get an ACTION_CANCEL event.
if (mInterceptingOnItemTouchListener == null) {
if (e.getAction() == MotionEvent.ACTION_DOWN) {
return false;
}
return findInterceptingOnItemTouchListener(e);
} else {
mInterceptingOnItemTouchListener.onTouchEvent(this, e);
final int action = e.getAction();
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
mInterceptingOnItemTouchListener = null;
}
return true;
}
}
/**
* Looks for an OnItemTouchListener that wants to intercept.
*
* <p>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 (stopGlowAnimations(e) || mScrollState == SCROLL_STATE_SETTLING) {
getParent().requestDisallowInterceptTouchEvent(true);
setScrollState(SCROLL_STATE_DRAGGING);
stopNestedScroll(TYPE_NON_TOUCH);
}
// Clear the nested offsets
mNestedOffsets[0] = mNestedOffsets[1] = 0;
startNestedScrollForType(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;
}
/**
* This stops any edge glow animation that is currently running by applying a
* 0 length pull at the displacement given by the provided MotionEvent. On pre-S devices,
* this method does nothing, allowing any animating edge effect to continue animating and
* returning <code>false</code> always.
*
* @param e The motion event to use to indicate the finger position for the displacement of
* the current pull.
* @return <code>true</code> if any edge effect had an existing effect to be drawn ond the
* animation was stopped or <code>false</code> if no edge effect had a value to display.
*/
private boolean stopGlowAnimations(MotionEvent e) {
boolean stopped = false;
if (mLeftGlow != null && EdgeEffectCompat.getDistance(mLeftGlow) != 0
&& !canScrollHorizontally(-1)) {
EdgeEffectCompat.onPullDistance(mLeftGlow, 0, 1 - (e.getY() / getHeight()));
stopped = true;
}
if (mRightGlow != null && EdgeEffectCompat.getDistance(mRightGlow) != 0
&& !canScrollHorizontally(1)) {
EdgeEffectCompat.onPullDistance(mRightGlow, 0, e.getY() / getHeight());
stopped = true;
}
if (mTopGlow != null && EdgeEffectCompat.getDistance(mTopGlow) != 0
&& !canScrollVertically(-1)) {
EdgeEffectCompat.onPullDistance(mTopGlow, 0, e.getX() / getWidth());
stopped = true;
}
if (mBottomGlow != null && EdgeEffectCompat.getDistance(mBottomGlow) != 0
&& !canScrollVertically(1)) {
EdgeEffectCompat.onPullDistance(mBottomGlow, 0, 1 - e.getX() / getWidth());
stopped = true;
}
return stopped;
}
@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 int action = e.getActionMasked();
final int actionIndex = e.getActionIndex();
if (action == MotionEvent.ACTION_DOWN) {
mNestedOffsets[0] = mNestedOffsets[1] = 0;
}
final MotionEvent vtev = MotionEvent.obtain(e);
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);
startNestedScrollForType(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;
if (mScrollState != SCROLL_STATE_DRAGGING) {
boolean startScroll = false;
if (canScrollHorizontally) {
if (dx > 0) {
dx = Math.max(0, dx - mTouchSlop);
} else {
dx = Math.min(0, dx + mTouchSlop);
}
if (dx != 0) {
startScroll = true;
}
}
if (canScrollVertically) {
if (dy > 0) {
dy = Math.max(0, dy - mTouchSlop);
} else {
dy = Math.min(0, dy + mTouchSlop);
}
if (dy != 0) {
startScroll = true;
}
}
if (startScroll) {
setScrollState(SCROLL_STATE_DRAGGING);
}
}
if (mScrollState == SCROLL_STATE_DRAGGING) {
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
dx -= releaseHorizontalGlow(dx, e.getY());
dy -= releaseVerticalGlow(dy, e.getX());
if (dispatchNestedPreScroll(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
mReusableIntPair, mScrollOffset, TYPE_TOUCH
)) {
dx -= mReusableIntPair[0];
dy -= mReusableIntPair[1];
// Updated the nested offsets
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
// Scroll has initiated, prevent parents from intercepting
getParent().requestDisallowInterceptTouchEvent(true);
}
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
e, TYPE_TOUCH)) {
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;
}
int flingAxis = 0;
boolean useSmoothScroll = 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;
}
// Use smooth scrolling for low resolution rotary encoders to avoid the visible
// pixel jumps that would be caused by doing regular scrolling.
useSmoothScroll = mLowResRotaryEncoderFeature;
// Support fling for rotary encoders.
flingAxis = MotionEventCompat.AXIS_SCROLL;
} else {
vScroll = 0f;
hScroll = 0f;
}
int scaledVScroll = (int) (vScroll * mScaledVerticalScrollFactor);
int scaledHScroll = (int) (hScroll * mScaledHorizontalScrollFactor);
if (useSmoothScroll) {
OverScroller overScroller = mViewFlinger.mOverScroller;
// Account for any remaining scroll from a previous generic motion event.
scaledVScroll += overScroller.getFinalY() - overScroller.getCurrY();
scaledHScroll += overScroller.getFinalX() - overScroller.getCurrX();
smoothScrollBy(scaledHScroll, scaledVScroll, /* interpolator= */ null,
UNDEFINED_DURATION, /* withNestedScrolling= */ true);
} else {
nestedScrollByInternal(scaledHScroll, scaledVScroll, event, TYPE_NON_TOUCH);
}
if (flingAxis != 0 && !useSmoothScroll) {
mDifferentialMotionFlingController.onMotionEvent(event, flingAxis);
}
}
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);
// Calculate and track whether we should skip measurement here because the MeasureSpec
// modes in both dimensions are EXACTLY.
mLastAutoMeasureSkippedDueToExact =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
if (mLastAutoMeasureSkippedDueToExact || 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);
}
mLastAutoMeasureNonExactMeasuredWidth = getMeasuredWidth();
mLastAutoMeasureNonExactMeasuredHeight = getMeasuredHeight();
} 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 (sDebugAssertionsEnabled && 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.
* <p>
* 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.
* <p>
* 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).
* <p>
* 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 <code>true</code> if RecyclerView is currently computing a layout, <code>false</code>
* 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.
* <p>
* 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.w(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 the last time we measured children in onMeasure, we skipped the measurement and layout
// of RV children because the MeasureSpec in both dimensions was EXACTLY, and current
// dimensions of the RV are not equal to the last measured dimensions of RV, we need to
// measure and layout children one last time.
boolean needsRemeasureDueToExactSkip = mLastAutoMeasureSkippedDueToExact
&& (mLastAutoMeasureNonExactMeasuredWidth != getWidth()
|| mLastAutoMeasureNonExactMeasuredHeight != getHeight());
mLastAutoMeasureNonExactMeasuredWidth = 0;
mLastAutoMeasureNonExactMeasuredHeight = 0;
mLastAutoMeasureSkippedDueToExact = false;
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else if (mAdapterHelper.hasUpdates()
|| needsRemeasureDueToExactSkip
|| 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.
// TODO(shepshapard): Worth a note that I believe
// "mLayout.getWidth() != getWidth() || mLayout.getHeight() != getHeight()" above is
// not actually correct, causes unnecessary work to be done, and should be
// removed. Removing causes many tests to fail and I didn't have the time to
// investigate. Just a note for the a future reader or bug fixer.
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.getAbsoluteAdapterPosition());
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 (!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;
if (mPendingSavedState != null && mAdapter.canRestoreState()) {
if (mPendingSavedState.mLayoutState != null) {
mLayout.onRestoreInstanceState(mPendingSavedState.mLayoutState);
}
mPendingSavedState = null;
}
// Step 2: Run layout
mState.mInPreLayout = false;
mLayout.onLayoutChildren(mRecycler, mState);
mState.mStructureChanged = false;
// 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.
* <p>
* We might be able to detect the error in the application which will help the developer to
* resolve the issue.
* <p>
* 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());
}
} else {
if (sDebugAssertionsEnabled) {
throw new IllegalArgumentException(
"No ViewHolder found for child: " + child + 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) {
Trace.beginSection(TRACE_ON_LAYOUT_TAG);
dispatchLayout();
Trace.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(@NonNull 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 (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());
}
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());
}
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) {
postInvalidateOnAnimation();
}
}
@Override
public void onDraw(@NonNull 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.
* <p>
* 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 (sDebugAssertionsEnabled && 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 (sVerboseLoggingEnabled) {
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 (sVerboseLoggingEnabled) {
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 (sVerboseLoggingEnabled) {
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 (sVerboseLoggingEnabled) {
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.
*
* <ul>
* <li>Once layout occurs, all attached items should be discarded or animated.
* <li>Attached items are labeled as invalid.
* <li>Because items may still be prefetched between a "data set completely changed"
* event and a layout event, all cached items are discarded.
* </ul>
*
* @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.
* <p>
* 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.
* <p>
* 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.getAbsoluteAdapterPosition() : NO_POSITION;
}
/**
* Return the adapter position of the given child view as of the latest completed layout pass.
* <p>
* 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.
* <p>
* This method checks only the children of RecyclerView. If the item at the given
* <code>position</code> is not laid out, it <em>will not</em> create a new one.
* <p>
* 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#getBindingAdapterPosition()} to get the current adapter position
* of a ViewHolder. If the {@link Adapter} that is assigned to the RecyclerView is an adapter
* that combines other adapters (e.g. {@link ConcatAdapter}), you can use the
* {@link ViewHolder#getBindingAdapter()}) to find the position relative to the {@link Adapter}
* that bound the {@link ViewHolder}.
* <p>
* 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 <code>position</code> 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 <code>null</code> since the new positions of views
* are unknown until the layout is calculated.
* <p>
* This method checks only the children of RecyclerView. If the item at the given
* <code>position</code> is not laid out, it <em>will not</em> create a new one.
* <p>
* 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 <code>position</code> 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()
&& getAdapterPositionInRecyclerView(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.
* <p>
* This method checks only the children of RecyclerView. If the item with the given
* <code>id</code> is not laid out, it <em>will not</em> 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 <code>id</code> 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(@NonNull Canvas canvas, View child, long drawingTime) {
return super.drawChild(canvas, child, drawingTime);
}
/**
* Offset the bounds of all child views by <code>dy</code> 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.
*
* <p>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.</p>
*
* @param child Child view that is now attached to this RecyclerView and its associated window
*/
public void onChildAttachedToWindow(@NonNull View child) {
}
/**
* Called when an item view is detached from this RecyclerView.
*
* <p>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.</p>
*
* @param child Child view that is now detached from this RecyclerView and its associated window
*/
public void onChildDetachedFromWindow(@NonNull View child) {
}
/**
* Offset the bounds of all child views by <code>dx</code> 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.
*
* <p>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.</p>
*
* <p>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, <code>onScrollChanged</code> 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.</p>
*
* @param dx horizontal distance scrolled in pixels
* @param dy vertical distance scrolled in pixels
*/
public void onScrolled(@Px int dx, @Px int dy) {
// Do nothing
}
void dispatchOnScrolled(int hresult, int vresult) {
mDispatchScrollCounter++;
// Pass the current scrollX/scrollY values as current values. No actual change in these
// properties occurred. Pass negative hresult and vresult as old values so that
// postSendViewScrolledAccessibilityEventCallback(l - oldl, t - oldt) in onScrollChanged
// sends the scrolled accessibility event correctly.
final int scrollX = getScrollX();
final int scrollY = getScrollY();
onScrollChanged(scrollX, scrollY, scrollX - hresult, scrollY - vresult);
// Pass the real deltas to onScrolled, the RecyclerView-specific method.
onScrolled(hresult, vresult);
// Invoke listeners last. Subclassed view methods always handle the event first.
// All internal state is consistent by the time listeners are invoked.
if (mScrollListener != null) {
mScrollListener.onScrolled(this, hresult, vresult);
}
if (mScrollListeners != null) {
for (int i = mScrollListeners.size() - 1; i >= 0; i--) {
mScrollListeners.get(i).onScrolled(this, hresult, vresult);
}
}
mDispatchScrollCounter--;
}
/**
* Called when the scroll state of this RecyclerView changes. Subclasses should use this
* method to respond to state changes instead of an explicit listener.
*
* <p>This method will always be invoked before listeners, but after the LayoutManager
* responds to the scroll state change.</p>
*
* @param state the new scroll state, one of {@link #SCROLL_STATE_IDLE},
* {@link #SCROLL_STATE_DRAGGING} or {@link #SCROLL_STATE_SETTLING}
*/
public void onScrollStateChanged(int state) {
// Do nothing
}
/**
* Copied from OverScroller, this returns the distance that a fling with the given velocity
* will go.
* @param velocity The velocity of the fling
* @return The distance that will be traveled by a fling of the given velocity.
*/
private float getSplineFlingDistance(int velocity) {
final double l =
Math.log(INFLEXION * Math.abs(velocity) / (SCROLL_FRICTION * mPhysicalCoef));
final double decelMinusOne = DECELERATION_RATE - 1.0;
return (float) (SCROLL_FRICTION * mPhysicalCoef
* Math.exp(DECELERATION_RATE / decelMinusOne * l));
}
void dispatchOnScrollStateChanged(int state) {
// Let the LayoutManager go first; this allows it to bring any properties into
// a consistent state before the RecyclerView subclass responds.
if (mLayout != null) {
mLayout.onScrollStateChanged(state);
}
// Let the RecyclerView subclass handle this event next; any LayoutManager property
// changes will be reflected by this time.
onScrollStateChanged(state);
// Listeners go last. All other internal state is consistent by this point.
if (mScrollListener != null) {
mScrollListener.onScrollStateChanged(this, state);
}
if (mScrollListeners != null) {
for (int i = mScrollListeners.size() - 1; i >= 0; i--) {
mScrollListeners.get(i).onScrollStateChanged(this, state);
}
}
}
/**
* Returns whether there are pending adapter updates which are not yet applied to the layout.
* <p>
* If this method returns <code>true</code>, 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.
* <p>
* 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();
}
// Effectively private. Set to default to avoid synthetic accessor.
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;
unconsumedX = consumeFlingInHorizontalStretch(unconsumedX);
unconsumedY = consumeFlingInVerticalStretch(unconsumedY);
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, consumedX, consumedY);
}
}
if (Build.VERSION.SDK_INT >= 35) {
Api35Impl.setFrameContentVelocity(RecyclerView.this,
Math.abs(scroller.getCurrVelocity()));
}
}
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();
}
/**
* Smooth scrolls the RecyclerView by a given distance.
*
* @param dx x distance in pixels.
* @param dy y distance in pixels.
* @param duration Duration of the animation in milliseconds. Set to
* {@link #UNDEFINED_DURATION} to have the duration automatically
* calculated
* based on an internally defined standard velocity.
* @param interpolator {@link Interpolator} to be used for scrolling. If it is {@code null},
* RecyclerView will use an internal default interpolator.
*/
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);
}
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();
}
/**
* Computes of an animated scroll in milliseconds.
* @param dx x distance in pixels.
* @param dy y distance in pixels.
* @return The duration of the animated scroll in milliseconds.
*/
private int computeScrollDuration(int dx, int dy) {
final int absDx = Math.abs(dx);
final int absDy = Math.abs(dy);
final boolean horizontal = absDx > absDy;
final int containerSize = horizontal ? getWidth() : getHeight();
float absDelta = (float) (horizontal ? absDx : absDy);
final int 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 (mHasFixedSize && mIsAttached) {
ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
} else {
mAdapterUpdateDuringMeasure = true;
requestLayout();
}
}
@Override
public void onStateRestorationPolicyChanged() {
if (mPendingSavedState == null) {
return;
}
// If there is a pending saved state and the new mode requires us to restore it,
// we'll request a layout which will call the adapter to see if it can restore state
// and trigger state restoration
Adapter<?> adapter = mAdapter;
if (adapter != null && adapter.canRestoreState()) {
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());
}
}
/**
* The default EdgeEffectFactory sets the edge effect type of the EdgeEffect.
*/
static class StretchEdgeEffectFactory extends EdgeEffectFactory {
@NonNull
@Override
protected EdgeEffect createEdgeEffect(@NonNull RecyclerView view, int direction) {
return new EdgeEffect(view.getContext());
}
}
/**
* RecycledViewPool lets you share Views between multiple RecyclerViews.
* <p>
* If you want to recycle views across RecyclerViews, create an instance of RecycledViewPool
* and use {@link RecyclerView#setRecycledViewPool(RecycledViewPool)}.
* <p>
* RecyclerView automatically creates a pool for itself if you don't provide one.
*/
public static class RecycledViewPool {
private static final int DEFAULT_MAX_SCRAP = 5;
/**
* 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<ViewHolder> mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
long mCreateRunningAverageNs = 0;
long mBindRunningAverageNs = 0;
}
SparseArray<ScrapData> mScrap = new SparseArray<>();
/**
* Attach counts for clearing (that is, emptying the pool when there are no adapters
* attached) and for PoolingContainer release are tracked separately to maintain the
* historical behavior of this functionality.
*
* The count for clearing is inaccurate in certain scenarios: for instance, if a
* RecyclerView is removed from the view hierarchy and thrown away to be GCed, the
* attach count will never be correspondingly decreased. However, it has been this way
* for years without any complaints, so we are not going to potentially increase the
* number of scenarios where the pool would be cleared.
*
* The attached adapters for PoolingContainer purposes strives to be more accurate, as
* it will be decremented whenever a RecyclerView is detached from the window. This
* could potentially be inaccurate in the unlikely event that someone is manually driving
* a detached RecyclerView by calling measure, layout, draw, etc. However, the
* implementation of {@link RecyclerView#onDetachedFromWindow()} suggests this is not the
* only unexpected behavior that doing so might provoke, so this should be acceptable.
*/
int mAttachCountForClearing = 0;
/**
* The set of adapters for PoolingContainer release purposes
*
* @see #mAttachCountForClearing
*/
Set<Adapter<?>> mAttachedAdaptersForPoolingContainer =
Collections.newSetFromMap(new IdentityHashMap<>());
/**
* Discard all ViewHolders.
*/
public void clear() {
for (int i = 0; i < mScrap.size(); i++) {
ScrapData data = mScrap.valueAt(i);
for (ViewHolder scrap: data.mScrapHeap) {
PoolingContainer.callPoolingContainerOnRelease(scrap.itemView);
}
data.mScrapHeap.clear();
}
}
/**
* Sets the maximum number of ViewHolders to hold in the pool before discarding.
*
* @param viewType ViewHolder Type
* @param max Maximum number
*/
public void setMaxRecycledViews(int viewType, int max) {
ScrapData scrapData = getScrapDataForType(viewType);
scrapData.mMaxScrap = max;
final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
while (scrapHeap.size() > max) {
scrapHeap.remove(scrapHeap.size() - 1);
}
}
/**
* Returns the current number of Views held by the RecycledViewPool of the given view type.
*/
public int getRecycledViewCount(int viewType) {
return getScrapDataForType(viewType).mScrapHeap.size();
}
/**
* Acquire a ViewHolder of the specified type from the pool, or {@code null} if none are
* present.
*
* @param viewType ViewHolder type.
* @return ViewHolder of the specified type acquired from the pool, or {@code null} if none
* are present.
*/
@Nullable
public ViewHolder getRecycledView(int viewType) {
final ScrapData scrapData = mScrap.get(viewType);
if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
for (int i = scrapHeap.size() - 1; i >= 0; i--) {
if (!scrapHeap.get(i).isAttachedToTransitionOverlay()) {
return scrapHeap.remove(i);
}
}
}
return null;
}
/**
* Total number of ViewHolders held by the pool.
*
* @return Number of ViewHolders held by the pool.
*/
int size() {
int count = 0;
for (int i = 0; i < mScrap.size(); i++) {
ArrayList<ViewHolder> viewHolders = mScrap.valueAt(i).mScrapHeap;
if (viewHolders != null) {
count += viewHolders.size();
}
}
return count;
}
/**
* Add a scrap ViewHolder to the pool.
* <p>
* 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<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
PoolingContainer.callPoolingContainerOnRelease(scrap.itemView);
return;
}
if (sDebugAssertionsEnabled && scrapHeap.contains(scrap)) {
throw new IllegalArgumentException("this scrap item already exists");
}
scrap.resetInternal();
scrapHeap.add(scrap);
}
long runningAverage(long oldAverage, long newValue) {
if (oldAverage == 0) {
return newValue;
}
return (oldAverage / 4 * 3) + (newValue / 4);
}
void factorInCreateTime(int viewType, long createTimeNs) {
ScrapData scrapData = getScrapDataForType(viewType);
scrapData.mCreateRunningAverageNs = runningAverage(
scrapData.mCreateRunningAverageNs, createTimeNs);
}
void factorInBindTime(int viewType, long bindTimeNs) {
ScrapData scrapData = getScrapDataForType(viewType);
scrapData.mBindRunningAverageNs = runningAverage(
scrapData.mBindRunningAverageNs, bindTimeNs);
}
boolean willCreateInTime(int viewType, long approxCurrentNs, long deadlineNs) {
long expectedDurationNs = getScrapDataForType(viewType).mCreateRunningAverageNs;
return expectedDurationNs == 0 || (approxCurrentNs + expectedDurationNs < deadlineNs);
}
boolean willBindInTime(int viewType, long approxCurrentNs, long deadlineNs) {
long expectedDurationNs = getScrapDataForType(viewType).mBindRunningAverageNs;
return expectedDurationNs == 0 || (approxCurrentNs + expectedDurationNs < deadlineNs);
}
void attach() {
mAttachCountForClearing++;
}
void detach() {
mAttachCountForClearing--;
}
/**
* Adds this adapter to the set of adapters being tracked for PoolingContainer release
* purposes. This method may validly be called multiple times for a given adapter.
* Additional calls to this method for an already-attached adapter are a no-op.
*
* @param adapter the adapter to ensure is in the set
*/
void attachForPoolingContainer(@NonNull Adapter<?> adapter) {
mAttachedAdaptersForPoolingContainer.add(adapter);
}
/**
* Removes this adapter from the set of adapters being tracked for PoolingContainer
* release purposes. This method may validly be called multiple times for a given adapter.
+ Additional calls to this method for an already-detached adapter are a no-op.
*
* @param adapter the adapter to be removed from the set
* @param isBeingReplaced {@code true} if this detach is immediately preceding a call to
* {@link #attachForPoolingContainer(Adapter)} and
* {@link PoolingContainerListener#onRelease()} should not be triggered, or false otherwise
*/
void detachForPoolingContainer(@NonNull Adapter<?> adapter, boolean isBeingReplaced) {
mAttachedAdaptersForPoolingContainer.remove(adapter);
if (mAttachedAdaptersForPoolingContainer.size() == 0 && !isBeingReplaced) {
for (int keyIndex = 0; keyIndex < mScrap.size(); keyIndex++) {
ArrayList<ViewHolder> scrapHeap = mScrap.get(mScrap.keyAt(keyIndex)).mScrapHeap;
for (int i = 0; i < scrapHeap.size(); i++) {
PoolingContainer.callPoolingContainerOnRelease(
scrapHeap.get(i).itemView
);
}
}
}
}
/**
* Detaches the old adapter and attaches the new one.
* <p>
* 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 && mAttachCountForClearing == 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.
*
* <p>A "scrapped" view is a view that is still attached to its parent RecyclerView but
* that has been marked for removal or reuse.</p>
*
* <p>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.</p>
*/
public final class Recycler {
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
ArrayList<ViewHolder> mChangedScrap = null;
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
private final List<ViewHolder>
mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);
private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
int mViewCacheMax = DEFAULT_CACHE_SIZE;
RecycledViewPool mRecyclerPool;
private ViewCacheExtension mViewCacheExtension;
static final int DEFAULT_CACHE_SIZE = 2;
/**
* Clear scrap views out of this recycler. Detached views contained within a
* recycled view pool will remain.
*/
public void clear() {
mAttachedScrap.clear();
recycleAndClearCachedViews();
}
/**
* Set the maximum number of detached, valid views we should retain for later use.
*
* @param viewCount Number of views to keep before sending views to the shared pool
*/
public void setViewCacheSize(int viewCount) {
mRequestedCacheMax = viewCount;
updateViewCacheSize();
}
void updateViewCacheSize() {
int extraCache = mLayout != null ? mLayout.mPrefetchMaxCountObserved : 0;
mViewCacheMax = mRequestedCacheMax + extraCache;
// first, try the views that can be recycled
for (int i = mCachedViews.size() - 1;
i >= 0 && mCachedViews.size() > mViewCacheMax; i--) {
recycleCachedViewAt(i);
}
}
/**
* Returns an unmodifiable list of ViewHolders that are currently in the scrap list.
*
* @return List of ViewHolders in the scrap list.
*/
@NonNull
public List<ViewHolder> getScrapList() {
return mUnmodifiableAttachedScrap;
}
/**
* Helper method for getViewForPosition.
* <p>
* 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 (sDebugAssertionsEnabled && !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.
*/
@SuppressWarnings("unchecked")
private boolean tryBindViewHolderByDeadline(@NonNull ViewHolder holder, int offsetPosition,
int position, long deadlineNs) {
holder.mBindingAdapter = null;
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;
}
// Holders being bound should be either fully attached or fully detached.
// We don't want to bind with views that are temporarily detached, because that
// creates a situation in which they are unable to reason about their attach state
// properly.
// For example, isAttachedToWindow will return true, but the itemView will lack a
// parent. This breaks, among other possible issues, anything involving traversing
// the view tree, such as ViewTreeLifecycleOwner.
// Thus, we temporarily reattach any temp-detached holders for the bind operation.
// See https://issuetracker.google.com/265347515 for additional details on problems
// resulting from this
boolean reattachedForBind = false;
if (holder.isTmpDetached()) {
attachViewToParent(holder.itemView, getChildCount(),
holder.itemView.getLayoutParams());
reattachedForBind = true;
}
mAdapter.bindViewHolder(holder, offsetPosition);
if (reattachedForBind) {
detachViewFromParent(holder.itemView);
}
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)}.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* Note that if the provided position belongs to a deleted ViewHolder, this method will
* return -1.
* <p>
* 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}.
* <p>
* 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 <code>position</code> from <code>adapter</code>
*/
@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.
* <p>
* 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 (sVerboseLoggingEnabled) {
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 (sVerboseLoggingEnabled) {
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 (sDebugAssertionsEnabled && 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 (itemView.getImportantForAccessibility()
== View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
itemView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
}
if (mAccessibilityDelegate == null) {
return;
}
AccessibilityDelegateCompat itemDelegate = mAccessibilityDelegate.getItemDelegate();
if (itemDelegate instanceof RecyclerViewAccessibilityDelegate.ItemDelegate) {
// If there was already an a11y delegate set on the itemView, store it in the
// itemDelegate and then set the itemDelegate as the a11y delegate.
((RecyclerViewAccessibilityDelegate.ItemDelegate) itemDelegate)
.saveOriginalDelegate(itemView);
}
ViewCompat.setAccessibilityDelegate(itemView, itemDelegate);
}
}
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.
*
* <p>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.</p>
*
* @param view Removed view for recycling
* @see LayoutManager#removeAndRecycleView(View, Recycler)
*/
public void recycleView(@NonNull View view) {
// This public recycle method tries to make view recycle-able since layout manager
// intended to recycle this view (e.g. even if it is in scrap or change cache)
ViewHolder holder = getChildViewHolderInt(view);
if (holder.isTmpDetached()) {
removeDetachedView(view, false);
}
if (holder.isScrap()) {
holder.unScrap();
} else if (holder.wasReturnedFromScrap()) {
holder.clearReturnedFromScrapFlag();
}
recycleViewHolderInternal(holder);
// If the ViewHolder is running ItemAnimator, we want the recycleView() in scroll pass
// to stop the ItemAnimator and put ViewHolder back in cache or Pool.
// There are three situations:
// 1. If the custom Adapter clears ViewPropertyAnimator in view detach like the
// leanback (TV) app does, the ItemAnimator is likely to be stopped and
// recycleViewHolderInternal will succeed.
// 2. If the custom Adapter clears ViewPropertyAnimator, but the ItemAnimator uses
// "pending runnable" and ViewPropertyAnimator has not started yet, the ItemAnimator
// on the view will not be cleared. See b/73552923.
// 3. If the custom Adapter does not clear ViewPropertyAnimator in view detach, the
// ItemAnimator will not be cleared.
// Since both 2&3 lead to failure of recycleViewHolderInternal(), we just explicitly end
// the ItemAnimator, the callback of ItemAnimator.endAnimations() will recycle the View.
//
// Note the order: we must call endAnimation() after recycleViewHolderInternal()
// to avoid recycle twice. If ViewHolder isRecyclable is false,
// recycleViewHolderInternal() will not recycle it, endAnimation() will reset
// isRecyclable flag and recycle the view.
if (mItemAnimator != null && !holder.isRecyclable()) {
mItemAnimator.endAnimation(holder);
}
}
void recycleAndClearCachedViews() {
final int count = mCachedViews.size();
for (int i = count - 1; i >= 0; i--) {
recycleCachedViewAt(i);
}
mCachedViews.clear();
if (ALLOW_THREAD_GAP_WORK) {
mPrefetchRegistry.clearPrefetchPositions();
}
}
/**
* Recycles a cached view and removes the view from the list. Views are added to cache
* if and only if they are recyclable, so this method does not check it again.
* <p>
* 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 (sVerboseLoggingEnabled) {
Log.d(TAG, "Recycling cached view at index " + cachedViewIndex);
}
ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
if (sVerboseLoggingEnabled) {
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 (sDebugAssertionsEnabled && 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 (sVerboseLoggingEnabled) {
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) {
PoolingContainer.callPoolingContainerOnRelease(holder.itemView);
holder.mBindingAdapter = null;
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);
View itemView = holder.itemView;
if (mAccessibilityDelegate != null) {
AccessibilityDelegateCompat itemDelegate = mAccessibilityDelegate.getItemDelegate();
AccessibilityDelegateCompat originalDelegate = null;
if (itemDelegate instanceof RecyclerViewAccessibilityDelegate.ItemDelegate) {
originalDelegate =
((RecyclerViewAccessibilityDelegate.ItemDelegate) itemDelegate)
.getAndRemoveOriginalDelegateForItem(itemView);
}
// Set the a11y delegate back to whatever the original delegate was.
ViewCompat.setAccessibilityDelegate(itemView, originalDelegate);
}
if (dispatchRecycled) {
dispatchViewRecycled(holder);
}
holder.mBindingAdapter = null;
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.
*
* <p>"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.</p>
*
* @param view View to scrap
*/
void scrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
|| !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {
throw new IllegalArgumentException("Called scrap view with an invalid view."
+ " Invalid views cannot be reused from scrap, they should rebound from"
+ " recycler pool." + exceptionLabel());
}
holder.setScrapContainer(this, false);
mAttachedScrap.add(holder);
} else {
if (mChangedScrap == null) {
mChangedScrap = new ArrayList<ViewHolder>();
}
holder.setScrapContainer(this, true);
mChangedScrap.add(holder);
}
}
/**
* Remove a previously scrapped view from the pool of eligible scrap.
*
* <p>This view will no longer be eligible for reuse until re-scrapped or
* until it is explicitly removed and recycled.</p>
*/
void unscrapView(ViewHolder holder) {
if (holder.mInChangeScrap) {
mChangedScrap.remove(holder);
} else {
mAttachedScrap.remove(holder);
}
holder.mScrapContainer = null;
holder.mInChangeScrap = false;
holder.clearReturnedFromScrapFlag();
}
int getScrapCount() {
return mAttachedScrap.size();
}
View getScrapViewAt(int index) {
return mAttachedScrap.get(index).itemView;
}
void clearScrap() {
mAttachedScrap.clear();
if (mChangedScrap != null) {
mChangedScrap.clear();
}
}
ViewHolder getChangedScrapViewForPosition(int position) {
// If pre-layout, check the changed scrap for an exact match.
final int changedScrapSize;
if (mChangedScrap == null || (changedScrapSize = mChangedScrap.size()) == 0) {
return null;
}
// find by position
for (int i = 0; i < changedScrapSize; i++) {
final ViewHolder holder = mChangedScrap.get(i);
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
// find by id
if (mAdapter.hasStableIds()) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
if (offsetPosition > 0 && offsetPosition < mAdapter.getItemCount()) {
final long id = mAdapter.getItemId(offsetPosition);
for (int i = 0; i < changedScrapSize; i++) {
final ViewHolder holder = mChangedScrap.get(i);
if (!holder.wasReturnedFromScrap() && holder.getItemId() == id) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
}
}
return null;
}
/**
* Returns a view for the position either from attach scrap, hidden children, or cache.
*
* @param position Item position
* @param dryRun Does a dry run, finds the ViewHolder but does not remove
* @return a ViewHolder that can be re-used for this position.
*/
ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
final int scrapCount = mAttachedScrap.size();
// Try first for an exact, non-invalid match from scrap.
for (int i = 0; i < scrapCount; i++) {
final ViewHolder holder = mAttachedScrap.get(i);
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
&& !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
if (!dryRun) {
View view = mChildHelper.findHiddenNonRemovedView(position);
if (view != null) {
// This View is good to be used. We just need to unhide, detach and move to the
// scrap list.
final ViewHolder vh = getChildViewHolderInt(view);
mChildHelper.unhide(view);
int layoutIndex = mChildHelper.indexOfChild(view);
if (layoutIndex == RecyclerView.NO_POSITION) {
throw new IllegalStateException("layout index should not be -1 after "
+ "unhiding a view:" + vh + exceptionLabel());
}
mChildHelper.detachViewFromParent(layoutIndex);
scrapView(view);
vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP
| ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
return vh;
}
}
// Search in our first-level recycled view cache.
final int cacheSize = mCachedViews.size();
for (int i = 0; i < cacheSize; i++) {
final ViewHolder holder = mCachedViews.get(i);
// invalid view holders may be in cache if adapter has stable ids as they can be
// retrieved via getScrapOrCachedViewForId
if (!holder.isInvalid() && holder.getLayoutPosition() == position
&& !holder.isAttachedToTransitionOverlay()) {
if (!dryRun) {
mCachedViews.remove(i);
}
if (sVerboseLoggingEnabled) {
Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position
+ ") found match in cache: " + holder);
}
return holder;
}
}
return null;
}
ViewHolder getScrapOrCachedViewForId(long id, int type, boolean dryRun) {
// Look in our attached views first
final int count = mAttachedScrap.size();
for (int i = count - 1; i >= 0; i--) {
final ViewHolder holder = mAttachedScrap.get(i);
if (holder.getItemId() == id && !holder.wasReturnedFromScrap()) {
if (type == holder.getItemViewType()) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
if (holder.isRemoved()) {
// this might be valid in two cases:
// > item is removed but we are in pre-layout pass
// >> do nothing. return as is. make sure we don't rebind
// > item is removed then added to another position and we are in
// post layout.
// >> remove removed and invalid flags, add update flag to rebind
// because item was invisible to us and we don't know what happened in
// between.
if (!mState.isPreLayout()) {
holder.setFlags(ViewHolder.FLAG_UPDATE, ViewHolder.FLAG_UPDATE
| ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED);
}
}
return holder;
} else if (!dryRun) {
// if we are running animations, it is actually better to keep it in scrap
// but this would force layout manager to lay it out which would be bad.
// Recycle this scrap. Type mismatch.
mAttachedScrap.remove(i);
removeDetachedView(holder.itemView, false);
quickRecycleScrapView(holder.itemView);
}
}
}
// Search the first-level cache
final int cacheSize = mCachedViews.size();
for (int i = cacheSize - 1; i >= 0; i--) {
final ViewHolder holder = mCachedViews.get(i);
if (holder.getItemId() == id && !holder.isAttachedToTransitionOverlay()) {
if (type == holder.getItemViewType()) {
if (!dryRun) {
mCachedViews.remove(i);
}
return holder;
} else if (!dryRun) {
recycleCachedViewAt(i);
return null;
}
}
}
return null;
}
@SuppressWarnings("unchecked")
void dispatchViewRecycled(@NonNull ViewHolder holder) {
// TODO: Remove this once setRecyclerListener (currently deprecated) is deleted.
if (mRecyclerListener != null) {
mRecyclerListener.onViewRecycled(holder);
}
final int listenerCount = mRecyclerListeners.size();
for (int i = 0; i < listenerCount; i++) {
mRecyclerListeners.get(i).onViewRecycled(holder);
}
if (mAdapter != null) {
mAdapter.onViewRecycled(holder);
}
if (mState != null) {
mViewInfoStore.removeViewHolder(holder);
}
if (sVerboseLoggingEnabled) Log.d(TAG, "dispatchViewRecycled: " + holder);
}
void onAdapterChanged(Adapter<?> oldAdapter, Adapter<?> newAdapter,
boolean compatibleWithPrevious) {
clear();
poolingContainerDetach(oldAdapter, true);
getRecycledViewPool().onAdapterChanged(oldAdapter, newAdapter,
compatibleWithPrevious);
maybeSendPoolingContainerAttach();
}
void offsetPositionRecordsForMove(int from, int to) {
final int start, end, inBetweenOffset;
if (from < to) {
start = from;
end = to;
inBetweenOffset = -1;
} else {
start = to;
end = from;
inBetweenOffset = 1;
}
final int cachedCount = mCachedViews.size();
for (int i = 0; i < cachedCount; i++) {
final ViewHolder holder = mCachedViews.get(i);
if (holder == null || holder.mPosition < start || holder.mPosition > end) {
continue;
}
if (holder.mPosition == from) {
holder.offsetPosition(to - from, false);
} else {
holder.offsetPosition(inBetweenOffset, false);
}
if (sVerboseLoggingEnabled) {
Log.d(TAG, "offsetPositionRecordsForMove cached child " + i + " holder "
+ holder);
}
}
}
void offsetPositionRecordsForInsert(int insertedAt, int count) {
final int cachedCount = mCachedViews.size();
for (int i = 0; i < cachedCount; i++) {
final ViewHolder holder = mCachedViews.get(i);
if (holder != null && holder.mPosition >= insertedAt) {
if (sVerboseLoggingEnabled) {
Log.d(TAG, "offsetPositionRecordsForInsert cached " + i + " holder "
+ holder + " now at position " + (holder.mPosition + count));
}
// insertions only affect post layout hence don't apply them to pre-layout.
holder.offsetPosition(count, false);
}
}
}
/**
* @param removedFrom Remove start index
* @param count Remove count
* @param applyToPreLayout If true, changes will affect ViewHolder's pre-layout position, if
* false, they'll be applied before the second layout pass
*/
void offsetPositionRecordsForRemove(int removedFrom, int count, boolean applyToPreLayout) {
final int removedEnd = removedFrom + count;
final int cachedCount = mCachedViews.size();
for (int i = cachedCount - 1; i >= 0; i--) {
final ViewHolder holder = mCachedViews.get(i);
if (holder != null) {
if (holder.mPosition >= removedEnd) {
if (sVerboseLoggingEnabled) {
Log.d(TAG, "offsetPositionRecordsForRemove cached " + i
+ " holder " + holder + " now at position "
+ (holder.mPosition - count));
}
holder.offsetPosition(-count, applyToPreLayout);
} else if (holder.mPosition >= removedFrom) {
// Item for this view was removed. Dump it from the cache.
holder.addFlags(ViewHolder.FLAG_REMOVED);
recycleCachedViewAt(i);
}
}
}
}
void setViewCacheExtension(ViewCacheExtension extension) {
mViewCacheExtension = extension;
}
void setRecycledViewPool(RecycledViewPool pool) {
poolingContainerDetach(mAdapter);
if (mRecyclerPool != null) {
mRecyclerPool.detach();
}
mRecyclerPool = pool;
if (mRecyclerPool != null && getAdapter() != null) {
mRecyclerPool.attach();
}
maybeSendPoolingContainerAttach();
}
private void maybeSendPoolingContainerAttach() {
if (mRecyclerPool != null
&& mAdapter != null
&& isAttachedToWindow()) {
mRecyclerPool.attachForPoolingContainer(mAdapter);
}
}
private void poolingContainerDetach(Adapter<?> adapter) {
poolingContainerDetach(adapter, false);
}
private void poolingContainerDetach(Adapter<?> adapter, boolean isBeingReplaced) {
if (mRecyclerPool != null) {
mRecyclerPool.detachForPoolingContainer(adapter, isBeingReplaced);
}
}
void onAttachedToWindow() {
maybeSendPoolingContainerAttach();
}
void onDetachedFromWindow() {
for (int i = 0; i < mCachedViews.size(); i++) {
PoolingContainer.callPoolingContainerOnRelease(mCachedViews.get(i).itemView);
}
poolingContainerDetach(mAdapter);
}
RecycledViewPool getRecycledViewPool() {
if (mRecyclerPool == null) {
mRecyclerPool = new RecycledViewPool();
maybeSendPoolingContainerAttach();
}
return mRecyclerPool;
}
void viewRangeUpdate(int positionStart, int itemCount) {
final int positionEnd = positionStart + itemCount;
final int cachedCount = mCachedViews.size();
for (int i = cachedCount - 1; i >= 0; i--) {
final ViewHolder holder = mCachedViews.get(i);
if (holder == null) {
continue;
}
final int pos = holder.mPosition;
if (pos >= positionStart && pos < positionEnd) {
holder.addFlags(ViewHolder.FLAG_UPDATE);
recycleCachedViewAt(i);
// cached views should not be flagged as changed because this will cause them
// to animate when they are returned from cache.
}
}
}
void markKnownViewsInvalid() {
final int cachedCount = mCachedViews.size();
for (int i = 0; i < cachedCount; i++) {
final ViewHolder holder = mCachedViews.get(i);
if (holder != null) {
holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID);
holder.addChangePayload(null);
}
}
if (mAdapter == null || !mAdapter.hasStableIds()) {
// we cannot re-use cached views in this case. Recycle them all
recycleAndClearCachedViews();
}
}
void clearOldPositions() {
final int cachedCount = mCachedViews.size();
for (int i = 0; i < cachedCount; i++) {
final ViewHolder holder = mCachedViews.get(i);
holder.clearOldPosition();
}
final int scrapCount = mAttachedScrap.size();
for (int i = 0; i < scrapCount; i++) {
mAttachedScrap.get(i).clearOldPosition();
}
if (mChangedScrap != null) {
final int changedScrapCount = mChangedScrap.size();
for (int i = 0; i < changedScrapCount; i++) {
mChangedScrap.get(i).clearOldPosition();
}
}
}
void markItemDecorInsetsDirty() {
final int cachedCount = mCachedViews.size();
for (int i = 0; i < cachedCount; i++) {
final ViewHolder holder = mCachedViews.get(i);
LayoutParams layoutParams = (LayoutParams) holder.itemView.getLayoutParams();
if (layoutParams != null) {
layoutParams.mInsetsDirty = true;
}
}
}
}
/**
* ViewCacheExtension is a helper class to provide an additional layer of view caching that can
* be controlled by the developer.
* <p>
* 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}.
* <p>
* 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.
* <p>
* This method should <b>not</b> 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.
* <p>
* 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
*
* <p>Adapters provide a binding from an app-specific data set to views that are displayed
* within a {@link RecyclerView}.</p>
*
* @param <VH> A class that extends ViewHolder that will be used by the adapter.
*/
public abstract static class Adapter<VH extends ViewHolder> {
private final AdapterDataObservable mObservable = new AdapterDataObservable();
private boolean mHasStableIds = false;
private StateRestorationPolicy mStateRestorationPolicy = StateRestorationPolicy.ALLOW;
/**
* Called when RecyclerView needs a new {@link ViewHolder} of the given type to represent
* an item.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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 <code>position</code> 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#getBindingAdapterPosition()} 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.
* <p>
* 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 <code>position</code> 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#getBindingAdapterPosition()} which
* will have the updated adapter position.
* <p>
* Partial bind vs full bind:
* <p>
* 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 List<Object> payloads) {
onBindViewHolder(holder, position);
}
/**
* Returns the position of the given {@link ViewHolder} in the given {@link Adapter}.
*
* If the given {@link Adapter} is not part of this {@link Adapter},
* {@link RecyclerView#NO_POSITION} is returned.
*
* @param adapter The adapter which is a sub adapter of this adapter or itself.
* @param viewHolder The ViewHolder whose local position in the given adapter will be
* returned.
* @param localPosition The position of the given {@link ViewHolder} in this
* {@link Adapter}.
*
* @return The local position of the given {@link ViewHolder} in this {@link Adapter}
* or {@link RecyclerView#NO_POSITION} if the {@link ViewHolder} is not bound to an item
* or the given {@link Adapter} is not part of this Adapter (if this Adapter merges other
* adapters).
*/
public int findRelativeAdapterPositionIn(
@NonNull Adapter<? extends ViewHolder> adapter,
@NonNull ViewHolder viewHolder,
int localPosition
) {
if (adapter == this) {
return localPosition;
}
return NO_POSITION;
}
/**
* This method calls {@link #onCreateViewHolder(ViewGroup, int)} to create a new
* {@link ViewHolder} and initializes some private fields to be used by RecyclerView.
*
* @see #onCreateViewHolder(ViewGroup, int)
*/
@NonNull
public final VH createViewHolder(@NonNull ViewGroup parent, int viewType) {
try {
if (TraceCompat.isEnabled()) {
Trace.beginSection(String.format("RV onCreateViewHolder type=0x%X", viewType));
}
final VH holder = onCreateViewHolder(parent, viewType);
if (holder.itemView.getParent() != null) {
throw new IllegalStateException("ViewHolder views must not be attached when"
+ " created. Ensure that you are not passing 'true' to the attachToRoot"
+ " parameter of LayoutInflater.inflate(..., boolean attachToRoot)");
}
holder.mItemViewType = viewType;
return holder;
} finally {
Trace.endSection();
}
}
/**
* This method internally calls {@link #onBindViewHolder(ViewHolder, int)} to update the
* {@link ViewHolder} contents with the item at the given position and also sets up some
* private fields to be used by RecyclerView.
*
* Adapters that merge other adapters should use
* {@link #bindViewHolder(ViewHolder, int)} when calling nested adapters so that
* RecyclerView can track which adapter bound the {@link ViewHolder} to return the correct
* position from {@link ViewHolder#getBindingAdapterPosition()} method.
* They should also override
* the {@link #findRelativeAdapterPositionIn(Adapter, ViewHolder, int)} method.
*
* @param holder The view holder whose contents should be updated
* @param position The position of the holder with respect to this adapter
* @see #onBindViewHolder(ViewHolder, int)
*/
public final void bindViewHolder(@NonNull VH holder, int position) {
boolean rootBind = holder.mBindingAdapter == null;
if (rootBind) {
holder.mPosition = position;
if (hasStableIds()) {
holder.mItemId = getItemId(position);
}
holder.setFlags(ViewHolder.FLAG_BOUND,
ViewHolder.FLAG_BOUND | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN);
if (TraceCompat.isEnabled()) {
// Note: we only trace when rootBind=true to avoid duplicate trace sections
Trace.beginSection(
String.format("RV onBindViewHolder type=0x%X", holder.mItemViewType)
);
}
}
holder.mBindingAdapter = this;
if (sDebugAssertionsEnabled) {
if (holder.itemView.getParent() == null
&& (holder.itemView.isAttachedToWindow()
!= holder.isTmpDetached())) {
throw new IllegalStateException("Temp-detached state out of sync with reality. "
+ "holder.isTmpDetached(): " + holder.isTmpDetached()
+ ", attached to window: "
+ holder.itemView.isAttachedToWindow()
+ ", holder: " + holder);
}
if (holder.itemView.getParent() == null
&& holder.itemView.isAttachedToWindow()) {
throw new IllegalStateException(
"Attempting to bind attached holder with no parent"
+ " (AKA temp detached): " + holder);
}
}
onBindViewHolder(holder, position, holder.getUnmodifiedPayloads());
if (rootBind) {
holder.clearPayload();
final ViewGroup.LayoutParams layoutParams = holder.itemView.getLayoutParams();
if (layoutParams instanceof RecyclerView.LayoutParams) {
((LayoutParams) layoutParams).mInsetsDirty = true;
}
Trace.endSection();
}
}
/**
* Return the view type of the item at <code>position</code> for the purposes
* of view recycling.
*
* <p>The default implementation of this method returns 0, making the assumption of
* a single view type for the adapter. Unlike ListView adapters, types need not
* be contiguous. Consider using id resources to uniquely identify item view types.
*
* @param position position to query
* @return integer value identifying the type of the view needed to represent the item at
* <code>position</code>. Type codes need not be contiguous.
*/
public int getItemViewType(int position) {
return 0;
}
/**
* Indicates whether each item in the data set can be represented with a unique identifier
* of type {@link java.lang.Long}.
*
* @param hasStableIds Whether items in data set have unique identifiers or not.
* @see #hasStableIds()
* @see #getItemId(int)
*/
public void setHasStableIds(boolean hasStableIds) {
if (hasObservers()) {
throw new IllegalStateException("Cannot change whether this adapter has "
+ "stable IDs while the adapter has registered observers.");
}
mHasStableIds = hasStableIds;
}
/**
* Return the stable ID for the item at <code>position</code>. If {@link #hasStableIds()}
* would return false this method should return {@link #NO_ID}. The default implementation
* of this method returns {@link #NO_ID}.
*
* @param position Adapter position to query
* @return the stable ID of the item at position
*/
public long getItemId(int position) {
return NO_ID;
}
/**
* Returns the total number of items in the data set held by the adapter.
*
* @return The total number of items in this adapter.
*/
public abstract int getItemCount();
/**
* Returns true if this adapter publishes a unique <code>long</code> value that can
* act as a key for the item at a given position in the data set. If that item is relocated
* in the data set, the ID returned for that item should be the same.
*
* @return true if this adapter's items have stable IDs
*/
public final boolean hasStableIds() {
return mHasStableIds;
}
/**
* Called when a view created by this adapter has been recycled.
*
* <p>A view is recycled when a {@link LayoutManager} decides that it no longer
* needs to be attached to its parent {@link RecyclerView}. This can be because it has
* fallen out of visibility or a set of cached views represented by views still
* attached to the parent RecyclerView. If an item view has large or expensive data
* bound to it such as large bitmaps, this may be a good place to release those
* resources.</p>
* <p>
* RecyclerView calls this method right before clearing ViewHolder's internal data and
* sending it to RecycledViewPool. This way, if ViewHolder was holding valid information
* before being recycled, you can call {@link ViewHolder#getBindingAdapterPosition()} to get
* its adapter position.
*
* @param holder The ViewHolder for the view being recycled
*/
public void onViewRecycled(@NonNull VH holder) {
}
/**
* Called by the RecyclerView if a ViewHolder created by this Adapter cannot be recycled
* due to its transient state. Upon receiving this callback, Adapter can clear the
* animation(s) that effect the View's transient state and return <code>true</code> so that
* the View can be recycled. Keep in mind that the View in question is already removed from
* the RecyclerView.
* <p>
* In some cases, it is acceptable to recycle a View although it has transient state. Most
* of the time, this is a case where the transient state will be cleared in
* {@link #onBindViewHolder(ViewHolder, int)} call when View is rebound to a new position.
* For this reason, RecyclerView leaves the decision to the Adapter and uses the return
* value of this method to decide whether the View should be recycled or not.
* <p>
* Note that when all animations are created by {@link RecyclerView.ItemAnimator}, you
* should never receive this callback because RecyclerView keeps those Views as children
* until their animations are complete. This callback is useful when children of the item
* views create animations which may not be easy to implement using an {@link ItemAnimator}.
* <p>
* You should <em>never</em> fix this issue by calling
* <code>holder.itemView.setHasTransientState(false);</code> unless you've previously called
* <code>holder.itemView.setHasTransientState(true);</code>. Each
* <code>View.setHasTransientState(true)</code> call must be matched by a
* <code>View.setHasTransientState(false)</code> call, otherwise, the state of the View
* may become inconsistent. You should always prefer to end or cancel animations that are
* triggering the transient state instead of handling it manually.
*
* @param holder The ViewHolder containing the View that could not be recycled due to its
* transient state.
* @return True if the View should be recycled, false otherwise. Note that if this method
* returns <code>true</code>, RecyclerView <em>will ignore</em> the transient state of
* the View and recycle it regardless. If this method returns <code>false</code>,
* RecyclerView will check the View's transient state again before giving a final decision.
* Default implementation returns false.
*/
public boolean onFailedToRecycleView(@NonNull VH holder) {
return false;
}
/**
* Called when a view created by this adapter has been attached to a window.
*
* <p>This can be used as a reasonable signal that the view is about to be seen
* by the user. If the adapter previously freed any resources in
* {@link #onViewDetachedFromWindow(RecyclerView.ViewHolder) onViewDetachedFromWindow}
* those resources should be restored here.</p>
*
* @param holder Holder of the view being attached
*/
public void onViewAttachedToWindow(@NonNull VH holder) {
}
/**
* Called when a view created by this adapter has been detached from its window.
*
* <p>Becoming detached from the window is not necessarily a permanent condition;
* the consumer of an Adapter's views may choose to cache views offscreen while they
* are not visible, attaching and detaching them as appropriate.</p>
*
* @param holder Holder of the view being detached
*/
public void onViewDetachedFromWindow(@NonNull VH holder) {
}
/**
* Returns true if one or more observers are attached to this adapter.
*
* @return true if this adapter has observers
*/
public final boolean hasObservers() {
return mObservable.hasObservers();
}
/**
* Register a new observer to listen for data changes.
*
* <p>The adapter may publish a variety of events describing specific changes.
* Not all adapters may support all change types and some may fall back to a generic
* {@link RecyclerView.AdapterDataObserver#onChanged()
* "something changed"} event if more specific data is not available.</p>
*
* <p>Components registering observers with an adapter are responsible for
* {@link #unregisterAdapterDataObserver(RecyclerView.AdapterDataObserver)
* unregistering} those observers when finished.</p>
*
* @param observer Observer to register
* @see #unregisterAdapterDataObserver(RecyclerView.AdapterDataObserver)
*/
public void registerAdapterDataObserver(@NonNull AdapterDataObserver observer) {
mObservable.registerObserver(observer);
}
/**
* Unregister an observer currently listening for data changes.
*
* <p>The unregistered observer will no longer receive events about changes
* to the adapter.</p>
*
* @param observer Observer to unregister
* @see #registerAdapterDataObserver(RecyclerView.AdapterDataObserver)
*/
public void unregisterAdapterDataObserver(@NonNull AdapterDataObserver observer) {
mObservable.unregisterObserver(observer);
}
/**
* Called by RecyclerView when it starts observing this Adapter.
* <p>
* Keep in mind that same adapter may be observed by multiple RecyclerViews.
*
* @param recyclerView The RecyclerView instance which started observing this adapter.
* @see #onDetachedFromRecyclerView(RecyclerView)
*/
public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
}
/**
* Called by RecyclerView when it stops observing this Adapter.
*
* @param recyclerView The RecyclerView instance which stopped observing this adapter.
* @see #onAttachedToRecyclerView(RecyclerView)
*/
public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
}
/**
* Notify any registered observers that the data set has changed.
*
* <p>There are two different classes of data change events, item changes and structural
* changes. Item changes are when a single item has its data updated but no positional
* changes have occurred. Structural changes are when items are inserted, removed or moved
* within the data set.</p>
*
* <p>This event does not specify what about the data set has changed, forcing
* any observers to assume that all existing items and structure may no longer be valid.
* LayoutManagers will be forced to fully rebind and relayout all visible views.</p>
*
* <p><code>RecyclerView</code> will attempt to synthesize visible structural change events
* for adapters that report that they have {@link #hasStableIds() stable IDs} when
* this method is used. This can help for the purposes of animation and visual
* object persistence but individual item views will still need to be rebound
* and relaid out.</p>
*
* <p>If you are writing an adapter it will always be more efficient to use the more
* specific change events if you can. Rely on <code>notifyDataSetChanged()</code>
* as a last resort.</p>
*
* @see #notifyItemChanged(int)
* @see #notifyItemInserted(int)
* @see #notifyItemRemoved(int)
* @see #notifyItemRangeChanged(int, int)
* @see #notifyItemRangeInserted(int, int)
* @see #notifyItemRangeRemoved(int, int)
*/
public final void notifyDataSetChanged() {
mObservable.notifyChanged();
}
/**
* Notify any registered observers that the item at <code>position</code> has changed.
* Equivalent to calling <code>notifyItemChanged(position, null);</code>.
*
* <p>This is an item change event, not a structural change event. It indicates that any
* reflection of the data at <code>position</code> is out of date and should be updated.
* The item at <code>position</code> retains the same identity.</p>
*
* @param position Position of the item that has changed
* @see #notifyItemRangeChanged(int, int)
*/
public final void notifyItemChanged(int position) {
mObservable.notifyItemRangeChanged(position, 1);
}
/**
* Notify any registered observers that the item at <code>position</code> has changed with
* an optional payload object.
*
* <p>This is an item change event, not a structural change event. It indicates that any
* reflection of the data at <code>position</code> is out of date and should be updated.
* The item at <code>position</code> retains the same identity.
* </p>
*
* <p>
* Client can optionally pass a payload for partial change. These payloads will be merged
* and may be passed to adapter's {@link #onBindViewHolder(ViewHolder, int, List)} if the
* item is already represented by a ViewHolder and it will be rebound to the same
* ViewHolder. A notifyItemRangeChanged() with null payload will clear all existing
* payloads on that item and prevent future payload until
* {@link #onBindViewHolder(ViewHolder, int, List)} is called. Adapter should not assume
* that the payload will always be passed to onBindViewHolder(), e.g. when the view is not
* attached, the payload will be simply dropped.
*
* @param position Position of the item that has changed
* @param payload Optional parameter, use null to identify a "full" update
* @see #notifyItemRangeChanged(int, int)
*/
public final void notifyItemChanged(int position, @Nullable Object payload) {
mObservable.notifyItemRangeChanged(position, 1, payload);
}
/**
* Notify any registered observers that the <code>itemCount</code> items starting at
* position <code>positionStart</code> have changed.
* Equivalent to calling <code>notifyItemRangeChanged(position, itemCount, null);</code>.
*
* <p>This is an item change event, not a structural change event. It indicates that
* any reflection of the data in the given position range is out of date and should
* be updated. The items in the given range retain the same identity.</p>
*
* @param positionStart Position of the first item that has changed
* @param itemCount Number of items that have changed
* @see #notifyItemChanged(int)
*/
public final void notifyItemRangeChanged(int positionStart, int itemCount) {
mObservable.notifyItemRangeChanged(positionStart, itemCount);
}
/**
* Notify any registered observers that the <code>itemCount</code> items starting at
* position <code>positionStart</code> have changed. An optional payload can be
* passed to each changed item.
*
* <p>This is an item change event, not a structural change event. It indicates that any
* reflection of the data in the given position range is out of date and should be updated.
* The items in the given range retain the same identity.
* </p>
*
* <p>
* Client can optionally pass a payload for partial change. These payloads will be merged
* and may be passed to adapter's {@link #onBindViewHolder(ViewHolder, int, List)} if the
* item is already represented by a ViewHolder and it will be rebound to the same
* ViewHolder. A notifyItemRangeChanged() with null payload will clear all existing
* payloads on that item and prevent future payload until
* {@link #onBindViewHolder(ViewHolder, int, List)} is called. Adapter should not assume
* that the payload will always be passed to onBindViewHolder(), e.g. when the view is not
* attached, the payload will be simply dropped.
*
* @param positionStart Position of the first item that has changed
* @param itemCount Number of items that have changed
* @param payload Optional parameter, use null to identify a "full" update
* @see #notifyItemChanged(int)
*/
public final void notifyItemRangeChanged(int positionStart, int itemCount,
@Nullable Object payload) {
mObservable.notifyItemRangeChanged(positionStart, itemCount, payload);
}
/**
* Notify any registered observers that the item reflected at <code>position</code>
* has been newly inserted. The item previously at <code>position</code> is now at
* position <code>position + 1</code>.
*
* <p>This is a structural change event. Representations of other existing items in the
* data set are still considered up to date and will not be rebound, though their
* positions may be altered.</p>
*
* @param position Position of the newly inserted item in the data set
* @see #notifyItemRangeInserted(int, int)
*/
public final void notifyItemInserted(int position) {
mObservable.notifyItemRangeInserted(position, 1);
}
/**
* Notify any registered observers that the item reflected at <code>fromPosition</code>
* has been moved to <code>toPosition</code>.
*
* <p>This is a structural change event. Representations of other existing items in the
* data set are still considered up to date and will not be rebound, though their
* positions may be altered.</p>
*
* @param fromPosition Previous position of the item.
* @param toPosition New position of the item.
*/
public final void notifyItemMoved(int fromPosition, int toPosition) {
mObservable.notifyItemMoved(fromPosition, toPosition);
}
/**
* Notify any registered observers that the currently reflected <code>itemCount</code>
* items starting at <code>positionStart</code> have been newly inserted. The items
* previously located at <code>positionStart</code> and beyond can now be found starting
* at position <code>positionStart + itemCount</code>.
*
* <p>This is a structural change event. Representations of other existing items in the
* data set are still considered up to date and will not be rebound, though their positions
* may be altered.</p>
*
* @param positionStart Position of the first item that was inserted
* @param itemCount Number of items inserted
* @see #notifyItemInserted(int)
*/
public final void notifyItemRangeInserted(int positionStart, int itemCount) {
mObservable.notifyItemRangeInserted(positionStart, itemCount);
}
/**
* Notify any registered observers that the item previously located at <code>position</code>
* has been removed from the data set. The items previously located at and after
* <code>position</code> may now be found at <code>oldPosition - 1</code>.
*
* <p>This is a structural change event. Representations of other existing items in the
* data set are still considered up to date and will not be rebound, though their positions
* may be altered.</p>
*
* @param position Position of the item that has now been removed
* @see #notifyItemRangeRemoved(int, int)
*/
public final void notifyItemRemoved(int position) {
mObservable.notifyItemRangeRemoved(position, 1);
}
/**
* Notify any registered observers that the <code>itemCount</code> items previously
* located at <code>positionStart</code> have been removed from the data set. The items
* previously located at and after <code>positionStart + itemCount</code> may now be found
* at <code>oldPosition - itemCount</code>.
*
* <p>This is a structural change event. Representations of other existing items in the data
* set are still considered up to date and will not be rebound, though their positions
* may be altered.</p>
*
* @param positionStart Previous position of the first item that was removed
* @param itemCount Number of items removed from the data set
*/
public final void notifyItemRangeRemoved(int positionStart, int itemCount) {
mObservable.notifyItemRangeRemoved(positionStart, itemCount);
}
/**
* Sets the state restoration strategy for the Adapter.
*
* By default, it is set to {@link StateRestorationPolicy#ALLOW} which means RecyclerView
* expects any set Adapter to be immediately capable of restoring the RecyclerView's saved
* scroll position.
* <p>
* This behaviour might be undesired if the Adapter's data is loaded asynchronously, and
* thus unavailable during initial layout (e.g. after Activity rotation). To avoid losing
* scroll position, you can change this to be either
* {@link StateRestorationPolicy#PREVENT_WHEN_EMPTY} or
* {@link StateRestorationPolicy#PREVENT}.
* Note that the former means your RecyclerView will restore state as soon as Adapter has
* 1 or more items while the latter requires you to call
* {@link #setStateRestorationPolicy(StateRestorationPolicy)} with either
* {@link StateRestorationPolicy#ALLOW} or
* {@link StateRestorationPolicy#PREVENT_WHEN_EMPTY} again when the Adapter is
* ready to restore its state.
* <p>
* RecyclerView will still layout even when State restoration is disabled. The behavior of
* how State is restored is up to the {@link LayoutManager}. All default LayoutManagers
* will override current state with restored state when state restoration happens (unless
* an explicit call to {@link LayoutManager#scrollToPosition(int)} is made).
* <p>
* Calling this method after state is restored will not have any effect other than changing
* the return value of {@link #getStateRestorationPolicy()}.
*
* @param strategy The saved state restoration strategy for this Adapter.
* @see #getStateRestorationPolicy()
*/
public void setStateRestorationPolicy(@NonNull StateRestorationPolicy strategy) {
mStateRestorationPolicy = strategy;
mObservable.notifyStateRestorationPolicyChanged();
}
/**
* Returns when this Adapter wants to restore the state.
*
* @return The current {@link StateRestorationPolicy} for this Adapter. Defaults to
* {@link StateRestorationPolicy#ALLOW}.
* @see #setStateRestorationPolicy(StateRestorationPolicy)
*/
@NonNull
public final StateRestorationPolicy getStateRestorationPolicy() {
return mStateRestorationPolicy;
}
/**
* Called by the RecyclerView to decide whether the SavedState should be given to the
* LayoutManager or not.
*
* @return {@code true} if the Adapter is ready to restore its state, {@code false}
* otherwise.
*/
boolean canRestoreState() {
switch (mStateRestorationPolicy) {
case PREVENT:
return false;
case PREVENT_WHEN_EMPTY:
return getItemCount() > 0;
default:
return true;
}
}
/**
* Defines how this Adapter wants to restore its state after a view reconstruction (e.g.
* configuration change).
*/
public enum StateRestorationPolicy {
/**
* Adapter is ready to restore State immediately, RecyclerView will provide the state
* to the LayoutManager in the next layout pass.
*/
ALLOW,
/**
* Adapter is ready to restore State when it has more than 0 items. RecyclerView will
* provide the state to the LayoutManager as soon as the Adapter has 1 or more items.
*/
PREVENT_WHEN_EMPTY,
/**
* RecyclerView will not restore the state for the Adapter until a call to
* {@link #setStateRestorationPolicy(StateRestorationPolicy)} is made with either
* {@link #ALLOW} or {@link #PREVENT_WHEN_EMPTY}.
*/
PREVENT
}
}
@SuppressWarnings("unchecked")
void dispatchChildDetached(View child) {
final ViewHolder viewHolder = getChildViewHolderInt(child);
onChildDetachedFromWindow(child);
if (mAdapter != null && viewHolder != null) {
mAdapter.onViewDetachedFromWindow(viewHolder);
}
if (mOnChildAttachStateListeners != null) {
final int cnt = mOnChildAttachStateListeners.size();
for (int i = cnt - 1; i >= 0; i--) {
mOnChildAttachStateListeners.get(i).onChildViewDetachedFromWindow(child);
}
}
}
@SuppressWarnings("unchecked")
void dispatchChildAttached(View child) {
final ViewHolder viewHolder = getChildViewHolderInt(child);
onChildAttachedToWindow(child);
if (mAdapter != null && viewHolder != null) {
mAdapter.onViewAttachedToWindow(viewHolder);
}
if (mOnChildAttachStateListeners != null) {
final int cnt = mOnChildAttachStateListeners.size();
for (int i = cnt - 1; i >= 0; i--) {
mOnChildAttachStateListeners.get(i).onChildViewAttachedToWindow(child);
}
}
}
/**
* A <code>LayoutManager</code> is responsible for measuring and positioning item views
* within a <code>RecyclerView</code> as well as determining the policy for when to recycle
* item views that are no longer visible to the user. By changing the <code>LayoutManager</code>
* a <code>RecyclerView</code> can be used to implement a standard vertically scrolling list,
* a uniform grid, staggered grids, horizontally scrolling collections and more. Several stock
* layout managers are provided for general use.
* <p/>
* If the LayoutManager specifies a default constructor or one with the signature
* ({@link Context}, {@link AttributeSet}, {@code int}, {@code int}), RecyclerView will
* instantiate and set the LayoutManager when being inflated. Most used properties can
* be then obtained from {@link #getProperties(Context, AttributeSet, int, int)}. In case
* a LayoutManager specifies both constructors, the non-default constructor will take
* precedence.
*/
public abstract static class LayoutManager {
ChildHelper mChildHelper;
RecyclerView mRecyclerView;
/**
* The callback used for retrieving information about a RecyclerView and its children in the
* horizontal direction.
*/
private final ViewBoundsCheck.Callback mHorizontalBoundCheckCallback =
new ViewBoundsCheck.Callback() {
@Override
public View getChildAt(int index) {
return LayoutManager.this.getChildAt(index);
}
@Override
public int getParentStart() {
return LayoutManager.this.getPaddingLeft();
}
@Override
public int getParentEnd() {
return LayoutManager.this.getWidth() - LayoutManager.this.getPaddingRight();
}
@Override
public int getChildStart(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return LayoutManager.this.getDecoratedLeft(view) - params.leftMargin;
}
@Override
public int getChildEnd(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return LayoutManager.this.getDecoratedRight(view) + params.rightMargin;
}
};
/**
* The callback used for retrieving information about a RecyclerView and its children in the
* vertical direction.
*/
private final ViewBoundsCheck.Callback mVerticalBoundCheckCallback =
new ViewBoundsCheck.Callback() {
@Override
public View getChildAt(int index) {
return LayoutManager.this.getChildAt(index);
}
@Override
public int getParentStart() {
return LayoutManager.this.getPaddingTop();
}
@Override
public int getParentEnd() {
return LayoutManager.this.getHeight()
- LayoutManager.this.getPaddingBottom();
}
@Override
public int getChildStart(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return LayoutManager.this.getDecoratedTop(view) - params.topMargin;
}
@Override
public int getChildEnd(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return LayoutManager.this.getDecoratedBottom(view) + params.bottomMargin;
}
};
/**
* Utility objects used to check the boundaries of children against their parent
* RecyclerView.
*
* @see #isViewPartiallyVisible(View, boolean, boolean),
* {@link LinearLayoutManager#findOneVisibleChild(int, int, boolean, boolean)},
* and {@link LinearLayoutManager#findOnePartiallyOrCompletelyInvisibleChild(int, int)}.
*/
ViewBoundsCheck mHorizontalBoundCheck = new ViewBoundsCheck(mHorizontalBoundCheckCallback);
ViewBoundsCheck mVerticalBoundCheck = new ViewBoundsCheck(mVerticalBoundCheckCallback);
@Nullable
SmoothScroller mSmoothScroller;
boolean mRequestedSimpleAnimations = false;
boolean mIsAttachedToWindow = false;
/**
* This field is only set via the deprecated {@link #setAutoMeasureEnabled(boolean)} and is
* only accessed via {@link #isAutoMeasureEnabled()} for backwards compatability reasons.
*/
boolean mAutoMeasure = false;
/**
* LayoutManager has its own more strict measurement cache to avoid re-measuring a child
* if the space that will be given to it is already larger than what it has measured before.
*/
private boolean mMeasurementCacheEnabled = true;
private boolean mItemPrefetchEnabled = true;
/**
* Written by {@link GapWorker} when prefetches occur to track largest number of view ever
* requested by a {@link #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)} or
* {@link #collectAdjacentPrefetchPositions(int, int, State, LayoutPrefetchRegistry)} call.
*
* If expanded by a {@link #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)},
* will be reset upon layout to prevent initial prefetches (often large, since they're
* proportional to expected child count) from expanding cache permanently.
*/
int mPrefetchMaxCountObserved;
/**
* If true, mPrefetchMaxCountObserved is only valid until next layout, and should be reset.
*/
boolean mPrefetchMaxObservedInInitialPrefetch;
/**
* These measure specs might be the measure specs that were passed into RecyclerView's
* onMeasure method OR fake measure specs created by the RecyclerView.
* For example, when a layout is run, RecyclerView always sets these specs to be
* EXACTLY because a LayoutManager cannot resize RecyclerView during a layout pass.
* <p>
* Also, to be able to use the hint in unspecified measure specs, RecyclerView checks the
* API level and sets the size to 0 pre-M to avoid any issue that might be caused by
* corrupt values. Older platforms have no responsibility to provide a size if they set
* mode to unspecified.
*/
private int mWidthMode, mHeightMode;
private int mWidth, mHeight;
/**
* Interface for LayoutManagers to request items to be prefetched, based on position, with
* specified distance from viewport, which indicates priority.
*
* @see LayoutManager#collectAdjacentPrefetchPositions(int, int, State, LayoutPrefetchRegistry)
* @see LayoutManager#collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)
*/
public interface LayoutPrefetchRegistry {
/**
* Requests an an item to be prefetched, based on position, with a specified distance,
* indicating priority.
*
* @param layoutPosition Position of the item to prefetch.
* @param pixelDistance Distance from the current viewport to the bounds of the item,
* must be non-negative.
*/
void addPosition(int layoutPosition, int pixelDistance);
}
void setRecyclerView(RecyclerView recyclerView) {
if (recyclerView == null) {
mRecyclerView = null;
mChildHelper = null;
mWidth = 0;
mHeight = 0;
} else {
mRecyclerView = recyclerView;
mChildHelper = recyclerView.mChildHelper;
mWidth = recyclerView.getWidth();
mHeight = recyclerView.getHeight();
}
mWidthMode = MeasureSpec.EXACTLY;
mHeightMode = MeasureSpec.EXACTLY;
}
void setMeasureSpecs(int wSpec, int hSpec) {
mWidth = MeasureSpec.getSize(wSpec);
mWidthMode = MeasureSpec.getMode(wSpec);
if (mWidthMode == MeasureSpec.UNSPECIFIED && !ALLOW_SIZE_IN_UNSPECIFIED_SPEC) {
mWidth = 0;
}
mHeight = MeasureSpec.getSize(hSpec);
mHeightMode = MeasureSpec.getMode(hSpec);
if (mHeightMode == MeasureSpec.UNSPECIFIED && !ALLOW_SIZE_IN_UNSPECIFIED_SPEC) {
mHeight = 0;
}
}
/**
* Called after a layout is calculated during a measure pass when using auto-measure.
* <p>
* It simply traverses all children to calculate a bounding box then calls
* {@link #setMeasuredDimension(Rect, int, int)}. LayoutManagers can override that method
* if they need to handle the bounding box differently.
* <p>
* For example, GridLayoutManager override that method to ensure that even if a column is
* empty, the GridLayoutManager still measures wide enough to include it.
*
* @param widthSpec The widthSpec that was passing into RecyclerView's onMeasure
* @param heightSpec The heightSpec that was passing into RecyclerView's onMeasure
*/
void setMeasuredDimensionFromChildren(int widthSpec, int heightSpec) {
final int count = getChildCount();
if (count == 0) {
mRecyclerView.defaultOnMeasure(widthSpec, heightSpec);
return;
}
int minX = Integer.MAX_VALUE;
int minY = Integer.MAX_VALUE;
int maxX = Integer.MIN_VALUE;
int maxY = Integer.MIN_VALUE;
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
final Rect bounds = mRecyclerView.mTempRect;
getDecoratedBoundsWithMargins(child, bounds);
if (bounds.left < minX) {
minX = bounds.left;
}
if (bounds.right > maxX) {
maxX = bounds.right;
}
if (bounds.top < minY) {
minY = bounds.top;
}
if (bounds.bottom > maxY) {
maxY = bounds.bottom;
}
}
mRecyclerView.mTempRect.set(minX, minY, maxX, maxY);
setMeasuredDimension(mRecyclerView.mTempRect, widthSpec, heightSpec);
}
/**
* Sets the measured dimensions from the given bounding box of the children and the
* measurement specs that were passed into {@link RecyclerView#onMeasure(int, int)}. It is
* only called if a LayoutManager returns <code>true</code> from
* {@link #isAutoMeasureEnabled()} and it is called after the RecyclerView calls
* {@link LayoutManager#onLayoutChildren(Recycler, State)} in the execution of
* {@link RecyclerView#onMeasure(int, int)}.
* <p>
* This method must call {@link #setMeasuredDimension(int, int)}.
* <p>
* The default implementation adds the RecyclerView's padding to the given bounding box
* then caps the value to be within the given measurement specs.
*
* @param childrenBounds The bounding box of all children
* @param wSpec The widthMeasureSpec that was passed into the RecyclerView.
* @param hSpec The heightMeasureSpec that was passed into the RecyclerView.
* @see #isAutoMeasureEnabled()
* @see #setMeasuredDimension(int, int)
*/
public void setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec) {
int usedWidth = childrenBounds.width() + getPaddingLeft() + getPaddingRight();
int usedHeight = childrenBounds.height() + getPaddingTop() + getPaddingBottom();
int width = chooseSize(wSpec, usedWidth, getMinimumWidth());
int height = chooseSize(hSpec, usedHeight, getMinimumHeight());
setMeasuredDimension(width, height);
}
/**
* Calls {@code RecyclerView#requestLayout} on the underlying RecyclerView
*/
public void requestLayout() {
if (mRecyclerView != null) {
mRecyclerView.requestLayout();
}
}
/**
* Checks if RecyclerView is in the middle of a layout or scroll and throws an
* {@link IllegalStateException} if it <b>is not</b>.
*
* @param message The message for the exception. Can be null.
* @see #assertNotInLayoutOrScroll(String)
*/
public void assertInLayoutOrScroll(String message) {
if (mRecyclerView != null) {
mRecyclerView.assertInLayoutOrScroll(message);
}
}
/**
* Chooses a size from the given specs and parameters that is closest to the desired size
* and also complies with the spec.
*
* @param spec The measureSpec
* @param desired The preferred measurement
* @param min The minimum value
* @return A size that fits to the given specs
*/
public static int chooseSize(int spec, int desired, int min) {
final int mode = View.MeasureSpec.getMode(spec);
final int size = View.MeasureSpec.getSize(spec);
switch (mode) {
case View.MeasureSpec.EXACTLY:
return size;
case View.MeasureSpec.AT_MOST:
return Math.min(size, Math.max(desired, min));
case View.MeasureSpec.UNSPECIFIED:
default:
return Math.max(desired, min);
}
}
/**
* Checks if RecyclerView is in the middle of a layout or scroll and throws an
* {@link IllegalStateException} if it <b>is</b>.
*
* @param message The message for the exception. Can be null.
* @see #assertInLayoutOrScroll(String)
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public void assertNotInLayoutOrScroll(String message) {
if (mRecyclerView != null) {
mRecyclerView.assertNotInLayoutOrScroll(message);
}
}
/**
* Defines whether the measuring pass of layout should use the AutoMeasure mechanism of
* {@link RecyclerView} or if it should be done by the LayoutManager's implementation of
* {@link LayoutManager#onMeasure(Recycler, State, int, int)}.
*
* @param enabled <code>True</code> if layout measurement should be done by the
* RecyclerView, <code>false</code> if it should be done by this
* LayoutManager.
* @see #isAutoMeasureEnabled()
* @deprecated Implementors of LayoutManager should define whether or not it uses
* AutoMeasure by overriding {@link #isAutoMeasureEnabled()}.
*/
@Deprecated
public void setAutoMeasureEnabled(boolean enabled) {
mAutoMeasure = enabled;
}
/**
* Returns whether the measuring pass of layout should use the AutoMeasure mechanism of
* {@link RecyclerView} or if it should be done by the LayoutManager's implementation of
* {@link LayoutManager#onMeasure(Recycler, State, int, int)}.
* <p>
* This method returns false by default (it actually returns the value passed to the
* deprecated {@link #setAutoMeasureEnabled(boolean)}) and should be overridden to return
* true if a LayoutManager wants to be auto measured by the RecyclerView.
* <p>
* If this method is overridden to return true,
* {@link LayoutManager#onMeasure(Recycler, State, int, int)} should not be overridden.
* <p>
* AutoMeasure is a RecyclerView mechanism that handles the measuring pass of layout in a
* simple and contract satisfying way, including the wrapping of children laid out by
* LayoutManager. Simply put, it handles wrapping children by calling
* {@link LayoutManager#onLayoutChildren(Recycler, State)} during a call to
* {@link RecyclerView#onMeasure(int, int)}, and then calculating desired dimensions based
* on children's dimensions and positions. It does this while supporting all existing
* animation capabilities of the RecyclerView.
* <p>
* More specifically:
* <ol>
* <li>When {@link RecyclerView#onMeasure(int, int)} is called, if the provided measure
* specs both have a mode of {@link View.MeasureSpec#EXACTLY}, RecyclerView will set its
* measured dimensions accordingly and return, allowing layout to continue as normal
* (Actually, RecyclerView will call
* {@link LayoutManager#onMeasure(Recycler, State, int, int)} for backwards compatibility
* reasons but it should not be overridden if AutoMeasure is being used).</li>
* <li>If one of the layout specs is not {@code EXACT}, the RecyclerView will start the
* layout process. It will first process all pending Adapter updates and
* then decide whether to run a predictive layout. If it decides to do so, it will first
* call {@link #onLayoutChildren(Recycler, State)} with {@link State#isPreLayout()} set to
* {@code true}. At this stage, {@link #getWidth()} and {@link #getHeight()} will still
* return the width and height of the RecyclerView as of the last layout calculation.
* <p>
* After handling the predictive case, RecyclerView will call
* {@link #onLayoutChildren(Recycler, State)} with {@link State#isMeasuring()} set to
* {@code true} and {@link State#isPreLayout()} set to {@code false}. The LayoutManager can
* access the measurement specs via {@link #getHeight()}, {@link #getHeightMode()},
* {@link #getWidth()} and {@link #getWidthMode()}.</li>
* <li>After the layout calculation, RecyclerView sets the measured width & height by
* calculating the bounding box for the children (+ RecyclerView's padding). The
* LayoutManagers can override {@link #setMeasuredDimension(Rect, int, int)} to choose
* different values. For instance, GridLayoutManager overrides this value to handle the case
* where if it is vertical and has 3 columns but only 2 items, it should still measure its
* width to fit 3 items, not 2.</li>
* <li>Any following calls to {@link RecyclerView#onMeasure(int, int)} will run
* {@link #onLayoutChildren(Recycler, State)} with {@link State#isMeasuring()} set to
* {@code true} and {@link State#isPreLayout()} set to {@code false}. RecyclerView will
* take care of which views are actually added / removed / moved / changed for animations so
* that the LayoutManager should not worry about them and handle each
* {@link #onLayoutChildren(Recycler, State)} call as if it is the last one.</li>
* <li>When measure is complete and RecyclerView's
* {@link #onLayout(boolean, int, int, int, int)} method is called, RecyclerView checks
* whether it already did layout calculations during the measure pass and if so, it re-uses
* that information. It may still decide to call {@link #onLayoutChildren(Recycler, State)}
* if the last measure spec was different from the final dimensions or adapter contents
* have changed between the measure call and the layout call.</li>
* <li>Finally, animations are calculated and run as usual.</li>
* </ol>
*
* @return <code>True</code> if the measuring pass of layout should use the AutoMeasure
* mechanism of {@link RecyclerView} or <code>False</code> if it should be done by the
* LayoutManager's implementation of
* {@link LayoutManager#onMeasure(Recycler, State, int, int)}.
* @see #setMeasuredDimension(Rect, int, int)
* @see #onMeasure(Recycler, State, int, int)
*/
public boolean isAutoMeasureEnabled() {
return mAutoMeasure;
}
/**
* Returns whether this LayoutManager supports "predictive item animations".
* <p>
* "Predictive item animations" are automatically created animations that show
* where items came from, and where they are going to, as items are added, removed,
* or moved within a layout.
* <p>
* A LayoutManager wishing to support predictive item animations must override this
* method to return true (the default implementation returns false) and must obey certain
* behavioral contracts outlined in {@link #onLayoutChildren(Recycler, State)}.
* <p>
* Whether item animations actually occur in a RecyclerView is actually determined by both
* the return value from this method and the
* {@link RecyclerView#setItemAnimator(ItemAnimator) ItemAnimator} set on the
* RecyclerView itself. If the RecyclerView has a non-null ItemAnimator but this
* method returns false, then only "simple item animations" will be enabled in the
* RecyclerView, in which views whose position are changing are simply faded in/out. If the
* RecyclerView has a non-null ItemAnimator and this method returns true, then predictive
* item animations will be enabled in the RecyclerView.
*
* @return true if this LayoutManager supports predictive item animations, false otherwise.
*/
public boolean supportsPredictiveItemAnimations() {
return false;
}
/**
* Sets whether the LayoutManager should be queried for views outside of
* its viewport while the UI thread is idle between frames.
*
* <p>If enabled, the LayoutManager will be queried for items to inflate/bind in between
* view system traversals on devices running API 21 or greater. Default value is true.</p>
*
* <p>On platforms API level 21 and higher, the UI thread is idle between passing a frame
* to RenderThread and the starting up its next frame at the next VSync pulse. By
* prefetching out of window views in this time period, delays from inflation and view
* binding are much less likely to cause jank and stuttering during scrolls and flings.</p>
*
* <p>While prefetch is enabled, it will have the side effect of expanding the effective
* size of the View cache to hold prefetched views.</p>
*
* @param enabled <code>True</code> if items should be prefetched in between traversals.
* @see #isItemPrefetchEnabled()
*/
public final void setItemPrefetchEnabled(boolean enabled) {
if (enabled != mItemPrefetchEnabled) {
mItemPrefetchEnabled = enabled;
mPrefetchMaxCountObserved = 0;
if (mRecyclerView != null) {
mRecyclerView.mRecycler.updateViewCacheSize();
}
}
}
/**
* Sets whether the LayoutManager should be queried for views outside of
* its viewport while the UI thread is idle between frames.
*
* @return true if item prefetch is enabled, false otherwise
* @see #setItemPrefetchEnabled(boolean)
*/
public final boolean isItemPrefetchEnabled() {
return mItemPrefetchEnabled;
}
/**
* Gather all positions from the LayoutManager to be prefetched, given specified momentum.
*
* <p>If item prefetch is enabled, this method is called in between traversals to gather
* which positions the LayoutManager will soon need, given upcoming movement in subsequent
* traversals.</p>
*
* <p>The LayoutManager should call {@link LayoutPrefetchRegistry#addPosition(int, int)} for
* each item to be prepared, and these positions will have their ViewHolders created and
* bound, if there is sufficient time available, in advance of being needed by a
* scroll or layout.</p>
*
* @param dx X movement component.
* @param dy Y movement component.
* @param state State of RecyclerView
* @param layoutPrefetchRegistry PrefetchRegistry to add prefetch entries into.
* @see #isItemPrefetchEnabled()
* @see #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public void collectAdjacentPrefetchPositions(int dx, int dy, State state,
LayoutPrefetchRegistry layoutPrefetchRegistry) {
}
/**
* Gather all positions from the LayoutManager to be prefetched in preperation for its
* RecyclerView to come on screen, due to the movement of another, containing RecyclerView.
*
* <p>This method is only called when a RecyclerView is nested in another RecyclerView.</p>
*
* <p>If item prefetch is enabled for this LayoutManager, as well in another containing
* LayoutManager, this method is called in between draw traversals to gather
* which positions this LayoutManager will first need, once it appears on the screen.</p>
*
* <p>For example, if this LayoutManager represents a horizontally scrolling list within a
* vertically scrolling LayoutManager, this method would be called when the horizontal list
* is about to come onscreen.</p>
*
* <p>The LayoutManager should call {@link LayoutPrefetchRegistry#addPosition(int, int)} for
* each item to be prepared, and these positions will have their ViewHolders created and
* bound, if there is sufficient time available, in advance of being needed by a
* scroll or layout.</p>
*
* @param adapterItemCount number of items in the associated adapter.
* @param layoutPrefetchRegistry PrefetchRegistry to add prefetch entries into.
* @see #isItemPrefetchEnabled()
* @see #collectAdjacentPrefetchPositions(int, int, State, LayoutPrefetchRegistry)
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public void collectInitialPrefetchPositions(int adapterItemCount,
LayoutPrefetchRegistry layoutPrefetchRegistry) {
}
void dispatchAttachedToWindow(RecyclerView view) {
mIsAttachedToWindow = true;
onAttachedToWindow(view);
}
void dispatchDetachedFromWindow(RecyclerView view, Recycler recycler) {
mIsAttachedToWindow = false;
onDetachedFromWindow(view, recycler);
}
/**
* Returns whether LayoutManager is currently attached to a RecyclerView which is attached
* to a window.
*
* @return True if this LayoutManager is controlling a RecyclerView and the RecyclerView
* is attached to window.
*/
public boolean isAttachedToWindow() {
return mIsAttachedToWindow;
}
/**
* Causes the Runnable to execute on the next animation time step.
* The runnable will be run on the user interface thread.
* <p>
* Calling this method when LayoutManager is not attached to a RecyclerView has no effect.
*
* @param action The Runnable that will be executed.
* @see #removeCallbacks
*/
public void postOnAnimation(Runnable action) {
if (mRecyclerView != null) {
ViewCompat.postOnAnimation(mRecyclerView, action);
}
}
/**
* Removes the specified Runnable from the message queue.
* <p>
* Calling this method when LayoutManager is not attached to a RecyclerView has no effect.
*
* @param action The Runnable to remove from the message handling queue
* @return true if RecyclerView could ask the Handler to remove the Runnable,
* false otherwise. When the returned value is true, the Runnable
* may or may not have been actually removed from the message queue
* (for instance, if the Runnable was not in the queue already.)
* @see #postOnAnimation
*/
public boolean removeCallbacks(Runnable action) {
if (mRecyclerView != null) {
return mRecyclerView.removeCallbacks(action);
}
return false;
}
/**
* Called when this LayoutManager is both attached to a RecyclerView and that RecyclerView
* is attached to a window.
* <p>
* If the RecyclerView is re-attached with the same LayoutManager and Adapter, it may not
* call {@link #onLayoutChildren(Recycler, State)} if nothing has changed and a layout was
* not requested on the RecyclerView while it was detached.
* <p>
* Subclass implementations should always call through to the superclass implementation.
*
* @param view The RecyclerView this LayoutManager is bound to
* @see #onDetachedFromWindow(RecyclerView, Recycler)
*/
@CallSuper
public void onAttachedToWindow(RecyclerView view) {
}
/**
* @deprecated override {@link #onDetachedFromWindow(RecyclerView, Recycler)}
*/
@Deprecated
public void onDetachedFromWindow(RecyclerView view) {
}
/**
* Called when this LayoutManager is detached from its parent RecyclerView or when
* its parent RecyclerView is detached from its window.
* <p>
* LayoutManager should clear all of its View references as another LayoutManager might be
* assigned to the RecyclerView.
* <p>
* If the RecyclerView is re-attached with the same LayoutManager and Adapter, it may not
* call {@link #onLayoutChildren(Recycler, State)} if nothing has changed and a layout was
* not requested on the RecyclerView while it was detached.
* <p>
* If your LayoutManager has View references that it cleans in on-detach, it should also
* call {@link RecyclerView#requestLayout()} to ensure that it is re-laid out when
* RecyclerView is re-attached.
* <p>
* Subclass implementations should always call through to the superclass implementation.
*
* @param view The RecyclerView this LayoutManager is bound to
* @param recycler The recycler to use if you prefer to recycle your children instead of
* keeping them around.
* @see #onAttachedToWindow(RecyclerView)
*/
@CallSuper
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public void onDetachedFromWindow(RecyclerView view, Recycler recycler) {
onDetachedFromWindow(view);
}
/**
* Check if the RecyclerView is configured to clip child views to its padding.
*
* @return true if this RecyclerView clips children to its padding, false otherwise
*/
public boolean getClipToPadding() {
return mRecyclerView != null && mRecyclerView.mClipToPadding;
}
/**
* Lay out all relevant child views from the given adapter.
*
* The LayoutManager is in charge of the behavior of item animations. By default,
* RecyclerView has a non-null {@link #getItemAnimator() ItemAnimator}, and simple
* item animations are enabled. This means that add/remove operations on the
* adapter will result in animations to add new or appearing items, removed or
* disappearing items, and moved items. If a LayoutManager returns false from
* {@link #supportsPredictiveItemAnimations()}, which is the default, and runs a
* normal layout operation during {@link #onLayoutChildren(Recycler, State)}, the
* RecyclerView will have enough information to run those animations in a simple
* way. For example, the default ItemAnimator, {@link DefaultItemAnimator}, will
* simply fade views in and out, whether they are actually added/removed or whether
* they are moved on or off the screen due to other add/remove operations.
*
* <p>A LayoutManager wanting a better item animation experience, where items can be
* animated onto and off of the screen according to where the items exist when they
* are not on screen, then the LayoutManager should return true from
* {@link #supportsPredictiveItemAnimations()} and add additional logic to
* {@link #onLayoutChildren(Recycler, State)}. Supporting predictive animations
* means that {@link #onLayoutChildren(Recycler, State)} will be called twice;
* once as a "pre" layout step to determine where items would have been prior to
* a real layout, and again to do the "real" layout. In the pre-layout phase,
* items will remember their pre-layout positions to allow them to be laid out
* appropriately. Also, {@link LayoutParams#isItemRemoved() removed} items will
* be returned from the scrap to help determine correct placement of other items.
* These removed items should not be added to the child list, but should be used
* to help calculate correct positioning of other views, including views that
* were not previously onscreen (referred to as APPEARING views), but whose
* pre-layout offscreen position can be determined given the extra
* information about the pre-layout removed views.</p>
*
* <p>The second layout pass is the real layout in which only non-removed views
* will be used. The only additional requirement during this pass is, if
* {@link #supportsPredictiveItemAnimations()} returns true, to note which
* views exist in the child list prior to layout and which are not there after
* layout (referred to as DISAPPEARING views), and to position/layout those views
* appropriately, without regard to the actual bounds of the RecyclerView. This allows
* the animation system to know the location to which to animate these disappearing
* views.</p>
*
* <p>The default LayoutManager implementations for RecyclerView handle all of these
* requirements for animations already. Clients of RecyclerView can either use one
* of these layout managers directly or look at their implementations of
* onLayoutChildren() to see how they account for the APPEARING and
* DISAPPEARING views.</p>
*
* @param recycler Recycler to use for fetching potentially cached views for a
* position
* @param state Transient state of RecyclerView
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public void onLayoutChildren(Recycler recycler, State state) {
Log.e(TAG, "You must override onLayoutChildren(Recycler recycler, State state) ");
}
/**
* Called after a full layout calculation is finished. The layout calculation may include
* multiple {@link #onLayoutChildren(Recycler, State)} calls due to animations or
* layout measurement but it will include only one {@link #onLayoutCompleted(State)} call.
* This method will be called at the end of {@link View#layout(int, int, int, int)} call.
* <p>
* This is a good place for the LayoutManager to do some cleanup like pending scroll
* position, saved state etc.
*
* @param state Transient state of RecyclerView
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public void onLayoutCompleted(State state) {
}
/**
* Create a default <code>LayoutParams</code> object for a child of the RecyclerView.
*
* <p>LayoutManagers will often want to use a custom <code>LayoutParams</code> type
* to store extra information specific to the layout. Client code should subclass
* {@link RecyclerView.LayoutParams} for this purpose.</p>
*
* <p><em>Important:</em> if you use your own custom <code>LayoutParams</code> type
* you must also override
* {@link #checkLayoutParams(LayoutParams)},
* {@link #generateLayoutParams(android.view.ViewGroup.LayoutParams)} and
* {@link #generateLayoutParams(android.content.Context, android.util.AttributeSet)}.</p>
*
* @return A new LayoutParams for a child view
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public abstract LayoutParams generateDefaultLayoutParams();
/**
* Determines the validity of the supplied LayoutParams object.
*
* <p>This should check to make sure that the object is of the correct type
* and all values are within acceptable ranges. The default implementation
* returns <code>true</code> for non-null params.</p>
*
* @param lp LayoutParams object to check
* @return true if this LayoutParams object is valid, false otherwise
*/
public boolean checkLayoutParams(LayoutParams lp) {
return lp != null;
}
/**
* Create a LayoutParams object suitable for this LayoutManager, copying relevant
* values from the supplied LayoutParams object if possible.
*
* <p><em>Important:</em> if you use your own custom <code>LayoutParams</code> type
* you must also override
* {@link #checkLayoutParams(LayoutParams)},
* {@link #generateLayoutParams(android.view.ViewGroup.LayoutParams)} and
* {@link #generateLayoutParams(android.content.Context, android.util.AttributeSet)}.</p>
*
* @param lp Source LayoutParams object to copy values from
* @return a new LayoutParams object
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
if (lp instanceof LayoutParams) {
return new LayoutParams((LayoutParams) lp);
} else if (lp instanceof MarginLayoutParams) {
return new LayoutParams((MarginLayoutParams) lp);
} else {
return new LayoutParams(lp);
}
}
/**
* Create a LayoutParams object suitable for this LayoutManager from
* an inflated layout resource.
*
* <p><em>Important:</em> if you use your own custom <code>LayoutParams</code> type
* you must also override
* {@link #checkLayoutParams(LayoutParams)},
* {@link #generateLayoutParams(android.view.ViewGroup.LayoutParams)} and
* {@link #generateLayoutParams(android.content.Context, android.util.AttributeSet)}.</p>
*
* @param c Context for obtaining styled attributes
* @param attrs AttributeSet describing the supplied arguments
* @return a new LayoutParams object
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public LayoutParams generateLayoutParams(Context c, AttributeSet attrs) {
return new LayoutParams(c, attrs);
}
/**
* Scroll horizontally by dx pixels in screen coordinates and return the distance traveled.
* The default implementation does nothing and returns 0.
*
* @param dx distance to scroll by in pixels. X increases as scroll position
* approaches the right.
* @param recycler Recycler to use for fetching potentially cached views for a
* position
* @param state Transient state of RecyclerView
* @return The actual distance scrolled. The return value will be negative if dx was
* negative and scrolling proceeeded in that direction.
* <code>Math.abs(result)</code> may be less than dx if a boundary was reached.
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public int scrollHorizontallyBy(int dx, Recycler recycler, State state) {
return 0;
}
/**
* Scroll vertically by dy pixels in screen coordinates and return the distance traveled.
* The default implementation does nothing and returns 0.
*
* @param dy distance to scroll in pixels. Y increases as scroll position
* approaches the bottom.
* @param recycler Recycler to use for fetching potentially cached views for a
* position
* @param state Transient state of RecyclerView
* @return The actual distance scrolled. The return value will be negative if dy was
* negative and scrolling proceeeded in that direction.
* <code>Math.abs(result)</code> may be less than dy if a boundary was reached.
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public int scrollVerticallyBy(int dy, Recycler recycler, State state) {
return 0;
}
/**
* Query if horizontal scrolling is currently supported. The default implementation
* returns false.
*
* @return True if this LayoutManager can scroll the current contents horizontally
*/
public boolean canScrollHorizontally() {
return false;
}
/**
* Query if vertical scrolling is currently supported. The default implementation
* returns false.
*
* @return True if this LayoutManager can scroll the current contents vertically
*/
public boolean canScrollVertically() {
return false;
}
/**
* Scroll to the specified adapter position.
*
* Actual position of the item on the screen depends on the LayoutManager implementation.
*
* @param position Scroll to this adapter position.
*/
public void scrollToPosition(int position) {
if (sVerboseLoggingEnabled) {
Log.e(TAG, "You MUST implement scrollToPosition. It will soon become abstract");
}
}
/**
* <p>Smooth scroll to the specified adapter position.</p>
* <p>To support smooth scrolling, override this method, create your {@link SmoothScroller}
* instance and call {@link #startSmoothScroll(SmoothScroller)}.
* </p>
*
* @param recyclerView The RecyclerView to which this layout manager is attached
* @param state Current State of RecyclerView
* @param position Scroll to this adapter position.
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public void smoothScrollToPosition(RecyclerView recyclerView, State state,
int position) {
Log.e(TAG, "You must override smoothScrollToPosition to support smooth scrolling");
}
/**
* Starts a smooth scroll using the provided {@link SmoothScroller}.
*
* <p>Each instance of SmoothScroller is intended to only be used once. Provide a new
* SmoothScroller instance each time this method is called.
*
* <p>Calling this method will cancel any previous smooth scroll request.
*
* @param smoothScroller Instance which defines how smooth scroll should be animated
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public void startSmoothScroll(SmoothScroller smoothScroller) {
if (mSmoothScroller != null && smoothScroller != mSmoothScroller
&& mSmoothScroller.isRunning()) {
mSmoothScroller.stop();
}
mSmoothScroller = smoothScroller;
mSmoothScroller.start(mRecyclerView, this);
}
/**
* @return true if RecyclerView is currently in the state of smooth scrolling.
*/
public boolean isSmoothScrolling() {
return mSmoothScroller != null && mSmoothScroller.isRunning();
}
/**
* Returns the resolved layout direction for this RecyclerView.
*
* @return {@link androidx.core.view.ViewCompat#LAYOUT_DIRECTION_RTL} if the layout
* direction is RTL or returns
* {@link androidx.core.view.ViewCompat#LAYOUT_DIRECTION_LTR} if the layout direction
* is not RTL.
*/
public int getLayoutDirection() {
return mRecyclerView.getLayoutDirection();
}
/**
* Query if the layout is in reverse order. This will affect, for example, keyboard
* navigation via page up/page down. The default implementation returns false.
*
* @return true if this LayoutManager is currently in reverse order.
*/
public boolean isLayoutReversed() {
return false;
}
/**
* Ends all animations on the view created by the {@link ItemAnimator}.
*
* @param view The View for which the animations should be ended.
* @see RecyclerView.ItemAnimator#endAnimations()
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public void endAnimation(View view) {
if (mRecyclerView.mItemAnimator != null) {
mRecyclerView.mItemAnimator.endAnimation(getChildViewHolderInt(view));
}
}
/**
* To be called only during {@link #onLayoutChildren(Recycler, State)} to add a view
* to the layout that is known to be going away, either because it has been
* {@link Adapter#notifyItemRemoved(int) removed} or because it is actually not in the
* visible portion of the container but is being laid out in order to inform RecyclerView
* in how to animate the item out of view.
* <p>
* Views added via this method are going to be invisible to LayoutManager after the
* dispatchLayout pass is complete. They cannot be retrieved via {@link #getChildAt(int)}
* or won't be included in {@link #getChildCount()} method.
*
* @param child View to add and then remove with animation.
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public void addDisappearingView(View child) {
addDisappearingView(child, -1);
}
/**
* To be called only during {@link #onLayoutChildren(Recycler, State)} to add a view
* to the layout that is known to be going away, either because it has been
* {@link Adapter#notifyItemRemoved(int) removed} or because it is actually not in the
* visible portion of the container but is being laid out in order to inform RecyclerView
* in how to animate the item out of view.
* <p>
* Views added via this method are going to be invisible to LayoutManager after the
* dispatchLayout pass is complete. They cannot be retrieved via {@link #getChildAt(int)}
* or won't be included in {@link #getChildCount()} method.
*
* @param child View to add and then remove with animation.
* @param index Index of the view.
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public void addDisappearingView(View child, int index) {
addViewInt(child, index, true);
}
/**
* Add a view to the currently attached RecyclerView if needed. LayoutManagers should
* use this method to add views obtained from a {@link Recycler} using
* {@link Recycler#getViewForPosition(int)}.
*
* @param child View to add
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public void addView(View child) {
addView(child, -1);
}
/**
* Add a view to the currently attached RecyclerView if needed. LayoutManagers should
* use this method to add views obtained from a {@link Recycler} using
* {@link Recycler#getViewForPosition(int)}.
*
* @param child View to add
* @param index Index to add child at
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public void addView(View child, int index) {
addViewInt(child, index, false);
}
private void addViewInt(View child, int index, boolean disappearing) {
final ViewHolder holder = getChildViewHolderInt(child);
if (disappearing || holder.isRemoved()) {
// these views will be hidden at the end of the layout pass.
mRecyclerView.mViewInfoStore.addToDisappearedInLayout(holder);
} else {
// This may look like unnecessary but may happen if layout manager supports
// predictive layouts and adapter removed then re-added the same item.
// In this case, added version will be visible in the post layout (because add is
// deferred) but RV will still bind it to the same View.
// So if a View re-appears in post layout pass, remove it from disappearing list.
mRecyclerView.mViewInfoStore.removeFromDisappearedInLayout(holder);
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (holder.wasReturnedFromScrap() || holder.isScrap()) {
if (holder.isScrap()) {
holder.unScrap();
} else {
holder.clearReturnedFromScrapFlag();
}
mChildHelper.attachViewToParent(child, index, child.getLayoutParams(), false);
if (DISPATCH_TEMP_DETACH) {
ViewCompat.dispatchFinishTemporaryDetach(child);
}
} else if (child.getParent() == mRecyclerView) { // it was not a scrap but a valid child
// ensure in correct position
int currentIndex = mChildHelper.indexOfChild(child);
if (index == -1) {
index = mChildHelper.getChildCount();
}
if (currentIndex == -1) {
throw new IllegalStateException("Added View has RecyclerView as parent but"
+ " view is not a real child. Unfiltered index:"
+ mRecyclerView.indexOfChild(child) + mRecyclerView.exceptionLabel());
}
if (currentIndex != index) {
mRecyclerView.mLayout.moveView(currentIndex, index);
}
} else {
mChildHelper.addView(child, index, false);
lp.mInsetsDirty = true;
if (mSmoothScroller != null && mSmoothScroller.isRunning()) {
mSmoothScroller.onChildAttachedToWindow(child);
}
}
if (lp.mPendingInvalidate) {
if (sVerboseLoggingEnabled) {
Log.d(TAG, "consuming pending invalidate on child " + lp.mViewHolder);
}
holder.itemView.invalidate();
lp.mPendingInvalidate = false;
}
}
/**
* Remove a view from the currently attached RecyclerView if needed. LayoutManagers should
* use this method to completely remove a child view that is no longer needed.
* LayoutManagers should strongly consider recycling removed views using
* {@link Recycler#recycleView(android.view.View)}.
*
* @param child View to remove
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public void removeView(View child) {
mChildHelper.removeView(child);
}
/**
* Remove a view from the currently attached RecyclerView if needed. LayoutManagers should
* use this method to completely remove a child view that is no longer needed.
* LayoutManagers should strongly consider recycling removed views using
* {@link Recycler#recycleView(android.view.View)}.
*
* @param index Index of the child view to remove
*/
public void removeViewAt(int index) {
final View child = getChildAt(index);
if (child != null) {
mChildHelper.removeViewAt(index);
}
}
/**
* Remove all views from the currently attached RecyclerView. This will not recycle
* any of the affected views; the LayoutManager is responsible for doing so if desired.
*/
public void removeAllViews() {
// Only remove non-animating views
final int childCount = getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
mChildHelper.removeViewAt(i);
}
}
/**
* Returns offset of the RecyclerView's text baseline from the its top boundary.
*
* @return The offset of the RecyclerView's text baseline from the its top boundary; -1 if
* there is no baseline.
*/
public int getBaseline() {
return -1;
}
/**
* Returns the adapter position of the item represented by the given View. This does not
* contain any adapter changes that might have happened after the last layout.
*
* @param view The view to query
* @return The adapter position of the item which is rendered by this View.
*/
public int getPosition(@NonNull View view) {
return ((RecyclerView.LayoutParams) view.getLayoutParams()).getViewLayoutPosition();
}
/**
* Returns the View type defined by the adapter.
*
* @param view The view to query
* @return The type of the view assigned by the adapter.
*/
public int getItemViewType(@NonNull View view) {
return getChildViewHolderInt(view).getItemViewType();
}
/**
* Traverses the ancestors of the given view and returns the item view that contains it
* and also a direct child of the LayoutManager.
* <p>
* Note that this method may return null if the view is a child of the RecyclerView but
* not a child of the LayoutManager (e.g. running a disappear animation).
*
* @param view The view that is a descendant of the LayoutManager.
* @return The direct child of the LayoutManager which contains the given view or null if
* the provided view is not a descendant of this LayoutManager.
* @see RecyclerView#getChildViewHolder(View)
* @see RecyclerView#findContainingViewHolder(View)
*/
@Nullable
public View findContainingItemView(@NonNull View view) {
if (mRecyclerView == null) {
return null;
}
View found = mRecyclerView.findContainingItemView(view);
if (found == null) {
return null;
}
if (mChildHelper.isHidden(found)) {
return null;
}
return found;
}
/**
* Finds the view which represents the given adapter position.
* <p>
* This method traverses each child since it has no information about child order.
* Override this method to improve performance if your LayoutManager keeps data about
* child views.
* <p>
* If a view is ignored via {@link #ignoreView(View)}, it is also ignored by this method.
*
* @param position Position of the item in adapter
* @return The child view that represents the given position or null if the position is not
* laid out
*/
@Nullable
public View findViewByPosition(int position) {
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
ViewHolder vh = getChildViewHolderInt(child);
if (vh == null) {
continue;
}
if (vh.getLayoutPosition() == position && !vh.shouldIgnore()
&& (mRecyclerView.mState.isPreLayout() || !vh.isRemoved())) {
return child;
}
}
return null;
}
/**
* Temporarily detach a child view.
*
* <p>LayoutManagers may want to perform a lightweight detach operation to rearrange
* views currently attached to the RecyclerView. Generally LayoutManager implementations
* will want to use {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)}
* so that the detached view may be rebound and reused.</p>
*
* <p>If a LayoutManager uses this method to detach a view, it <em>must</em>
* {@link #attachView(android.view.View, int, RecyclerView.LayoutParams) reattach}
* or {@link #removeDetachedView(android.view.View) fully remove} the detached view
* before the LayoutManager entry point method called by RecyclerView returns.</p>
*
* @param child Child to detach
*/
public void detachView(@NonNull View child) {
final int ind = mChildHelper.indexOfChild(child);
if (ind >= 0) {
detachViewInternal(ind, child);
}
}
/**
* Temporarily detach a child view.
*
* <p>LayoutManagers may want to perform a lightweight detach operation to rearrange
* views currently attached to the RecyclerView. Generally LayoutManager implementations
* will want to use {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)}
* so that the detached view may be rebound and reused.</p>
*
* <p>If a LayoutManager uses this method to detach a view, it <em>must</em>
* {@link #attachView(android.view.View, int, RecyclerView.LayoutParams) reattach}
* or {@link #removeDetachedView(android.view.View) fully remove} the detached view
* before the LayoutManager entry point method called by RecyclerView returns.</p>
*
* @param index Index of the child to detach
*/
public void detachViewAt(int index) {
detachViewInternal(index, getChildAt(index));
}
private void detachViewInternal(int index, @NonNull View view) {
if (DISPATCH_TEMP_DETACH) {
ViewCompat.dispatchStartTemporaryDetach(view);
}
mChildHelper.detachViewFromParent(index);
}
/**
* Reattach a previously {@link #detachView(android.view.View) detached} view.
* This method should not be used to reattach views that were previously
* {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)} scrapped}.
*
* @param child Child to reattach
* @param index Intended child index for child
* @param lp LayoutParams for child
*/
public void attachView(@NonNull View child, int index, LayoutParams lp) {
ViewHolder vh = getChildViewHolderInt(child);
if (vh.isRemoved()) {
mRecyclerView.mViewInfoStore.addToDisappearedInLayout(vh);
} else {
mRecyclerView.mViewInfoStore.removeFromDisappearedInLayout(vh);
}
mChildHelper.attachViewToParent(child, index, lp, vh.isRemoved());
if (DISPATCH_TEMP_DETACH) {
ViewCompat.dispatchFinishTemporaryDetach(child);
}
}
/**
* Reattach a previously {@link #detachView(android.view.View) detached} view.
* This method should not be used to reattach views that were previously
* {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)} scrapped}.
*
* @param child Child to reattach
* @param index Intended child index for child
*/
public void attachView(@NonNull View child, int index) {
attachView(child, index, (LayoutParams) child.getLayoutParams());
}
/**
* Reattach a previously {@link #detachView(android.view.View) detached} view.
* This method should not be used to reattach views that were previously
* {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)} scrapped}.
*
* @param child Child to reattach
*/
public void attachView(@NonNull View child) {
attachView(child, -1);
}
/**
* Finish removing a view that was previously temporarily
* {@link #detachView(android.view.View) detached}.
*
* @param child Detached child to remove
*/
public void removeDetachedView(@NonNull View child) {
mRecyclerView.removeDetachedView(child, false);
}
/**
* Moves a View from one position to another.
*
* @param fromIndex The View's initial index
* @param toIndex The View's target index
*/
public void moveView(int fromIndex, int toIndex) {
View view = getChildAt(fromIndex);
if (view == null) {
throw new IllegalArgumentException("Cannot move a child from non-existing index:"
+ fromIndex + mRecyclerView.toString());
}
detachViewAt(fromIndex);
attachView(view, toIndex);
}
/**
* Detach a child view and add it to a {@link Recycler Recycler's} scrap heap.
*
* <p>Scrapping a view allows it to be rebound and reused to show updated or
* different data.</p>
*
* @param child Child to detach and scrap
* @param recycler Recycler to deposit the new scrap view into
*/
public void detachAndScrapView(@NonNull View child, @NonNull Recycler recycler) {
int index = mChildHelper.indexOfChild(child);
scrapOrRecycleView(recycler, index, child);
}
/**
* Detach a child view and add it to a {@link Recycler Recycler's} scrap heap.
*
* <p>Scrapping a view allows it to be rebound and reused to show updated or
* different data.</p>
*
* @param index Index of child to detach and scrap
* @param recycler Recycler to deposit the new scrap view into
*/
public void detachAndScrapViewAt(int index, @NonNull Recycler recycler) {
final View child = getChildAt(index);
scrapOrRecycleView(recycler, index, child);
}
/**
* Remove a child view and recycle it using the given Recycler.
*
* @param child Child to remove and recycle
* @param recycler Recycler to use to recycle child
*/
public void removeAndRecycleView(@NonNull View child, @NonNull Recycler recycler) {
removeView(child);
recycler.recycleView(child);
}
/**
* Remove a child view and recycle it using the given Recycler.
*
* @param index Index of child to remove and recycle
* @param recycler Recycler to use to recycle child
*/
public void removeAndRecycleViewAt(int index, @NonNull Recycler recycler) {
final View view = getChildAt(index);
removeViewAt(index);
recycler.recycleView(view);
}
/**
* Return the current number of child views attached to the parent RecyclerView.
* This does not include child views that were temporarily detached and/or scrapped.
*
* @return Number of attached children
*/
public int getChildCount() {
return mChildHelper != null ? mChildHelper.getChildCount() : 0;
}
/**
* Return the child view at the given index
*
* @param index Index of child to return
* @return Child view at index
*/
@Nullable
public View getChildAt(int index) {
return mChildHelper != null ? mChildHelper.getChildAt(index) : null;
}
/**
* Return the width measurement spec mode that is currently relevant to the LayoutManager.
*
* <p>This value is set only if the LayoutManager opts into the AutoMeasure api via
* {@link #setAutoMeasureEnabled(boolean)}.
*
* <p>When RecyclerView is running a layout, this value is always set to
* {@link View.MeasureSpec#EXACTLY} even if it was measured with a different spec mode.
*
* @return Width measure spec mode
* @see View.MeasureSpec#getMode(int)
*/
public int getWidthMode() {
return mWidthMode;
}
/**
* Return the height measurement spec mode that is currently relevant to the LayoutManager.
*
* <p>This value is set only if the LayoutManager opts into the AutoMeasure api via
* {@link #setAutoMeasureEnabled(boolean)}.
*
* <p>When RecyclerView is running a layout, this value is always set to
* {@link View.MeasureSpec#EXACTLY} even if it was measured with a different spec mode.
*
* @return Height measure spec mode
* @see View.MeasureSpec#getMode(int)
*/
public int getHeightMode() {
return mHeightMode;
}
/**
* Returns the width that is currently relevant to the LayoutManager.
*
* <p>This value is usually equal to the laid out width of the {@link RecyclerView} but may
* reflect the current {@link android.view.View.MeasureSpec} width if the
* {@link LayoutManager} is using AutoMeasure and the RecyclerView is in the process of
* measuring. The LayoutManager must always use this method to retrieve the width relevant
* to it at any given time.
*
* @return Width in pixels
*/
@Px
public int getWidth() {
return mWidth;
}
/**
* Returns the height that is currently relevant to the LayoutManager.
*
* <p>This value is usually equal to the laid out height of the {@link RecyclerView} but may
* reflect the current {@link android.view.View.MeasureSpec} height if the
* {@link LayoutManager} is using AutoMeasure and the RecyclerView is in the process of
* measuring. The LayoutManager must always use this method to retrieve the height relevant
* to it at any given time.
*
* @return Height in pixels
*/
@Px
public int getHeight() {
return mHeight;
}
/**
* Return the left padding of the parent RecyclerView
*
* @return Padding in pixels
*/
@Px
public int getPaddingLeft() {
return mRecyclerView != null ? mRecyclerView.getPaddingLeft() : 0;
}
/**
* Return the top padding of the parent RecyclerView
*
* @return Padding in pixels
*/
@Px
public int getPaddingTop() {
return mRecyclerView != null ? mRecyclerView.getPaddingTop() : 0;
}
/**
* Return the right padding of the parent RecyclerView
*
* @return Padding in pixels
*/
@Px
public int getPaddingRight() {
return mRecyclerView != null ? mRecyclerView.getPaddingRight() : 0;
}
/**
* Return the bottom padding of the parent RecyclerView
*
* @return Padding in pixels
*/
@Px
public int getPaddingBottom() {
return mRecyclerView != null ? mRecyclerView.getPaddingBottom() : 0;
}
/**
* Return the start padding of the parent RecyclerView
*
* @return Padding in pixels
*/
@Px
public int getPaddingStart() {
return mRecyclerView != null ? ViewCompat.getPaddingStart(mRecyclerView) : 0;
}
/**
* Return the end padding of the parent RecyclerView
*
* @return Padding in pixels
*/
@Px
public int getPaddingEnd() {
return mRecyclerView != null ? ViewCompat.getPaddingEnd(mRecyclerView) : 0;
}
/**
* Returns true if the RecyclerView this LayoutManager is bound to has focus.
*
* @return True if the RecyclerView has focus, false otherwise.
* @see View#isFocused()
*/
public boolean isFocused() {
return mRecyclerView != null && mRecyclerView.isFocused();
}
/**
* Returns true if the RecyclerView this LayoutManager is bound to has or contains focus.
*
* @return true if the RecyclerView has or contains focus
* @see View#hasFocus()
*/
public boolean hasFocus() {
return mRecyclerView != null && mRecyclerView.hasFocus();
}
/**
* Returns the item View which has or contains focus.
*
* @return A direct child of RecyclerView which has focus or contains the focused child.
*/
@Nullable
public View getFocusedChild() {
if (mRecyclerView == null) {
return null;
}
final View focused = mRecyclerView.getFocusedChild();
if (focused == null || mChildHelper.isHidden(focused)) {
return null;
}
return focused;
}
/**
* Returns the number of items in the adapter bound to the parent RecyclerView.
* <p>
* Note that this number is not necessarily equal to
* {@link State#getItemCount() State#getItemCount()}. In methods where {@link State} is
* available, you should use {@link State#getItemCount() State#getItemCount()} instead.
* For more details, check the documentation for
* {@link State#getItemCount() State#getItemCount()}.
*
* @return The number of items in the bound adapter
* @see State#getItemCount()
*/
public int getItemCount() {
final Adapter a = mRecyclerView != null ? mRecyclerView.getAdapter() : null;
return a != null ? a.getItemCount() : 0;
}
/**
* Offset all child views attached to the parent RecyclerView by dx pixels along
* the horizontal axis.
*
* @param dx Pixels to offset by
*/
public void offsetChildrenHorizontal(@Px int dx) {
if (mRecyclerView != null) {
mRecyclerView.offsetChildrenHorizontal(dx);
}
}
/**
* Offset all child views attached to the parent RecyclerView by dy pixels along
* the vertical axis.
*
* @param dy Pixels to offset by
*/
public void offsetChildrenVertical(@Px int dy) {
if (mRecyclerView != null) {
mRecyclerView.offsetChildrenVertical(dy);
}
}
/**
* Flags a view so that it will not be scrapped or recycled.
* <p>
* Scope of ignoring a child is strictly restricted to position tracking, scrapping and
* recyling. Methods like {@link #removeAndRecycleAllViews(Recycler)} will ignore the child
* whereas {@link #removeAllViews()} or {@link #offsetChildrenHorizontal(int)} will not
* ignore the child.
* <p>
* Before this child can be recycled again, you have to call
* {@link #stopIgnoringView(View)}.
* <p>
* You can call this method only if your LayoutManger is in onLayout or onScroll callback.
*
* @param view View to ignore.
* @see #stopIgnoringView(View)
*/
public void ignoreView(@NonNull View view) {
if (view.getParent() != mRecyclerView || mRecyclerView.indexOfChild(view) == -1) {
// checking this because calling this method on a recycled or detached view may
// cause loss of state.
throw new IllegalArgumentException("View should be fully attached to be ignored"
+ mRecyclerView.exceptionLabel());
}
final ViewHolder vh = getChildViewHolderInt(view);
vh.addFlags(ViewHolder.FLAG_IGNORE);
mRecyclerView.mViewInfoStore.removeViewHolder(vh);
}
/**
* View can be scrapped and recycled again.
* <p>
* Note that calling this method removes all information in the view holder.
* <p>
* You can call this method only if your LayoutManger is in onLayout or onScroll callback.
*
* @param view View to ignore.
*/
public void stopIgnoringView(@NonNull View view) {
final ViewHolder vh = getChildViewHolderInt(view);
vh.stopIgnoring();
vh.resetInternal();
vh.addFlags(ViewHolder.FLAG_INVALID);
}
/**
* Temporarily detach and scrap all currently attached child views. Views will be scrapped
* into the given Recycler. The Recycler may prefer to reuse scrap views before
* other views that were previously recycled.
*
* @param recycler Recycler to scrap views into
*/
public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
final int childCount = getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
final View v = getChildAt(i);
scrapOrRecycleView(recycler, i, v);
}
}
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
if (viewHolder.shouldIgnore()) {
if (sVerboseLoggingEnabled) {
Log.d(TAG, "ignoring view " + viewHolder);
}
return;
}
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {
removeViewAt(index);
recycler.recycleViewHolderInternal(viewHolder);
} else {
detachViewAt(index);
recycler.scrapView(view);
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
}
}
/**
* Recycles the scrapped views.
* <p>
* When a view is detached and removed, it does not trigger a ViewGroup invalidate. This is
* the expected behavior if scrapped views are used for animations. Otherwise, we need to
* call remove and invalidate RecyclerView to ensure UI update.
*
* @param recycler Recycler
*/
void removeAndRecycleScrapInt(Recycler recycler) {
final int scrapCount = recycler.getScrapCount();
// Loop backward, recycler might be changed by removeDetachedView()
for (int i = scrapCount - 1; i >= 0; i--) {
final View scrap = recycler.getScrapViewAt(i);
final ViewHolder vh = getChildViewHolderInt(scrap);
if (vh.shouldIgnore()) {
continue;
}
// If the scrap view is animating, we need to cancel them first. If we cancel it
// here, ItemAnimator callback may recycle it which will cause double recycling.
// To avoid this, we mark it as not recyclable before calling the item animator.
// Since removeDetachedView calls a user API, a common mistake (ending animations on
// the view) may recycle it too, so we guard it before we call user APIs.
vh.setIsRecyclable(false);
if (vh.isTmpDetached()) {
mRecyclerView.removeDetachedView(scrap, false);
}
if (mRecyclerView.mItemAnimator != null) {
mRecyclerView.mItemAnimator.endAnimation(vh);
}
vh.setIsRecyclable(true);
recycler.quickRecycleScrapView(scrap);
}
recycler.clearScrap();
if (scrapCount > 0) {
mRecyclerView.invalidate();
}
}
/**
* Measure a child view using standard measurement policy, taking the padding
* of the parent RecyclerView and any added item decorations into account.
*
* <p>If the RecyclerView can be scrolled in either dimension the caller may
* pass 0 as the widthUsed or heightUsed parameters as they will be irrelevant.</p>
*
* @param child Child view to measure
* @param widthUsed Width in pixels currently consumed by other views, if relevant
* @param heightUsed Height in pixels currently consumed by other views, if relevant
*/
public void measureChild(@NonNull View child, int widthUsed, int heightUsed) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
widthUsed += insets.left + insets.right;
heightUsed += insets.top + insets.bottom;
final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
getPaddingLeft() + getPaddingRight() + widthUsed, lp.width,
canScrollHorizontally());
final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
getPaddingTop() + getPaddingBottom() + heightUsed, lp.height,
canScrollVertically());
if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
child.measure(widthSpec, heightSpec);
}
}
/**
* RecyclerView internally does its own View measurement caching which should help with
* WRAP_CONTENT.
* <p>
* Use this method if the View is already measured once in this layout pass.
*/
boolean shouldReMeasureChild(View child, int widthSpec, int heightSpec, LayoutParams lp) {
return !mMeasurementCacheEnabled
|| !isMeasurementUpToDate(child.getMeasuredWidth(), widthSpec, lp.width)
|| !isMeasurementUpToDate(child.getMeasuredHeight(), heightSpec, lp.height);
}
// we may consider making this public
/**
* RecyclerView internally does its own View measurement caching which should help with
* WRAP_CONTENT.
* <p>
* Use this method if the View is not yet measured and you need to decide whether to
* measure this View or not.
*/
boolean shouldMeasureChild(View child, int widthSpec, int heightSpec, LayoutParams lp) {
return child.isLayoutRequested()
|| !mMeasurementCacheEnabled
|| !isMeasurementUpToDate(child.getWidth(), widthSpec, lp.width)
|| !isMeasurementUpToDate(child.getHeight(), heightSpec, lp.height);
}
/**
* In addition to the View Framework's measurement cache, RecyclerView uses its own
* additional measurement cache for its children to avoid re-measuring them when not
* necessary. It is on by default but it can be turned off via
* {@link #setMeasurementCacheEnabled(boolean)}.
*
* @return True if measurement cache is enabled, false otherwise.
* @see #setMeasurementCacheEnabled(boolean)
*/
public boolean isMeasurementCacheEnabled() {
return mMeasurementCacheEnabled;
}
/**
* Sets whether RecyclerView should use its own measurement cache for the children. This is
* a more aggressive cache than the framework uses.
*
* @param measurementCacheEnabled True to enable the measurement cache, false otherwise.
* @see #isMeasurementCacheEnabled()
*/
public void setMeasurementCacheEnabled(boolean measurementCacheEnabled) {
mMeasurementCacheEnabled = measurementCacheEnabled;
}
private static boolean isMeasurementUpToDate(int childSize, int spec, int dimension) {
final int specMode = MeasureSpec.getMode(spec);
final int specSize = MeasureSpec.getSize(spec);
if (dimension > 0 && childSize != dimension) {
return false;
}
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
return true;
case MeasureSpec.AT_MOST:
return specSize >= childSize;
case MeasureSpec.EXACTLY:
return specSize == childSize;
}
return false;
}
/**
* Measure a child view using standard measurement policy, taking the padding
* of the parent RecyclerView, any added item decorations and the child margins
* into account.
*
* <p>If the RecyclerView can be scrolled in either dimension the caller may
* pass 0 as the widthUsed or heightUsed parameters as they will be irrelevant.</p>
*
* @param child Child view to measure
* @param widthUsed Width in pixels currently consumed by other views, if relevant
* @param heightUsed Height in pixels currently consumed by other views, if relevant
*/
public void measureChildWithMargins(@NonNull View child, int widthUsed, int heightUsed) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
widthUsed += insets.left + insets.right;
heightUsed += insets.top + insets.bottom;
final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
getPaddingLeft() + getPaddingRight()
+ lp.leftMargin + lp.rightMargin + widthUsed, lp.width,
canScrollHorizontally());
final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
getPaddingTop() + getPaddingBottom()
+ lp.topMargin + lp.bottomMargin + heightUsed, lp.height,
canScrollVertically());
if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
child.measure(widthSpec, heightSpec);
}
}
/**
* Calculate a MeasureSpec value for measuring a child view in one dimension.
*
* @param parentSize Size of the parent view where the child will be placed
* @param padding Total space currently consumed by other elements of the parent
* @param childDimension Desired size of the child view, or MATCH_PARENT/WRAP_CONTENT.
* Generally obtained from the child view's LayoutParams
* @param canScroll true if the parent RecyclerView can scroll in this dimension
* @return a MeasureSpec value for the child view
* @deprecated use {@link #getChildMeasureSpec(int, int, int, int, boolean)}
*/
@Deprecated
public static int getChildMeasureSpec(int parentSize, int padding, int childDimension,
boolean canScroll) {
int size = Math.max(0, parentSize - padding);
int resultSize = 0;
int resultMode = 0;
if (canScroll) {
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else {
// MATCH_PARENT can't be applied since we can scroll in this dimension, wrap
// instead using UNSPECIFIED.
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
} else {
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
// TODO this should be my spec.
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
/**
* Calculate a MeasureSpec value for measuring a child view in one dimension.
*
* @param parentSize Size of the parent view where the child will be placed
* @param parentMode The measurement spec mode of the parent
* @param padding Total space currently consumed by other elements of parent
* @param childDimension Desired size of the child view, or MATCH_PARENT/WRAP_CONTENT.
* Generally obtained from the child view's LayoutParams
* @param canScroll true if the parent RecyclerView can scroll in this dimension
* @return a MeasureSpec value for the child view
*/
public static int getChildMeasureSpec(int parentSize, int parentMode, int padding,
int childDimension, boolean canScroll) {
int size = Math.max(0, parentSize - padding);
int resultSize = 0;
int resultMode = 0;
if (canScroll) {
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
switch (parentMode) {
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
resultSize = size;
resultMode = parentMode;
break;
case MeasureSpec.UNSPECIFIED:
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
break;
}
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
} else {
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = parentMode;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
if (parentMode == MeasureSpec.AT_MOST || parentMode == MeasureSpec.EXACTLY) {
resultMode = MeasureSpec.AT_MOST;
} else {
resultMode = MeasureSpec.UNSPECIFIED;
}
}
}
//noinspection WrongConstant
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
/**
* Returns the measured width of the given child, plus the additional size of
* any insets applied by {@link ItemDecoration ItemDecorations}.
*
* @param child Child view to query
* @return child's measured width plus <code>ItemDecoration</code> insets
* @see View#getMeasuredWidth()
*/
public int getDecoratedMeasuredWidth(@NonNull View child) {
final Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets;
return child.getMeasuredWidth() + insets.left + insets.right;
}
/**
* Returns the measured height of the given child, plus the additional size of
* any insets applied by {@link ItemDecoration ItemDecorations}.
*
* @param child Child view to query
* @return child's measured height plus <code>ItemDecoration</code> insets
* @see View#getMeasuredHeight()
*/
public int getDecoratedMeasuredHeight(@NonNull View child) {
final Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets;
return child.getMeasuredHeight() + insets.top + insets.bottom;
}
/**
* Lay out the given child view within the RecyclerView using coordinates that
* include any current {@link ItemDecoration ItemDecorations}.
*
* <p>LayoutManagers should prefer working in sizes and coordinates that include
* item decoration insets whenever possible. This allows the LayoutManager to effectively
* ignore decoration insets within measurement and layout code. See the following
* methods:</p>
* <ul>
* <li>{@link #layoutDecoratedWithMargins(View, int, int, int, int)}</li>
* <li>{@link #getDecoratedBoundsWithMargins(View, Rect)}</li>
* <li>{@link #measureChild(View, int, int)}</li>
* <li>{@link #measureChildWithMargins(View, int, int)}</li>
* <li>{@link #getDecoratedLeft(View)}</li>
* <li>{@link #getDecoratedTop(View)}</li>
* <li>{@link #getDecoratedRight(View)}</li>
* <li>{@link #getDecoratedBottom(View)}</li>
* <li>{@link #getDecoratedMeasuredWidth(View)}</li>
* <li>{@link #getDecoratedMeasuredHeight(View)}</li>
* </ul>
*
* @param child Child to lay out
* @param left Left edge, with item decoration insets included
* @param top Top edge, with item decoration insets included
* @param right Right edge, with item decoration insets included
* @param bottom Bottom edge, with item decoration insets included
* @see View#layout(int, int, int, int)
* @see #layoutDecoratedWithMargins(View, int, int, int, int)
*/
public void layoutDecorated(@NonNull View child, int left, int top, int right, int bottom) {
final Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets;
child.layout(left + insets.left, top + insets.top, right - insets.right,
bottom - insets.bottom);
}
/**
* Lay out the given child view within the RecyclerView using coordinates that
* include any current {@link ItemDecoration ItemDecorations} and margins.
*
* <p>LayoutManagers should prefer working in sizes and coordinates that include
* item decoration insets whenever possible. This allows the LayoutManager to effectively
* ignore decoration insets within measurement and layout code. See the following
* methods:</p>
* <ul>
* <li>{@link #layoutDecorated(View, int, int, int, int)}</li>
* <li>{@link #measureChild(View, int, int)}</li>
* <li>{@link #measureChildWithMargins(View, int, int)}</li>
* <li>{@link #getDecoratedLeft(View)}</li>
* <li>{@link #getDecoratedTop(View)}</li>
* <li>{@link #getDecoratedRight(View)}</li>
* <li>{@link #getDecoratedBottom(View)}</li>
* <li>{@link #getDecoratedMeasuredWidth(View)}</li>
* <li>{@link #getDecoratedMeasuredHeight(View)}</li>
* </ul>
*
* @param child Child to lay out
* @param left Left edge, with item decoration insets and left margin included
* @param top Top edge, with item decoration insets and top margin included
* @param right Right edge, with item decoration insets and right margin included
* @param bottom Bottom edge, with item decoration insets and bottom margin included
* @see View#layout(int, int, int, int)
* @see #layoutDecorated(View, int, int, int, int)
*/
public void layoutDecoratedWithMargins(@NonNull View child, int left, int top, int right,
int bottom) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Rect insets = lp.mDecorInsets;
child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin,
right - insets.right - lp.rightMargin,
bottom - insets.bottom - lp.bottomMargin);
}
/**
* Calculates the bounding box of the View while taking into account its matrix changes
* (translation, scale etc) with respect to the RecyclerView.
* <p>
* If {@code includeDecorInsets} is {@code true}, they are applied first before applying
* the View's matrix so that the decor offsets also go through the same transformation.
*
* @param child The ItemView whose bounding box should be calculated.
* @param includeDecorInsets True if the decor insets should be included in the bounding box
* @param out The rectangle into which the output will be written.
*/
public void getTransformedBoundingBox(@NonNull View child, boolean includeDecorInsets,
@NonNull Rect out) {
if (includeDecorInsets) {
Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets;
out.set(-insets.left, -insets.top,
child.getWidth() + insets.right, child.getHeight() + insets.bottom);
} else {
out.set(0, 0, child.getWidth(), child.getHeight());
}
if (mRecyclerView != null) {
final Matrix childMatrix = child.getMatrix();
if (childMatrix != null && !childMatrix.isIdentity()) {
final RectF tempRectF = mRecyclerView.mTempRectF;
tempRectF.set(out);
childMatrix.mapRect(tempRectF);
out.set(
(int) Math.floor(tempRectF.left),
(int) Math.floor(tempRectF.top),
(int) Math.ceil(tempRectF.right),
(int) Math.ceil(tempRectF.bottom)
);
}
}
out.offset(child.getLeft(), child.getTop());
}
/**
* 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) {
RecyclerView.getDecoratedBoundsWithMarginsInt(view, outBounds);
}
/**
* Returns the left edge of the given child view within its parent, offset by any applied
* {@link ItemDecoration ItemDecorations}.
*
* @param child Child to query
* @return Child left edge with offsets applied
* @see #getLeftDecorationWidth(View)
*/
public int getDecoratedLeft(@NonNull View child) {
return child.getLeft() - getLeftDecorationWidth(child);
}
/**
* Returns the top edge of the given child view within its parent, offset by any applied
* {@link ItemDecoration ItemDecorations}.
*
* @param child Child to query
* @return Child top edge with offsets applied
* @see #getTopDecorationHeight(View)
*/
public int getDecoratedTop(@NonNull View child) {
return child.getTop() - getTopDecorationHeight(child);
}
/**
* Returns the right edge of the given child view within its parent, offset by any applied
* {@link ItemDecoration ItemDecorations}.
*
* @param child Child to query
* @return Child right edge with offsets applied
* @see #getRightDecorationWidth(View)
*/
public int getDecoratedRight(@NonNull View child) {
return child.getRight() + getRightDecorationWidth(child);
}
/**
* Returns the bottom edge of the given child view within its parent, offset by any applied
* {@link ItemDecoration ItemDecorations}.
*
* @param child Child to query
* @return Child bottom edge with offsets applied
* @see #getBottomDecorationHeight(View)
*/
public int getDecoratedBottom(@NonNull View child) {
return child.getBottom() + getBottomDecorationHeight(child);
}
/**
* Calculates the item decor insets applied to the given child and updates the provided
* Rect instance with the inset values.
* <ul>
* <li>The Rect's left is set to the total width of left decorations.</li>
* <li>The Rect's top is set to the total height of top decorations.</li>
* <li>The Rect's right is set to the total width of right decorations.</li>
* <li>The Rect's bottom is set to total height of bottom decorations.</li>
* </ul>
* <p>
* Note that item decorations are automatically calculated when one of the LayoutManager's
* measure child methods is called. If you need to measure the child with custom specs via
* {@link View#measure(int, int)}, you can use this method to get decorations.
*
* @param child The child view whose decorations should be calculated
* @param outRect The Rect to hold result values
*/
public void calculateItemDecorationsForChild(@NonNull View child, @NonNull Rect outRect) {
if (mRecyclerView == null) {
outRect.set(0, 0, 0, 0);
return;
}
Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
outRect.set(insets);
}
/**
* Returns the total height of item decorations applied to child's top.
* <p>
* Note that this value is not updated until the View is measured or
* {@link #calculateItemDecorationsForChild(View, Rect)} is called.
*
* @param child Child to query
* @return The total height of item decorations applied to the child's top.
* @see #getDecoratedTop(View)
* @see #calculateItemDecorationsForChild(View, Rect)
*/
public int getTopDecorationHeight(@NonNull View child) {
return ((LayoutParams) child.getLayoutParams()).mDecorInsets.top;
}
/**
* Returns the total height of item decorations applied to child's bottom.
* <p>
* Note that this value is not updated until the View is measured or
* {@link #calculateItemDecorationsForChild(View, Rect)} is called.
*
* @param child Child to query
* @return The total height of item decorations applied to the child's bottom.
* @see #getDecoratedBottom(View)
* @see #calculateItemDecorationsForChild(View, Rect)
*/
public int getBottomDecorationHeight(@NonNull View child) {
return ((LayoutParams) child.getLayoutParams()).mDecorInsets.bottom;
}
/**
* Returns the total width of item decorations applied to child's left.
* <p>
* Note that this value is not updated until the View is measured or
* {@link #calculateItemDecorationsForChild(View, Rect)} is called.
*
* @param child Child to query
* @return The total width of item decorations applied to the child's left.
* @see #getDecoratedLeft(View)
* @see #calculateItemDecorationsForChild(View, Rect)
*/
public int getLeftDecorationWidth(@NonNull View child) {
return ((LayoutParams) child.getLayoutParams()).mDecorInsets.left;
}
/**
* Returns the total width of item decorations applied to child's right.
* <p>
* Note that this value is not updated until the View is measured or
* {@link #calculateItemDecorationsForChild(View, Rect)} is called.
*
* @param child Child to query
* @return The total width of item decorations applied to the child's right.
* @see #getDecoratedRight(View)
* @see #calculateItemDecorationsForChild(View, Rect)
*/
public int getRightDecorationWidth(@NonNull View child) {
return ((LayoutParams) child.getLayoutParams()).mDecorInsets.right;
}
/**
* Called when searching for a focusable view in the given direction has failed
* for the current content of the RecyclerView.
*
* <p>This is the LayoutManager's opportunity to populate views in the given direction
* to fulfill the request if it can. The LayoutManager should attach and return
* the view to be focused, if a focusable view in the given direction is found.
* Otherwise, if all the existing (or the newly populated views) are unfocusable, it returns
* the next unfocusable view to become visible on the screen. This unfocusable view is
* typically the first view that's either partially or fully out of RV's padded bounded
* area in the given direction. The default implementation returns null.</p>
*
* @param focused The currently focused view
* @param direction One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN},
* {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT},
* {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD}
* or 0 for not applicable
* @param recycler The recycler to use for obtaining views for currently offscreen items
* @param state Transient state of RecyclerView
* @return The chosen view to be focused if a focusable view is found, otherwise an
* unfocusable view to become visible onto the screen, else null.
*/
@Nullable
public View onFocusSearchFailed(@NonNull View focused, int direction,
@NonNull Recycler recycler, @NonNull State state) {
return null;
}
/**
* This method gives a LayoutManager an opportunity to intercept the initial focus search
* before the default behavior of {@link FocusFinder} is used. If this method returns
* null FocusFinder will attempt to find a focusable child view. If it fails
* then {@link #onFocusSearchFailed(View, int, RecyclerView.Recycler, RecyclerView.State)}
* will be called to give the LayoutManager an opportunity to add new views for items
* that did not have attached views representing them. The LayoutManager should not add
* or remove views from this method.
*
* @param focused The currently focused view
* @param direction One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN},
* {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT},
* {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD}
* @return A descendant view to focus or null to fall back to default behavior.
* The default implementation returns null.
*/
@Nullable
public View onInterceptFocusSearch(@NonNull View focused, int direction) {
return null;
}
/**
* Returns the scroll amount that brings the given rect in child's coordinate system within
* the padded area of RecyclerView.
*
* @param child The direct child making the request.
* @param rect The rectangle in the child's coordinates the child
* wishes to be on the screen.
* @return The array containing the scroll amount in x and y directions that brings the
* given rect into RV's padded area.
*/
private int[] getChildRectangleOnScreenScrollAmount(View child, Rect rect) {
int[] out = new int[2];
final int parentLeft = getPaddingLeft();
final int parentTop = getPaddingTop();
final int parentRight = getWidth() - getPaddingRight();
final int parentBottom = getHeight() - getPaddingBottom();
final int childLeft = child.getLeft() + rect.left - child.getScrollX();
final int childTop = child.getTop() + rect.top - child.getScrollY();
final int childRight = childLeft + rect.width();
final int childBottom = childTop + rect.height();
final int offScreenLeft = Math.min(0, childLeft - parentLeft);
final int offScreenTop = Math.min(0, childTop - parentTop);
final int offScreenRight = Math.max(0, childRight - parentRight);
final int offScreenBottom = Math.max(0, childBottom - parentBottom);
// Favor the "start" layout direction over the end when bringing one side or the other
// of a large rect into view. If we decide to bring in end because start is already
// visible, limit the scroll such that start won't go out of bounds.
final int dx;
if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
dx = offScreenRight != 0 ? offScreenRight
: Math.max(offScreenLeft, childRight - parentRight);
} else {
dx = offScreenLeft != 0 ? offScreenLeft
: Math.min(childLeft - parentLeft, offScreenRight);
}
// Favor bringing the top into view over the bottom. If top is already visible and
// we should scroll to make bottom visible, make sure top does not go out of bounds.
final int dy = offScreenTop != 0 ? offScreenTop
: Math.min(childTop - parentTop, offScreenBottom);
out[0] = dx;
out[1] = dy;
return out;
}
/**
* Called when a child of the RecyclerView wants a particular rectangle to be positioned
* onto the screen. See {@link ViewParent#requestChildRectangleOnScreen(android.view.View,
* android.graphics.Rect, boolean)} for more details.
*
* <p>The base implementation will attempt to perform a standard programmatic scroll
* to bring the given rect into view, within the padded area of the RecyclerView.</p>
*
* @param parent The parent RecyclerView.
* @param child The direct child making the request.
* @param rect The rectangle in the child's coordinates the child
* wishes to be on the screen.
* @param immediate True to forbid animated or delayed scrolling,
* false otherwise
* @return Whether the group scrolled to handle the operation
*/
public boolean requestChildRectangleOnScreen(@NonNull RecyclerView parent,
@NonNull View child, @NonNull Rect rect, boolean immediate) {
return requestChildRectangleOnScreen(parent, child, rect, immediate, false);
}
/**
* 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, focusedChildVisible is typically true in which case, layout manager
* makes the child view visible only if the currently focused child stays in-bounds of RV.
*
* @param parent The parent RecyclerView.
* @param child The direct child making the request.
* @param rect The rectangle in the child's coordinates the child
* wishes to be on the screen.
* @param immediate True to forbid animated or delayed scrolling,
* false otherwise
* @param focusedChildVisible Whether the currently focused view must stay visible.
* @return Whether the group scrolled to handle the operation
*/
public boolean requestChildRectangleOnScreen(@NonNull RecyclerView parent,
@NonNull View child, @NonNull Rect rect, boolean immediate,
boolean focusedChildVisible) {
int[] scrollAmount = getChildRectangleOnScreenScrollAmount(child, rect
);
int dx = scrollAmount[0];
int dy = scrollAmount[1];
if (!focusedChildVisible || isFocusedChildVisibleAfterScrolling(parent, dx, dy)) {
if (dx != 0 || dy != 0) {
if (immediate) {
parent.scrollBy(dx, dy);
} else {
parent.smoothScrollBy(dx, dy);
}
return true;
}
}
return false;
}
/**
* Returns whether the given child view is partially or fully visible within the padded
* bounded area of RecyclerView, depending on the input parameters.
* A view is partially visible if it has non-zero overlap with RV's padded bounded area.
* If acceptEndPointInclusion flag is set to true, it's also considered partially
* visible if it's located outside RV's bounds and it's hitting either RV's start or end
* bounds.
*
* @param child The child view to be examined.
* @param completelyVisible If true, the method returns true if and only if the
* child is
* completely visible. If false, the method returns true
* if and
* only if the child is only partially visible (that is it
* will
* return false if the child is either completely visible
* or out
* of RV's bounds).
* @param acceptEndPointInclusion If the view's endpoint intersection with RV's start of end
* bounds is enough to consider it partially visible,
* false otherwise.
* @return True if the given child is partially or fully visible, false otherwise.
*/
public boolean isViewPartiallyVisible(@NonNull View child, boolean completelyVisible,
boolean acceptEndPointInclusion) {
int boundsFlag = (ViewBoundsCheck.FLAG_CVS_GT_PVS | ViewBoundsCheck.FLAG_CVS_EQ_PVS
| ViewBoundsCheck.FLAG_CVE_LT_PVE | ViewBoundsCheck.FLAG_CVE_EQ_PVE);
boolean isViewFullyVisible = mHorizontalBoundCheck.isViewWithinBoundFlags(child,
boundsFlag)
&& mVerticalBoundCheck.isViewWithinBoundFlags(child, boundsFlag);
if (completelyVisible) {
return isViewFullyVisible;
} else {
return !isViewFullyVisible;
}
}
/**
* Returns whether the currently focused child stays within RV's bounds with the given
* amount of scrolling.
*
* @param parent The parent RecyclerView.
* @param dx The scrolling in x-axis direction to be performed.
* @param dy The scrolling in y-axis direction to be performed.
* @return {@code false} if the focused child is not at least partially visible after
* scrolling or no focused child exists, {@code true} otherwise.
*/
private boolean isFocusedChildVisibleAfterScrolling(RecyclerView parent, int dx, int dy) {
final View focusedChild = parent.getFocusedChild();
if (focusedChild == null) {
return false;
}
final int parentLeft = getPaddingLeft();
final int parentTop = getPaddingTop();
final int parentRight = getWidth() - getPaddingRight();
final int parentBottom = getHeight() - getPaddingBottom();
final Rect bounds = mRecyclerView.mTempRect;
getDecoratedBoundsWithMargins(focusedChild, bounds);
if (bounds.left - dx >= parentRight || bounds.right - dx <= parentLeft
|| bounds.top - dy >= parentBottom || bounds.bottom - dy <= parentTop) {
return false;
}
return true;
}
/**
* @deprecated Use {@link #onRequestChildFocus(RecyclerView, State, View, View)}
*/
@Deprecated
public boolean onRequestChildFocus(@NonNull RecyclerView parent, @NonNull View child,
@Nullable View focused) {
// eat the request if we are in the middle of a scroll or layout
return isSmoothScrolling() || parent.isComputingLayout();
}
/**
* Called when a descendant view of the RecyclerView requests focus.
*
* <p>A LayoutManager wishing to keep focused views aligned in a specific
* portion of the view may implement that behavior in an override of this method.</p>
*
* <p>If the LayoutManager executes different behavior that should override the default
* behavior of scrolling the focused child on screen instead of running alongside it,
* this method should return true.</p>
*
* @param parent The RecyclerView hosting this LayoutManager
* @param state Current state of RecyclerView
* @param child Direct child of the RecyclerView containing the newly focused view
* @param focused The newly focused view. This may be the same view as child or it may be
* null
* @return true if the default scroll behavior should be suppressed
*/
public boolean onRequestChildFocus(@NonNull RecyclerView parent, @NonNull State state,
@NonNull View child, @Nullable View focused) {
return onRequestChildFocus(parent, child, focused);
}
/**
* Called if the RecyclerView this LayoutManager is bound to has a different adapter set via
* {@link RecyclerView#setAdapter(Adapter)} or
* {@link RecyclerView#swapAdapter(Adapter, boolean)}. The LayoutManager may use this
* opportunity to clear caches and configure state such that it can relayout appropriately
* with the new data and potentially new view types.
*
* <p>The default implementation removes all currently attached views.</p>
*
* @param oldAdapter The previous adapter instance. Will be null if there was previously no
* adapter.
* @param newAdapter The new adapter instance. Might be null if
* {@link RecyclerView#setAdapter(RecyclerView.Adapter)} is called with
* {@code null}.
*/
public void onAdapterChanged(@Nullable Adapter oldAdapter, @Nullable Adapter newAdapter) {
}
/**
* Called to populate focusable views within the RecyclerView.
*
* <p>The LayoutManager implementation should return <code>true</code> if the default
* behavior of {@link ViewGroup#addFocusables(java.util.ArrayList, int)} should be
* suppressed.</p>
*
* <p>The default implementation returns <code>false</code> to trigger RecyclerView
* to fall back to the default ViewGroup behavior.</p>
*
* @param recyclerView The RecyclerView hosting this LayoutManager
* @param views List of output views. This method should add valid focusable views
* to this list.
* @param direction One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN},
* {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT},
* {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD}
* @param focusableMode The type of focusables to be added.
* @return true to suppress the default behavior, false to add default focusables after
* this method returns.
* @see #FOCUSABLES_ALL
* @see #FOCUSABLES_TOUCH_MODE
*/
public boolean onAddFocusables(@NonNull RecyclerView recyclerView,
@NonNull ArrayList<View> views, int direction, int focusableMode) {
return false;
}
/**
* Called in response to a call to {@link Adapter#notifyDataSetChanged()} or
* {@link RecyclerView#swapAdapter(Adapter, boolean)} ()} and signals that the the entire
* data set has changed.
*/
public void onItemsChanged(@NonNull RecyclerView recyclerView) {
}
/**
* Called when items have been added to the adapter. The LayoutManager may choose to
* requestLayout if the inserted items would require refreshing the currently visible set
* of child views. (e.g. currently empty space would be filled by appended items, etc.)
*/
public void onItemsAdded(@NonNull RecyclerView recyclerView, int positionStart,
int itemCount) {
}
/**
* Called when items have been removed from the adapter.
*/
public void onItemsRemoved(@NonNull RecyclerView recyclerView, int positionStart,
int itemCount) {
}
/**
* Called when items have been changed in the adapter.
* To receive payload, override {@link #onItemsUpdated(RecyclerView, int, int, Object)}
* instead, then this callback will not be invoked.
*/
public void onItemsUpdated(@NonNull RecyclerView recyclerView, int positionStart,
int itemCount) {
}
/**
* Called when items have been changed in the adapter and with optional payload.
* Default implementation calls {@link #onItemsUpdated(RecyclerView, int, int)}.
*/
public void onItemsUpdated(@NonNull RecyclerView recyclerView, int positionStart,
int itemCount, @Nullable Object payload) {
onItemsUpdated(recyclerView, positionStart, itemCount);
}
/**
* Called when an item is moved withing the adapter.
* <p>
* Note that, an item may also change position in response to another ADD/REMOVE/MOVE
* operation. This callback is only called if and only if {@link Adapter#notifyItemMoved}
* is called.
*/
public void onItemsMoved(@NonNull RecyclerView recyclerView, int from, int to,
int itemCount) {
}
/**
* <p>Override this method if you want to support scroll bars.</p>
*
* <p>Read {@link RecyclerView#computeHorizontalScrollExtent()} for details.</p>
*
* <p>Default implementation returns 0.</p>
*
* @param state Current state of RecyclerView
* @return The horizontal extent of the scrollbar's thumb
* @see RecyclerView#computeHorizontalScrollExtent()
*/
public int computeHorizontalScrollExtent(@NonNull State state) {
return 0;
}
/**
* <p>Override this method if you want to support scroll bars.</p>
*
* <p>Read {@link RecyclerView#computeHorizontalScrollOffset()} for details.</p>
*
* <p>Default implementation returns 0.</p>
*
* @param state Current State of RecyclerView where you can find total item count
* @return The horizontal offset of the scrollbar's thumb
* @see RecyclerView#computeHorizontalScrollOffset()
*/
public int computeHorizontalScrollOffset(@NonNull State state) {
return 0;
}
/**
* <p>Override this method if you want to support scroll bars.</p>
*
* <p>Read {@link RecyclerView#computeHorizontalScrollRange()} for details.</p>
*
* <p>Default implementation returns 0.</p>
*
* @param state Current State of RecyclerView where you can find total item count
* @return The total horizontal range represented by the horizontal scrollbar
* @see RecyclerView#computeHorizontalScrollRange()
*/
public int computeHorizontalScrollRange(@NonNull State state) {
return 0;
}
/**
* <p>Override this method if you want to support scroll bars.</p>
*
* <p>Read {@link RecyclerView#computeVerticalScrollExtent()} for details.</p>
*
* <p>Default implementation returns 0.</p>
*
* @param state Current state of RecyclerView
* @return The vertical extent of the scrollbar's thumb
* @see RecyclerView#computeVerticalScrollExtent()
*/
public int computeVerticalScrollExtent(@NonNull State state) {
return 0;
}
/**
* <p>Override this method if you want to support scroll bars.</p>
*
* <p>Read {@link RecyclerView#computeVerticalScrollOffset()} for details.</p>
*
* <p>Default implementation returns 0.</p>
*
* @param state Current State of RecyclerView where you can find total item count
* @return The vertical offset of the scrollbar's thumb
* @see RecyclerView#computeVerticalScrollOffset()
*/
public int computeVerticalScrollOffset(@NonNull State state) {
return 0;
}
/**
* <p>Override this method if you want to support scroll bars.</p>
*
* <p>Read {@link RecyclerView#computeVerticalScrollRange()} for details.</p>
*
* <p>Default implementation returns 0.</p>
*
* @param state Current State of RecyclerView where you can find total item count
* @return The total vertical range represented by the vertical scrollbar
* @see RecyclerView#computeVerticalScrollRange()
*/
public int computeVerticalScrollRange(@NonNull State state) {
return 0;
}
/**
* Measure the attached RecyclerView. Implementations must call
* {@link #setMeasuredDimension(int, int)} before returning.
* <p>
* It is strongly advised to use the AutoMeasure mechanism by overriding
* {@link #isAutoMeasureEnabled()} to return true as AutoMeasure handles all the standard
* measure cases including when the RecyclerView's layout_width or layout_height have been
* set to wrap_content. If {@link #isAutoMeasureEnabled()} is overridden to return true,
* this method should not be overridden.
* <p>
* The default implementation will handle EXACTLY measurements and respect
* the minimum width and height properties of the host RecyclerView if measured
* as UNSPECIFIED. AT_MOST measurements will be treated as EXACTLY and the RecyclerView
* will consume all available space.
*
* @param recycler Recycler
* @param state Transient state of RecyclerView
* @param widthSpec Width {@link android.view.View.MeasureSpec}
* @param heightSpec Height {@link android.view.View.MeasureSpec}
* @see #isAutoMeasureEnabled()
* @see #setMeasuredDimension(int, int)
*/
public void onMeasure(@NonNull Recycler recycler, @NonNull State state, int widthSpec,
int heightSpec) {
mRecyclerView.defaultOnMeasure(widthSpec, heightSpec);
}
/**
* {@link View#setMeasuredDimension(int, int) Set the measured dimensions} of the
* host RecyclerView.
*
* @param widthSize Measured width
* @param heightSize Measured height
*/
public void setMeasuredDimension(int widthSize, int heightSize) {
mRecyclerView.setMeasuredDimension(widthSize, heightSize);
}
/**
* @return The host RecyclerView's {@link View#getMinimumWidth()}
*/
@Px
public int getMinimumWidth() {
return ViewCompat.getMinimumWidth(mRecyclerView);
}
/**
* @return The host RecyclerView's {@link View#getMinimumHeight()}
*/
@Px
public int getMinimumHeight() {
return ViewCompat.getMinimumHeight(mRecyclerView);
}
/**
* <p>Called when the LayoutManager should save its state. This is a good time to save your
* scroll position, configuration and anything else that may be required to restore the same
* layout state if the LayoutManager is recreated.</p>
* <p>RecyclerView does NOT verify if the LayoutManager has changed between state save and
* restore. This will let you share information between your LayoutManagers but it is also
* your responsibility to make sure they use the same parcelable class.</p>
*
* @return Necessary information for LayoutManager to be able to restore its state
*/
@Nullable
public Parcelable onSaveInstanceState() {
return null;
}
/**
* Called when the RecyclerView is ready to restore the state based on a previous
* RecyclerView.
*
* Notice that this might happen after an actual layout, based on how Adapter prefers to
* restore State. See {@link Adapter#getStateRestorationPolicy()} for more information.
*
* @param state The parcelable that was returned by the previous LayoutManager's
* {@link #onSaveInstanceState()} method.
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public void onRestoreInstanceState(Parcelable state) {
}
void stopSmoothScroller() {
if (mSmoothScroller != null) {
mSmoothScroller.stop();
}
}
void onSmoothScrollerStopped(SmoothScroller smoothScroller) {
if (mSmoothScroller == smoothScroller) {
mSmoothScroller = null;
}
}
/**
* RecyclerView calls this method to notify LayoutManager that scroll state has changed.
*
* @param state The new scroll state for RecyclerView
*/
public void onScrollStateChanged(int state) {
}
/**
* Removes all views and recycles them using the given recycler.
* <p>
* If you want to clean cached views as well, you should call {@link Recycler#clear()} too.
* <p>
* If a View is marked as "ignored", it is not removed nor recycled.
*
* @param recycler Recycler to use to recycle children
* @see #removeAndRecycleView(View, Recycler)
* @see #removeAndRecycleViewAt(int, Recycler)
* @see #ignoreView(View)
*/
public void removeAndRecycleAllViews(@NonNull Recycler recycler) {
for (int i = getChildCount() - 1; i >= 0; i--) {
final View view = getChildAt(i);
if (!getChildViewHolderInt(view).shouldIgnore()) {
removeAndRecycleViewAt(i, recycler);
}
}
}
// called by accessibility delegate
void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfoCompat info) {
onInitializeAccessibilityNodeInfo(mRecyclerView.mRecycler, mRecyclerView.mState, info);
}
/**
* Called by the AccessibilityDelegate when the information about the current layout should
* be populated.
* <p>
* Default implementation adds a {@link
* androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionInfoCompat}.
* <p>
* You should override
* {@link #getRowCountForAccessibility(RecyclerView.Recycler, RecyclerView.State)},
* {@link #getColumnCountForAccessibility(RecyclerView.Recycler, RecyclerView.State)},
* {@link #isLayoutHierarchical(RecyclerView.Recycler, RecyclerView.State)} and
* {@link #getSelectionModeForAccessibility(RecyclerView.Recycler, RecyclerView.State)} for
* more accurate accessibility information.
*
* @param recycler The Recycler that can be used to convert view positions into adapter
* positions
* @param state The current state of RecyclerView
* @param info The info that should be filled by the LayoutManager
* @see View#onInitializeAccessibilityNodeInfo(
*android.view.accessibility.AccessibilityNodeInfo)
* @see #getRowCountForAccessibility(RecyclerView.Recycler, RecyclerView.State)
* @see #getColumnCountForAccessibility(RecyclerView.Recycler, RecyclerView.State)
* @see #isLayoutHierarchical(RecyclerView.Recycler, RecyclerView.State)
* @see #getSelectionModeForAccessibility(RecyclerView.Recycler, RecyclerView.State)
*/
public void onInitializeAccessibilityNodeInfo(@NonNull Recycler recycler,
@NonNull State state, @NonNull AccessibilityNodeInfoCompat info) {
if (mRecyclerView.canScrollVertically(-1) || mRecyclerView.canScrollHorizontally(-1)) {
info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);
info.setScrollable(true);
info.setGranularScrollingSupported(true);
}
if (mRecyclerView.canScrollVertically(1) || mRecyclerView.canScrollHorizontally(1)) {
info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);
info.setScrollable(true);
info.setGranularScrollingSupported(true);
}
final AccessibilityNodeInfoCompat.CollectionInfoCompat collectionInfo =
AccessibilityNodeInfoCompat.CollectionInfoCompat
.obtain(getRowCountForAccessibility(recycler, state),
getColumnCountForAccessibility(recycler, state),
isLayoutHierarchical(recycler, state),
getSelectionModeForAccessibility(recycler, state));
info.setCollectionInfo(collectionInfo);
}
// called by accessibility delegate
public void onInitializeAccessibilityEvent(@NonNull AccessibilityEvent event) {
onInitializeAccessibilityEvent(mRecyclerView.mRecycler, mRecyclerView.mState, event);
}
/**
* Called by the accessibility delegate to initialize an accessibility event.
* <p>
* Default implementation adds item count and scroll information to the event.
*
* @param recycler The Recycler that can be used to convert view positions into adapter
* positions
* @param state The current state of RecyclerView
* @param event The event instance to initialize
* @see View#onInitializeAccessibilityEvent(android.view.accessibility.AccessibilityEvent)
*/
public void onInitializeAccessibilityEvent(@NonNull Recycler recycler, @NonNull State state,
@NonNull AccessibilityEvent event) {
if (mRecyclerView == null || event == null) {
return;
}
event.setScrollable(mRecyclerView.canScrollVertically(1)
|| mRecyclerView.canScrollVertically(-1)
|| mRecyclerView.canScrollHorizontally(-1)
|| mRecyclerView.canScrollHorizontally(1));
if (mRecyclerView.mAdapter != null) {
event.setItemCount(mRecyclerView.mAdapter.getItemCount());
}
}
// called by accessibility delegate
void onInitializeAccessibilityNodeInfoForItem(View host, AccessibilityNodeInfoCompat info) {
final ViewHolder vh = getChildViewHolderInt(host);
// avoid trying to create accessibility node info for removed children
if (vh != null && !vh.isRemoved() && !mChildHelper.isHidden(vh.itemView)) {
onInitializeAccessibilityNodeInfoForItem(mRecyclerView.mRecycler,
mRecyclerView.mState, host, info);
}
}
/**
* Called by the AccessibilityDelegate when the accessibility information for a specific
* item should be populated.
* <p>
* Default implementation adds basic positioning information about the item.
*
* @param recycler The Recycler that can be used to convert view positions into adapter
* positions
* @param state The current state of RecyclerView
* @param host The child for which accessibility node info should be populated
* @param info The info to fill out about the item
* @see android.widget.AbsListView#onInitializeAccessibilityNodeInfoForItem(View, int,
* android.view.accessibility.AccessibilityNodeInfo)
*/
public void onInitializeAccessibilityNodeInfoForItem(@NonNull Recycler recycler,
@NonNull State state, @NonNull View host,
@NonNull AccessibilityNodeInfoCompat info) {
int rowIndexGuess = canScrollVertically() ? getPosition(host) : 0;
int columnIndexGuess = canScrollHorizontally() ? getPosition(host) : 0;
final AccessibilityNodeInfoCompat.CollectionItemInfoCompat itemInfo =
AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(rowIndexGuess, 1,
columnIndexGuess, 1, false, false);
info.setCollectionItemInfo(itemInfo);
}
/**
* A LayoutManager can call this method to force RecyclerView to run simple animations in
* the next layout pass, even if there is not any trigger to do so. (e.g. adapter data
* change).
* <p>
* Note that, calling this method will not guarantee that RecyclerView will run animations
* at all. For example, if there is not any {@link ItemAnimator} set, RecyclerView will
* not run any animations but will still clear this flag after the layout is complete.
*/
public void requestSimpleAnimationsInNextLayout() {
mRequestedSimpleAnimations = true;
}
/**
* Returns the selection mode for accessibility. Should be
* {@link AccessibilityNodeInfoCompat.CollectionInfoCompat#SELECTION_MODE_NONE},
* {@link AccessibilityNodeInfoCompat.CollectionInfoCompat#SELECTION_MODE_SINGLE} or
* {@link AccessibilityNodeInfoCompat.CollectionInfoCompat#SELECTION_MODE_MULTIPLE}.
* <p>
* Default implementation returns
* {@link AccessibilityNodeInfoCompat.CollectionInfoCompat#SELECTION_MODE_NONE}.
*
* @param recycler The Recycler that can be used to convert view positions into adapter
* positions
* @param state The current state of RecyclerView
* @return Selection mode for accessibility. Default implementation returns
* {@link AccessibilityNodeInfoCompat.CollectionInfoCompat#SELECTION_MODE_NONE}.
*/
public int getSelectionModeForAccessibility(@NonNull Recycler recycler,
@NonNull State state) {
return AccessibilityNodeInfoCompat.CollectionInfoCompat.SELECTION_MODE_NONE;
}
/**
* Returns the number of rows for accessibility.
* <p>
* Default implementation returns the number of items in the adapter if LayoutManager
* supports vertical scrolling or 1 if LayoutManager does not support vertical
* scrolling.
*
* @param recycler The Recycler that can be used to convert view positions into adapter
* positions
* @param state The current state of RecyclerView
* @return The number of rows in LayoutManager for accessibility.
*/
public int getRowCountForAccessibility(@NonNull Recycler recycler, @NonNull State state) {
if (mRecyclerView == null || mRecyclerView.mAdapter == null) {
return 1;
}
return canScrollVertically() ? mRecyclerView.mAdapter.getItemCount() : 1;
}
/**
* Returns the number of columns for accessibility.
* <p>
* Default implementation returns the number of items in the adapter if LayoutManager
* supports horizontal scrolling or 1 if LayoutManager does not support horizontal
* scrolling.
*
* @param recycler The Recycler that can be used to convert view positions into adapter
* positions
* @param state The current state of RecyclerView
* @return The number of rows in LayoutManager for accessibility.
*/
public int getColumnCountForAccessibility(@NonNull Recycler recycler,
@NonNull State state) {
if (mRecyclerView == null || mRecyclerView.mAdapter == null) {
return 1;
}
return canScrollHorizontally() ? mRecyclerView.mAdapter.getItemCount() : 1;
}
/**
* Returns whether layout is hierarchical or not to be used for accessibility.
* <p>
* Default implementation returns false.
*
* @param recycler The Recycler that can be used to convert view positions into adapter
* positions
* @param state The current state of RecyclerView
* @return True if layout is hierarchical.
*/
public boolean isLayoutHierarchical(@NonNull Recycler recycler, @NonNull State state) {
return false;
}
// called by accessibility delegate
boolean performAccessibilityAction(int action, @Nullable Bundle args) {
return performAccessibilityAction(mRecyclerView.mRecycler, mRecyclerView.mState,
action, args);
}
/**
* Called by AccessibilityDelegate when an action is requested from the RecyclerView.
*
* @param recycler The Recycler that can be used to convert view positions into adapter
* positions
* @param state The current state of RecyclerView
* @param action The action to perform
* @param args Optional action arguments
* @see View#performAccessibilityAction(int, android.os.Bundle)
*/
public boolean performAccessibilityAction(@NonNull Recycler recycler, @NonNull State state,
int action, @Nullable Bundle args) {
if (mRecyclerView == null) {
return false;
}
int vScroll = 0, hScroll = 0;
int height = getHeight();
int width = getWidth();
Rect rect = new Rect();
// Gets the visible rect on the screen except for the rotation or scale cases which
// might affect the result.
if (mRecyclerView.getMatrix().isIdentity() && mRecyclerView.getGlobalVisibleRect(
rect)) {
height = rect.height();
width = rect.width();
}
switch (action) {
case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD:
if (mRecyclerView.canScrollVertically(-1)) {
vScroll = -(height - getPaddingTop() - getPaddingBottom());
}
if (mRecyclerView.canScrollHorizontally(-1)) {
hScroll = -(width - getPaddingLeft() - getPaddingRight());
}
break;
case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD:
if (mRecyclerView.canScrollVertically(1)) {
vScroll = height - getPaddingTop() - getPaddingBottom();
}
if (mRecyclerView.canScrollHorizontally(1)) {
hScroll = width - getPaddingLeft() - getPaddingRight();
}
break;
}
if (vScroll == 0 && hScroll == 0) {
return false;
}
float granularScrollAmount = 1F; // The default value.
if (args != null) {
granularScrollAmount = args.getFloat(
AccessibilityNodeInfoCompat.ACTION_ARGUMENT_SCROLL_AMOUNT_FLOAT, 1F);
if (granularScrollAmount < 0) {
if (sDebugAssertionsEnabled) {
throw new IllegalArgumentException(
"attempting to use ACTION_ARGUMENT_SCROLL_AMOUNT_FLOAT with a "
+ "negative value (" + granularScrollAmount + ")");
}
return false;
}
}
if (Float.compare(granularScrollAmount, Float.POSITIVE_INFINITY) == 0) {
// Assume that the client wants to scroll as far as possible. For
// ACTION_SCROLL_BACKWARD, this means scrolling to the beginning of the collection.
// For ACTION_SCROLL_FORWARD, this means scrolling to the end of the collection.
if (mRecyclerView.mAdapter == null) {
return false;
}
switch (action) {
case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD:
mRecyclerView.smoothScrollToPosition(0);
break;
case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD:
mRecyclerView.smoothScrollToPosition(
mRecyclerView.mAdapter.getItemCount() - 1);
break;
}
return true;
}
// No adjustments needed to scroll values if granular scroll amount is 1F, which is
// the default, or 0F, which is undefined.
if (Float.compare(1F, granularScrollAmount) != 0 && Float.compare(0F,
granularScrollAmount) != 0) {
hScroll = (int) (hScroll * granularScrollAmount);
vScroll = (int) (vScroll * granularScrollAmount);
}
mRecyclerView.smoothScrollBy(hScroll, vScroll, null, UNDEFINED_DURATION, true);
return true;
}
// called by accessibility delegate
boolean performAccessibilityActionForItem(@NonNull View view, int action,
@Nullable Bundle args) {
return performAccessibilityActionForItem(mRecyclerView.mRecycler, mRecyclerView.mState,
view, action, args);
}
/**
* Called by AccessibilityDelegate when an accessibility action is requested on one of the
* children of LayoutManager.
* <p>
* Default implementation does not do anything.
*
* @param recycler The Recycler that can be used to convert view positions into adapter
* positions
* @param state The current state of RecyclerView
* @param view The child view on which the action is performed
* @param action The action to perform
* @param args Optional action arguments
* @return true if action is handled
* @see View#performAccessibilityAction(int, android.os.Bundle)
*/
public boolean performAccessibilityActionForItem(@NonNull Recycler recycler,
@NonNull State state, @NonNull View view, int action, @Nullable Bundle args) {
return false;
}
/**
* Parse the xml attributes to get the most common properties used by layout managers.
*
* {@link android.R.attr#orientation}
* {@link androidx.recyclerview.R.attr#spanCount}
* {@link androidx.recyclerview.R.attr#reverseLayout}
* {@link androidx.recyclerview.R.attr#stackFromEnd}
*
* @return an object containing the properties as specified in the attrs.
*/
public static Properties getProperties(@NonNull Context context,
@Nullable AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
Properties properties = new Properties();
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RecyclerView,
defStyleAttr, defStyleRes);
properties.orientation = a.getInt(R.styleable.RecyclerView_android_orientation,
DEFAULT_ORIENTATION);
properties.spanCount = a.getInt(R.styleable.RecyclerView_spanCount, 1);
properties.reverseLayout = a.getBoolean(R.styleable.RecyclerView_reverseLayout, false);
properties.stackFromEnd = a.getBoolean(R.styleable.RecyclerView_stackFromEnd, false);
a.recycle();
return properties;
}
void setExactMeasureSpecsFrom(RecyclerView recyclerView) {
setMeasureSpecs(
MeasureSpec.makeMeasureSpec(recyclerView.getWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(recyclerView.getHeight(), MeasureSpec.EXACTLY)
);
}
/**
* Internal API to allow LayoutManagers to be measured twice.
* <p>
* This is not public because LayoutManagers should be able to handle their layouts in one
* pass but it is very convenient to make existing LayoutManagers support wrapping content
* when both orientations are undefined.
* <p>
* This API will be removed after default LayoutManagers properly implement wrap content in
* non-scroll orientation.
*/
boolean shouldMeasureTwice() {
return false;
}
boolean hasFlexibleChildInBothOrientations() {
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final ViewGroup.LayoutParams lp = child.getLayoutParams();
if (lp.width < 0 && lp.height < 0) {
return true;
}
}
return false;
}
/**
* Some general properties that a LayoutManager may want to use.
*/
public static class Properties {
/** {@link android.R.attr#orientation} */
public int orientation;
/** {@link androidx.recyclerview.R.attr#spanCount} */
public int spanCount;
/** {@link androidx.recyclerview.R.attr#reverseLayout} */
public boolean reverseLayout;
/** {@link androidx.recyclerview.R.attr#stackFromEnd} */
public boolean stackFromEnd;
}
}
/**
* An ItemDecoration allows the application to add a special drawing and layout offset
* to specific item views from the adapter's data set. This can be useful for drawing dividers
* between items, highlights, visual grouping boundaries and more.
*
* <p>All ItemDecorations are drawn in the order they were added, before the item
* views (in {@link ItemDecoration#onDraw(Canvas, RecyclerView, RecyclerView.State) onDraw()}
* and after the items (in {@link ItemDecoration#onDrawOver(Canvas, RecyclerView,
* RecyclerView.State)}.</p>
*/
public abstract static class ItemDecoration {
/**
* Draw any appropriate decorations into the Canvas supplied to the RecyclerView.
* Any content drawn by this method will be drawn before the item views are drawn,
* and will thus appear underneath the views.
*
* @param c Canvas to draw into
* @param parent RecyclerView this ItemDecoration is drawing into
* @param state The current state of RecyclerView
*/
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull State state) {
onDraw(c, parent);
}
/**
* @deprecated Override {@link #onDraw(Canvas, RecyclerView, RecyclerView.State)}
*/
@Deprecated
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent) {
}
/**
* Draw any appropriate decorations into the Canvas supplied to the RecyclerView.
* Any content drawn by this method will be drawn after the item views are drawn
* and will thus appear over the views.
*
* @param c Canvas to draw into
* @param parent RecyclerView this ItemDecoration is drawing into
* @param state The current state of RecyclerView.
*/
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent,
@NonNull State state) {
onDrawOver(c, parent);
}
/**
* @deprecated Override {@link #onDrawOver(Canvas, RecyclerView, RecyclerView.State)}
*/
@Deprecated
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent) {
}
/**
* @deprecated Use {@link #getItemOffsets(Rect, View, RecyclerView, State)}
*/
@Deprecated
public void getItemOffsets(@NonNull Rect outRect, int itemPosition,
@NonNull RecyclerView parent) {
outRect.set(0, 0, 0, 0);
}
/**
* Retrieve any offsets for the given item. Each field of <code>outRect</code> specifies
* the number of pixels that the item view should be inset by, similar to padding or margin.
* The default implementation sets the bounds of outRect to 0 and returns.
*
* <p>
* If this ItemDecoration does not affect the positioning of item views, it should set
* all four fields of <code>outRect</code> (left, top, right, bottom) to zero
* before returning.
*
* <p>
* If you need to access Adapter for additional data, you can call
* {@link RecyclerView#getChildAdapterPosition(View)} to get the adapter position of the
* View.
*
* @param outRect Rect to receive the output.
* @param view The child view to decorate
* @param parent RecyclerView this ItemDecoration is decorating
* @param state The current state of RecyclerView.
*/
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
@NonNull RecyclerView parent, @NonNull State state) {
getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),
parent);
}
}
/**
* An OnItemTouchListener allows the application to intercept touch events in progress at the
* view hierarchy level of the RecyclerView before those touch events are considered for
* RecyclerView's own scrolling behavior.
*
* <p>This can be useful for applications that wish to implement various forms of gestural
* manipulation of item views within the RecyclerView. OnItemTouchListeners may intercept
* a touch interaction already in progress even if the RecyclerView is already handling that
* gesture stream itself for the purposes of scrolling.</p>
*
* @see SimpleOnItemTouchListener
*/
public interface OnItemTouchListener {
/**
* Silently observe and/or take over touch events sent to the RecyclerView
* before they are handled by either the RecyclerView itself or its child views.
*
* <p>The onInterceptTouchEvent methods of each attached OnItemTouchListener will be run
* in the order in which each listener was added, before any other touch processing
* by the RecyclerView itself or child views occurs.</p>
*
* @param rv The RecyclerView whose scroll state has changed.
* @param e MotionEvent describing the touch event. All coordinates are in
* the RecyclerView's coordinate system.
* @return true if this OnItemTouchListener wishes to begin intercepting touch events, false
* to continue with the current behavior and continue observing future events in
* the gesture.
*/
boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e);
/**
* Process a touch event as part of a gesture that was claimed by returning true from
* a previous call to {@link #onInterceptTouchEvent}.
*
* @param rv The RecyclerView whose scroll state has changed.
* @param e MotionEvent describing the touch event. All coordinates are in
* the RecyclerView's coordinate system.
*/
void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e);
/**
* Called when a child of RecyclerView does not want RecyclerView and its ancestors to
* intercept touch events with
* {@link ViewGroup#onInterceptTouchEvent(MotionEvent)}.
*
* @param disallowIntercept True if the child does not want the parent to
* intercept touch events.
* @see ViewParent#requestDisallowInterceptTouchEvent(boolean)
*/
void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept);
}
/**
* An implementation of {@link RecyclerView.OnItemTouchListener} that has empty method bodies
* and default return values.
* <p>
* You may prefer to extend this class if you don't need to override all methods. Another
* benefit of using this class is future compatibility. As the interface may change, we'll
* always provide a default implementation on this class so that your code won't break when
* you update to a new version of the support library.
*/
public static class SimpleOnItemTouchListener implements RecyclerView.OnItemTouchListener {
/** {@inheritDoc} */
@Override
public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
return false;
}
/** {@inheritDoc} */
@Override
public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
}
/** {@inheritDoc} */
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
}
}
/**
* An OnScrollListener can be added to a RecyclerView to receive messages when a scrolling event
* has occurred on that RecyclerView.
* <p>
*
* @see RecyclerView#addOnScrollListener(OnScrollListener)
* @see RecyclerView#clearOnChildAttachStateChangeListeners()
*/
public abstract static class OnScrollListener {
/**
* Callback method to be invoked when RecyclerView's scroll state changes.
*
* @param recyclerView The RecyclerView whose scroll state has changed.
* @param newState The updated scroll state. One of {@link #SCROLL_STATE_IDLE},
* {@link #SCROLL_STATE_DRAGGING} or {@link #SCROLL_STATE_SETTLING}.
*/
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
}
/**
* Callback method to be invoked when the RecyclerView has been scrolled. This will be
* called after the scroll has completed.
* <p>
* This callback will also be called if visible item range changes after a layout
* calculation. In that case, dx and dy will be 0.
*
* @param recyclerView The RecyclerView which scrolled.
* @param dx The amount of horizontal scroll.
* @param dy The amount of vertical scroll.
*/
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
}
}
/**
* A RecyclerListener can be set on a RecyclerView to receive messages whenever
* a view is recycled.
*
* @see RecyclerView#setRecyclerListener(RecyclerListener)
*/
public interface RecyclerListener {
/**
* This method is called whenever the view in the ViewHolder is recycled.
*
* RecyclerView calls this method right before clearing ViewHolder's internal data and
* sending it to RecycledViewPool. This way, if ViewHolder was holding valid information
* before being recycled, you can call {@link ViewHolder#getBindingAdapterPosition()} to get
* its adapter position.
*
* @param holder The ViewHolder containing the view that was recycled
*/
void onViewRecycled(@NonNull ViewHolder holder);
}
/**
* A Listener interface that can be attached to a RecylcerView to get notified
* whenever a ViewHolder is attached to or detached from RecyclerView.
*/
public interface OnChildAttachStateChangeListener {
/**
* Called when a view is attached to the RecyclerView.
*
* @param view The View which is attached to the RecyclerView
*/
void onChildViewAttachedToWindow(@NonNull View view);
/**
* Called when a view is detached from RecyclerView.
*
* @param view The View which is being detached from the RecyclerView
*/
void onChildViewDetachedFromWindow(@NonNull View view);
}
/**
* A ViewHolder describes an item view and metadata about its place within the RecyclerView.
*
* <p>{@link Adapter} implementations should subclass ViewHolder and add fields for caching
* potentially expensive {@link View#findViewById(int)} results.</p>
*
* <p>While {@link LayoutParams} belong to the {@link LayoutManager},
* {@link ViewHolder ViewHolders} belong to the adapter. Adapters should feel free to use
* their own custom ViewHolder implementations to store data that makes binding view contents
* easier. Implementations should assume that individual item views will hold strong references
* to <code>ViewHolder</code> objects and that <code>RecyclerView</code> instances may hold
* strong references to extra off-screen item views for caching purposes</p>
*/
public abstract static class ViewHolder {
@NonNull
public final View itemView;
WeakReference<RecyclerView> mNestedRecyclerView;
int mPosition = NO_POSITION;
int mOldPosition = NO_POSITION;
long mItemId = NO_ID;
int mItemViewType = INVALID_TYPE;
int mPreLayoutPosition = NO_POSITION;
// The item that this holder is shadowing during an item change event/animation
ViewHolder mShadowedHolder = null;
// The item that is shadowing this holder during an item change event/animation
ViewHolder mShadowingHolder = null;
/**
* This ViewHolder has been bound to a position; mPosition, mItemId and mItemViewType
* are all valid.
*/
static final int FLAG_BOUND = 1 << 0;
/**
* The data this ViewHolder's view reflects is stale and needs to be rebound
* by the adapter. mPosition and mItemId are consistent.
*/
static final int FLAG_UPDATE = 1 << 1;
/**
* This ViewHolder's data is invalid. The identity implied by mPosition and mItemId
* are not to be trusted and may no longer match the item view type.
* This ViewHolder must be fully rebound to different data.
*/
static final int FLAG_INVALID = 1 << 2;
/**
* This ViewHolder points at data that represents an item previously removed from the
* data set. Its view may still be used for things like outgoing animations.
*/
static final int FLAG_REMOVED = 1 << 3;
/**
* This ViewHolder should not be recycled. This flag is set via setIsRecyclable()
* and is intended to keep views around during animations.
*/
static final int FLAG_NOT_RECYCLABLE = 1 << 4;
/**
* This ViewHolder is returned from scrap which means we are expecting an addView call
* for this itemView. When returned from scrap, ViewHolder stays in the scrap list until
* the end of the layout pass and then recycled by RecyclerView if it is not added back to
* the RecyclerView.
*/
static final int FLAG_RETURNED_FROM_SCRAP = 1 << 5;
/**
* This ViewHolder is fully managed by the LayoutManager. We do not scrap, recycle or remove
* it unless LayoutManager is replaced.
* It is still fully visible to the LayoutManager.
*/
static final int FLAG_IGNORE = 1 << 7;
/**
* When the View is detached form the parent, we set this flag so that we can take correct
* action when we need to remove it or add it back.
*/
static final int FLAG_TMP_DETACHED = 1 << 8;
/**
* Set when we can no longer determine the adapter position of this ViewHolder until it is
* rebound to a new position. It is different than FLAG_INVALID because FLAG_INVALID is
* set even when the type does not match. Also, FLAG_ADAPTER_POSITION_UNKNOWN is set as soon
* as adapter notification arrives vs FLAG_INVALID is set lazily before layout is
* re-calculated.
*/
static final int FLAG_ADAPTER_POSITION_UNKNOWN = 1 << 9;
/**
* Set when a addChangePayload(null) is called
*/
static final int FLAG_ADAPTER_FULLUPDATE = 1 << 10;
/**
* Used by ItemAnimator when a ViewHolder's position changes
*/
static final int FLAG_MOVED = 1 << 11;
/**
* Used by ItemAnimator when a ViewHolder appears in pre-layout
*/
static final int FLAG_APPEARED_IN_PRE_LAYOUT = 1 << 12;
static final int PENDING_ACCESSIBILITY_STATE_NOT_SET = -1;
/**
* Used when a ViewHolder starts the layout pass as a hidden ViewHolder but is re-used from
* hidden list (as if it was scrap) without being recycled in between.
*
* When a ViewHolder is hidden, there are 2 paths it can be re-used:
* a) Animation ends, view is recycled and used from the recycle pool.
* b) LayoutManager asks for the View for that position while the ViewHolder is hidden.
*
* This flag is used to represent "case b" where the ViewHolder is reused without being
* recycled (thus "bounced" from the hidden list). This state requires special handling
* because the ViewHolder must be added to pre layout maps for animations as if it was
* already there.
*/
static final int FLAG_BOUNCED_FROM_HIDDEN_LIST = 1 << 13;
int mFlags;
private static final List<Object> FULLUPDATE_PAYLOADS = Collections.emptyList();
List<Object> mPayloads = null;
List<Object> mUnmodifiedPayloads = null;
private int mIsRecyclableCount = 0;
// If non-null, view is currently considered scrap and may be reused for other data by the
// scrap container.
Recycler mScrapContainer = null;
// Keeps whether this ViewHolder lives in Change scrap or Attached scrap
boolean mInChangeScrap = false;
// Saves isImportantForAccessibility value for the view item while it's in hidden state and
// marked as unimportant for accessibility.
private int mWasImportantForAccessibilityBeforeHidden =
View.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
// set if we defer the accessibility state change of the view holder
@VisibleForTesting
int mPendingAccessibilityState = PENDING_ACCESSIBILITY_STATE_NOT_SET;
/**
* Is set when VH is bound from the adapter and cleaned right before it is sent to
* {@link RecycledViewPool}.
*/
RecyclerView mOwnerRecyclerView;
// The last adapter that bound this ViewHolder. It is cleaned before VH is recycled.
Adapter<? extends ViewHolder> mBindingAdapter;
public ViewHolder(@NonNull View itemView) {
if (itemView == null) {
throw new IllegalArgumentException("itemView may not be null");
}
this.itemView = itemView;
}
void flagRemovedAndOffsetPosition(int newPosition, int offset, boolean applyToPreLayout) {
addFlags(ViewHolder.FLAG_REMOVED);
offsetPosition(offset, applyToPreLayout);
mPosition = newPosition;
}
void offsetPosition(int offset, boolean applyToPreLayout) {
if (mOldPosition == NO_POSITION) {
mOldPosition = mPosition;
}
if (mPreLayoutPosition == NO_POSITION) {
mPreLayoutPosition = mPosition;
}
if (applyToPreLayout) {
mPreLayoutPosition += offset;
}
mPosition += offset;
if (itemView.getLayoutParams() != null) {
((LayoutParams) itemView.getLayoutParams()).mInsetsDirty = true;
}
}
void clearOldPosition() {
mOldPosition = NO_POSITION;
mPreLayoutPosition = NO_POSITION;
}
void saveOldPosition() {
if (mOldPosition == NO_POSITION) {
mOldPosition = mPosition;
}
}
boolean shouldIgnore() {
return (mFlags & FLAG_IGNORE) != 0;
}
/**
* @see #getLayoutPosition()
* @see #getBindingAdapterPosition()
* @see #getAbsoluteAdapterPosition()
* @deprecated This method is deprecated because its meaning is ambiguous due to the async
* handling of adapter updates. You should use {@link #getLayoutPosition()},
* {@link #getBindingAdapterPosition()} or {@link #getAbsoluteAdapterPosition()}
* depending on your use case.
*/
@Deprecated
public final int getPosition() {
return mPreLayoutPosition == NO_POSITION ? mPosition : mPreLayoutPosition;
}
/**
* Returns the position of the ViewHolder in terms of the latest layout pass.
* <p>
* This position is mostly used by RecyclerView components to be consistent while
* RecyclerView lazily processes adapter updates.
* <p>
* For performance and animation reasons, RecyclerView batches all adapter updates until the
* next layout pass. This may cause mismatches between the Adapter position of the item and
* the position it had in the latest layout calculations.
* <p>
* LayoutManagers should always call this method while doing calculations based on item
* positions. All methods in {@link RecyclerView.LayoutManager}, {@link RecyclerView.State},
* {@link RecyclerView.Recycler} that receive a position expect it to be the layout position
* of the item.
* <p>
* If LayoutManager needs to call an external method that requires the adapter position of
* the item, it can use {@link #getAbsoluteAdapterPosition()} or
* {@link RecyclerView.Recycler#convertPreLayoutPositionToPostLayout(int)}.
*
* @return Returns the adapter position of the ViewHolder in the latest layout pass.
* @see #getBindingAdapterPosition()
* @see #getAbsoluteAdapterPosition()
*/
public final int getLayoutPosition() {
return mPreLayoutPosition == NO_POSITION ? mPosition : mPreLayoutPosition;
}
/**
* @return {@link #getBindingAdapterPosition()}
* @deprecated This method is confusing when adapters nest other adapters.
* If you are calling this in the context of an Adapter, you probably want to call
* {@link #getBindingAdapterPosition()} or if you want the position as {@link RecyclerView}
* sees it, you should call {@link #getAbsoluteAdapterPosition()}.
*/
@Deprecated
public final int getAdapterPosition() {
return getBindingAdapterPosition();
}
/**
* Returns the Adapter position of the item represented by this ViewHolder with respect to
* the {@link Adapter} that bound it.
* <p>
* Note that this might be different than the {@link #getLayoutPosition()} if there are
* pending adapter updates but a new layout pass has not happened yet.
* <p>
* RecyclerView does not handle any adapter updates until the next layout traversal. This
* may create temporary inconsistencies between what user sees on the screen and what
* adapter contents have. This inconsistency is not important since it will be less than
* 16ms but it might be a problem if you want to use ViewHolder position to access the
* adapter. Sometimes, you may need to get the exact adapter position to do
* some actions in response to user events. In that case, you should use this method which
* will calculate the Adapter position of the ViewHolder.
* <p>
* Note that if you've called {@link RecyclerView.Adapter#notifyDataSetChanged()}, until the
* next layout pass, the return value of this method will be {@link #NO_POSITION}.
* <p>
* If the {@link Adapter} that bound this {@link ViewHolder} is inside another
* {@link Adapter} (e.g. {@link ConcatAdapter}), this position might be different than
* {@link #getAbsoluteAdapterPosition()}. If you would like to know the position that
* {@link RecyclerView} considers (e.g. for saved state), you should use
* {@link #getAbsoluteAdapterPosition()}.
*
* @return The adapter position of the item if it still exists in the adapter.
* {@link RecyclerView#NO_POSITION} if item has been removed from the adapter,
* {@link RecyclerView.Adapter#notifyDataSetChanged()} has been called after the last
* layout pass or the ViewHolder has already been recycled.
* @see #getAbsoluteAdapterPosition()
* @see #getLayoutPosition()
*/
public final int getBindingAdapterPosition() {
if (mBindingAdapter == null) {
return NO_POSITION;
}
if (mOwnerRecyclerView == null) {
return NO_POSITION;
}
@SuppressWarnings("unchecked")
Adapter<? extends ViewHolder> rvAdapter = mOwnerRecyclerView.getAdapter();
if (rvAdapter == null) {
return NO_POSITION;
}
int globalPosition = mOwnerRecyclerView.getAdapterPositionInRecyclerView(this);
if (globalPosition == NO_POSITION) {
return NO_POSITION;
}
return rvAdapter.findRelativeAdapterPositionIn(mBindingAdapter, this, globalPosition);
}
/**
* Returns the Adapter position of the item represented by this ViewHolder with respect to
* the {@link RecyclerView}'s {@link Adapter}. If the {@link Adapter} that bound this
* {@link ViewHolder} is inside another adapter (e.g. {@link ConcatAdapter}), this
* position might be different and will include
* the offsets caused by other adapters in the {@link ConcatAdapter}.
* <p>
* Note that this might be different than the {@link #getLayoutPosition()} if there are
* pending adapter updates but a new layout pass has not happened yet.
* <p>
* RecyclerView does not handle any adapter updates until the next layout traversal. This
* may create temporary inconsistencies between what user sees on the screen and what
* adapter contents have. This inconsistency is not important since it will be less than
* 16ms but it might be a problem if you want to use ViewHolder position to access the
* adapter. Sometimes, you may need to get the exact adapter position to do
* some actions in response to user events. In that case, you should use this method which
* will calculate the Adapter position of the ViewHolder.
* <p>
* Note that if you've called {@link RecyclerView.Adapter#notifyDataSetChanged()}, until the
* next layout pass, the return value of this method will be {@link #NO_POSITION}.
* <p>
* Note that if you are querying the position as {@link RecyclerView} sees, you should use
* {@link #getAbsoluteAdapterPosition()} (e.g. you want to use it to save scroll
* state). If you are querying the position to access the {@link Adapter} contents,
* you should use {@link #getBindingAdapterPosition()}.
*
* @return The adapter position of the item from {@link RecyclerView}'s perspective if it
* still exists in the adapter and bound to a valid item.
* {@link RecyclerView#NO_POSITION} if item has been removed from the adapter,
* {@link RecyclerView.Adapter#notifyDataSetChanged()} has been called after the last
* layout pass or the ViewHolder has already been recycled.
* @see #getBindingAdapterPosition()
* @see #getLayoutPosition()
*/
public final int getAbsoluteAdapterPosition() {
if (mOwnerRecyclerView == null) {
return NO_POSITION;
}
return mOwnerRecyclerView.getAdapterPositionInRecyclerView(this);
}
/**
* Returns the {@link Adapter} that last bound this {@link ViewHolder}.
* Might return {@code null} if this {@link ViewHolder} is not bound to any adapter.
*
* @return The {@link Adapter} that last bound this {@link ViewHolder} or {@code null} if
* this {@link ViewHolder} is not bound by any adapter (e.g. recycled).
*/
@Nullable
public final Adapter<? extends ViewHolder> getBindingAdapter() {
return mBindingAdapter;
}
/**
* When LayoutManager supports animations, RecyclerView tracks 3 positions for ViewHolders
* to perform animations.
* <p>
* If a ViewHolder was laid out in the previous onLayout call, old position will keep its
* adapter index in the previous layout.
*
* @return The previous adapter index of the Item represented by this ViewHolder or
* {@link #NO_POSITION} if old position does not exists or cleared (pre-layout is
* complete).
*/
public final int getOldPosition() {
return mOldPosition;
}
/**
* Returns The itemId represented by this ViewHolder.
*
* @return The item's id if adapter has stable ids, {@link RecyclerView#NO_ID}
* otherwise
*/
public final long getItemId() {
return mItemId;
}
/**
* @return The view type of this ViewHolder.
*/
public final int getItemViewType() {
return mItemViewType;
}
boolean isScrap() {
return mScrapContainer != null;
}
void unScrap() {
mScrapContainer.unscrapView(this);
}
boolean wasReturnedFromScrap() {
return (mFlags & FLAG_RETURNED_FROM_SCRAP) != 0;
}
void clearReturnedFromScrapFlag() {
mFlags = mFlags & ~FLAG_RETURNED_FROM_SCRAP;
}
void clearTmpDetachFlag() {
mFlags = mFlags & ~FLAG_TMP_DETACHED;
}
void stopIgnoring() {
mFlags = mFlags & ~FLAG_IGNORE;
}
void setScrapContainer(Recycler recycler, boolean isChangeScrap) {
mScrapContainer = recycler;
mInChangeScrap = isChangeScrap;
}
boolean isInvalid() {
return (mFlags & FLAG_INVALID) != 0;
}
boolean needsUpdate() {
return (mFlags & FLAG_UPDATE) != 0;
}
boolean isBound() {
return (mFlags & FLAG_BOUND) != 0;
}
boolean isRemoved() {
return (mFlags & FLAG_REMOVED) != 0;
}
boolean hasAnyOfTheFlags(int flags) {
return (mFlags & flags) != 0;
}
boolean isTmpDetached() {
return (mFlags & FLAG_TMP_DETACHED) != 0;
}
boolean isAttachedToTransitionOverlay() {
return itemView.getParent() != null && itemView.getParent() != mOwnerRecyclerView;
}
boolean isAdapterPositionUnknown() {
return (mFlags & FLAG_ADAPTER_POSITION_UNKNOWN) != 0 || isInvalid();
}
void setFlags(int flags, int mask) {
mFlags = (mFlags & ~mask) | (flags & mask);
}
void addFlags(int flags) {
mFlags |= flags;
}
void addChangePayload(Object payload) {
if (payload == null) {
addFlags(FLAG_ADAPTER_FULLUPDATE);
} else if ((mFlags & FLAG_ADAPTER_FULLUPDATE) == 0) {
createPayloadsIfNeeded();
mPayloads.add(payload);
}
}
private void createPayloadsIfNeeded() {
if (mPayloads == null) {
mPayloads = new ArrayList<Object>();
mUnmodifiedPayloads = Collections.unmodifiableList(mPayloads);
}
}
void clearPayload() {
if (mPayloads != null) {
mPayloads.clear();
}
mFlags = mFlags & ~FLAG_ADAPTER_FULLUPDATE;
}
List<Object> getUnmodifiedPayloads() {
if ((mFlags & FLAG_ADAPTER_FULLUPDATE) == 0) {
if (mPayloads == null || mPayloads.size() == 0) {
// Initial state, no update being called.
return FULLUPDATE_PAYLOADS;
}
// there are none-null payloads
return mUnmodifiedPayloads;
} else {
// a full update has been called.
return FULLUPDATE_PAYLOADS;
}
}
void resetInternal() {
if (sDebugAssertionsEnabled && isTmpDetached()) {
throw new IllegalStateException("Attempting to reset temp-detached ViewHolder: "
+ this + ". ViewHolders should be fully detached before resetting.");
}
mFlags = 0;
mPosition = NO_POSITION;
mOldPosition = NO_POSITION;
mItemId = NO_ID;
mPreLayoutPosition = NO_POSITION;
mIsRecyclableCount = 0;
mShadowedHolder = null;
mShadowingHolder = null;
clearPayload();
mWasImportantForAccessibilityBeforeHidden = View.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
mPendingAccessibilityState = PENDING_ACCESSIBILITY_STATE_NOT_SET;
clearNestedRecyclerViewIfNotNested(this);
}
/**
* Called when the child view enters the hidden state
*/
void onEnteredHiddenState(RecyclerView parent) {
// While the view item is in hidden state, make it invisible for the accessibility.
if (mPendingAccessibilityState != PENDING_ACCESSIBILITY_STATE_NOT_SET) {
mWasImportantForAccessibilityBeforeHidden = mPendingAccessibilityState;
} else {
mWasImportantForAccessibilityBeforeHidden =
itemView.getImportantForAccessibility();
}
parent.setChildImportantForAccessibilityInternal(this,
View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
}
/**
* Called when the child view leaves the hidden state
*/
void onLeftHiddenState(RecyclerView parent) {
parent.setChildImportantForAccessibilityInternal(this,
mWasImportantForAccessibilityBeforeHidden);
mWasImportantForAccessibilityBeforeHidden = View.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
}
@Override
public String toString() {
String className =
getClass().isAnonymousClass() ? "ViewHolder" : getClass().getSimpleName();
final StringBuilder sb = new StringBuilder(className + "{"
+ Integer.toHexString(hashCode()) + " position=" + mPosition + " id=" + mItemId
+ ", oldPos=" + mOldPosition + ", pLpos:" + mPreLayoutPosition);
if (isScrap()) {
sb.append(" scrap ")
.append(mInChangeScrap ? "[changeScrap]" : "[attachedScrap]");
}
if (isInvalid()) sb.append(" invalid");
if (!isBound()) sb.append(" unbound");
if (needsUpdate()) sb.append(" update");
if (isRemoved()) sb.append(" removed");
if (shouldIgnore()) sb.append(" ignored");
if (isTmpDetached()) sb.append(" tmpDetached");
if (!isRecyclable()) sb.append(" not recyclable(" + mIsRecyclableCount + ")");
if (isAdapterPositionUnknown()) sb.append(" undefined adapter position");
if (itemView.getParent() == null) sb.append(" no parent");
sb.append("}");
return sb.toString();
}
/**
* Informs the recycler whether this item can be recycled. Views which are not
* recyclable will not be reused for other items until setIsRecyclable() is
* later set to true. Calls to setIsRecyclable() should always be paired (one
* call to setIsRecyclabe(false) should always be matched with a later call to
* setIsRecyclable(true)). Pairs of calls may be nested, as the state is internally
* reference-counted.
*
* @param recyclable Whether this item is available to be recycled. Default value
* is true.
* @see #isRecyclable()
*/
public final void setIsRecyclable(boolean recyclable) {
mIsRecyclableCount = recyclable ? mIsRecyclableCount - 1 : mIsRecyclableCount + 1;
if (mIsRecyclableCount < 0) {
mIsRecyclableCount = 0;
if (sDebugAssertionsEnabled) {
throw new RuntimeException("isRecyclable decremented below 0: "
+ "unmatched pair of setIsRecyable() calls for " + this);
}
Log.e(VIEW_LOG_TAG, "isRecyclable decremented below 0: "
+ "unmatched pair of setIsRecyable() calls for " + this);
} else if (!recyclable && mIsRecyclableCount == 1) {
mFlags |= FLAG_NOT_RECYCLABLE;
} else if (recyclable && mIsRecyclableCount == 0) {
mFlags &= ~FLAG_NOT_RECYCLABLE;
}
if (sVerboseLoggingEnabled) {
Log.d(TAG, "setIsRecyclable val:" + recyclable + ":" + this);
}
}
/**
* @return true if this item is available to be recycled, false otherwise.
* @see #setIsRecyclable(boolean)
*/
public final boolean isRecyclable() {
return (mFlags & FLAG_NOT_RECYCLABLE) == 0
&& !ViewCompat.hasTransientState(itemView);
}
/**
* Returns whether we have animations referring to this view holder or not.
* This is similar to isRecyclable flag but does not check transient state.
*/
boolean shouldBeKeptAsChild() {
return (mFlags & FLAG_NOT_RECYCLABLE) != 0;
}
/**
* @return True if ViewHolder is not referenced by RecyclerView animations but has
* transient state which will prevent it from being recycled.
*/
boolean doesTransientStatePreventRecycling() {
return (mFlags & FLAG_NOT_RECYCLABLE) == 0 && ViewCompat.hasTransientState(itemView);
}
boolean isUpdated() {
return (mFlags & FLAG_UPDATE) != 0;
}
}
/**
* This method is here so that we can control the important for a11y changes and test it.
*/
@VisibleForTesting
boolean setChildImportantForAccessibilityInternal(ViewHolder viewHolder,
int importantForAccessibility) {
if (isComputingLayout()) {
viewHolder.mPendingAccessibilityState = importantForAccessibility;
mPendingAccessibilityImportanceChange.add(viewHolder);
return false;
}
viewHolder.itemView.setImportantForAccessibility(importantForAccessibility);
return true;
}
void dispatchPendingImportantForAccessibilityChanges() {
for (int i = mPendingAccessibilityImportanceChange.size() - 1; i >= 0; i--) {
ViewHolder viewHolder = mPendingAccessibilityImportanceChange.get(i);
if (viewHolder.itemView.getParent() != this || viewHolder.shouldIgnore()) {
continue;
}
int state = viewHolder.mPendingAccessibilityState;
if (state != ViewHolder.PENDING_ACCESSIBILITY_STATE_NOT_SET) {
//noinspection WrongConstant
viewHolder.itemView.setImportantForAccessibility(state);
viewHolder.mPendingAccessibilityState =
ViewHolder.PENDING_ACCESSIBILITY_STATE_NOT_SET;
}
}
mPendingAccessibilityImportanceChange.clear();
}
int getAdapterPositionInRecyclerView(ViewHolder viewHolder) {
if (viewHolder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)
|| !viewHolder.isBound()) {
return RecyclerView.NO_POSITION;
}
return mAdapterHelper.applyPendingUpdatesToPosition(viewHolder.mPosition);
}
@VisibleForTesting
void initFastScroller(StateListDrawable verticalThumbDrawable,
Drawable verticalTrackDrawable, StateListDrawable horizontalThumbDrawable,
Drawable horizontalTrackDrawable) {
if (verticalThumbDrawable == null || verticalTrackDrawable == null
|| horizontalThumbDrawable == null || horizontalTrackDrawable == null) {
throw new IllegalArgumentException(
"Trying to set fast scroller without both required drawables."
+ exceptionLabel());
}
Resources resources = getContext().getResources();
new FastScroller(this, verticalThumbDrawable, verticalTrackDrawable,
horizontalThumbDrawable, horizontalTrackDrawable,
resources.getDimensionPixelSize(R.dimen.fastscroll_default_thickness),
resources.getDimensionPixelSize(R.dimen.fastscroll_minimum_range),
resources.getDimensionPixelOffset(R.dimen.fastscroll_margin));
}
// NestedScrollingChild
@Override
public void setNestedScrollingEnabled(boolean enabled) {
getScrollingChildHelper().setNestedScrollingEnabled(enabled);
}
@Override
public boolean isNestedScrollingEnabled() {
return getScrollingChildHelper().isNestedScrollingEnabled();
}
@Override
public boolean startNestedScroll(int axes) {
return getScrollingChildHelper().startNestedScroll(axes);
}
@Override
public boolean startNestedScroll(int axes, int type) {
return getScrollingChildHelper().startNestedScroll(axes, type);
}
@Override
public void stopNestedScroll() {
getScrollingChildHelper().stopNestedScroll();
}
@Override
public void stopNestedScroll(int type) {
getScrollingChildHelper().stopNestedScroll(type);
}
@Override
public boolean hasNestedScrollingParent() {
return getScrollingChildHelper().hasNestedScrollingParent();
}
@Override
public boolean hasNestedScrollingParent(int type) {
return getScrollingChildHelper().hasNestedScrollingParent(type);
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow) {
return getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, offsetInWindow);
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow, int type) {
return getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, offsetInWindow, type);
}
@Override
public final void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow, int type, @NonNull int[] consumed) {
getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, offsetInWindow, type, consumed);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
int type) {
return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow,
type);
}
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return getScrollingChildHelper().dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return getScrollingChildHelper().dispatchNestedPreFling(velocityX, velocityY);
}
/**
* {@link android.view.ViewGroup.MarginLayoutParams LayoutParams} subclass for children of
* {@link RecyclerView}. Custom {@link LayoutManager layout managers} are encouraged
* to create their own subclass of this <code>LayoutParams</code> class
* to store any additional required per-child view metadata about the layout.
*/
public static class LayoutParams extends android.view.ViewGroup.MarginLayoutParams {
ViewHolder mViewHolder;
final Rect mDecorInsets = new Rect();
boolean mInsetsDirty = true;
// Flag is set to true if the view is bound while it is detached from RV.
// In this case, we need to manually call invalidate after view is added to guarantee that
// invalidation is populated through the View hierarchy
boolean mPendingInvalidate = false;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(MarginLayoutParams source) {
super(source);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
public LayoutParams(LayoutParams source) {
super((ViewGroup.LayoutParams) source);
}
/**
* Returns true if the view this LayoutParams is attached to needs to have its content
* updated from the corresponding adapter.
*
* @return true if the view should have its content updated
*/
public boolean viewNeedsUpdate() {
return mViewHolder.needsUpdate();
}
/**
* Returns true if the view this LayoutParams is attached to is now representing
* potentially invalid data. A LayoutManager should scrap/recycle it.
*
* @return true if the view is invalid
*/
public boolean isViewInvalid() {
return mViewHolder.isInvalid();
}
/**
* Returns true if the adapter data item corresponding to the view this LayoutParams
* is attached to has been removed from the data set. A LayoutManager may choose to
* treat it differently in order to animate its outgoing or disappearing state.
*
* @return true if the item the view corresponds to was removed from the data set
*/
public boolean isItemRemoved() {
return mViewHolder.isRemoved();
}
/**
* Returns true if the adapter data item corresponding to the view this LayoutParams
* is attached to has been changed in the data set. A LayoutManager may choose to
* treat it differently in order to animate its changing state.
*
* @return true if the item the view corresponds to was changed in the data set
*/
public boolean isItemChanged() {
return mViewHolder.isUpdated();
}
/**
* @deprecated use {@link #getViewLayoutPosition()} or {@link #getViewAdapterPosition()}
*/
@Deprecated
public int getViewPosition() {
return mViewHolder.getPosition();
}
/**
* Returns the adapter position that the view this LayoutParams is attached to corresponds
* to as of latest layout calculation.
*
* @return the adapter position this view as of latest layout pass
*/
public int getViewLayoutPosition() {
return mViewHolder.getLayoutPosition();
}
/**
* @deprecated This method is confusing when nested adapters are used.
* If you are calling from the context of an {@link Adapter},
* use {@link #getBindingAdapterPosition()}. If you need the position that
* {@link RecyclerView} sees, use {@link #getAbsoluteAdapterPosition()}.
*/
@Deprecated
public int getViewAdapterPosition() {
return mViewHolder.getBindingAdapterPosition();
}
/**
* Returns the up-to-date adapter position that the view this LayoutParams is attached to
* corresponds to in the {@link RecyclerView}. If the {@link RecyclerView} has an
* {@link Adapter} that merges other adapters, this position will be with respect to the
* adapter that is assigned to the {@link RecyclerView}.
*
* @return the up-to-date adapter position this view with respect to the RecyclerView. It
* may return {@link RecyclerView#NO_POSITION} if item represented by this View has been
* removed or
* its up-to-date position cannot be calculated.
*/
public int getAbsoluteAdapterPosition() {
return mViewHolder.getAbsoluteAdapterPosition();
}
/**
* Returns the up-to-date adapter position that the view this LayoutParams is attached to
* corresponds to with respect to the {@link Adapter} that bound this View.
*
* @return the up-to-date adapter position this view relative to the {@link Adapter} that
* bound this View. It may return {@link RecyclerView#NO_POSITION} if item represented by
* this View has been removed or its up-to-date position cannot be calculated.
*/
public int getBindingAdapterPosition() {
return mViewHolder.getBindingAdapterPosition();
}
}
/**
* Observer base class for watching changes to an {@link Adapter}.
* See {@link Adapter#registerAdapterDataObserver(AdapterDataObserver)}.
*/
public abstract static class AdapterDataObserver {
public void onChanged() {
// Do nothing
}
public void onItemRangeChanged(int positionStart, int itemCount) {
// do nothing
}
public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) {
// fallback to onItemRangeChanged(positionStart, itemCount) if app
// does not override this method.
onItemRangeChanged(positionStart, itemCount);
}
public void onItemRangeInserted(int positionStart, int itemCount) {
// do nothing
}
public void onItemRangeRemoved(int positionStart, int itemCount) {
// do nothing
}
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
// do nothing
}
/**
* Called when the {@link Adapter.StateRestorationPolicy} of the {@link Adapter} changed.
* When this method is called, the Adapter might be ready to restore its state if it has
* not already been restored.
*
* @see Adapter#getStateRestorationPolicy()
* @see Adapter#setStateRestorationPolicy(Adapter.StateRestorationPolicy)
*/
public void onStateRestorationPolicyChanged() {
// do nothing
}
}
/**
* Base class for smooth scrolling. Handles basic tracking of the target view position and
* provides methods to trigger a programmatic scroll.
*
* <p>An instance of SmoothScroller is only intended to be used once. You should create a new
* instance for each call to {@link LayoutManager#startSmoothScroll(SmoothScroller)}.
*
* @see LinearSmoothScroller
*/
public abstract static class SmoothScroller {
private int mTargetPosition = RecyclerView.NO_POSITION;
private RecyclerView mRecyclerView;
private LayoutManager mLayoutManager;
private boolean mPendingInitialRun;
private boolean mRunning;
private View mTargetView;
private final Action mRecyclingAction;
private boolean mStarted;
public SmoothScroller() {
mRecyclingAction = new Action(0, 0);
}
/**
* Starts a smooth scroll for the given target position.
* <p>In each animation step, {@link RecyclerView} will check
* for the target view and call either
* {@link #onTargetFound(android.view.View, RecyclerView.State, SmoothScroller.Action)} or
* {@link #onSeekTargetStep(int, int, RecyclerView.State, SmoothScroller.Action)} until
* SmoothScroller is stopped.</p>
*
* <p>Note that if RecyclerView finds the target view, it will automatically stop the
* SmoothScroller. This <b>does not</b> mean that scroll will stop, it only means it will
* stop calling SmoothScroller in each animation step.</p>
*/
void start(RecyclerView recyclerView, LayoutManager layoutManager) {
// Stop any previous ViewFlinger animations now because we are about to start a new one.
recyclerView.mViewFlinger.stop();
if (mStarted) {
Log.w(TAG, "An instance of " + this.getClass().getSimpleName() + " was started "
+ "more than once. Each instance of" + this.getClass().getSimpleName() + " "
+ "is intended to only be used once. You should create a new instance for "
+ "each use.");
}
mRecyclerView = recyclerView;
mLayoutManager = layoutManager;
if (mTargetPosition == RecyclerView.NO_POSITION) {
throw new IllegalArgumentException("Invalid target position");
}
mRecyclerView.mState.mTargetPosition = mTargetPosition;
mRunning = true;
mPendingInitialRun = true;
mTargetView = findViewByPosition(getTargetPosition());
onStart();
mRecyclerView.mViewFlinger.postOnAnimation();
mStarted = true;
}
public void setTargetPosition(int targetPosition) {
mTargetPosition = targetPosition;
}
/**
* Compute the scroll vector for a given target position.
* <p>
* This method can return null if the layout manager cannot calculate a scroll vector
* for the given position (e.g. it has no current scroll position).
*
* @param targetPosition the position to which the scroller is scrolling
* @return the scroll vector for a given target position
*/
@Nullable
public PointF computeScrollVectorForPosition(int targetPosition) {
LayoutManager layoutManager = getLayoutManager();
if (layoutManager instanceof ScrollVectorProvider) {
return ((ScrollVectorProvider) layoutManager)
.computeScrollVectorForPosition(targetPosition);
}
Log.w(TAG, "You should override computeScrollVectorForPosition when the LayoutManager"
+ " does not implement " + ScrollVectorProvider.class.getCanonicalName());
return null;
}
/**
* @return The LayoutManager to which this SmoothScroller is attached. Will return
* <code>null</code> after the SmoothScroller is stopped.
*/
@Nullable
public LayoutManager getLayoutManager() {
return mLayoutManager;
}
/**
* Stops running the SmoothScroller in each animation callback. Note that this does not
* cancel any existing {@link Action} updated by
* {@link #onTargetFound(android.view.View, RecyclerView.State, SmoothScroller.Action)} or
* {@link #onSeekTargetStep(int, int, RecyclerView.State, SmoothScroller.Action)}.
*/
protected final void stop() {
if (!mRunning) {
return;
}
mRunning = false;
onStop();
mRecyclerView.mState.mTargetPosition = RecyclerView.NO_POSITION;
mTargetView = null;
mTargetPosition = RecyclerView.NO_POSITION;
mPendingInitialRun = false;
// trigger a cleanup
mLayoutManager.onSmoothScrollerStopped(this);
// clear references to avoid any potential leak by a custom smooth scroller
mLayoutManager = null;
mRecyclerView = null;
}
/**
* Returns true if SmoothScroller has been started but has not received the first
* animation
* callback yet.
*
* @return True if this SmoothScroller is waiting to start
*/
public boolean isPendingInitialRun() {
return mPendingInitialRun;
}
/**
* @return True if SmoothScroller is currently active
*/
public boolean isRunning() {
return mRunning;
}
/**
* Returns the adapter position of the target item
*
* @return Adapter position of the target item or
* {@link RecyclerView#NO_POSITION} if no target view is set.
*/
public int getTargetPosition() {
return mTargetPosition;
}
void onAnimation(int dx, int dy) {
final RecyclerView recyclerView = mRecyclerView;
if (mTargetPosition == RecyclerView.NO_POSITION || recyclerView == null) {
stop();
}
// The following if block exists to have the LayoutManager scroll 1 pixel in the correct
// direction in order to cause the LayoutManager to draw two pages worth of views so
// that the target view may be found before scrolling any further. This is done to
// prevent an initial scroll distance from scrolling past the view, which causes a
// jittery looking animation.
if (mPendingInitialRun && mTargetView == null && mLayoutManager != null) {
PointF pointF = computeScrollVectorForPosition(mTargetPosition);
if (pointF != null && (pointF.x != 0 || pointF.y != 0)) {
recyclerView.scrollStep(
(int) Math.signum(pointF.x),
(int) Math.signum(pointF.y),
null);
}
}
mPendingInitialRun = false;
if (mTargetView != null) {
// verify target position
if (getChildPosition(mTargetView) == mTargetPosition) {
onTargetFound(mTargetView, recyclerView.mState, mRecyclingAction);
mRecyclingAction.runIfNecessary(recyclerView);
stop();
} else {
Log.e(TAG, "Passed over target position while smooth scrolling.");
mTargetView = null;
}
}
if (mRunning) {
onSeekTargetStep(dx, dy, recyclerView.mState, mRecyclingAction);
boolean hadJumpTarget = mRecyclingAction.hasJumpTarget();
mRecyclingAction.runIfNecessary(recyclerView);
if (hadJumpTarget) {
// It is not stopped so needs to be restarted
if (mRunning) {
mPendingInitialRun = true;
recyclerView.mViewFlinger.postOnAnimation();
}
}
}
}
/**
* @see RecyclerView#getChildLayoutPosition(android.view.View)
*/
public int getChildPosition(View view) {
return mRecyclerView.getChildLayoutPosition(view);
}
/**
* @see RecyclerView.LayoutManager#getChildCount()
*/
public int getChildCount() {
return mRecyclerView.mLayout.getChildCount();
}
/**
* @see RecyclerView.LayoutManager#findViewByPosition(int)
*/
public View findViewByPosition(int position) {
return mRecyclerView.mLayout.findViewByPosition(position);
}
/**
* @see RecyclerView#scrollToPosition(int)
* @deprecated Use {@link Action#jumpTo(int)}.
*/
@Deprecated
public void instantScrollToPosition(int position) {
mRecyclerView.scrollToPosition(position);
}
protected void onChildAttachedToWindow(View child) {
if (getChildPosition(child) == getTargetPosition()) {
mTargetView = child;
if (sVerboseLoggingEnabled) {
Log.d(TAG, "smooth scroll target view has been attached");
}
}
}
/**
* Normalizes the vector.
*
* @param scrollVector The vector that points to the target scroll position
*/
protected void normalize(@NonNull PointF scrollVector) {
final float magnitude = (float) Math.sqrt(scrollVector.x * scrollVector.x
+ scrollVector.y * scrollVector.y);
scrollVector.x /= magnitude;
scrollVector.y /= magnitude;
}
/**
* Called when smooth scroll is started. This might be a good time to do setup.
*/
protected abstract void onStart();
/**
* Called when smooth scroller is stopped. This is a good place to cleanup your state etc.
*
* @see #stop()
*/
protected abstract void onStop();
/**
* <p>RecyclerView will call this method each time it scrolls until it can find the target
* position in the layout.</p>
* <p>SmoothScroller should check dx, dy and if scroll should be changed, update the
* provided {@link Action} to define the next scroll.</p>
*
* @param dx Last scroll amount horizontally
* @param dy Last scroll amount vertically
* @param state Transient state of RecyclerView
* @param action If you want to trigger a new smooth scroll and cancel the previous one,
* update this object.
*/
protected abstract void onSeekTargetStep(@Px int dx, @Px int dy, @NonNull State state,
@NonNull Action action);
/**
* Called when the target position is laid out. This is the last callback SmoothScroller
* will receive and it should update the provided {@link Action} to define the scroll
* details towards the target view.
*
* @param targetView The view element which render the target position.
* @param state Transient state of RecyclerView
* @param action Action instance that you should update to define final scroll action
* towards the targetView
*/
protected abstract void onTargetFound(@NonNull View targetView, @NonNull State state,
@NonNull Action action);
/**
* Holds information about a smooth scroll request by a {@link SmoothScroller}.
*/
public static class Action {
public static final int UNDEFINED_DURATION = RecyclerView.UNDEFINED_DURATION;
private int mDx;
private int mDy;
private int mDuration;
private int mJumpToPosition = NO_POSITION;
private Interpolator mInterpolator;
private boolean mChanged = false;
// we track this variable to inform custom implementer if they are updating the action
// in every animation callback
private int mConsecutiveUpdates = 0;
/**
* @param dx Pixels to scroll horizontally
* @param dy Pixels to scroll vertically
*/
public Action(@Px int dx, @Px int dy) {
this(dx, dy, UNDEFINED_DURATION, null);
}
/**
* @param dx Pixels to scroll horizontally
* @param dy Pixels to scroll vertically
* @param duration Duration of the animation in milliseconds
*/
public Action(@Px int dx, @Px int dy, int duration) {
this(dx, dy, duration, null);
}
/**
* @param dx Pixels to scroll horizontally
* @param dy Pixels to scroll vertically
* @param duration Duration of the animation in milliseconds
* @param interpolator Interpolator to be used when calculating scroll position in each
* animation step
*/
public Action(@Px int dx, @Px int dy, int duration,
@Nullable Interpolator interpolator) {
mDx = dx;
mDy = dy;
mDuration = duration;
mInterpolator = interpolator;
}
/**
* Instead of specifying pixels to scroll, use the target position to jump using
* {@link RecyclerView#scrollToPosition(int)}.
* <p>
* You may prefer using this method if scroll target is really far away and you prefer
* to jump to a location and smooth scroll afterwards.
* <p>
* Note that calling this method takes priority over other update methods such as
* {@link #update(int, int, int, Interpolator)}, {@link #setX(float)},
* {@link #setY(float)} and #{@link #setInterpolator(Interpolator)}. If you call
* {@link #jumpTo(int)}, the other changes will not be considered for this animation
* frame.
*
* @param targetPosition The target item position to scroll to using instant scrolling.
*/
public void jumpTo(int targetPosition) {
mJumpToPosition = targetPosition;
}
boolean hasJumpTarget() {
return mJumpToPosition >= 0;
}
void runIfNecessary(RecyclerView recyclerView) {
if (mJumpToPosition >= 0) {
final int position = mJumpToPosition;
mJumpToPosition = NO_POSITION;
recyclerView.jumpToPositionForSmoothScroller(position);
mChanged = false;
return;
}
if (mChanged) {
validate();
recyclerView.mViewFlinger.smoothScrollBy(mDx, mDy, mDuration, mInterpolator);
mConsecutiveUpdates++;
if (mConsecutiveUpdates > 10) {
// A new action is being set in every animation step. This looks like a bad
// implementation. Inform developer.
Log.e(TAG, "Smooth Scroll action is being updated too frequently. Make sure"
+ " you are not changing it unless necessary");
}
mChanged = false;
} else {
mConsecutiveUpdates = 0;
}
}
private void validate() {
if (mInterpolator != null && mDuration < 1) {
throw new IllegalStateException("If you provide an interpolator, you must"
+ " set a positive duration");
} else if (mDuration < 1) {
throw new IllegalStateException("Scroll duration must be a positive number");
}
}
@Px
public int getDx() {
return mDx;
}
public void setDx(@Px int dx) {
mChanged = true;
mDx = dx;
}
@Px
public int getDy() {
return mDy;
}
public void setDy(@Px int dy) {
mChanged = true;
mDy = dy;
}
public int getDuration() {
return mDuration;
}
public void setDuration(int duration) {
mChanged = true;
mDuration = duration;
}
@Nullable
public Interpolator getInterpolator() {
return mInterpolator;
}
/**
* Sets the interpolator to calculate scroll steps
*
* @param interpolator The interpolator to use. If you specify an interpolator, you must
* also set the duration.
* @see #setDuration(int)
*/
public void setInterpolator(@Nullable Interpolator interpolator) {
mChanged = true;
mInterpolator = interpolator;
}
/**
* Updates the action with given parameters.
*
* @param dx Pixels to scroll horizontally
* @param dy Pixels to scroll vertically
* @param duration Duration of the animation in milliseconds
* @param interpolator Interpolator to be used when calculating scroll position in each
* animation step
*/
public void update(@Px int dx, @Px int dy, int duration,
@Nullable Interpolator interpolator) {
mDx = dx;
mDy = dy;
mDuration = duration;
mInterpolator = interpolator;
mChanged = true;
}
}
/**
* An interface which is optionally implemented by custom {@link RecyclerView.LayoutManager}
* to provide a hint to a {@link SmoothScroller} about the location of the target position.
*/
public interface ScrollVectorProvider {
/**
* Should calculate the vector that points to the direction where the target position
* can be found.
* <p>
* This method is used by the {@link LinearSmoothScroller} to initiate a scroll towards
* the target position.
* <p>
* The magnitude of the vector is not important. It is always normalized before being
* used by the {@link LinearSmoothScroller}.
* <p>
* LayoutManager should not check whether the position exists in the adapter or not.
*
* @param targetPosition the target position to which the returned vector should point
* @return the scroll vector for a given position.
*/
@Nullable
PointF computeScrollVectorForPosition(int targetPosition);
}
}
static class AdapterDataObservable extends Observable<AdapterDataObserver> {
public boolean hasObservers() {
return !mObservers.isEmpty();
}
public void notifyChanged() {
// since onChanged() is implemented by the app, it could do anything, including
// removing itself from {@link mObservers} - and that could cause problems if
// an iterator is used on the ArrayList {@link mObservers}.
// to avoid such problems, just march thru the list in the reverse order.
for (int i = mObservers.size() - 1; i >= 0; i--) {
mObservers.get(i).onChanged();
}
}
public void notifyStateRestorationPolicyChanged() {
for (int i = mObservers.size() - 1; i >= 0; i--) {
mObservers.get(i).onStateRestorationPolicyChanged();
}
}
public void notifyItemRangeChanged(int positionStart, int itemCount) {
notifyItemRangeChanged(positionStart, itemCount, null);
}
public void notifyItemRangeChanged(int positionStart, int itemCount,
@Nullable Object payload) {
// since onItemRangeChanged() is implemented by the app, it could do anything, including
// removing itself from {@link mObservers} - and that could cause problems if
// an iterator is used on the ArrayList {@link mObservers}.
// to avoid such problems, just march thru the list in the reverse order.
for (int i = mObservers.size() - 1; i >= 0; i--) {
mObservers.get(i).onItemRangeChanged(positionStart, itemCount, payload);
}
}
public void notifyItemRangeInserted(int positionStart, int itemCount) {
// since onItemRangeInserted() is implemented by the app, it could do anything,
// including removing itself from {@link mObservers} - and that could cause problems if
// an iterator is used on the ArrayList {@link mObservers}.
// to avoid such problems, just march thru the list in the reverse order.
for (int i = mObservers.size() - 1; i >= 0; i--) {
mObservers.get(i).onItemRangeInserted(positionStart, itemCount);
}
}
public void notifyItemRangeRemoved(int positionStart, int itemCount) {
// since onItemRangeRemoved() is implemented by the app, it could do anything, including
// removing itself from {@link mObservers} - and that could cause problems if
// an iterator is used on the ArrayList {@link mObservers}.
// to avoid such problems, just march thru the list in the reverse order.
for (int i = mObservers.size() - 1; i >= 0; i--) {
mObservers.get(i).onItemRangeRemoved(positionStart, itemCount);
}
}
public void notifyItemMoved(int fromPosition, int toPosition) {
for (int i = mObservers.size() - 1; i >= 0; i--) {
mObservers.get(i).onItemRangeMoved(fromPosition, toPosition, 1);
}
}
}
/**
* This is public so that the CREATOR can be accessed on cold launch.
*
*/
@RestrictTo(LIBRARY)
public static class SavedState extends AbsSavedState {
Parcelable mLayoutState;
/**
* called by CREATOR
*/
@SuppressWarnings("deprecation")
SavedState(Parcel in, ClassLoader loader) {
super(in, loader);
mLayoutState = in.readParcelable(
loader != null ? loader : LayoutManager.class.getClassLoader());
}
/**
* Called by onSaveInstanceState
*/
SavedState(Parcelable superState) {
super(superState);
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeParcelable(mLayoutState, 0);
}
void copyFrom(SavedState other) {
mLayoutState = other.mLayoutState;
}
public static final Creator<SavedState> CREATOR = new ClassLoaderCreator<SavedState>() {
@Override
public SavedState createFromParcel(Parcel in, ClassLoader loader) {
return new SavedState(in, loader);
}
@Override
public SavedState createFromParcel(Parcel in) {
return new SavedState(in, null);
}
@Override
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
/**
* <p>Contains useful information about the current RecyclerView state like target scroll
* position or view focus. State object can also keep arbitrary data, identified by resource
* ids.</p>
* <p>Often times, RecyclerView components will need to pass information between each other.
* To provide a well defined data bus between components, RecyclerView passes the same State
* object to component callbacks and these components can use it to exchange data.</p>
* <p>If you implement custom components, you can use State's put/get/remove methods to pass
* data between your components without needing to manage their lifecycles.</p>
*/
public static class State {
static final int STEP_START = 1;
static final int STEP_LAYOUT = 1 << 1;
static final int STEP_ANIMATIONS = 1 << 2;
void assertLayoutStep(int accepted) {
if ((accepted & mLayoutStep) == 0) {
throw new IllegalStateException("Layout state should be one of "
+ Integer.toBinaryString(accepted) + " but it is "
+ Integer.toBinaryString(mLayoutStep));
}
}
/** Owned by SmoothScroller */
int mTargetPosition = RecyclerView.NO_POSITION;
private SparseArray<Object> mData;
////////////////////////////////////////////////////////////////////////////////////////////
// Fields below are carried from one layout pass to the next
////////////////////////////////////////////////////////////////////////////////////////////
/**
* Number of items adapter had in the previous layout.
*/
int mPreviousLayoutItemCount = 0;
/**
* Number of items that were NOT laid out but has been deleted from the adapter after the
* previous layout.
*/
int mDeletedInvisibleItemCountSincePreviousLayout = 0;
////////////////////////////////////////////////////////////////////////////////////////////
// Fields below must be updated or cleared before they are used (generally before a pass)
////////////////////////////////////////////////////////////////////////////////////////////
@IntDef(flag = true, value = {
STEP_START, STEP_LAYOUT, STEP_ANIMATIONS
})
@Retention(RetentionPolicy.SOURCE)
@interface LayoutState {
}
@LayoutState
int mLayoutStep = STEP_START;
/**
* Number of items adapter has.
*/
int mItemCount = 0;
boolean mStructureChanged = false;
/**
* True if the associated {@link RecyclerView} is in the pre-layout step where it is having
* its {@link LayoutManager} layout items where they will be at the beginning of a set of
* predictive item animations.
*/
boolean mInPreLayout = false;
boolean mTrackOldChangeHolders = false;
boolean mIsMeasuring = false;
////////////////////////////////////////////////////////////////////////////////////////////
// Fields below are always reset outside of the pass (or passes) that use them
////////////////////////////////////////////////////////////////////////////////////////////
boolean mRunSimpleAnimations = false;
boolean mRunPredictiveAnimations = false;
/**
* This data is saved before a layout calculation happens. After the layout is finished,
* if the previously focused view has been replaced with another view for the same item, we
* move the focus to the new item automatically.
*/
int mFocusedItemPosition;
long mFocusedItemId;
// when a sub child has focus, record its id and see if we can directly request focus on
// that one instead
int mFocusedSubChildId;
int mRemainingScrollHorizontal;
int mRemainingScrollVertical;
////////////////////////////////////////////////////////////////////////////////////////////
/**
* Prepare for a prefetch occurring on the RecyclerView in between traversals, potentially
* prior to any layout passes.
*
* <p>Don't touch any state stored between layout passes, only reset per-layout state, so
* that Recycler#getViewForPosition() can function safely.</p>
*/
void prepareForNestedPrefetch(Adapter adapter) {
mLayoutStep = STEP_START;
mItemCount = adapter.getItemCount();
mInPreLayout = false;
mTrackOldChangeHolders = false;
mIsMeasuring = false;
}
/**
* Returns true if the RecyclerView is currently measuring the layout. This value is
* {@code true} only if the LayoutManager opted into the auto measure API and RecyclerView
* has non-exact measurement specs.
* <p>
* Note that if the LayoutManager supports predictive animations and it is calculating the
* pre-layout step, this value will be {@code false} even if the RecyclerView is in
* {@code onMeasure} call. This is because pre-layout means the previous state of the
* RecyclerView and measurements made for that state cannot change the RecyclerView's size.
* LayoutManager is always guaranteed to receive another call to
* {@link LayoutManager#onLayoutChildren(Recycler, State)} when this happens.
*
* @return True if the RecyclerView is currently calculating its bounds, false otherwise.
*/
public boolean isMeasuring() {
return mIsMeasuring;
}
/**
* Returns true if the {@link RecyclerView} is in the pre-layout step where it is having its
* {@link LayoutManager} layout items where they will be at the beginning of a set of
* predictive item animations.
*/
public boolean isPreLayout() {
return mInPreLayout;
}
/**
* Returns whether RecyclerView will run predictive animations in this layout pass
* or not.
*
* @return true if RecyclerView is calculating predictive animations to be run at the end
* of the layout pass.
*/
public boolean willRunPredictiveAnimations() {
return mRunPredictiveAnimations;
}
/**
* Returns whether RecyclerView will run simple animations in this layout pass
* or not.
*
* @return true if RecyclerView is calculating simple animations to be run at the end of
* the layout pass.
*/
public boolean willRunSimpleAnimations() {
return mRunSimpleAnimations;
}
/**
* Removes the mapping from the specified id, if there was any.
*
* @param resourceId Id of the resource you want to remove. It is suggested to use R.id.* to
* preserve cross functionality and avoid conflicts.
*/
public void remove(int resourceId) {
if (mData == null) {
return;
}
mData.remove(resourceId);
}
/**
* Gets the Object mapped from the specified id, or <code>null</code>
* if no such data exists.
*
* @param resourceId Id of the resource you want to remove. It is suggested to use R.id.*
* to
* preserve cross functionality and avoid conflicts.
*/
@SuppressWarnings({"TypeParameterUnusedInFormals", "unchecked"})
public <T> T get(int resourceId) {
if (mData == null) {
return null;
}
return (T) mData.get(resourceId);
}
/**
* Adds a mapping from the specified id to the specified value, replacing the previous
* mapping from the specified key if there was one.
*
* @param resourceId Id of the resource you want to add. It is suggested to use R.id.* to
* preserve cross functionality and avoid conflicts.
* @param data The data you want to associate with the resourceId.
*/
public void put(int resourceId, Object data) {
if (mData == null) {
mData = new SparseArray<Object>();
}
mData.put(resourceId, data);
}
/**
* If scroll is triggered to make a certain item visible, this value will return the
* adapter index of that item.
*
* @return Adapter index of the target item or
* {@link RecyclerView#NO_POSITION} if there is no target
* position.
*/
public int getTargetScrollPosition() {
return mTargetPosition;
}
/**
* Returns if current scroll has a target position.
*
* @return true if scroll is being triggered to make a certain position visible
* @see #getTargetScrollPosition()
*/
public boolean hasTargetScrollPosition() {
return mTargetPosition != RecyclerView.NO_POSITION;
}
/**
* @return true if the structure of the data set has changed since the last call to
* onLayoutChildren, false otherwise
*/
public boolean didStructureChange() {
return mStructureChanged;
}
/**
* Returns the total number of items that can be laid out. Note that this number is not
* necessarily equal to the number of items in the adapter, so you should always use this
* number for your position calculations and never access the adapter directly.
* <p>
* RecyclerView listens for Adapter's notify events and calculates the effects of adapter
* data changes on existing Views. These calculations are used to decide which animations
* should be run.
* <p>
* To support predictive animations, RecyclerView may rewrite or reorder Adapter changes to
* present the correct state to LayoutManager in pre-layout pass.
* <p>
* For example, a newly added item is not included in pre-layout item count because
* pre-layout reflects the contents of the adapter before the item is added. Behind the
* scenes, RecyclerView offsets {@link Recycler#getViewForPosition(int)} calls such that
* LayoutManager does not know about the new item's existence in pre-layout. The item will
* be available in second layout pass and will be included in the item count. Similar
* adjustments are made for moved and removed items as well.
* <p>
* You can get the adapter's item count via {@link LayoutManager#getItemCount()} method.
*
* @return The number of items currently available
* @see LayoutManager#getItemCount()
*/
public int getItemCount() {
return mInPreLayout
? (mPreviousLayoutItemCount - mDeletedInvisibleItemCountSincePreviousLayout)
: mItemCount;
}
/**
* Returns remaining horizontal scroll distance of an ongoing scroll animation(fling/
* smoothScrollTo/SmoothScroller) in pixels. Returns zero if {@link #getScrollState()} is
* other than {@link #SCROLL_STATE_SETTLING}.
*
* @return Remaining horizontal scroll distance
*/
public int getRemainingScrollHorizontal() {
return mRemainingScrollHorizontal;
}
/**
* Returns remaining vertical scroll distance of an ongoing scroll animation(fling/
* smoothScrollTo/SmoothScroller) in pixels. Returns zero if {@link #getScrollState()} is
* other than {@link #SCROLL_STATE_SETTLING}.
*
* @return Remaining vertical scroll distance
*/
public int getRemainingScrollVertical() {
return mRemainingScrollVertical;
}
@Override
public String toString() {
return "State{"
+ "mTargetPosition=" + mTargetPosition
+ ", mData=" + mData
+ ", mItemCount=" + mItemCount
+ ", mIsMeasuring=" + mIsMeasuring
+ ", mPreviousLayoutItemCount=" + mPreviousLayoutItemCount
+ ", mDeletedInvisibleItemCountSincePreviousLayout="
+ mDeletedInvisibleItemCountSincePreviousLayout
+ ", mStructureChanged=" + mStructureChanged
+ ", mInPreLayout=" + mInPreLayout
+ ", mRunSimpleAnimations=" + mRunSimpleAnimations
+ ", mRunPredictiveAnimations=" + mRunPredictiveAnimations
+ '}';
}
}
/**
* This class defines the behavior of fling if the developer wishes to handle it.
* <p>
* Subclasses of {@link OnFlingListener} can be used to implement custom fling behavior.
*
* @see #setOnFlingListener(OnFlingListener)
*/
public abstract static class OnFlingListener {
/**
* Override this to handle a fling given the velocities in both x and y directions.
* Note that this method will only be called if the associated {@link LayoutManager}
* supports scrolling and the fling is not handled by nested scrolls first.
*
* @param velocityX the fling velocity on the X axis
* @param velocityY the fling velocity on the Y axis
* @return true if the fling was handled, false otherwise.
*/
public abstract boolean onFling(int velocityX, int velocityY);
}
/**
* Internal listener that manages items after animations finish. This is how items are
* retained (not recycled) during animations, but allowed to be recycled afterwards.
* It depends on the contract with the ItemAnimator to call the appropriate dispatch*Finished()
* method on the animator's listener when it is done animating any item.
*/
private class ItemAnimatorRestoreListener implements ItemAnimator.ItemAnimatorListener {
ItemAnimatorRestoreListener() {
}
@Override
public void onAnimationFinished(ViewHolder item) {
item.setIsRecyclable(true);
if (item.mShadowedHolder != null && item.mShadowingHolder == null) { // old vh
item.mShadowedHolder = null;
}
// always null this because an OldViewHolder can never become NewViewHolder w/o being
// recycled.
item.mShadowingHolder = null;
if (!item.shouldBeKeptAsChild()) {
if (!removeAnimatingView(item.itemView) && item.isTmpDetached()) {
removeDetachedView(item.itemView, false);
}
}
}
}
/**
* This class defines the animations that take place on items as changes are made
* to the adapter.
*
* Subclasses of ItemAnimator can be used to implement custom animations for actions on
* ViewHolder items. The RecyclerView will manage retaining these items while they
* are being animated, but implementors must call {@link #dispatchAnimationFinished(ViewHolder)}
* when a ViewHolder's animation is finished. In other words, there must be a matching
* {@link #dispatchAnimationFinished(ViewHolder)} call for each
* {@link #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) animateAppearance()},
* {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)
* animateChange()}
* {@link #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo) animatePersistence()},
* and
* {@link #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
* animateDisappearance()} call.
*
* <p>By default, RecyclerView uses {@link DefaultItemAnimator}.</p>
*
* @see #setItemAnimator(ItemAnimator)
*/
@SuppressWarnings("UnusedParameters")
public abstract static class ItemAnimator {
/**
* The Item represented by this ViewHolder is updated.
* <p>
*
* @see #recordPreLayoutInformation(State, ViewHolder, int, List)
*/
public static final int FLAG_CHANGED = ViewHolder.FLAG_UPDATE;
/**
* The Item represented by this ViewHolder is removed from the adapter.
* <p>
*
* @see #recordPreLayoutInformation(State, ViewHolder, int, List)
*/
public static final int FLAG_REMOVED = ViewHolder.FLAG_REMOVED;
/**
* Adapter {@link Adapter#notifyDataSetChanged()} has been called and the content
* represented by this ViewHolder is invalid.
* <p>
*
* @see #recordPreLayoutInformation(State, ViewHolder, int, List)
*/
public static final int FLAG_INVALIDATED = ViewHolder.FLAG_INVALID;
/**
* The position of the Item represented by this ViewHolder has been changed. This flag is
* not bound to {@link Adapter#notifyItemMoved(int, int)}. It might be set in response to
* any adapter change that may have a side effect on this item. (e.g. The item before this
* one has been removed from the Adapter).
* <p>
*
* @see #recordPreLayoutInformation(State, ViewHolder, int, List)
*/
public static final int FLAG_MOVED = ViewHolder.FLAG_MOVED;
/**
* This ViewHolder was not laid out but has been added to the layout in pre-layout state
* by the {@link LayoutManager}. This means that the item was already in the Adapter but
* invisible and it may become visible in the post layout phase. LayoutManagers may prefer
* to add new items in pre-layout to specify their virtual location when they are invisible
* (e.g. to specify the item should <i>animate in</i> from below the visible area).
* <p>
*
* @see #recordPreLayoutInformation(State, ViewHolder, int, List)
*/
public static final int FLAG_APPEARED_IN_PRE_LAYOUT =
ViewHolder.FLAG_APPEARED_IN_PRE_LAYOUT;
/**
* The set of flags that might be passed to
* {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}.
*/
@IntDef(flag = true, value = {
FLAG_CHANGED, FLAG_REMOVED, FLAG_MOVED, FLAG_INVALIDATED,
FLAG_APPEARED_IN_PRE_LAYOUT
})
@Retention(RetentionPolicy.SOURCE)
public @interface AdapterChanges {
}
private ItemAnimatorListener mListener = null;
private ArrayList<ItemAnimatorFinishedListener> mFinishedListeners =
new ArrayList<ItemAnimatorFinishedListener>();
private long mAddDuration = 120;
private long mRemoveDuration = 120;
private long mMoveDuration = 250;
private long mChangeDuration = 250;
/**
* Gets the current duration for which all move animations will run.
*
* @return The current move duration
*/
public long getMoveDuration() {
return mMoveDuration;
}
/**
* Sets the duration for which all move animations will run.
*
* @param moveDuration The move duration
*/
public void setMoveDuration(long moveDuration) {
mMoveDuration = moveDuration;
}
/**
* Gets the current duration for which all add animations will run.
*
* @return The current add duration
*/
public long getAddDuration() {
return mAddDuration;
}
/**
* Sets the duration for which all add animations will run.
*
* @param addDuration The add duration
*/
public void setAddDuration(long addDuration) {
mAddDuration = addDuration;
}
/**
* Gets the current duration for which all remove animations will run.
*
* @return The current remove duration
*/
public long getRemoveDuration() {
return mRemoveDuration;
}
/**
* Sets the duration for which all remove animations will run.
*
* @param removeDuration The remove duration
*/
public void setRemoveDuration(long removeDuration) {
mRemoveDuration = removeDuration;
}
/**
* Gets the current duration for which all change animations will run.
*
* @return The current change duration
*/
public long getChangeDuration() {
return mChangeDuration;
}
/**
* Sets the duration for which all change animations will run.
*
* @param changeDuration The change duration
*/
public void setChangeDuration(long changeDuration) {
mChangeDuration = changeDuration;
}
/**
* Internal only:
* Sets the listener that must be called when the animator is finished
* animating the item (or immediately if no animation happens). This is set
* internally and is not intended to be set by external code.
*
* @param listener The listener that must be called.
*/
void setListener(ItemAnimatorListener listener) {
mListener = listener;
}
/**
* Called by the RecyclerView before the layout begins. Item animator should record
* necessary information about the View before it is potentially rebound, moved or removed.
* <p>
* The data returned from this method will be passed to the related <code>animate**</code>
* methods.
* <p>
* Note that this method may be called after pre-layout phase if LayoutManager adds new
* Views to the layout in pre-layout pass.
* <p>
* The default implementation returns an {@link ItemHolderInfo} which holds the bounds of
* the View and the adapter change flags.
*
* @param state The current State of RecyclerView which includes some useful data
* about the layout that will be calculated.
* @param viewHolder The ViewHolder whose information should be recorded.
* @param changeFlags Additional information about what changes happened in the Adapter
* about the Item represented by this ViewHolder. For instance, if
* item is deleted from the adapter, {@link #FLAG_REMOVED} will be set.
* @param payloads The payload list that was previously passed to
* {@link Adapter#notifyItemChanged(int, Object)} or
* {@link Adapter#notifyItemRangeChanged(int, int, Object)}.
* @return An ItemHolderInfo instance that preserves necessary information about the
* ViewHolder. This object will be passed back to related <code>animate**</code> methods
* after layout is complete.
* @see #recordPostLayoutInformation(State, ViewHolder)
* @see #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
* @see #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
* @see #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)
* @see #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo)
*/
public @NonNull
ItemHolderInfo recordPreLayoutInformation(@NonNull State state,
@NonNull ViewHolder viewHolder, @AdapterChanges int changeFlags,
@NonNull List<Object> payloads) {
return obtainHolderInfo().setFrom(viewHolder);
}
/**
* Called by the RecyclerView after the layout is complete. Item animator should record
* necessary information about the View's final state.
* <p>
* The data returned from this method will be passed to the related <code>animate**</code>
* methods.
* <p>
* The default implementation returns an {@link ItemHolderInfo} which holds the bounds of
* the View.
*
* @param state The current State of RecyclerView which includes some useful data about
* the layout that will be calculated.
* @param viewHolder The ViewHolder whose information should be recorded.
* @return An ItemHolderInfo that preserves necessary information about the ViewHolder.
* This object will be passed back to related <code>animate**</code> methods when
* RecyclerView decides how items should be animated.
* @see #recordPreLayoutInformation(State, ViewHolder, int, List)
* @see #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
* @see #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
* @see #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)
* @see #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo)
*/
public @NonNull
ItemHolderInfo recordPostLayoutInformation(@NonNull State state,
@NonNull ViewHolder viewHolder) {
return obtainHolderInfo().setFrom(viewHolder);
}
/**
* Called by the RecyclerView when a ViewHolder has disappeared from the layout.
* <p>
* This means that the View was a child of the LayoutManager when layout started but has
* been removed by the LayoutManager. It might have been removed from the adapter or simply
* become invisible due to other factors. You can distinguish these two cases by checking
* the change flags that were passed to
* {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}.
* <p>
* Note that when a ViewHolder both changes and disappears in the same layout pass, the
* animation callback method which will be called by the RecyclerView depends on the
* ItemAnimator's decision whether to re-use the same ViewHolder or not, and also the
* LayoutManager's decision whether to layout the changed version of a disappearing
* ViewHolder or not. RecyclerView will call
* {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)
* animateChange} instead of {@code animateDisappearance} if and only if the ItemAnimator
* returns {@code false} from
* {@link #canReuseUpdatedViewHolder(ViewHolder) canReuseUpdatedViewHolder} and the
* LayoutManager lays out a new disappearing view that holds the updated information.
* Built-in LayoutManagers try to avoid laying out updated versions of disappearing views.
* <p>
* If LayoutManager supports predictive animations, it might provide a target disappear
* location for the View by laying it out in that location. When that happens,
* RecyclerView will call {@link #recordPostLayoutInformation(State, ViewHolder)} and the
* response of that call will be passed to this method as the <code>postLayoutInfo</code>.
* <p>
* ItemAnimator must call {@link #dispatchAnimationFinished(ViewHolder)} when the animation
* is complete (or instantly call {@link #dispatchAnimationFinished(ViewHolder)} if it
* decides not to animate the view).
*
* @param viewHolder The ViewHolder which should be animated
* @param preLayoutInfo The information that was returned from
* {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}.
* @param postLayoutInfo The information that was returned from
* {@link #recordPostLayoutInformation(State, ViewHolder)}. Might be
* null if the LayoutManager did not layout the item.
* @return true if a later call to {@link #runPendingAnimations()} is requested,
* false otherwise.
*/
public abstract boolean animateDisappearance(@NonNull ViewHolder viewHolder,
@NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo);
/**
* Called by the RecyclerView when a ViewHolder is added to the layout.
* <p>
* In detail, this means that the ViewHolder was <b>not</b> a child when the layout started
* but has been added by the LayoutManager. It might be newly added to the adapter or
* simply become visible due to other factors.
* <p>
* ItemAnimator must call {@link #dispatchAnimationFinished(ViewHolder)} when the animation
* is complete (or instantly call {@link #dispatchAnimationFinished(ViewHolder)} if it
* decides not to animate the view).
*
* @param viewHolder The ViewHolder which should be animated
* @param preLayoutInfo The information that was returned from
* {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}.
* Might be null if Item was just added to the adapter or
* LayoutManager does not support predictive animations or it could
* not predict that this ViewHolder will become visible.
* @param postLayoutInfo The information that was returned from {@link
* #recordPreLayoutInformation(State, ViewHolder, int, List)}.
* @return true if a later call to {@link #runPendingAnimations()} is requested,
* false otherwise.
*/
public abstract boolean animateAppearance(@NonNull ViewHolder viewHolder,
@Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo);
/**
* Called by the RecyclerView when a ViewHolder is present in both before and after the
* layout and RecyclerView has not received a {@link Adapter#notifyItemChanged(int)} call
* for it or a {@link Adapter#notifyDataSetChanged()} call.
* <p>
* This ViewHolder still represents the same data that it was representing when the layout
* started but its position / size may be changed by the LayoutManager.
* <p>
* If the Item's layout position didn't change, RecyclerView still calls this method because
* it does not track this information (or does not necessarily know that an animation is
* not required). Your ItemAnimator should handle this case and if there is nothing to
* animate, it should call {@link #dispatchAnimationFinished(ViewHolder)} and return
* <code>false</code>.
* <p>
* ItemAnimator must call {@link #dispatchAnimationFinished(ViewHolder)} when the animation
* is complete (or instantly call {@link #dispatchAnimationFinished(ViewHolder)} if it
* decides not to animate the view).
*
* @param viewHolder The ViewHolder which should be animated
* @param preLayoutInfo The information that was returned from
* {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}.
* @param postLayoutInfo The information that was returned from {@link
* #recordPreLayoutInformation(State, ViewHolder, int, List)}.
* @return true if a later call to {@link #runPendingAnimations()} is requested,
* false otherwise.
*/
public abstract boolean animatePersistence(@NonNull ViewHolder viewHolder,
@NonNull ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo);
/**
* Called by the RecyclerView when an adapter item is present both before and after the
* layout and RecyclerView has received a {@link Adapter#notifyItemChanged(int)} call
* for it. This method may also be called when
* {@link Adapter#notifyDataSetChanged()} is called and adapter has stable ids so that
* RecyclerView could still rebind views to the same ViewHolders. If viewType changes when
* {@link Adapter#notifyDataSetChanged()} is called, this method <b>will not</b> be called,
* instead, {@link #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)} will be
* called for the new ViewHolder and the old one will be recycled.
* <p>
* If this method is called due to a {@link Adapter#notifyDataSetChanged()} call, there is
* a good possibility that item contents didn't really change but it is rebound from the
* adapter. {@link DefaultItemAnimator} will skip animating the View if its location on the
* screen didn't change and your animator should handle this case as well and avoid creating
* unnecessary animations.
* <p>
* When an item is updated, ItemAnimator has a chance to ask RecyclerView to keep the
* previous presentation of the item as-is and supply a new ViewHolder for the updated
* presentation (see: {@link #canReuseUpdatedViewHolder(ViewHolder, List)}.
* This is useful if you don't know the contents of the Item and would like
* to cross-fade the old and the new one ({@link DefaultItemAnimator} uses this technique).
* <p>
* When you are writing a custom item animator for your layout, it might be more performant
* and elegant to re-use the same ViewHolder and animate the content changes manually.
* <p>
* When {@link Adapter#notifyItemChanged(int)} is called, the Item's view type may change.
* If the Item's view type has changed or ItemAnimator returned <code>false</code> for
* this ViewHolder when {@link #canReuseUpdatedViewHolder(ViewHolder, List)} was called, the
* <code>oldHolder</code> and <code>newHolder</code> will be different ViewHolder instances
* which represent the same Item. In that case, only the new ViewHolder is visible
* to the LayoutManager but RecyclerView keeps old ViewHolder attached for animations.
* <p>
* ItemAnimator must call {@link #dispatchAnimationFinished(ViewHolder)} for each distinct
* ViewHolder when their animation is complete
* (or instantly call {@link #dispatchAnimationFinished(ViewHolder)} if it decides not to
* animate the view).
* <p>
* If oldHolder and newHolder are the same instance, you should call
* {@link #dispatchAnimationFinished(ViewHolder)} <b>only once</b>.
* <p>
* Note that when a ViewHolder both changes and disappears in the same layout pass, the
* animation callback method which will be called by the RecyclerView depends on the
* ItemAnimator's decision whether to re-use the same ViewHolder or not, and also the
* LayoutManager's decision whether to layout the changed version of a disappearing
* ViewHolder or not. RecyclerView will call
* {@code animateChange} instead of
* {@link #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
* animateDisappearance} if and only if the ItemAnimator returns {@code false} from
* {@link #canReuseUpdatedViewHolder(ViewHolder) canReuseUpdatedViewHolder} and the
* LayoutManager lays out a new disappearing view that holds the updated information.
* Built-in LayoutManagers try to avoid laying out updated versions of disappearing views.
*
* @param oldHolder The ViewHolder before the layout is started, might be the same
* instance with newHolder.
* @param newHolder The ViewHolder after the layout is finished, might be the same
* instance with oldHolder.
* @param preLayoutInfo The information that was returned from
* {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}.
* @param postLayoutInfo The information that was returned from {@link
* #recordPreLayoutInformation(State, ViewHolder, int, List)}.
* @return true if a later call to {@link #runPendingAnimations()} is requested,
* false otherwise.
*/
public abstract boolean animateChange(@NonNull ViewHolder oldHolder,
@NonNull ViewHolder newHolder,
@NonNull ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo);
@AdapterChanges
static int buildAdapterChangeFlagsForAnimations(ViewHolder viewHolder) {
int flags = viewHolder.mFlags & (FLAG_INVALIDATED | FLAG_REMOVED | FLAG_CHANGED);
if (viewHolder.isInvalid()) {
return FLAG_INVALIDATED;
}
if ((flags & FLAG_INVALIDATED) == 0) {
final int oldPos = viewHolder.getOldPosition();
final int pos = viewHolder.getAbsoluteAdapterPosition();
if (oldPos != NO_POSITION && pos != NO_POSITION && oldPos != pos) {
flags |= FLAG_MOVED;
}
}
return flags;
}
/**
* Called when there are pending animations waiting to be started. This state
* is governed by the return values from
* {@link #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
* animateAppearance()},
* {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)
* animateChange()}
* {@link #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo)
* animatePersistence()}, and
* {@link #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
* animateDisappearance()}, which inform the RecyclerView that the ItemAnimator wants to be
* called later to start the associated animations. runPendingAnimations() will be scheduled
* to be run on the next frame.
*/
public abstract void runPendingAnimations();
/**
* Method called when an animation on a view should be ended immediately.
* This could happen when other events, like scrolling, occur, so that
* animating views can be quickly put into their proper end locations.
* Implementations should ensure that any animations running on the item
* are canceled and affected properties are set to their end values.
* Also, {@link #dispatchAnimationFinished(ViewHolder)} should be called for each finished
* animation since the animations are effectively done when this method is called.
*
* @param item The item for which an animation should be stopped.
*/
public abstract void endAnimation(@NonNull ViewHolder item);
/**
* Method called when all item animations should be ended immediately.
* This could happen when other events, like scrolling, occur, so that
* animating views can be quickly put into their proper end locations.
* Implementations should ensure that any animations running on any items
* are canceled and affected properties are set to their end values.
* Also, {@link #dispatchAnimationFinished(ViewHolder)} should be called for each finished
* animation since the animations are effectively done when this method is called.
*/
public abstract void endAnimations();
/**
* Method which returns whether there are any item animations currently running.
* This method can be used to determine whether to delay other actions until
* animations end.
*
* @return true if there are any item animations currently running, false otherwise.
*/
public abstract boolean isRunning();
/**
* Method to be called by subclasses when an animation is finished.
* <p>
* For each call RecyclerView makes to
* {@link #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
* animateAppearance()},
* {@link #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo)
* animatePersistence()}, or
* {@link #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
* animateDisappearance()}, there
* should
* be a matching {@link #dispatchAnimationFinished(ViewHolder)} call by the subclass.
* <p>
* For {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)
* animateChange()}, subclass should call this method for both the <code>oldHolder</code>
* and <code>newHolder</code> (if they are not the same instance).
*
* @param viewHolder The ViewHolder whose animation is finished.
* @see #onAnimationFinished(ViewHolder)
*/
public final void dispatchAnimationFinished(@NonNull ViewHolder viewHolder) {
onAnimationFinished(viewHolder);
if (mListener != null) {
mListener.onAnimationFinished(viewHolder);
}
}
/**
* Called after {@link #dispatchAnimationFinished(ViewHolder)} is called by the
* ItemAnimator.
*
* @param viewHolder The ViewHolder whose animation is finished. There might still be other
* animations running on this ViewHolder.
* @see #dispatchAnimationFinished(ViewHolder)
*/
public void onAnimationFinished(@NonNull ViewHolder viewHolder) {
}
/**
* Method to be called by subclasses when an animation is started.
* <p>
* For each call RecyclerView makes to
* {@link #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
* animateAppearance()},
* {@link #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo)
* animatePersistence()}, or
* {@link #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
* animateDisappearance()}, there should be a matching
* {@link #dispatchAnimationStarted(ViewHolder)} call by the subclass.
* <p>
* For {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)
* animateChange()}, subclass should call this method for both the <code>oldHolder</code>
* and <code>newHolder</code> (if they are not the same instance).
* <p>
* If your ItemAnimator decides not to animate a ViewHolder, it should call
* {@link #dispatchAnimationFinished(ViewHolder)} <b>without</b> calling
* {@link #dispatchAnimationStarted(ViewHolder)}.
*
* @param viewHolder The ViewHolder whose animation is starting.
* @see #onAnimationStarted(ViewHolder)
*/
public final void dispatchAnimationStarted(@NonNull ViewHolder viewHolder) {
onAnimationStarted(viewHolder);
}
/**
* Called when a new animation is started on the given ViewHolder.
*
* @param viewHolder The ViewHolder which started animating. Note that the ViewHolder
* might already be animating and this might be another animation.
* @see #dispatchAnimationStarted(ViewHolder)
*/
public void onAnimationStarted(@NonNull ViewHolder viewHolder) {
}
/**
* Like {@link #isRunning()}, this method returns whether there are any item
* animations currently running. Additionally, the listener passed in will be called
* when there are no item animations running, either immediately (before the method
* returns) if no animations are currently running, or when the currently running
* animations are {@link #dispatchAnimationsFinished() finished}.
*
* <p>Note that the listener is transient - it is either called immediately and not
* stored at all, or stored only until it is called when running animations
* are finished sometime later.</p>
*
* @param listener A listener to be called immediately if no animations are running
* or later when currently-running animations have finished. A null
* listener is
* equivalent to calling {@link #isRunning()}.
* @return true if there are any item animations currently running, false otherwise.
*/
public final boolean isRunning(@Nullable ItemAnimatorFinishedListener listener) {
boolean running = isRunning();
if (listener != null) {
if (!running) {
listener.onAnimationsFinished();
} else {
mFinishedListeners.add(listener);
}
}
return running;
}
/**
* When an item is changed, ItemAnimator can decide whether it wants to re-use
* the same ViewHolder for animations or RecyclerView should create a copy of the
* item and ItemAnimator will use both to run the animation (e.g. cross-fade).
* <p>
* Note that this method will only be called if the {@link ViewHolder} still has the same
* type ({@link Adapter#getItemViewType(int)}). Otherwise, ItemAnimator will always receive
* both {@link ViewHolder}s in the
* {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)} method.
* <p>
* If your application is using change payloads, you can override
* {@link #canReuseUpdatedViewHolder(ViewHolder, List)} to decide based on payloads.
*
* @param viewHolder The ViewHolder which represents the changed item's old content.
* @return True if RecyclerView should just rebind to the same ViewHolder or false if
* RecyclerView should create a new ViewHolder and pass this ViewHolder to the
* ItemAnimator to animate. Default implementation returns <code>true</code>.
* @see #canReuseUpdatedViewHolder(ViewHolder, List)
*/
public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder) {
return true;
}
/**
* When an item is changed, ItemAnimator can decide whether it wants to re-use
* the same ViewHolder for animations or RecyclerView should create a copy of the
* item and ItemAnimator will use both to run the animation (e.g. cross-fade).
* <p>
* Note that this method will only be called if the {@link ViewHolder} still has the same
* type ({@link Adapter#getItemViewType(int)}). Otherwise, ItemAnimator will always receive
* both {@link ViewHolder}s in the
* {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)} method.
*
* @param viewHolder The ViewHolder which represents the changed item's old content.
* @param payloads A non-null list of merged payloads that were sent with change
* notifications. Can be empty if the adapter is invalidated via
* {@link RecyclerView.Adapter#notifyDataSetChanged()}. The same list of
* payloads will be passed into
* {@link RecyclerView.Adapter#onBindViewHolder(ViewHolder, int, List)}
* method <b>if</b> this method returns <code>true</code>.
* @return True if RecyclerView should just rebind to the same ViewHolder or false if
* RecyclerView should create a new ViewHolder and pass this ViewHolder to the
* ItemAnimator to animate. Default implementation calls
* {@link #canReuseUpdatedViewHolder(ViewHolder)}.
* @see #canReuseUpdatedViewHolder(ViewHolder)
*/
public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder,
@NonNull List<Object> payloads) {
return canReuseUpdatedViewHolder(viewHolder);
}
/**
* This method should be called by ItemAnimator implementations to notify
* any listeners that all pending and active item animations are finished.
*/
public final void dispatchAnimationsFinished() {
final int count = mFinishedListeners.size();
for (int i = 0; i < count; ++i) {
mFinishedListeners.get(i).onAnimationsFinished();
}
mFinishedListeners.clear();
}
/**
* Returns a new {@link ItemHolderInfo} which will be used to store information about the
* ViewHolder. This information will later be passed into <code>animate**</code> methods.
* <p>
* You can override this method if you want to extend {@link ItemHolderInfo} and provide
* your own instances.
*
* @return A new {@link ItemHolderInfo}.
*/
@NonNull
public ItemHolderInfo obtainHolderInfo() {
return new ItemHolderInfo();
}
/**
* The interface to be implemented by listeners to animation events from this
* ItemAnimator. This is used internally and is not intended for developers to
* create directly.
*/
interface ItemAnimatorListener {
void onAnimationFinished(@NonNull ViewHolder item);
}
/**
* This interface is used to inform listeners when all pending or running animations
* in an ItemAnimator are finished. This can be used, for example, to delay an action
* in a data set until currently-running animations are complete.
*
* @see #isRunning(ItemAnimatorFinishedListener)
*/
public interface ItemAnimatorFinishedListener {
/**
* Notifies when all pending or running animations in an ItemAnimator are finished.
*/
void onAnimationsFinished();
}
/**
* A simple data structure that holds information about an item's bounds.
* This information is used in calculating item animations. Default implementation of
* {@link #recordPreLayoutInformation(RecyclerView.State, ViewHolder, int, List)} and
* {@link #recordPostLayoutInformation(RecyclerView.State, ViewHolder)} returns this data
* structure. You can extend this class if you would like to keep more information about
* the Views.
* <p>
* If you want to provide your own implementation but still use `super` methods to record
* basic information, you can override {@link #obtainHolderInfo()} to provide your own
* instances.
*/
public static class ItemHolderInfo {
/**
* The left edge of the View (excluding decorations)
*/
public int left;
/**
* The top edge of the View (excluding decorations)
*/
public int top;
/**
* The right edge of the View (excluding decorations)
*/
public int right;
/**
* The bottom edge of the View (excluding decorations)
*/
public int bottom;
/**
* The change flags that were passed to
* {@link #recordPreLayoutInformation(RecyclerView.State, ViewHolder, int, List)}.
*/
@AdapterChanges
public int changeFlags;
public ItemHolderInfo() {
}
/**
* Sets the {@link #left}, {@link #top}, {@link #right} and {@link #bottom} values from
* the given ViewHolder. Clears all {@link #changeFlags}.
*
* @param holder The ViewHolder whose bounds should be copied.
* @return This {@link ItemHolderInfo}
*/
@NonNull
public ItemHolderInfo setFrom(@NonNull RecyclerView.ViewHolder holder) {
return setFrom(holder, 0);
}
/**
* Sets the {@link #left}, {@link #top}, {@link #right} and {@link #bottom} values from
* the given ViewHolder and sets the {@link #changeFlags} to the given flags parameter.
*
* @param holder The ViewHolder whose bounds should be copied.
* @param flags The adapter change flags that were passed into
* {@link #recordPreLayoutInformation(RecyclerView.State, ViewHolder, int,
* List)}.
* @return This {@link ItemHolderInfo}
*/
@NonNull
public ItemHolderInfo setFrom(@NonNull RecyclerView.ViewHolder holder,
@AdapterChanges int flags) {
final View view = holder.itemView;
this.left = view.getLeft();
this.top = view.getTop();
this.right = view.getRight();
this.bottom = view.getBottom();
return this;
}
}
}
@Override
protected int getChildDrawingOrder(int childCount, int i) {
if (mChildDrawingOrderCallback == null) {
return super.getChildDrawingOrder(childCount, i);
} else {
return mChildDrawingOrderCallback.onGetChildDrawingOrder(childCount, i);
}
}
/**
* A callback interface that can be used to alter the drawing order of RecyclerView children.
* <p>
* It works using the {@link ViewGroup#getChildDrawingOrder(int, int)} method, so any case
* that applies to that method also applies to this callback. For example, changing the drawing
* order of two views will not have any effect if their elevation values are different since
* elevation overrides the result of this callback.
*/
public interface ChildDrawingOrderCallback {
/**
* Returns the index of the child to draw for this iteration. Override this
* if you want to change the drawing order of children. By default, it
* returns i.
*
* @param childCount The total number of children.
* @param i The current iteration.
* @return The index of the child to draw this iteration.
* @see RecyclerView#setChildDrawingOrderCallback(RecyclerView.ChildDrawingOrderCallback)
*/
int onGetChildDrawingOrder(int childCount, int i);
}
private NestedScrollingChildHelper getScrollingChildHelper() {
if (mScrollingChildHelper == null) {
mScrollingChildHelper = new NestedScrollingChildHelper(this);
}
return mScrollingChildHelper;
}
@RequiresApi(35)
private static final class Api35Impl {
@DoNotInline
public static void setFrameContentVelocity(View view, float velocity) {
try {
view.setFrameContentVelocity(velocity);
} catch (LinkageError e) {
// The setFrameContentVelocity method is unavailable on this device.
}
}
}
}