Невидимые элементы становятся видимыми при прокрутке в RecyclerView

#android #kotlin #android-recyclerview #visibility

#Android #kotlin #android-recyclerview #видимость

Вопрос:

У меня есть две кнопки для воспроизведения и приостановки трека в элементе 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() amp;amp; 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>
 

Комментарии:

1. Вероятно, вы пропустили часть else в своем коде. Вы должны охватить оба случая в bindViewHolder из-за перерабатываемого характера RecyclerView .

2. Я написал другое состояние в playPreviewSound функции, но оно не сработало. Даже если я добавлю другой код для кнопки, как насчет индикатора выполнения? Он также ведет себя странно

3. Когда вы изменяете видимость элемента, вы также изменяете состояние элемента данных, который поддерживает этот адаптер?

4. Простите, что вы имеете в виду под backing this adapter этим?

5. @tpbafk я имею в виду человека, BackStageProcessorItem от которого вы получаете данные, чтобы установить видимость.

Ответ №1:

RecyclerView s работают, создавая пул ViewHolder объектов (полученных при вызове onCreateViewHolder ), которые используются для отображения материала. Независимо от того, сколько элементов представляет представление, используется всего несколько ViewHolder s, которых достаточно, чтобы заполнить видимую часть RecyclerView и несколько с обеих сторон, чтобы вы могли заглянуть к следующему элементу.

Таким образом, он работает путем перетасовки этих ViewHolder элементов, чтобы поместить их перед прокруткой, и отображаемый ими материал обновляется для представления определенного элемента в списке. Это делается в onBindViewHolder .

В принципе, если у вас есть элементы с состоянием, то есть видна ли кнопка воспроизведения, находится ли панель поиска в определенном положении, если к ней подключен какой-то контроллер, который обновляет панель поиска — вам нужно восстановить все это, onBindViewHolder когда этот элемент появляется в поле зрения, и a ViewHolder получает указаниеотобразить этот элемент. Это означает, что вы должны где-то отслеживать это состояние (обычно в адаптере), чтобы вы могли восстановить его, когда элемент появится в поле зрения.