ANDROID 八月 02, 2022

Android • RecyclerView

文章字数 5.2k 阅读约需 9 mins. 阅读次数 0

引言

本篇将介绍我最近在对 Android 项目进行开发时,使用 RecyclerView 的一些笔记分享。

Google Android Developer 中对 RecyclerView 的介绍:

RecyclerView makes it easy to efficiently display large sets of data. You supply the data and define how each item looks, and the RecyclerView library dynamically creates the elements when they’re needed.

As the name implies, RecyclerView recycles those individual elements. When an item scrolls off the screen, RecyclerView doesn’t destroy its view. Instead, RecyclerView reuses the view for new items that have scrolled onscreen. This reuse vastly improves performance, improving your app’s responsiveness and reducing power consumption.


RecyclerView

ItemView状态改变(单选)

对点击Item的状态进行改变,实现类似RadioGroup的单选效果。

  • 适配器:
class RVAdapter(
    private val List: List<Data>,
    private val itemClickFunction: (position: Int) -> Unit // 将 itemView 点击事件暴露给外部
) : RecyclerView.Adapter<RVAdapter.RVVH>() {

    // 声明选中位置监听
    val selectedPosition: Int = 0

    inner class RVVH(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val binding = ItemVideoFaceBinding.bind(itemView)
        fun bindItemData(data: Data) {
            // ...
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FaceSwapVH {
        return RVVH(
            LayoutInflater.from(parent.context).inflate(
                R.layout.item_rv,
                parent,
                false
            )
        )
    }

    override fun onBindViewHolder(holder: RVAdapter.RVVH, position: Int) {

        val itemView = holder.itemView

        // 绑定你的 RecyclerView Item 数据
        val listItem = list[position]
        holder.bindData(listItem)

        // 处理你的 ItemView状态组件状态(若要对整个ItemView的状态进行修改,则直接修改 selectWidget 为 itemView 即可)
        // 这里只展示将状态修改为:可见 / 不可见
        if (selectedPosition == position) {
            itemView.findViewById<View>(R.id.selectWidget).visibility = View.VISIBLE
            // itemView.visibility = View.VISIBLE
        } else {
            itemView.findViewById<View>(R.id.selectWidget).visibility = View.INVISIBLE
            // itemView.visibility = View.INVISIBLE
        }

        itemView.setOnClickListener { 
           // 处理点击事件
           itemClickFunction.invoke(position)
        }
    }

    override fun getItemCount(): Int {
        return list.size
    }
}

  • Adapter处理ItemView点击事件:
RVAdapter() { position ->
    selectedPosition = position

    // 通知 RecyclerView 的 Item 发生变化
    notifyDataSetChanged()
}

获取指定位置 Item 的 childView

假如我们要规定让 RecyclerView 的某一个 itemView 展示特殊的 layout,如插入一条广告或者让其播放视频,则需要动态获取某一个position的itemView。

通常我们会使用 getChildAt(posotion) 方法获取 childView,该方法在带来便利的同时也有着巨大的缺陷。

我们很容易忽略 RecyclerView 的 “Recycler” 命名由来,若用户不滑动 RecyclerView,我们可以很方便的通过传入 position 获取当前 childView,但当用户开始滑动时,我们再次想通过 position 来获取 childView 则变得十分困难,由于 RecyclerView 的缓存与回收机制,旧的 childView 在不可见时会被缓存与回收,position 也因此会发生变化

建议的获取 childView 的代码 (Java) 如下:

// 此处我们模拟了一个使用 LinearLayoutManager 作为布局管理器的 RecyclerView
LinearLayoutManager layoutManager = (LinearLayoutManager) mRecyclerView.getLayoutManager();

// 使用 layoutManager 的 findFirstVisibleItemPosition 尝试获取当前第一个可见的Item
int firstItemPosition = layoutManager.findFirstVisibleItemPosition();

// 计算差值确定位置
int actualPosition = (position - firstItemPosition) >= 0 ? position - firstItemPosition : 0;

// 使用 layoutManager 的 findViewByPosition 方法,尝试获取 childView
View childView = layoutManager.findViewByPosition(actualPosition);

// 使用 childView
...

GridLayoutManager

等分间距

对除了第一个位置的 Item 设置左边与底边的边距,由于每行只有 spanCount 个 Item,故对于取余为0的 Item,设置左边距为0。

class GridSpacingItemDecoration(
    private val spanCount: Int,  // 列数
    private val spacing: Int,    // 间隔
) : RecyclerView.ItemDecoration() {

    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        outRect.left = spacing;
        outRect.bottom = spacing;
        if (parent.getChildLayoutPosition(view) % spanCount == 0) {
            outRect.left = 0;
        }
    }
}

事件穿透

当我们在一个已有的 View 上重叠使用 RecyclerView 时,通常会遇到事件穿透问题,这个问题通常也出现在多控件搭配使用的使用场景,对于 RecyclerView 的事件穿透,假设我们有如下例子:

当我们使用 RecyclerView 尝试实现类似下拉列表(与 Android 中的 Spinner、ExpandableListView 类似)时,下拉的RecyclerVire通常会重叠在其余控件之上,此时容易导致事件穿透问题(底部布局在 RecyclerView 出现时,仍响应操作手势操作等用户事件)。

对于这种情况,通常需要通过重写 Activity 的 dispatchTouchEvent 方法进行设置:

override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
        // 获得当前触摸动作的(x,y)轴坐标
        val x = event.rawX.toInt()
        val y = event.rawY.toInt()

        if (!isTouchPointInView(binding.folderButton, x, y) // 判断当前触摸的控件是否为所需要展示下拉 RecyclerView 的控件(如用于提示用户可以点击显示下拉菜单的图片或按钮)
            && !isTouchPointInView(binding.folderRecyclerContainer, x, y) // 判断当前触摸的控件是否为所需要展示下拉 RecyclerView
            && binding.folderRecycler.isVisible // 判断当前下拉的 RecyclerView 是否已经显示
        ) {
            // 当上述三个条件均满足时,则代表下拉列表已经完全展开并可处理用户事件,此时我们对事件进行拦截,避免穿透
            topButtonsTouchEvent()
            folderRecyclerTouchEvent()

            ...

            // 通过返回 false,对事件进行拦截
            return false
        }

        return super.dispatchTouchEvent(event)
    }

RecyclerView 配合 Animator 相关问题

动画出现位置

当使用RecyclerView实现下拉列表(与Android中的Spinner、ExpandableListView类似)的时候,我们通常需要对下拉菜单的动画进行设置,以下是一段简单的动画示例(XML实现):

in

<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="300"
    android:fromYDelta="-100%p"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:toYDelta="0" />

out

<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="300"
    android:fromYDelta="0"
    android:interpolator="@android:anim/accelerate_interpolator"
    android:toYDelta="-100%p" />

在上述两段动画中,不难发现:进入动画 in 中的 fromYDelta 与退出动画 out 中的 toYDelta均使用到了 "%p" 的参数。

该参数设置了动画的触发位置,"p"表示动画将从包裹了控件的父布局内出现,若我们需要对下拉RecyclerView的动画出现位置进行设置,则可以在RecyclerView外部包裹一层父View,将动画处理时间交由父View进行设置即可。


0%