I have two buttons to play and pause a track in a RecyclerView item. When play button tapped, I want to hide it and show pause button. I've done this and it's working but I have a problem. Once I scroll to (down or up), the play button appears again and pause button disappears. I also have a progress bar to show the time of the track. As the track play, the bar fills out and its progress is zero at the beginning. When I scroll the list, this progress bar also resets to zero and doesn't move but the track continues to play. I tried three ways to fix this:
Setting setIsRecyclable to false
Adding and else condition to views
Adding default visibility to the views in the XML file
Here's my complate code:
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)
}
}
And a part of my layout:
<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>
RecyclerViews work by creating a pool of ViewHolder objects (got by calling onCreateViewHolder) which are used to display stuff. No matter how many items the view represents, there are only a handful of ViewHolders being used, enough to fill the visible part of the RecyclerView and a few either side so you can peek to the next item.
So it works by shuffling those ViewHolders around to put them ahead of the scrolling, and the stuff they're displaying gets updated to represent a particular item in the list. This is done in onBindViewHolder.
Basically, if you have items with state, i.e. whether the play button is visible, whether a seek bar is at a particular position, if it has some kind of controller attached that updates the seek bar - you need to restore all that in onBindViewHolder when that item comes into view and a ViewHolder is being told to display that item. That means you have to keep track of that state somewhere (usually in the adapter), so you can restore it when an item pops into view.
Related
I am working on a chat app. I have the chat activity where the two users can send messages like WhatsApp, but I have a problem.
Like you can see in the picture (https://ibb.co/3cyYX01), the views are messing up when scrolling, and I think I know why.
After looking into those posts:
RecyclerView messes up when scrolling ,
Android: RecyclerView content messed up after scrolling
I assume the problem may be in the recycler view adapter in the function onBindViewHolder, because I am using the visibility option on some views(VIEW.GONE and VIEW.VISIBLE) and I think that these views are getting redrawn with wrong visibility.
In addition, I used holder.setIsRecyclable(false) in onBindViewHolder in order to check if it's the recycling part that cause the problem and when I used it, it worked perfectly.
This is the RecyclerView Adapter:
private const val SEND_LAYOUT = 0
private const val RECEIVED_LAYOUT = 1
class ChatRecyclerViewAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private lateinit var receiverUserPic: String
private lateinit var messageList: List<Message>
private lateinit var currentUserPic: String
private lateinit var currentUserUID: String
private lateinit var targetUID: String
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val viewHolder: RecyclerView.ViewHolder
val view: View
viewHolder = if (viewType == SEND_LAYOUT) {
view = LayoutInflater.from(parent.context)
.inflate(R.layout.sent_message_row, parent, false)
SentViewHolder(view)
} else {
view = LayoutInflater.from(parent.context)
.inflate(R.layout.recieved_message_row, parent, false)
ReceivedViewHolder(view)
}
return viewHolder
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
//holder.setIsRecyclable(false)
val currentMessage = messageList[position]
if (holder.itemViewType == SEND_LAYOUT) {
holder as SentViewHolder
holder.bindSentRow(currentMessage)
} else {
holder as ReceivedViewHolder
holder.bindReceivedRow(currentMessage)
}
}
override fun getItemCount(): Int {
return messageList.size
}
override fun getItemViewType(position: Int): Int {
val currentMessage = messageList[position]
return if (FirebaseAuth.getInstance().currentUser?.uid.equals(currentMessage.sender))
SEND_LAYOUT
else
RECEIVED_LAYOUT
}
inner class SentViewHolder(private val itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bindSentRow(message: Message) {
val sentMessageTextView =
itemView.findViewById<TextView>(R.id.sentMessage)
val sentImage = itemView.findViewById<ImageView>(R.id.sentImage)
val profileImage =
itemView.findViewById<ImageView>(R.id.sentMessageProfilePicture)
val sentIsSeenImageTextView =
itemView.findViewById<TextView>(R.id.sentIsSeenImageTextView)
val sentIsSeenTextView =
itemView.findViewById<TextView>(R.id.sentIsSeenTextView)
profileImage.setOnClickListener {
val visitProfileIntent = Intent(it.context, VisitProfileActivity::class.java)
visitProfileIntent.putExtra("targetUID", currentUserUID)
it.context.startActivity(visitProfileIntent)
}
if (message.message.equals("Sent you an image") && !message.url.equals("")) {
sentMessageTextView.visibility = View.GONE
sentIsSeenImageTextView.visibility = View.VISIBLE
sentIsSeenTextView.visibility = View.GONE
sentImage.visibility = View.VISIBLE
Glide.with(itemView.rootView).load(message.url)
.override(SIZE_ORIGINAL, SIZE_ORIGINAL)
.error(R.drawable.error_icon)
.placeholder(R.drawable.loading_icon)
.listener(object : RequestListener<Drawable?> {
override fun onLoadFailed(
#Nullable e: GlideException?,
model: Any,
target: Target<Drawable?>,
isFirstResource: Boolean
): Boolean {
return false
}
override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: Target<Drawable?>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
return false
}
}).into(sentImage)
if (adapterPosition == messageList.size - 1) {
sentIsSeenImageTextView.visibility = View.VISIBLE
sentIsSeenTextView.visibility = View.GONE
if (message.seen == true) {
sentIsSeenImageTextView.text = "Seen"
} else {
sentIsSeenImageTextView.text = "Sent"
}
} else {
sentIsSeenImageTextView.visibility = View.GONE
}
} else {
sentMessageTextView.visibility = View.VISIBLE
sentMessageTextView.text = message.message
sentIsSeenImageTextView.visibility = View.GONE
if (adapterPosition == messageList.size - 1) {
sentIsSeenTextView.visibility = View.VISIBLE
sentIsSeenImageTextView.visibility = View.GONE
if (message.seen == true) {
sentIsSeenTextView.text = "Seen"
} else {
sentIsSeenTextView.text = "Sent"
}
}
}
Glide.with(itemView.rootView).load(currentUserPic).into(profileImage)
}
}
inner class ReceivedViewHolder(private val itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bindReceivedRow(message: Message) {
val receiveMessageTextView =
itemView.findViewById<TextView>(R.id.receivedMessage)
val receiveImage =
itemView.findViewById<ImageView>(R.id.receivedImage)
val receiveProfileImage =
itemView.findViewById<ImageView>(R.id.receivedMessageProfileImage)
receiveProfileImage.setOnClickListener {
val visitProfileIntent = Intent(it.context, VisitProfileActivity::class.java)
visitProfileIntent.putExtra("targetUID", targetUID)
it.context.startActivity(visitProfileIntent)
}
if (message.message.equals("Sent you an image") && !message.url.equals("")) {
receiveMessageTextView.visibility = View.GONE
receiveImage.visibility = View.VISIBLE
Glide.with(itemView.rootView).load(message.url).into(receiveImage)
} else {
receiveMessageTextView.visibility = View.VISIBLE
receiveMessageTextView.text = message.message
}
Glide.with(itemView.rootView).load(receiverUserPic).into(receiveProfileImage)
}
}
fun getMessageList(): List<Message> {
return messageList
}
fun setMessagesList(
newList: List<Message>,
userProfilePic: String,
userProfilePic1: String,
currentUID: String,
receiverUID: String
) {
messageList = newList
currentUserPic = userProfilePic
receiverUserPic = userProfilePic1
currentUserUID = currentUID
targetUID = receiverUID
notifyDataSetChanged()
}
}
Pastebin Link:
https://pastebin.com/Ri5pUAdk
Thank you !
Working of the recyleView is based on that, it recycles views to show a list. When you scroll, views which go out of the screen, are not destroyed but are reused again to show the new list item. So, if you change visibility or any other property of a view and don't reset it again inside onBindViewHolder, then it would show all the properties which were set earlier before it got recycled.
fun bind(data: Data) {
val textView = itemView.findViewById<TextView>(R.id.tvText)
if(data.text.isEmpty()) {
textView.visibility = View.GONE
}
}
In the above method, we are hiding textView when text is empty, but we are not setting anything in the else condition. So when views, for which we've set visibility to gone would be recycled, they'd never show the textView as it is reusing the view. To deal with this, we've to set the properties of the views for true and false conditions each.
fun bind(data: Data) {
val textView = itemView.findViewById<TextView>(R.id.tvText)
if(data.text.isEmpty()) {
textView.visibility = View.GONE
} else {
textView.visibility = View.VISIBLE
}
}
In SentViewHolder and ReceivedViewHolder, you are setting the visibility of the ImageView to visible
sentImage.visibility = View.VISIBLE
receiveImage.visibility = View.VISIBLE
but you are never setting it to gone.
In the else condition of (message.message.equals("Sent you an image") && !message.url.equals("")), set the visibility of ImageView to GONE. Do the same for all other views too, so you don't get an unexpected UI.
Because the view holder is being recycled and reused that cause your views to be in the wrong state.
In SendViewHolder class you only handle the state of sentImage in if block by setting the visibility to visible. Therefore you also need to set its visibility to gone in else block.
Or you can reset the view visibility first then show it like below.
fun bindSendRow(message: Message) {
sentMessageTextView.visibility = View.GONE
sentImage.visibilty = View.GONE
if(shouldShowImage){
sentImage.visibility = View.VISIBLE
} else if(shouldShowText){
sentMessageTextView.visibility = View.VISIBLE
}
}
I am developing an Android application in Kotlin and I implemented a RecyclerView.
Each item of this RecyclerView contains one of the 3 combinations below:
a TextView + a TextView
a TextView + a Button
a TextView + a Spinner
In this activity, I have initialized my parameter list and I can add parameters of one of the 3 types mentioned above by clicking on the TEST button.
My problem is a UI problem: when there are a lot of parameters containing Spinners such that you have to scroll to see them, an arrow of a spinner is displayed at the very top of the RecyckerView (or at the very bottom sometimes). This spinner arrow starts flashing when I scroll and finally stops after a few seconds. Please note that I can't click on this arrow. I don't understand my mistake, can you help me?
Here is a link to a Youtube video recording of the behavior I have described that will help you better understand it.
Here is my view when I am on top of my RecyclerView:
This is my view when I scrolled down in the RecyclerView and there are parameters containing Spinners that are only visible when scrolling up again.
Here is my CustomAdapter class:
class CustomAdapter(private var parameterList: List<Parameter>) :
RecyclerView.Adapter<CustomAdapter.ViewHolder>() {
sealed class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
class TextViewHolder(itemView: View) : ViewHolder(itemView) {
val textViewName: TextView = itemView.findViewById(R.id.parameterName)
val textViewValue: TextView = itemView.findViewById(R.id.parameterValue)
}
class ButtonViewHolder(itemView: View) : ViewHolder(itemView) {
var textViewName: TextView = itemView.findViewById(R.id.parameterName)
val buttonViewValue: Button = itemView.findViewById(R.id.parameterButton)
}
class SpinnerViewHolder(itemView: View) : ViewHolder(itemView) {
val textViewName: TextView = itemView.findViewById(R.id.parameterName)
val spinnerViewValue: Spinner = itemView.findViewById(R.id.parameterSpinner)
}
}
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
return when (viewType) {
0 -> {
val view = LayoutInflater.from(viewGroup.context)
.inflate(R.layout.parameter_text, viewGroup, false)
ViewHolder.TextViewHolder(view)
}
1 -> {
val view = LayoutInflater.from(viewGroup.context)
.inflate(R.layout.parameter_button, viewGroup, false)
ViewHolder.ButtonViewHolder(view)
}
2 -> {
val view = LayoutInflater.from(viewGroup.context)
.inflate(R.layout.parameter_spinner, viewGroup, false)
ViewHolder.SpinnerViewHolder(view)
}
else -> throw IllegalArgumentException("Invalid view type")
}
}
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
when (viewHolder) {
is ViewHolder.TextViewHolder -> {
viewHolder.textViewName.text = parameterList[position].parameterName
viewHolder.textViewValue.text = parameterList[position].parameterValue as CharSequence?
}
is ViewHolder.ButtonViewHolder -> {
viewHolder.textViewName.text = parameterList[position].parameterName
viewHolder.buttonViewValue.text = parameterList[position].parameterValue as CharSequence?
}
is ViewHolder.SpinnerViewHolder -> {
viewHolder.textViewName.text = parameterList[position].parameterName
viewHolder.spinnerViewValue.adapter = parameterList[position].parameterValue as SpinnerAdapter?
}
}
}
override fun getItemViewType(position: Int) : Int {
return parameterList[position].parameterType
}
override fun getItemCount() = parameterList.size
fun addNewList(newList: List<Parameter>){
parameterList = newList;
notifyDataSetChanged();
}
}
Each of the different types of parameters inherited from the class Parameter:
open class Parameter(open var parameterName: String? = "", open var parameterType: Int = 0, open var parameterValue: Any) {
}
ParameterText class:
class ParameterText(override var parameterName: String?, override var parameterValue: Any = "") : Parameter(parameterName, parameterValue = parameterValue!!) {
override var parameterType: Int = 0
}
ParameterButton class:
class ParameterButton(override var parameterName: String?, override var parameterValue: Any = "") : Parameter(parameterName, parameterValue = parameterValue!!) {
override var parameterType: Int = 1
}
ParameterSpinner class:
class ParameterSpinner(override var parameterName: String?, override var parameterValue: Any) : Parameter(parameterName, parameterValue = parameterValue) {
override var parameterType: Int = 2
}
Here is my NFCActivity:
class NFCActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_nfc)
buttonTest.setOnClickListener() {
parametersList = parametersList + ParameterSpinner(
"Led", ArrayAdapter(
this,
android.R.layout.simple_spinner_item, resources.getStringArray(R.array.LedState)
)
)
(rv_parameters2.adapter as CustomAdapter).addNewList(parametersList)
}
rv_parameters2.layoutManager = LinearLayoutManager(this)
rv_parameters2.adapter = CustomAdapter(parametersList)
}
private var parametersList : List<Parameter> = listOf<Parameter> (
ParameterText("Temperature", "24°C"),
ParameterText("Temperature", "24°C")
)
companion object {
fun getStartIntent(context: Context): Intent {
return Intent(context, NFCActivity::class.java)
}
}
}
NFCActivity's layout:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".view.ble.NFCActivity">
<Button
android:id="#+id/buttonTest"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TEST"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rv_parameters2"
tools:listitem="#layout/parameter_text"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="#id/buttonTest"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
I have a viewPager with CubeTransformer, which is transforming every fragment. And inside every fragment is Image or Video view (with Exoplayer). And when you are trying to change a framgnet with transformation, exoplayer losses preview (I've got a black screen), even it's not playing. But after you changing condition to normal, preview is coming back
Ohterwise, if you will remove pageTransformer, review is not dissapears. How to keep preview always on screen?
CubeTransformer
class CubeTransformer : ViewPager.PageTransformer {
override fun transformPage(view: View, position: Float) {
if (view.visibility != View.VISIBLE) return
view.apply {
cameraDistance = (view.width * distanceMultiplier).toFloat()
pivotX = if (position < 0f) view.width.toFloat() else 0f
pivotY = view.height * 0.5f
rotationY = 90f * position
if (position < -1 || position > 1) {
alpha = 0f // item not visible
} else {
alpha = 1f
}
}
}
private companion object {
private const val distanceMultiplier: Int = 20
}
}
VideoView
class VideoView(context: Context) : ConstraintLayout(context, null) {
private val player = ExoPlayerFactory.newSimpleInstance(context, DefaultTrackSelector(), DefaultLoadControl())
private val dataSourceFactory = DefaultDataSourceFactory(context, "android")
private lateinit var model: Model
init {
inflate(context, R.layout.story_item_video, this)
video_view.player = player
video_view.keepScreenOn = true
video_view.setBackgroundColor(Color.TRANSPARENT)
video_view.setShutterBackgroundColor(Color.TRANSPARENT)
}
fun setData(model: Model?) {
if (model== null) return
this.model = model
val mediaSource = HlsMediaSource
.Factory(dataSourceFactory)
.setExtractorFactory(DefaultHlsExtractorFactory())
.createMediaSource(Uri.parse(model.streamLink))
player.playWhenReady = true
player.prepare(mediaSource)
player.addListener(object: Player.EventListener {
override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters?) {
}
override fun onSeekProcessed() {}
override fun onTracksChanged(trackGroups: TrackGroupArray?, trackSelections: TrackSelectionArray?) {
}
override fun onPlayerError(error: ExoPlaybackException?) {
}
override fun onLoadingChanged(isLoading: Boolean) {
}
override fun onPositionDiscontinuity(reason: Int) {
}
override fun onRepeatModeChanged(repeatMode: Int) {
}
override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
}
override fun onTimelineChanged(timeline: Timeline?, manifest: Any?, reason: Int) {
}
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
}
})
}
}
After a day of searching, I've found an answer to my question. You just need to add app:surface_type="texture_view" to your PlayerView
I have infinite scrolling RecyclerView with loading more items from API when scrolled to the last item, but after several scrolls my RecyclerView starting to lag on adding new items and getting
Skipped 197 frames! The application may be doing too much work on its main thread.
in the logs. I cannot find what is causing the lag.
Followings are my methods
val onLoadMore = object : IOnLoadMore {
override fun onLoadMore() {
if (!adapter.loadingMore) {
adapter.addLoadingItem()
requestSimple()
}
}
}
fun requestSimple() {
disposable = MyApplication.apiService.offerSearchWithPromo(
defaultSharedPreferences.getString(Config.PREF_LANG, Config.RU), request!!)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
adapter.removeLoadingItem()
adapter.items.addAll(it.offers.data)
if (it.promotions.data.size > 0) adapter.items.add(it.promotions.data)
adapter.notifyItemRangeInserted(adapter.items.size - it.offers.data.size - 1, it.offers.data.size)
adapter.meta = it.offers.meta
tv_found.text = resources.getString(R.string.found) + " " + adapter.meta?.pagination?.total.toString()
if (it.offers.data.size == 15) adapter.setOnLoadMoreListener(onLoadMore)
else adapter.removeListener()
request!!.page++
}, {
showError(it.message.toString())
})
}
and this is my adapter
class AdrResRvDynamic(var context: Context, nestedScrollView: NestedScrollView? = null, var items: MutableList<Any?>) : RVAdrMutableNullable<Any?, RecyclerView.ViewHolder>(items) {
var isLoading: Boolean = false
var meta: ObjMeta? = null
private var mIOnLoadMore: IOnLoadMore? = null
private val VIEW_TYPE_AUTO_SIMPLE = 0
private val VIEW_TYPE_AUTO_VIP = 1
private val VIEW_TYPE_AUTO_SUGGESTED = 2
private var VIEW_TYPE_LOADING = 99
var loadingMore: Boolean = false
var curr = ""
init {
curr = context.defaultSharedPreferences.getString(Config.PREF_CURRENCY, Config.UZS)
setHasStableIds(true)
nestedScrollView?.setOnScrollChangeListener { v: NestedScrollView, scrollX: Int, scrollY: Int, oldScrollX: Int, oldScrollY: Int ->
if (v.getChildAt(v.childCount - 1) != null) {
isLoading = if (scrollY >= v.getChildAt(v.childCount - 1).measuredHeight - v.measuredHeight && scrollY > oldScrollY) {
if (mIOnLoadMore != null) mIOnLoadMore!!.onLoadMore()
true
} else false
}
}
}
fun setOnLoadMoreListener(mIOnLoadMore: IOnLoadMore) {
this.mIOnLoadMore = mIOnLoadMore
}
fun removeListener() {
this.mIOnLoadMore = null
}
override fun getItemViewType(position: Int): Int {
return when {
items[position] == null -> VIEW_TYPE_LOADING
items[position]!!::class.simpleName == "ObjAuto" -> VIEW_TYPE_AUTO_SIMPLE
items[position]!!::class.simpleName == "ObjAutoVip" -> VIEW_TYPE_AUTO_VIP
items[position] is List<*> -> VIEW_TYPE_AUTO_SUGGESTED
else -> VIEW_TYPE_LOADING
}
}
#Suppress("UNCHECKED_CAST")
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val obj = items[position]
when (holder) {
is ItemViewAutoCard -> holder.bind(obj!! as ObjAuto)
is ItemViewAutoCardSUGGESTED -> holder.bind(obj!! as List<ObjAuto>)
is ItemViewAutoCardVIP -> holder.bind(obj!! as ObjAutoVip)
is ItemViewLoadingMore -> {
// holder.itemView.find<ProgressBar>(R.id.progressBar1).isIndeterminate = true
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
VIEW_TYPE_AUTO_SIMPLE -> ItemViewAutoCard(context, LayoutInflater.from(parent.context).inflate(R.layout.li_auto_card, parent, false))
VIEW_TYPE_AUTO_VIP -> ItemViewAutoCardVIP(context, LayoutInflater.from(parent.context).inflate(R.layout.li_auto_card_vip, parent, false))
VIEW_TYPE_AUTO_SUGGESTED -> ItemViewAutoCardSUGGESTED(context, LayoutInflater.from(parent.context).inflate(R.layout.li_auto_card_suggested, parent, false))
else -> ItemViewLoadingMore(LayoutInflater.from(parent.context).inflate(R.layout.progress_bar_load_more, parent, false))
}
}
override fun getItemCount(): Int {
return items.size
}
fun removeLoadingItem() {
loadingMore = false
if (items.size == 0) return
items.removeAt(items.size - 1)
notifyItemRemoved(items.size)
}
fun addLoadingItem() {
loadingMore = true
items.add(null)
notifyItemInserted(items.size - 1)
}
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
super.onViewRecycled(holder)
}
}
PS: I've commented out all the logic inside my ItemViews so there's nothing going on in onBind() method, i'm just showing empty layout, but still after several loads the recycler becoming laggy.
Answering my own question, The problem was placing RecyclerView inside NestedScrollView. I needed it because there was a view above RecyclerView which needed to be scrolled. I've removed it and put it as a first item in my RecyclerView. What was happening is that the items in RecyclerView was not being recycled as its height was just expanding.
Verdict: Never put RecyclerView inside NestedScrollView
I'm having this issue, with recyclerView, may you check two screenshots below:
So that's my issue, when onNotifyItemChange runs, other info are changed, incorrectlty. Now here goes my adapter:
class TimelineAdapter(var timeline: TimelineDTO,
var toggleLikeClicked: OnRowClick,
var onCommentClicked: OnRowClick,
var onMediaClick: OnRowClick,
val onUserClicked: OnRowClick,
val reportPost: OnRowClick,
val editPost : OnRowClick,
val deletePost: OnRowClick,
val contract: TimelineViewContract) : BaseAdapter<RecyclerView.ViewHolder>() {
init {
setHasStableIds(true)
}
private var currentItem: Int = 0
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
when (PostType.fromInt(viewType)) {
PostType.BASIC -> {
return PostViewHolder(parent.inflate(R.layout.row_post_default_item),
toggleLikeClicked, onCommentClicked, onMediaClick,
onUserClicked, reportPost,
editPost,
deletePost,
FirebaseAnalytics.getInstance(contract.returnContext()))
}
PostType.NEXT_TALKS -> {
return PostNextTalksViewHolder(parent.inflate(R.layout.row_post_next_talks_item),
contract)
}
else -> {
if(!BuildConfig.DEBUG) {
Crashlytics.log("Should not come here")
}
logE("adapter else!!")
return PostViewHolder(parent.inflate(R.layout.row_post_default_item),
toggleLikeClicked, onCommentClicked, onMediaClick,
onUserClicked, reportPost,
editPost,
deletePost,
FirebaseAnalytics.getInstance(contract.returnContext()))
}
}
}
override fun getItemCount(): Int {
var count = timeline.posts.size
if(hasValue(timeline.nextTalks.size)){
count++
}
return count
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
currentItem = position
val alignedPositon = getAlignedPosition(position)
when (holder) {
is PostViewHolder -> holder.bind(timeline.posts[alignedPositon])
is PostNextTalksViewHolder -> {
holder.bind(timeline.nextTalks)
}
is PostCarousselViewHolder -> {
holder.bind(ArrayList<String>())
}
}
}
fun getPostAt(position: Int): PostDTO {
val post: PostDTO
val alignedPositon = getAlignedPosition(position)
post = timeline.posts[alignedPositon]
return post
}
override fun getItemId(position: Int): Long {
val aligned = getAlignedPosition(position)
return aligned.toLong()
}
private fun getAlignedPosition(position: Int): Int {
var alignedPositon = position
if (hasValue(timeline.nextTalks.size)){
alignedPositon--
}
return alignedPositon
}
override fun getItemViewType(position: Int): Int {
val hasPinned = timeline.posts.any { it.postType == PostType.PINNED.id }
if(hasPinned) {
if(position == 1 && timeline.nextTalks.any()){
return PostType.NEXT_TALKS.id
}
}
else {
if(position == 0 && timeline.nextTalks.any()){
return PostType.NEXT_TALKS.id
}
}
return timeline.posts[getAlignedPosition(position)].postType
}
fun updateItemAt(postLocal: PostLocal, commentIndexPost: Int) {
timeline.posts.removeAt(commentIndexPost)
timeline.posts.add(commentIndexPost, PostDTO(postLocal))
notifyItemChanged(commentIndexPost)
}
fun addItems(newPosts: TimelineDTO) {
timeline.posts.addAll(newPosts.posts)
timeline.nextTalks.addAll(newPosts.nextTalks)
notifyItemRangeInserted(itemCount, newPosts.posts.size)
}
fun resetItems(nextPosts: TimelineDTO) {
timeline.posts.clear()
timeline.nextTalks.clear()
timeline.posts.addAll(nextPosts.posts)
timeline.nextTalks.addAll(nextPosts.nextTalks)
notifyDataSetChanged()
}
fun removeAt(position: Int) {
timeline.posts.removeAt(position)
notifyItemRemoved(position)
notifyItemRangeChanged(position, timeline.posts.size)
}
}
Using notifyItemChanged() might trigger "fading in and out" effect which is not necessarily desired (unless You use stable IDs or killed change animation in animator).
If You know what was changed in an item, it's better to use an update payload (see an example here) to partially update your ViewHolders without triggering full rebind.
Otherwise if list is relatively small and You don't know what changed, you can also use DiffUtil to help generate list of changes/change payloads "semi-automatically".