Странное поведение (ошибка?) С RecyclerView и LinearLayoutManager с reverseLayout == true - PullRequest
9 голосов
/ 20 февраля 2020

При использовании RecyclerView с LinearLayoutManager и флагом «reverseLayout», установленным в true, при уведомлении любого элемента через notifyItemChanged он также вызывает onBindViewHolder для первого невидимого элемента. И это не вызывает onViewRecycled для этого предмета впоследствии вообще. Так что в случае, если ViewHolder делает какую-то подписку в onBind, она никогда не будет выпущена, потому что onRecycle не будет вызываться.

На самом деле это похоже на ошибку в LinearLayoutManager. Если вы посмотрите на метод fill в LinearLayoutManager, то появится следующий код:

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;
}

Насколько я понимаю, мы перебираем дочерние представления, пока не заполним все необходимое пространство, другими словами layoutState.mAvailable и remainingSpace, которые измеряются в пикселях. И если вы посмотрите дальше на то, что происходит в методе layoutChunk, вы увидите этот фрагмент кода:


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

Таким образом, LLM пропустит любой элемент с FLAG_UPDATE, который, в моем случае, предмет, который я называю notifyItemChanged на. Пропуская, я имею в виду, что высота элемента не будет вычтена из этих двух переменных:

layoutState.mAvailable -= layoutChunkResult.mConsumed;
// we keep a separate remaining space because mAvailable is important for recycling
remainingSpace -= layoutChunkResult.mConsumed;

И это заставит l oop повторить еще одно дополнительное представление. И поскольку этот вид не кэшируется 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();
}

В случае reverseLayout == false , мы начинаем итерацию с текущей позиции в восходящем порядке, то есть:

    [ 0 ]   
 +--[ 1 ]--+
 |  [ 2 ]  |
 |  [ 3 ]  |
 |  [ 4 ]  |
 |  [ 5 ]  |
 +--[ 6 ]--+

Допустим, мы вызываем notifyItemChanged для позиции с позицией 3. Таким образом, LLM будет выполнять итерацию в течение 1, 2 (пропустить 3 ), 4, 5 и 6. Поскольку он пропустил 3, останутся пиксели, чтобы заполнить layoutState.mAvailable переменную НО, потому что мы находимся в конце l oop, он сразу остановится.

Теперь давайте посмотрим, что происходит, когда reverseLayout == true.

    [ 6 ]   
 +--[ 5 ]--+
 |  [ 4 ]  |
 |  [ 3 ]  |
 |  [ 2 ]  |
 |  [ 1 ]  |
 +--[ 0 ]--+

Итак, еще раз, мы называем notifyItemChanged(3). LLM начнет итерацию в обратном порядке: 0, 1, 2, (пропустить 3), 4 и 5. Затем, так как он пропустил 3, есть еще пиксели для заполнения, и мы НЕ в конце списка, поэтому он будет перебирать 6 как хорошо.

Самое странное, что в этом примере кода он воспроизводится только при первом долгом касании, после чего не будет вызван первый экранный вид onBind. Но в проекте, где эта вещь была обнаружена, она воспроизводится на 100% каждый раз, когда вы вызываете notifyItemChanged для представления.

Вот минимальный воспроизводимый пример:

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)
}

И вот log:

onLongClick(0)
onCreateViewHolder
onBind 3
onCreateViewHolder
onBind 0
onRecycle 0
onLongClick(0)
onBind 0
onRecycle 0
onLongClick(0)
onBind 0
onRecycle 0

Я накладываю представление на позицию с позицией 0, и дополнительный вид с позицией 3 (которая находится вне экрана) также связывается.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...