Android应用开发那些事高级UIandroid UI系列专题

郭霖的推荐:如何使用RecyclerView优雅地实现复杂列表效

2020-08-08  本文已影响0人  4ca1bbef6a0c

最近看见的好文章,文末有文中提到的代码获取地址,还有其他整套体系的学习资料《Android架构视频+BATZ面试专题PDF+学习笔记》,GitHub:https://github.com/xieyuliang/Note-Android希望对大家有帮助。

/ 今日科技快讯 /

发现一件很有意思的事情,今天这篇文章中介绍的RecyclerView,以及昨天文章中介绍的Lifecycles,它们共同的作者都是前天文章中介绍的Yigit Boyar大神。确实不是我有意为之,我都是按照投稿的顺序来安排推送的。而Yigit Boyar大神明天将会做客上海GDG,与大家进行一场问答式的技术活动。这种跟Google大神零距离接触的机会可不多,希望大家到时都能准时观看,我们明天见。/ 作者简介 /本篇文章来自秦川小将的投稿,给大家分享了如何使用RecyclerView实现复杂的列表,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章! 秦川小将的博客地址:

https://blog.csdn.net/mjb00000

/ 前言 /在RecyclerView实现多种Item类型列表时,有很多种实现方式,这里结合 AsyncListDiffer+DataBinding+Lifecycles 实现一种简单,方便,快捷并以数据驱动UI变化的MultiTypeAdapter

/ 效果图 /

定义一个基类MultiTypeBinder方便统一实现与管理MultiTypeBinder中部分函数说明:

继承MultiTypeBinder后进行Layout初始化和数据绑定及解绑处理。

<pre style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important; font-size: inherit; color: inherit; line-height: inherit;">abstract class MultiTypeBinder<V : ViewDataBinding> : ClickBinder() {

/**
 * BR.data
 */
protected open val variableId = BR.data

/**
 * 被绑定的ViewDataBinding
 */
open var binding: V? = null

/**
 * 给绑定的View设置tag
 */
private var bindingViewVersion = (0L until Long.MAX_VALUE).random()

/**
 * 返回LayoutId,供Adapter使用
 */
@LayoutRes
abstract fun layoutId(): Int

/**
 * 两次更新的Binder内容是否相同
 */
abstract fun areContentsTheSame(other: Any): Boolean

/**
 * 绑定ViewDataBinding
 */
fun bindViewDataBinding(binding: V) {
    // 如果此次绑定与已绑定的一至,则不做绑定
    if (this.binding === binding && binding.root.getTag(R.id.bindingVersion) == bindingViewVersion) return
    binding.root.setTag(R.id.bindingVersion, ++bindingViewVersion)
    onUnBindViewHolder()
    this.binding = binding
    binding.setVariable(variableId, this)
    // 给 binding 绑定生命周期,方便观察LiveData的值,进而更新UI。如果不绑定,LiveData的值改变时,UI不会更新
    if (binding.root.context is LifecycleOwner) {
        binding.lifecycleOwner = binding.root.context as LifecycleOwner
    } else {
        binding.lifecycleOwner = AlwaysActiveLifecycleOwner()
    }
    onBindViewHolder(binding)
    // 及时更新绑定数据的View
    binding.executePendingBindings()
}

/**
 * 解绑ViewDataBinding
 */
fun unbindDataBinding() {
    if (this.binding != null) {
        onUnBindViewHolder()
        this.binding = null
    }
}

/**
 * 绑定后对View的一些操作,如:赋值,修改属性
 */
protected open fun onBindViewHolder(binding: V) {

}

/**
 * 解绑操作
 */
protected open fun onUnBindViewHolder() {

}

/**
 * 为 Binder 绑定生命周期,在 {@link Lifecycle.Event#ON_RESUME} 时响应
 */
internal class AlwaysActiveLifecycleOwner : LifecycleOwner {

    override fun getLifecycle(): Lifecycle = object : LifecycleRegistry(this) {
        init {
            handleLifecycleEvent(Event.ON_RESUME)
        }
    }
}

}

在values中定义一个ids.xml文件,给 ViewDataBinding 中的 root View设置Tag。

<resources>
    <item name="bindingVersion" type="id" />
</resources> </pre>

处理MultiTypeBinder中View的点击事件

在ClickBinder中提供了两种事件点击方式 onClick 和 onLongClick,分别提供了携带参数和未带参数方法。

open class ClickBinder: OnViewClickListener {

    protected open var mOnClickListener: ((view: View, any: Any?) -> Unit)? = null

    protected open var mOnLongClickListener: ((view: View, any: Any?) -> Unit)? = null

    /**
     * 设置View点击事件
     */
    open fun setOnClickListener(listener: (view: View, any: Any?) -> Unit): ClickBinder {
        this.mOnClickListener = listener
        return this
    }

    /**
     * 设置View长按点击事件
     */
    open fun setOnLongClickListener(listener: (view: View, any: Any?) -> Unit): ClickBinder {
        this.mOnLongClickListener = listener
        return this
    }

    /**
     * 触发View点击事件时回调,携带参数
     */
    override fun onClick(view: View) {
        onClick(view, this)
    }

    override fun onClick(view: View, any: Any?) {
        if (mOnClickListener != null) {
            mOnClickListener?.invoke(view, any)
        } else {
            if (BuildConfig.DEBUG) throw NullPointerException("OnClick事件未绑定!")
        }
    }

    /**
     * 触发View长按事件时回调,携带参数
     */
    override fun onLongClick(view: View) {
        onLongClick(view, this)
    }

    override fun onLongClick(view: View, any: Any?){
        if (mOnLongClickListener != null) {
            mOnLongClickListener?.invoke(view, any)
        } else {
            throw NullPointerException("OnLongClick事件未绑定!")
        }
    }
}

定义MultiTypeViewHolder

MultiTypeViewHolder继承自RecyclerView.ViewHolder,传入一个ViewDataBinding对象,在这里对MultiTypeBinder中的ViewDataBinding对象进行解绑和绑定操作。

class MultiTypeViewHolder(private val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root), AutoCloseable {

    private var mAlreadyBinding: MultiTypeBinder<ViewDataBinding>? = null

    /**
     * 绑定Binder
     */
    fun onBindViewHolder(items: MultiTypeBinder<ViewDataBinding>) {
        // 如果两次绑定的 Binder 不一致,则直接销毁
        if (mAlreadyBinding != null && items !== mAlreadyBinding) close()
        // 开始绑定
        items.bindViewDataBinding(binding)
        // 保存绑定的 Binder
        mAlreadyBinding = items
    }

    /**
     * 销毁绑定的Binder
     */
    override fun close() {
        mAlreadyBinding?.unbindDataBinding()
        mAlreadyBinding = null
    }
} 

使用DiffUtil.ItemCallback进行差异性计算

在刷新列表时这里使用了DiffUtil.ItemCallback来做差异性计算,方法说明:

class DiffItemCallback<T : MultiTypeBinder<*>> : DiffUtil.ItemCallback<T>() {
    override fun areItemsTheSame(oldItem: T, newItem: T): Boolean = oldItem.layoutId() == newItem.layoutId()
    override fun areContentsTheSame(oldItem: T, newItem: T): Boolean = oldItem.hashCode() == newItem.hashCode() && oldItem.areContentsTheSame(newItem)
}

定义MultiTypeAdapter在MultiTypeAdapter中的逻辑实现思路如下:

// 这里将LayoutManager向外扩展,方便操作RecyclerView滚动平移等操作
class MultiTypeAdapter constructor(val layoutManager: RecyclerView.LayoutManager): RecyclerView.Adapter<MultiTypeViewHolder>() {

    // 使用后台线程通过差异性计算来更新列表
    private val mAsyncListChange by lazy { AsyncListDiffer(this, DiffItemCallback<MultiTypeBinder<*>>()) }

    // 存储 MultiTypeBinder 和 MultiTypeViewHolder Type
    private var mHashCodeViewType = LinkedHashMap<Int, MultiTypeBinder<*>>()

    init {
        setHasStableIds(true)
    }

    fun notifyAdapterChanged(binders: List<MultiTypeBinder<*>>) {
        mHashCodeViewType = LinkedHashMap()
        binders.forEach {
            mHashCodeViewType[it.hashCode()] = it
        }
        mAsyncListChange.submitList(mHashCodeViewType.map { it.value })
    }

    fun notifyAdapterChanged(binder: MultiTypeBinder<*>) {
        mHashCodeViewType = LinkedHashMap()
        mHashCodeViewType[binder.hashCode()] = binder
        mAsyncListChange.submitList(mHashCodeViewType.map { it.value })
    }

    override fun getItemViewType(position: Int): Int {
        val mItemBinder = mAsyncListChange.currentList[position]
        val mHasCode = mItemBinder.hashCode()
        // 如果Map中不存在当前Binder的hasCode,则向Map中添加当前类型的Binder
        if (!mHashCodeViewType.containsKey(mHasCode)) {
            mHashCodeViewType[mHasCode] = mItemBinder
        }
        return mHasCode
    }

    override fun getItemId(position: Int): Long = position.toLong()

    override fun getItemCount(): Int = mAsyncListChange.currentList.size

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MultiTypeViewHolder {
        try {
            return MultiTypeViewHolder(parent.inflateDataBinding(mHashCodeViewType[viewType]?.layoutId()!!))
        }catch (e: Exception){
            throw NullPointerException("不存在${mHashCodeViewType[viewType]}类型的ViewHolder!")
        }
    }

    @Suppress("UNCHECKED_CAST")
    override fun onBindViewHolder(holder: MultiTypeViewHolder, position: Int) {
        val mCurrentBinder = mAsyncListChange.currentList[position] as MultiTypeBinder<ViewDataBinding>
        holder.itemView.tag = mCurrentBinder.layoutId()
        holder.onBindViewHolder(mCurrentBinder)
    }
} 

定义扩展Adapters文件

>/**
 * 创建一个MultiTypeAdapter
 */
fun createMultiTypeAdapter(recyclerView: RecyclerView, layoutManager: RecyclerView.LayoutManager): MultiTypeAdapter {
    recyclerView.layoutManager = layoutManager
    val mMultiTypeAdapter = MultiTypeAdapter(layoutManager)
    recyclerView.adapter = mMultiTypeAdapter
    // 处理RecyclerView的触发回调
    recyclerView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
        override fun onViewDetachedFromWindow(v: View?) {
            mMultiTypeAdapter.onDetachedFromRecyclerView(recyclerView)
        }
        override fun onViewAttachedToWindow(v: View?) { }
    })
    return mMultiTypeAdapter
}

/**
 * MultiTypeAdapter扩展函数,重载MultiTypeAdapter类,使用invoke操作符调用MultiTypeAdapter内部函数。
 */
inline operator fun MultiTypeAdapter.invoke(block: MultiTypeAdapter.() -> Unit): MultiTypeAdapter {
    this.block()
    return this
}

/**
 * 将Layout转换成ViewDataBinding
 */
fun <T : ViewDataBinding> ViewGroup.inflateDataBinding(layoutId: Int): T = DataBindingUtil.inflate(LayoutInflater.from(context), layoutId, this, false)!!

/**
 * RecyclerView方向注解
 */
@IntDef(
    Orientation.VERTICAL,
    Orientation.HORIZONTAL
)
@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.SOURCE)
annotation class Orientation{

    companion object{
        const val VERTICAL = RecyclerView.VERTICAL
        const val HORIZONTAL = RecyclerView.HORIZONTAL
    }
} 

MultiTypeAdapter使用

创建 MultiTypeAdapter。

private val mAdapter by lazy { createMultiTypeAdapter(binding.recyclerView, LinearLayoutManager(this)) } 

将 Binder 添加到 Adapter 中。

mAdapter.notifyAdapterChanged(mutableListOf<MultiTypeBinder<*>>().apply {
    add(TopBannerBinder().apply {
        setOnClickListener(this@MainActivity::onClick)
    })
    add(CategoryContainerBinder(listOf("男装", "女装", "鞋靴", "内衣内饰", "箱包", "美妆护肤", "洗护", "腕表珠宝", "手机", "数码").map {
        CategoryItemBinder(it).apply {
            setOnClickListener(this@MainActivity::onClick)
        }
    }))
    add(RecommendContainerBinder((1..8).map { RecommendGoodsBinder().apply {
        setOnClickListener(this@MainActivity::onClick)
    } }))
    add(HorizontalScrollBinder((0..11).map { HorizontalItemBinder("$it").apply {
        setOnClickListener(this@MainActivity::onClick)
    } }))
    add(GoodsGridContainerBinder((1..20).map { GoodsBinder(it).apply {
        setOnClickListener(this@MainActivity::onClick)
    } }))
}) 

点击事件处理,在Activity或Fragment中实现 OnViewClickListener 接口,重写 onClick 方法。

 override fun onClick(view: View, any: Any?) {
        when(view.id) {
            R.id.top_banner -> {
                any as TopBannerBinder
                toast(view, "点击Banner")
            }
            R.id.category_tab -> {
                any as CategoryItemBinder
                toast(view,"点击分类+${any.title}")
            }
            R.id.recommend_goods -> {
                any as RecommendGoodsBinder
                toast(view, "点击精选会场Item")
            }
            R.id.theme_index -> {
                any as HorizontalItemBinder
                toast(view, "点击主题会场${any.index}")
            }
            R.id.goods_container -> {
                any as GoodsBinder
                toast(view, "点击商品${any.index}")
            }
        }
    }

AsyncListDiffer

一个在后台线程中使用DiffUtil计算两个列表之间的差异的辅助类。AsyncListDiffer 的计算主要submitList 方法中。Tip: 调用submitList()方法传递数据时,需要创建一个新的集合。

public class AsyncListDiffer<T> {

    // 省略其它代码......

    @SuppressWarnings("WeakerAccess")
    public void submitList(@Nullable final List<T> newList, @Nullable final Runnable commitCallback) {
        // 定义变量 runGeneration 递增生成,用于缓存当前预执行线程的次数的最大值
        final int runGeneration = ++mMaxScheduledGeneration;
        // 首先判断 newList 与 AsyncListDiffer 中缓存的数据集 mList 是否为同一个对象,如果是的话,直接返回。也就是说,调用 submitList() 方法所传递数据集时,需要new一个新的List。
        if (newList == mList) {
            // nothing to do (Note - still had to inc generation, since may have ongoing work)
            if (commitCallback != null) {
                commitCallback.run();
            }
            return;
        }

        final List<T> previousList = mReadOnlyList;

        // 判断 newList 是否为null。若 newList 为 null,将移除所有 Item 的操作并分发给 ListUpdateCallback,mList 置为 null,同时将只读List - mReadOnlyList 清空
        if (newList == null) {
            //noinspection ConstantConditions
            int countRemoved = mList.size();
            mList = null;
            mReadOnlyList = Collections.emptyList();
            // notify last, after list is updated
            mUpdateCallback.onRemoved(0, countRemoved);
            onCurrentListChanged(previousList, commitCallback);
            return;
        }

        // 判断 mList 是否为null。若 mList 为null,表示这是第一次向 Adapter 添加数据集,此时将添加最新数据集操的作分发给 ListUpdateCallback,将 mList 设置为 newList, 同时将 newList 赋值给 mReadOnlyList
        if (mList == null) {
            mList = newList;
            mReadOnlyList = Collections.unmodifiableList(newList);
            // notify last, after list is updated
            mUpdateCallback.onInserted(0, newList.size());
            onCurrentListChanged(previousList, commitCallback);
            return;
        }

        final List<T> oldList = mList;
        // 通过AsyncDifferConfig获取到一个后台线程,在后台线程中使用DiffUtil对两个List进行差异性比较
        mConfig.getBackgroundThreadExecutor().execute(new Runnable() {
            @Override
            public void run() {
                final DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() {
                    @Override
                    public int getOldListSize() {
                        return oldList.size();
                    }

                    @Override
                    public int getNewListSize() {
                        return newList.size();
                    }

                    @Override
                    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
                        T oldItem = oldList.get(oldItemPosition);
                        T newItem = newList.get(newItemPosition);
                        if (oldItem != null && newItem != null) {
                            return mConfig.getDiffCallback().areItemsTheSame(oldItem, newItem);
                        }
                        // If both items are null we consider them the same.
                        return oldItem == null && newItem == null;
                    }

                    @Override
                    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
                        T oldItem = oldList.get(oldItemPosition);
                        T newItem = newList.get(newItemPosition);
                        if (oldItem != null && newItem != null) {
                            return mConfig.getDiffCallback().areContentsTheSame(oldItem, newItem);
                        }
                        if (oldItem == null && newItem == null) {
                            return true;
                        }
                        throw new AssertionError();
                    }

                    @Nullable
                    @Override
                    public Object getChangePayload(int oldItemPosition, int newItemPosition) {
                        T oldItem = oldList.get(oldItemPosition);
                        T newItem = newList.get(newItemPosition);
                        if (oldItem != null && newItem != null) {
                            return mConfig.getDiffCallback().getChangePayload(oldItem, newItem);
                        }
                        throw new AssertionError();
                    }
                });
                // 使用AsyncDifferConfig中的主线程更新UI,先判断递增生成的 runGeneration 变量是否与 AsyncListDiffer 中当前与执行线程的次数的最大值是否相等,如果相等,将 newList 赋值给 mList ,将 newList添加到只读集合 mReadOnlyList 中,然后通知列表更新。
                mMainThreadExecutor.execute(new Runnable() {
                    @Override
                    public void run() {
                        if (mMaxScheduledGeneration == runGeneration) {
                            latchList(newList, result, commitCallback);
                        }
                    }
                });
            }
        });
    }

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    void latchList(@NonNull List<T> newList,  @NonNull DiffUtil.DiffResult diffResult,  @Nullable Runnable commitCallback) {
        final List<T> previousList = mReadOnlyList;
        mList = newList;
        // 将 newList 添加到 mReadOnlyList 中
        mReadOnlyList = Collections.unmodifiableList(newList);
        // 通知列表更新
        diffResult.dispatchUpdatesTo(mUpdateCallback);
        onCurrentListChanged(previousList, commitCallback);
    }

   // 省略其它代码......
} 

ListUpdateCallback

操作列表更新的接口,此类可与DiffUtil一起使用,以检测两个列表之间的变化。至于ListUpdateCallback接口具体做了那些事儿,切看以下函数:

onInserted 在指定位置插入Item时调用,position 指定位置, count 插入Item的数量。

void onInserted(int position, int count); 

onRemoved 在删除指定位置上的Item时调用,position 指定位置, count 删除的Item的数量。

void onRemoved(int position, int count); 

onMoved 当Item更改其在列表中的位置时调用, fromPosition 当前Item在移动之前的位置,toPosition 当前Item在移动之后的位置。

void onMoved(int fromPosition, int toPosition); </pre>

onChanged 在指定位置更新Item时调用,position 指定位置,count 要更新的Item个数,payload 可选参数,值为null时表示全部更新,否则表示局部更新。

void onChanged(int position, int count, @Nullable Object payload); 

ListUpdateCallback的实现类

AdapterListUpdateCallback

AdapterListUpdateCallback的作用是将更新事件调度回调给Adapter,如下:

public final class AdapterListUpdateCallback implements ListUpdateCallback {

    @NonNull
    private final RecyclerView.Adapter mAdapter;

    public AdapterListUpdateCallback(@NonNull RecyclerView.Adapter adapter) {
        mAdapter = adapter;
    }

    @Override
    public void onInserted(int position, int count) {
        mAdapter.notifyItemRangeInserted(position, count);
    }

    @Override
    public void onRemoved(int position, int count) {
        mAdapter.notifyItemRangeRemoved(position, count);
    }

    @Override
    public void onMoved(int fromPosition, int toPosition) {
        mAdapter.notifyItemMoved(fromPosition, toPosition);
    }

    @Override
    public void onChanged(int position, int count, Object payload) {
        mAdapter.notifyItemRangeChanged(position, count, payload);
    }
} 

AsyncDifferConfig

AsyncDifferConfig的角色很简单,是一个DiffUtil.ItemCallback的配置类,其内部创建了一个固定大小的线程池,提供了两种线程,即后台线程和主线程,主要用于差异性计算和更新UI。AsyncDifferConfig核心代码如下:

public final class AsyncDifferConfig<T> {

    // 省略其他代码...... 

    @SuppressWarnings("WeakerAccess")
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    @Nullable
    public Executor getMainThreadExecutor() {
        return mMainThreadExecutor;
    }

    @SuppressWarnings("WeakerAccess")
    @NonNull
    public Executor getBackgroundThreadExecutor() {
        return mBackgroundThreadExecutor;
    }

    @SuppressWarnings("WeakerAccess")
    @NonNull
    public DiffUtil.ItemCallback<T> getDiffCallback() {
        return mDiffCallback;
    }

    public static final class Builder<T> {

       // 省略其他代码...... 

        @NonNull
        public AsyncDifferConfig<T> build() {
            if (mBackgroundThreadExecutor == null) {
                synchronized (sExecutorLock) {
                    if (sDiffExecutor == null) {
                        // 创建一个固定大小的线程池
                        sDiffExecutor = Executors.newFixedThreadPool(2);
                    }
                }
                mBackgroundThreadExecutor = sDiffExecutor;
            }
            return new AsyncDifferConfig<>(
                    mMainThreadExecutor,
                    mBackgroundThreadExecutor,
                    mDiffCallback);
        }
       // 省略其他代码...... 
    }
} 

最近github访问特别慢,项目已经上传至码云,有兴趣的小伙伴可以前往下载看看,记得点赞哦~~~码云Gitee地址如下所示:

https://gitee.com/mengjingbo/multitype-adapter-sample

Github地址如下所示:

https://github.com/mengjingbo/multitype-adapter-sample

更多Android架构学习资料:

https://shimo.im/docs/vRdckVpk6XWRpCTp/

原文地址:https://mp.weixin.qq.com/s/SpCdNbBWXoOrUYA0HOeokg

上一篇下一篇

猜你喜欢

热点阅读