在 RecyclerView 中滚动时不可见的项目变得可见
Posted
技术标签:
【中文标题】在 RecyclerView 中滚动时不可见的项目变得可见【英文标题】:Invisible items becomes visible on scroll in RecyclerView 【发布时间】:2021-04-11 23:29:26 【问题描述】:我有两个按钮来播放和暂停 RecyclerView 项目中的曲目。当点击播放按钮时,我想隐藏它并显示暂停按钮。我已经这样做了,它正在工作,但我有一个问题。一旦我滚动到(向下或向上),播放按钮再次出现,暂停按钮消失。我还有一个进度条来显示曲目的时间。随着曲目的播放,该栏会填满,并且其进度在开始时为零。当我滚动列表时,此进度条也重置为零并且不会移动,但曲目会继续播放。我尝试了三种方法来解决这个问题:
-
将 setIsRecyclable 设置为 false
向视图添加和 else 条件
为 XML 文件中的视图添加默认可见性
这是我的编译代码:
class BackstageProcessorAdapter(private val stickyHeaderChangedCallback: (ProcessorGroupId) -> Unit) : RecyclerView.Adapter<RecyclerView.ViewHolder>(),
StickyHeaderItemDecoration.StickyHeaderInterface
private var callback: ProcessorViewHolderCallback? = null
private var backStageProcessorItemList = emptyList<BackStageProcessorItem>()
private var stickyHeaderPosition = 0
private val processorGroupHeaderPositionMap = mutableMapOf<ProcessorGroupId, Int>()
private var parentRecyclerViewHeight = 0
private var lastItemPosition = 0
private var currentPreviewSound: String = ""
private var processorHeaderNameForEvent: String = ""
private lateinit var timer: CountDownTimer
var prevHolder: ProcessorViewHolder? = null
var mediaPlayer: MediaPlayer? = null
fun registerCallback(callback: ProcessorViewHolderCallback)
this.callback = callback
fun setItems(items: List<BackStageProcessorItem>)
if (backStageProcessorItemList.isNotEmpty()) return
backStageProcessorItemList = items
var headerPos = 0
for ((index, item) in items.withIndex())
if (item is BackStageProcessorItem.Header)
headerPos = index
processorGroupHeaderPositionMap[item.processorGroupUiModel.processorGroupId] =
headerPos
item.headerPosition = headerPos
lastItemPosition = items.lastIndex
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder
return when (viewType)
HEADER_ITEM -> HeaderViewHolder(parent.inflate(R.layout.item_processor_header))
else -> ProcessorViewHolder(parent.inflate(R.layout.item_backstage_processor))
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int)
when (val backStageProcessorItem = backStageProcessorItemList[position])
is BackStageProcessorItem.Header ->
(holder as HeaderViewHolder).bindTo(backStageProcessorItem)
is BackStageProcessorItem.Content ->
(holder as ProcessorViewHolder).bindTo(backStageProcessorItem.processorUiModel)
holder.setMargin(position)
override fun getItemViewType(position: Int): Int
return when (backStageProcessorItemList.get(position))
is BackStageProcessorItem.Header -> HEADER_ITEM
else -> PROCESSOR_ITEM
override fun getItemCount() = backStageProcessorItemList.size
override fun onAttachedToRecyclerView(recyclerView: RecyclerView)
recyclerView.post
parentRecyclerViewHeight = recyclerView.height
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView)
callback = null
override fun getHeaderPositionForItem(itemPosition: Int) =
backStageProcessorItemList[itemPosition].headerPosition
override fun getHeaderLayout(headerPosition: Int) = R.layout.item_processor_header
override fun bindHeaderData(header: View, headerPosition: Int)
val headerItem = backStageProcessorItemList[headerPosition] as BackStageProcessorItem.Header
(header as TextView).setText(headerItem.processorGroupUiModel.nameResId)
if (headerPosition != stickyHeaderPosition)
stickyHeaderPosition = headerPosition
stickyHeaderChangedCallback(headerItem.processorGroupUiModel.processorGroupId)
override fun isHeader(itemPosition: Int): Boolean
if (itemPosition == backStageProcessorItemList.size) return true
return backStageProcessorItemList[itemPosition] is BackStageProcessorItem.Header
override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder)
super.onViewDetachedFromWindow(holder)
fun getHeaderPositionViewGroupId(processorGroupId: ProcessorGroupId): Int
return processorGroupHeaderPositionMap[processorGroupId]!!
inner class HeaderViewHolder(itemView: View) :
RecyclerView.ViewHolder(itemView)
fun bindTo(header: BackStageProcessorItem.Header)
(itemView as TextView).setText(header.processorGroupUiModel.nameResId)
inner class ProcessorViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
private val textViewProcessorName = itemView.findViewById<TextView>(R.id.textViewProcessorName)
private val textViewProcessorDescription = itemView.findViewById<TextView>(R.id.textViewProcessorDescription)
private val imageViewProcessorImage = itemView.findViewById<ImageView>(R.id.imageViewProcessorImage)
private val buttonAddProcessor = itemView.findViewById<Button>(R.id.buttonAddProcessor)
private val buttonUnlockEverything = itemView.findViewById<TextView>(R.id.buttonUnlockEverything)
private val buttonPlayPreview = itemView.findViewById<Button>(R.id.buttonPlayPreview)
private val buttonPausePreview = itemView.findViewById<Button>(R.id.buttonPausePreview)
fun setMargin(position: Int)
val margin =
if (position != lastItemPosition) dpToPx(20)
else
val contentHeight = getDimen(R.dimen.backstage_processor_item_height)
val headerHeight = getDimen(R.dimen.processor_header_height)
val topMargin = dpToPx(20)
parentRecyclerViewHeight - (contentHeight + headerHeight + topMargin)
(itemView.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin = margin
@SuppressLint("ClickableViewAccessibility")
fun bindTo(processor: ProcessorUiModel)
val processorId = processor.processorId
val canProcessorBeEnabled = callback?.canProcessorBeEnabled(processorId) == true
val isProcessorAdded = callback?.isProcessorAddedBefore(processorId) == true
val processorName = itemView.context.resources.getText(processor.nameId).toString()
val processorNameForEvent = processorName.toLowerCase().replace(" ", "_")
this.setIsRecyclable(false)
if (prevHolder != null) prevHolder?.setIsRecyclable(false)
imageViewProcessorImage.setImageResource(processor.storeIconResId)
textViewProcessorName.setText(processor.nameId)
textViewProcessorDescription.setText(processor.descriptionId)
buttonUnlockEverything.isVisible = canProcessorBeEnabled.not()
buttonAddProcessor.isGone = canProcessorBeEnabled.not()
buttonAddProcessor.isEnabled = isProcessorAdded.not()
this.setIsRecyclable(false)
buttonAddProcessor.setOnTouchListener v, event ->
return@setOnTouchListener when (event.action)
KeyEvent.ACTION_DOWN ->
v.alpha = 0.75f
true
KeyEvent.ACTION_UP ->
v.alpha = 1f
callback?.addProcessor(processorId)
true
else -> v.onTouchEvent(event)
buttonPlayPreview.setOnClickListener
if (currentPreviewSound.isNotEmpty())
pausePreviewSound()
if (currentPreviewSound.isNotEmpty() && prevHolder != this)
currentPreviewSound = ""
prevHolder?.itemView?.buttonPausePreview?.isVisible = false
prevHolder?.itemView?.buttonPlayPreview?.isVisible = true
else
prevHolder?.itemView?.buttonPausePreview?.isVisible = true
prevHolder?.itemView?.buttonPlayPreview?.isVisible = false
processorName.playPreviewSound(processorNameForEvent)
prevHolder = this
notifyDataSetChanged()
buttonPausePreview.setOnClickListener()
pausePreviewSound()
buttonUnlockEverything.setOnClickListener
getHeaderNameClickProcessorForEvent()
callback!!.sendEvent("goPremiumClicked", processorHeaderNameForEvent, processorName)
callback?.openInAppBilling()
private fun String.playPreviewSound(processorNameForEvent: String)
callback?.stopVG()
currentPreviewSound = this
buttonPlayPreview.isVisible = false
buttonPausePreview.isVisible = true
mediaPlayer = MediaPlayer.create(itemView.context, AmpSoundType.getAmpType(this))
mediaPlayer?.start()
val maxTrackDuration = mediaPlayer?.duration!!
itemView.progressBarPreview.max = maxTrackDuration
itemView.progressBarPreview.progress = 0
// The first arg of the CountDownTimer is the tick count. Which is (maxTrackDuration (lets say this is 18000) / 1000) = 18 ticks in total duration with 200ms interval
timer = object : CountDownTimer(maxTrackDuration.toLong(), 200)
override fun onTick(millisUntilFinished: Long)
updatePreviewSoundProgressBar()
override fun onFinish()
setPlayButton()
timer.start()
callback!!.sendEvent("playClicked", processorHeaderNameForEvent, processorNameForEvent)
private fun pausePreviewSound()
setPlayButton()
mediaPlayer?.stop()
timer.cancel()
private fun setPlayButton()
buttonPlayPreview.isVisible = true
buttonPausePreview.isVisible = false
private fun updatePreviewSoundProgressBar()
itemView.progressBarPreview.progress += 200
private fun getHeaderNameClickProcessorForEvent()
val processorHeaderPosition = backStageProcessorItemList[getHeaderPositionForItem(position)]
val processorHeaderData = (processorHeaderPosition as BackStageProcessorItem.Header).processorGroupUiModel.nameResId
val processorHeaderName = itemView.context.resources.getString(processorHeaderData)
processorHeaderNameForEvent = processorHeaderName.toLowerCase().substring(0, 3)
private fun dpToPx(dp: Int) = (dp * itemView.resources.displayMetrics.density).toInt()
private fun getDimen(dimenRes: Int) = itemView.resources.getDimensionPixelSize(dimenRes)
还有我布局的一部分:
<LinearLayout
android:id="@+id/layoutHearTone"
android:layout_
android:layout_
android:layout_marginBottom="8dp"
android:gravity="center"
android:orientation="horizontal"
app:layout_constraintBottom_toTopOf="@id/buttons"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.46"
app:layout_constraintStart_toStartOf="parent">
<RelativeLayout
android:layout_
android:layout_
android:layout_marginRight="12dp">
<Button
android:id="@+id/buttonPausePreview"
android:layout_
android:layout_
android:visibility="invisible"
tools:visibility="invisible"
android:background="@drawable/ic_preset_view_pause" />
<Button
android:id="@+id/buttonPlayPreview"
android:layout_
android:layout_
android:visibility="visible"
tools:visibility="visible"
android:background="@drawable/ic_preset_view_play" />
</RelativeLayout>
<ProgressBar
android:id="@+id/progressBarPreview"
style="?android:attr/progressBarStyleHorizontal"
android:layout_
android:layout_
android:clickable="false"
android:minWidth="140dp"
android:progress="0" />
</LinearLayout>
【问题讨论】:
您可能错过了代码中的 else 部分。由于RecyclerView
的可回收性质,您必须在 bindViewHolder 中涵盖这两种情况。
我在 playPreviewSound
函数中写了一个 else 状态,但它不起作用。即使我为按钮添加了 else 代码,进度条呢?它的行为也很奇怪
当您更改项目的可见性时,您是否也会更改支持此适配器的数据项的状态?
对不起,backing this adapter
是什么意思?
@tpbafk 我的意思是您从中获取数据以设置可见性的个人BackStageProcessorItem
。
【参考方案1】:
RecyclerView
s 通过创建用于显示内容的ViewHolder
对象池(通过调用onCreateViewHolder
获得)来工作。无论视图代表多少项目,只有少数ViewHolder
s 被使用,足以填充RecyclerView
的可见部分和两侧的一些,以便您可以查看下一个项目。
因此,它的工作原理是将这些ViewHolder
s 打乱以将它们放在滚动之前,并且它们显示的内容会更新以表示列表中的特定项目。这是在onBindViewHolder
中完成的。
基本上,如果您有具有状态的项目,即播放按钮是否可见,搜索栏是否位于特定位置,是否附加了某种控制器来更新搜索栏- 当该项目进入视野并且ViewHolder
被告知显示该项目时,您需要恢复onBindViewHolder
中的所有内容。这意味着您必须在某处(通常在适配器中)跟踪该状态,以便在项目弹出时恢复它。
【讨论】:
以上是关于在 RecyclerView 中滚动时不可见的项目变得可见的主要内容,如果未能解决你的问题,请参考以下文章
当滚动超出最初可见的范围时,无法访问 recyclerview 中的 textview
在键盘打开时将项目添加到 RecyclerView 时向下滚动
当在nestedscrollview的recyclerview中将项目拖出可见空间时-nestedscrollView不滚动