【问题标题】:Invisible items becomes visible on scroll in RecyclerView在 RecyclerView 中滚动时不可见的项目变得可见
【发布时间】:2021-04-11 23:29:26
【问题描述】:

我有两个按钮来播放和暂停 RecyclerView 项目中的曲目。当点击播放按钮时,我想隐藏它并显示暂停按钮。我已经这样做了,它正在工作,但我有一个问题。一旦我滚动到(向下或向上),播放按钮再次出现,暂停按钮消失。我还有一个进度条来显示曲目的时间。随着曲目的播放,该栏会填满,并且其进度在开始时为零。当我滚动列表时,此进度条也重置为零并且不会移动,但曲目会继续播放。我尝试了三种方法来解决这个问题:

  1. 将 setIsRecyclable 设置为 false
  2. 向视图添加和 else 条件
  3. 为 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_width="wrap_content"
    android:layout_height="wrap_content"
    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_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginRight="12dp">

        <Button
            android:id="@+id/buttonPausePreview"
            android:layout_width="30dp"
            android:layout_height="30dp"
            android:visibility="invisible"
            tools:visibility="invisible"
            android:background="@drawable/ic_preset_view_pause" />

        <Button
            android:id="@+id/buttonPlayPreview"
            android:layout_width="30dp"
            android:layout_height="30dp"
            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_width="match_parent"
        android:layout_height="wrap_content"
        android:clickable="false"
        android:minWidth="140dp"
        android:progress="0" />
</LinearLayout>

【问题讨论】:

  • 您可能错过了代码中的 else 部分。由于RecyclerView 的可回收性质,您必须在 bindViewHolder 中涵盖这两种情况。
  • 我在 playPreviewSound 函数中写了一个 else 状态,但它不起作用。即使我为按钮添加了 else 代码,进度条呢?它的行为也很奇怪
  • 当您更改项目的可见性时,您是否也会更改支持此适配器的数据项的状态?
  • 对不起,backing this adapter 是什么意思?
  • @tpbafk 我的意思是您从中获取数据以设置可见性的个人BackStageProcessorItem

标签: android kotlin android-recyclerview visibility


【解决方案1】:

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-09-01
    • 1970-01-01
    • 1970-01-01
    • 2017-07-28
    相关资源
    最近更新 更多