#android #kotlin #android-recyclerview #visibility
#Android #kotlin #android-recyclerview #видимость
Вопрос:
У меня есть две кнопки для воспроизведения и приостановки трека в элементе 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() 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
получает указаниеотобразить этот элемент. Это означает, что вы должны где-то отслеживать это состояние (обычно в адаптере), чтобы вы могли восстановить его, когда элемент появится в поле зрения.