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:

  1. LayoutManager: typesetting
  2. RecyclerView.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:

  1. Orientation: the direction
  2. 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.FIFOupdate
  • RecycledViewPool: CachedViewAfter elimination, only retainedViewHolderTo 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

onChildDrawThe 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.