Lambda. :
# warehouse address: https://github.com/lzyprime/android_demos/tree/recyclerview
git clone -b recyclerview https://github.com/lzyprime/android_demos
Copy the code
RecyclerView as Android list items display components. Compared to ListView, the caching mechanism is more detailed to improve the fluency. Space for time
Two important parameters:
LayoutManager
: typesettingRecyclerView.Adapter
: Indicates how to obtain a list item
LayoutManager
LayoutManager can be configured directly in XML. It can also be set in logical code.
// xml
<androidx.recyclerview.widget.RecyclerView
.
// LayoutManagertypeapp:layoutManager="androidx.recyclerview.widget.GridLayoutManager"/ / a few barapp:spanCount="1"
/>
Copy the code
All configurable parameters:
1. LinearLayoutManager
public class LinearLayoutManager extends RecyclerView.LayoutManager implements
ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider
Copy the code
Single column linear layout. It cannot be displayed in multiple columns. Constructor arguments:
- Orientation: the direction
- ReverseLayout: Reverses list items
StackFromEnd to compatible with android. Widget. AbsListView. SetStackFromBottom (Boolean). Equivalent to reverseLayout.
At the same time realize the ItemTouchHelper ViewDropHandler, RecyclerView. SmoothScroller. ScrollVectorProvider
2. GridLayoutManager
public class GridLayoutManager extends LinearLayoutManager
Copy the code
Grid layout. LinearLayoutManager is an updated version that can be set in several columns using spanCount
3. StaggeredGridLayoutManager
public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager implements
RecyclerView.SmoothScroller.ScrollVectorProvider
Copy the code
Flow layout. When the list items are of different sizes, the GridLayoutManager determines the grid size based on the larger items. Smaller items will have blank parts. Compact StaggeredGridLayoutManager is joining together each. Run setGapStrategy(int) to set the clearance processing strategy.
Adapter
RecyclerView.Adapter<VH : RecyclerView.ViewHolder>
public abstract static class Adapter<VH extends ViewHolder> {...@NonNull
public abstract VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType);
public abstract void onBindViewHolder(@NonNull VH holder, int position);
public abstract int getItemCount(a);
}
public abstract static class ViewHolder {
public ViewHolder(@NonNull View itemView) {... }}Copy the code
An Adapter needs to override at least three of these functions.
getItemCount
Returns the number of list items.
onCreateViewHolder
.getItemViewType
Create a ViewHolder. If the ViewHolder has multiple types, you can use the viewType argument. The value of viewType comes from the getItemViewType(position: Int) function. 0 is returned by default. 0 <= position < getItemCount()
Take chat messages for example:
sealed class Msg {
data class Text(val content: String) : Msg()
data class Image(val url: String) : Msg()
data class Video(...). : Msg() ... }class MsgListAdapter : RecyclerView.Adapter<MsgListAdapter.MsgViewHolder>() {
sealed class MsgViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
class Text(...). : MsgViewHolder(...)class Image(...). : MsgViewHolder(...) . }private var dataList: List<Msg> = listOf()
override fun getItemViewType(position: Int): Int =
when (dataList[position]) {
is Msg.Text -> 1
is Msg.Image -> 2. }override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MsgViewHolder =
when (viewType) {
1 -> MsgViewHolder.Text(...)
2-> MsgViewHolder.Image(...) . }}Copy the code
onBindViewHolder
View created, start binding data. This includes event listener registration.
class VBViewHolder<VB : ViewBinding>(private val binding : VB) : ViewHolder(binding.root) {
fun bind(data: T, onClick:() -> Unit) {
binding.data = data. binding.anyView.setOnClickListener { onClick() } ... }}class Adapter(private val onItemClick: () -> Unit) : RecyclerView.Adapter<VBViewHolder<XXX>>() {
override fun onBindViewHolder(holder: VBViewHolder<XXX>, position: Int) =
holder.bindHolder(dataList[position], onItemClick)
}
Copy the code
update
Due to caching, the ViewHolder is not flushed immediately after the data source is updated. The list items that have changed need to be explicitly notified through the Adapter’s series of methods.
notifyDataSetChanged()
notifyItemChanged(position: Int), notifyItemChanged(position: Int, payload: Any?)
notifyItemRangeChanged(positionStart: Int, itemCount: Int), notifyItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?)
notifyItemMoved(fromPosition: Int, toPosition: Int)
notifyItemInserted(position: Int)
notifyItemRangeInserted(positionStart: Int, itemCount: Int)
notifyItemRemoved(position: Int)
notifyItemRangeRemoved(positionStart: Int, itemCount: Int)
payload: Any? The local refresh of the View is implemented in conjunction with the Adapter onBindViewHolder(holder: VH, position: Int, payloads: MutableList
). Otherwise, onBindViewHolder(holder: VBViewHolder
, position: Int)
Caching mechanisms
The main logic is RecyclerView.Recycler. The main cache is Scrap, CachedView, RecycledViewPool. ViewCacheExtension is used for additional custom caches.
Scrap
: The part currently being displayed.CachedView
: The part of the display area just marked, the default maximum storageDEFAULT_CACHE_SIZE = 2
.FIFO
updateRecycledViewPool
:CachedView
After elimination, only retainedViewHolder
To clear the data binding. Re-execution is required for reuseonBindViewHolder
.
RecycledViewPool is a SparseArray
subscript holder.viewType. ScrapData inserts ArrayList
. DEFAULT_MAX_SCRAP = 5 ViewHolder. RecycledViewPool ~= SparseArray
>
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; . }Copy the code
public static class RecycledViewPool {
private static final int DEFAULT_MAX_SCRAP = 5;
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<>();
private int mAttachCount = 0; . }Copy the code
Take,getViewForPosition
Follow this function to get an idea of how the levels of caching work together.
@NonNull
public View getViewForPosition(int position) {
return getViewForPosition(position, false);
}
View getViewForPosition(int position, boolean dryRun) {
return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
Copy the code
@Nullable
RecyclerView.ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {...boolean fromScrapOrHiddenOrCache = false;
RecyclerView.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) {
final intoffsetPosition = mAdapterHelper.findPositionOffset(position); .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&& mViewCacheExtension ! =null) {
// We are NOT sending the offsetPosition because LayoutManager does not
// know it.
final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type); . }if (holder == null) { // fallback to pool. holder = getRecycledViewPool().getRecycledView(type); . }if (holder == null) {... holder = mAdapter.createViewHolder(RecyclerView.this, type); . }}...return holder;
}
Copy the code
getChangedScrapViewForPosition
getScrapOrHiddenOrCachedHolderForPosition
getScrapOrCachedViewForId
mViewCacheExtension.getViewForPositionAndType
getRecycledViewPool().getRecycledView(type)
mAdapter.createViewHolder(RecyclerView.this, type)
To put,recycleView
Follow this function to learn about the caching process and strategy
public void recycleView(@NonNull View view) { ViewHolder holder = getChildViewHolderInt(view); ./ / clear the flagrecycleViewHolderInternal(holder); . }void recycleViewHolderInternal(ViewHolder holder) {...final boolean transientStatePreventsRecycling = holder.doesTransientStatePreventRecycling();
@SuppressWarnings("unchecked") final booleanforceRecycle = mAdapter ! =null && transientStatePreventsRecycling && mAdapter.onFailedToRecycleView(holder);
boolean cached = false;
boolean recycled = false;
if (forceRecycle || holder.isRecyclable()) {
if (mViewCacheMax > 0&&! holder.hasAnyOfTheFlags(...) ) {// 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{...// Log
}
// even if the holder is not removed, we still call this method so that it is removed
// from view holder lists.
mViewInfoStore.removeViewHolder(holder);
if(! cached && ! recycled && transientStatePreventsRecycling) { holder.mBindingAdapter =null;
holder.mOwnerRecyclerView = null; }}Copy the code
mCachedViews.add(targetCacheIndex, holder)
addViewHolderToRecycledViewPool
Simplify & encapsulate & Tools
An Adapter implementation, for the most part, focuses only on the procedure of onBindViewHolder and the notify update logic when data is updated. The rest of the operation is basically repetitive.
ListAdapter
Fun getItemCount() = dataList. Size () by default.
A DiffUtil.itemCallback
is required, internally constructed with mDiffer: AsyncListDiffer
to compare changes to list items and then automatically refresh.
Through submitList (the List < T >? Submit data.
Run getItem(position: Int): T = dataList[position] to obtain the data of the current position.
Data update and notify processes are omitted, and only onCreateViewHolder, onBindViewHolder are concerned.
PS: Pay attention to submitList() and passing references. Diff (previousList[index], currentList[index]). So if submitList() if the same List is submitted, the diff comparison will not work.
If you use the Paging3 paging library, there will be a PagingDataAdapter in the View layer, similar to the ListAdapter. Once data source PagingData is set, the list can automatically refresh, load more, and so on.
public abstract class ListAdapter<T.VH extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<VH> {
final AsyncListDiffer<T> mDiffer;
private finalAsyncListDiffer.ListListener<T> mListener = ... ;protected ListAdapter(@NonNull DiffUtil.ItemCallback<T> diffCallback) {... }protected ListAdapter(@NonNull AsyncDifferConfig<T> config) {... }public void submitList(@Nullable List<T> list) { mDiffer.submitList(list); }
public void submitList(@Nullable List<T> list, @Nullable final Runnable commitCallback) { mDiffer.submitList(list, commitCallback); }
protected T getItem(int position) { return mDiffer.getCurrentList().get(position); }
@Override public int getItemCount(a) { return mDiffer.getCurrentList().size(); }
@NonNull public List<T> getCurrentList(a) { return mDiffer.getCurrentList(); }
public void onCurrentListChanged(@NonNull List<T> previousList, @NonNull List<T> currentList) {}}Copy the code
DSL + ViewBinding
Keep simplifying.
-
Most ViewHolder is implemented by ViewBinding. So onCreateViewHolder() is basically a repeat operation.
-
The creation of a ViewBinding is basically the same: ViewBinding.inflate(…) . You can do it the old way in Android ViewBinding, DataBinding, by reflection. So we only need Adapter
and onCreateViewHolder().
-
The implementation of DiffUtil.itemCallback
is also essentially repetitive. Usually only two lambda expressions are needed to illustrate the situation.
// ViewHolder
data class BindingViewHolder<VB : ViewBinding>(val binding: VB) : RecyclerView.ViewHolder(binding.root)
Copy the code
// DiffUtil.ItemCallback<T>
inline fun <reified T> diffItemCallback(
crossinline areItemsTheSame: (oldItem: T.newItem: T) - >Boolean.crossinline areContentsTheSame: (oldItem: T.newItem: T) - >Boolean = { o, n -> o == n },
) = object : DiffUtil.ItemCallback<T>() {
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean =
areItemsTheSame(oldItem, newItem)
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean =
areContentsTheSame(oldItem, newItem)
}
Copy the code
// ListAdapter<T, VH : ViewHolder>
fun <T, VH : RecyclerView.ViewHolder> dslListAdapter(
diffItemCallback: DiffUtil.ItemCallback<T>,
createHolder: (parent: ViewGroup.viewType: Int) - >VH,
bindHolder: VH. (position: Int.data: T) - >Unit.) = object : ListAdapter<T, VH>(diffItemCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH =
createHolder(parent, viewType)
override fun onBindViewHolder(holder: VH, position: Int) =
holder.bindHolder(position, getItem(position))
}
Copy the code
/** * ListAdapter
> the view is synchronized to the VB's inflate
,>
inline fun <T, reified VB : ViewBinding> dslBindingListAdapter(
diffItemCallback: DiffUtil.ItemCallback<T>,
noinline inflate: ((parent: ViewGroup.viewType: Int) - >VB)? = null.crossinline bindHolder: VB. (position: Int.data: T) - >Unit.)= dslListAdapter( diffItemCallback, { p, v -> BindingViewHolder( inflate? .invoke(p, v) ? : VB::class.java.getMethod(
"inflate",
LayoutInflater::class.java,
ViewGroup::class.java,
Boolean: :class.java
).invoke(null, LayoutInflater.from(p.context), p, false) as VB
)
},
{ p, d -> binding.bindHolder(p, d) },
)
Copy the code
Use:
val adapter = dslBindingListAdapter<Comment, ListItemSingleLineTextBinding>(
diffItemCallback({ o, n -> o.id == n.id }, { o, n -> o == n }),
) { _, data ->
// this is ListItemSingleLineTextBinding,
// data: Comment(id: Int, content: String)
titleText.text = data
}
Copy the code
In addition, various libraries are also packaged. It is best to rely on (KSP, KAPT) annotations and compiler plug-ins to do code generation at compile time
ItemTouchHelper
List items slide and drag.
public class ItemTouchHelper extends RecyclerView.ItemDecoration implements RecyclerView.OnChildAttachStateChangeListener
Copy the code
// use:
ItemTouchHelper(callback: ItemTouchHelper.Callback).attachToRecyclerView(recyclerView: RecyclerView?)
Copy the code
ItemTouchHelper.Callback
START(LEFT), END(RIGHT), UP, DOWN.
You can customize the behavior during sliding and dragging by onChildDraw(), onChildDrawOver(), and so on.
object: ItemTouchHelper.Callback() {
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
// Return to the sliding and dragging directions
}
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
viewHolder // Drag holder
target // Passing through Holder
// Returns whether sliding is allowed
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
direction // Slide direction}}Copy the code
ItemTouchHelper.SimpleCallback
Itemtouchhelper. Callback The constructor passes in slide and drag directions. Just focus on the onMove() and onSwiped() processes.
public abstract static class SimpleCallback extends Callback {
public SimpleCallback(int dragDirs, int swipeDirs). }Copy the code
Custom behavior
override fun onChildDraw(
c: Canvas.// Canvas of the area occupied by the holder
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
dX: Float.// The x shift caused by the user action
dY: Float.// The y shift caused by the user action
actionState: Int./ / interaction types, swipe | drag
isCurrentlyActive: Boolean.// Whether the user is controlling
){... }Copy the code
onChildDraw
The default implementation oftranslationX = dX, translationY = dY
public void onDraw(Canvas c, RecyclerView recyclerView, View view, float dX, float dY,
int actionState, boolean isCurrentlyActive) {... view.setTranslationX(dX); view.setTranslationY(dY); }Copy the code
dX, dY:
The calculation rules of dX and dY should be read from the beginning, after attachToRecyclerView().
public void attachToRecyclerView(@Nullable RecyclerView recyclerView) {... setupCallbacks(); . }private void setupCallbacks(a) {... mRecyclerView.addOnItemTouchListener(mOnItemTouchListener); . }private final OnItemTouchListener mOnItemTouchListener = newOnItemTouchListener() { ... select(...) . };void select(@Nullable ViewHolder selected, int actionState) {... swipeIfNecessary(...) . }private int swipeIfNecessary(ViewHolder viewHolder) {
checkHorizontalSwipe(...)
checkVerticalSwipe(...)
}
/ / flags: the direction
private int checkHorizontalSwipe(ViewHolder viewHolder, int flags) {... mCallback.getSwipeVelocityThreshold(...)// Speed critical point
mCallback.getSwipeEscapeVelocity(...) // Minimum speed
final float threshold = mRecyclerView.getWidth() *
mCallback.getSwipeThreshold(viewHolder); // Position critical, default 0.5. }Copy the code
Swipe, and after you let go.
// Take horizontal sliding as an example:
// if the default behavior is dx == holder.translationx
val oldDX // The slide starts where it stopped last time. abs(oldDX) == 0 || abs(oldDX) == holder.width
// While sliding
val diffX: Int // The slide offset of the finger
dX = oldDX + diffX
// Release:
valIsshate = whether it exceeds speed critical point or position critical pointif(true) {
// If exceeded, dx final value is determined according to oldDX and sliding direction.
// Final value = underline the screen if it was previously unswiped. If the screen has not been drawn before, set to unswiped
// The value changes by animation completion
dx = anim(curDX -> (abs(oldDX) == 0 ? holder.width : 0) * (direction == LEFT ? -1 : 1))}else {
// If not, dx begins to revert to its initial value
dx = anim(curDX -> oldDX)
}
Copy the code
The final position will be selected according to whether the critical value is exceeded.
Demo: Add vibration, translucent effect, custom drawing, etc
override fun onChildDraw(
c: Canvas,
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
dX: Float,
dY: Float,
actionState: Int,
isCurrentlyActive: Boolean
) {
val midWidth = c.width / 2
val absCurrentX = abs(viewHolder.itemView.translationX)
/ / vibration
if (absCurrentX < midWidth && abs(dX) >= midWidth) {
val vibrator = requireContext().getSystemService(Vibrator::class.java) as Vibrator
if (vibrator.hasVibrator()) {
vibrator.vibrate(VibrationEffect.createOneShot(50.255))}}/ / translucent
viewHolder.itemView.alpha = if (absCurrentX >= midWidth) 0.5 f else 1f
/ / the background
if(dX ! =0f) {
c.drawRect(
0f,
viewHolder.itemView.top.toFloat(),
c.width.toFloat(),
viewHolder.itemView.bottom.toFloat(),
Paint().apply { color = Color.RED },
)
}
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
}
Copy the code
Slide twice to display the side menu
The hardest thing to control is the change in dX dY. Will point the speed getSwipeVelocityThreshold banned, only rely on position prediction whether sliding success. Also determine the loss of focus when the restoration.
You can write it, but it’s not stable. If you really need it, you can implement your own ItemTouchHelper, leaving most of the code untouched and changing the slide rule and the anim animation Settings after release.
ConcatAdapter
Adapter.
Need to introduce recyclerView library
implementation("androidx.recyclerview:recyclerview:latest")
Copy the code
public ConcatAdapter(@NonNull Adapter<? extends ViewHolder>... adapters)
public ConcatAdapter(@NonNull List<? extends Adapter<? extends ViewHolder>> adapters)
@SafeVarargs
public ConcatAdapter(
@NonNull Config config,
@NonNull Adapter<? extends ViewHolder>... adapters)
public ConcatAdapter(
@NonNull Config config,
@NonNull List<? extends Adapter<? extends ViewHolder>> adapters)
Copy the code
~ lambda. :
2.25, now 3.4. It’s a lot of content, and it’s broken, but it’s still time consuming to write a summary. Just looking at the source code, writing the demo, also took an afternoon, but the sorting will take so long.
There hasn’t been a need for a while. There are not many corporate client development requirements. And it was my personal hobby, but it was only temporary support due to lack of staff. As a result, it was getting further and further away, and it was almost impossible to go back to the back end.
Now there is no need to go back to the back end. Since work, the client (Kotlin, flutter) has spent more time writing than the back end. LeetCode is mostly written in languages (kotlin, scala, c++, rust). I usually play with Linux, but I haven’t written much about real development.
So, even if change a job, also can deliver client end, cast back end basic have no chance.