При использовании 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 (которая находится вне экрана) также связывается.