在 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】:

RecyclerViews 通过创建用于显示内容的ViewHolder 对象池(通过调用onCreateViewHolder 获得)来工作。无论视图代表多少项目,只有少数ViewHolders 被使用,足以填充RecyclerView 的可见部分和两侧的一些,以便您可以查看下一个项目。

因此,它的工作原理是将这些ViewHolders 打乱以将它们放在滚动之前,并且它们显示的内容会更新以表示列表中的特定项目。这是在onBindViewHolder 中完成的。

基本上,如果您有具有状态的项目,即播放按钮是否可见,搜索栏是否位于特定位置,是否附加了某种控制器来更新搜索栏- 当该项目进入视野并且ViewHolder 被告知显示该项目时,您需要恢复onBindViewHolder 中的所有内容。这意味着您必须在某处(通常在适配器中)跟踪该状态,以便在项目弹出时恢复它。

【讨论】:

以上是关于在 RecyclerView 中滚动时不可见的项目变得可见的主要内容,如果未能解决你的问题,请参考以下文章

当滚动超出最初可见的范围时,无法访问 recyclerview 中的 textview

在键盘打开时将项目添加到 RecyclerView 时向下滚动

当在nestedscrollview的recyclerview中将项目拖出可见空间时-nestedscrollView不滚动

AdapterView 和 RecyclerView 的连续滚动

Recyclerview - 加载所有项目而不滚动

容器的剪辑子项在滚动时不可见