使用 RecyclerView 和 LinearLayoutManager 使用 reverseLayout == true 的奇怪行为(错误?)

Dmi*_*try 10 android android-recyclerview linearlayoutmanager

当使用带有 LinearLayoutManager 和“reverseLayout”标志设置为 true 的 RecyclerView 时,当通过notifyItemChanged它通知任何项目时,也会调用onBindViewHolder第一个不可见的项目。之后它不会要求onViewRecycled该项目。因此,如果 ViewHolder 在 onBind 中进行某种订阅,它将永远不会被释放,因为不会调用 onRecycle。

这实际上看起来像是LinearLayoutManager. 如果您查看fillLinearLayoutManager中的方法,则有以下代码:

if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null || !state.isPreLayout()) {
    layoutState.mAvailable -= layoutChunkResult.mConsumed;
    // we keep a separate remaining space because mAvailable is important for recycling
    remainingSpace -= layoutChunkResult.mConsumed;
}
Run Code Online (Sandbox Code Playgroud)

据我所知,我们迭代子视图,直到我们填满所有需要的空间,换句话说layoutState.mAvailableremainingSpace它们都以像素为单位。如果您进一步layoutChunk查看方法中发生的事情,您将看到这段代码:


// Consume the available space if the view is not removed OR changed
if (params.isItemRemoved() || params.isItemChanged()) {
    result.mIgnoreConsumed = true;
}

Run Code Online (Sandbox Code Playgroud)

所以法学硕士将跳过任何项目FLAG_UPDATE,在我的情况下,是我要求的项目notifyItemChanged。跳过我的意思是项目的高度不会从这两个变量中减去:

layoutState.mAvailable -= layoutChunkResult.mConsumed;
// we keep a separate remaining space because mAvailable is important for recycling
remainingSpace -= layoutChunkResult.mConsumed;
Run Code Online (Sandbox Code Playgroud)

这将使循环迭代一个额外的视图。而且由于该视图未由 LLM 缓存(请参阅tryGetViewHolderForPositionByDeadline-> getScrapOrHiddenOrCachedHolderForPosition)(因为如果我没有弄错,它在屏幕边界之外)它将重新创建。但是在reverseLayout设置为 false(默认 LLM 状态)的情况下,它不会迭代它,因为它将首先到达列表的末尾:

while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) { ... }

Namely here:

boolean hasMore(RecyclerView.State state) {
    return mCurrentPosition >= 0 && mCurrentPosition < state.getItemCount();
}
Run Code Online (Sandbox Code Playgroud)

reverseLayout== false 的情况下,我们从当前位置开始按升序迭代,即:

    [ 0 ]   
 +--[ 1 ]--+
 |  [ 2 ]  |
 |  [ 3 ]  |
 |  [ 4 ]  |
 |  [ 5 ]  |
 +--[ 6 ]--+
Run Code Online (Sandbox Code Playgroud)

假设我们调用notifyItemChanged位置为 3 的项目。因此 LLM 将迭代 1、2、(跳过 3)、4、5 和 6。由于它跳过了 3,因此将剩余像素来填充layoutState.mAvailable变量 BUT,因为我们处于循环结束,它将立即停止。

现在让我们看看当reverseLayout== true时会发生什么。

    [ 6 ]   
 +--[ 5 ]--+
 |  [ 4 ]  |
 |  [ 3 ]  |
 |  [ 2 ]  |
 |  [ 1 ]  |
 +--[ 0 ]--+
Run Code Online (Sandbox Code Playgroud)

因此,我们再次调用notifyItemChanged(3). LLM 将以相反的顺序开始迭代:0, 1, 2, (skip 3), 4 and 5. 然后因为它跳过了 3,所以仍然有像素要填充,我们不在列表的末尾,所以它将迭代 6 作为好。

最奇怪的是,在这个代码示例中,它只能在第一次长按时重现,之后第一个屏幕外视图onBind不会被调用。但是在发现这个东西的项目中,每次调用notifyItemChanged视图时它都是 100% 可重现的。

这是最小的可重现示例:

class MainActivity : AppCompatActivity() {
    private val id = AtomicLong(0)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val images = (0..6).map {
            return@map ImageItem(
                "https://i.imgur.com/BBcy6Wc.jpg",
                id.getAndIncrement()
            )
        }

        recycler.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, true)
        recycler.adapter = Adapter(images).apply { setHasStableIds(true) }
        recycler.setRecyclerListener { viewHolder ->
            if (viewHolder is Adapter.MyViewHolder) {
                viewHolder.onRecycle()
            }
        }
        recycler.adapter!!.notifyDataSetChanged()
    }

    data class ImageItem(val url: String, val id: Long)

    class Adapter(
        private val items: List<ImageItem>
    ) : RecyclerView.Adapter<Adapter.MyViewHolder>(), OnRecyclerItemClick {

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
            println("TTTAAA onCreateViewHolder")

            val view = LayoutInflater.from(parent.context)
                .inflate(R.layout.my_view_holder, parent, false)

            return MyViewHolder(view, this)
        }

        override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
            holder as MyViewHolder
            holder.onBind(items[position])
        }

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

        override fun getItemId(position: Int): Long {
            return items[position].id
        }

        override fun onLongClick(position: Int) {
            println("TTTAAA onLongClick($position)")
            notifyItemChanged(position)
        }

        class MyViewHolder(
            private val view: View,
            private val callback: OnRecyclerItemClick
        ) : RecyclerView.ViewHolder(view) {
            private val imageView: AppCompatImageView = view.findViewById(R.id.my_image)

            fun onBind(imageItem: ImageItem) {
                println("TTTAAA onBind $layoutPosition")

                imageView.setOnLongClickListener {
                    callback.onLongClick(layoutPosition)
                    return@setOnLongClickListener true
                }

                Glide.with(imageView.context)
                    .load(imageItem.url)
                    .centerCrop()
                    .into(imageView)
            }

            fun onRecycle() {
                println("TTTAAA onRecycle $layoutPosition")
                imageView.setOnClickListener(null)

                Glide.with(imageView.context)
                    .clear(imageView)
            }

        }
    }
}

interface OnRecyclerItemClick {
    fun onLongClick(position: Int)
}

Run Code Online (Sandbox Code Playgroud)

这是日志:

onLongClick(0)
onCreateViewHolder
onBind 3
onCreateViewHolder
onBind 0
onRecycle 0
onLongClick(0)
onBind 0
onRecycle 0
onLongClick(0)
onBind 0
onRecycle 0
Run Code Online (Sandbox Code Playgroud)

我正在长按位置 0 的视图,位置 3(屏幕外)的附加视图也被绑定。