Recyclerview becoming heavy - Skipped frames - android

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

Related

RecyclerView Messing Up Views When Scrolling - With Picture

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
}
}

Number of item submit in PagingDataAdapter

How to get number of items submitted to PagingDataAdapter
I have to display a RecyclerView with multiple viewType as Start, Middle and End RecyclerView.ViewHolder
Start and End ViewHolder is round where as inner items are using MiddleViewHolder
After debug I evaluate that when i submit 2 items to PagingDataAdapter
itemCount is still 1.
How to check total item submit in PagingDataAdapter
StartViewHolder -> bindData(item,itemCount == 1)
RecentPagerAdapter.kt
const val VIEW_TYPE_FIRST = 0
const val VIEW_TYPE_MIDDLE = 1
private const val VIEW_TYPE_LAST = 2
class RecentPagerAdapter(val clicked:(recent:RecentBO) -> Unit) : PagingDataAdapter<RecentBO, RecyclerView.ViewHolder>(recentDiffUtil) {
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
getItem(position)?.let { item ->
when (getItemViewType(position)) {
VIEW_TYPE_FIRST -> (holder as StartViewHolder).bindData(item,itemCount == 1)
VIEW_TYPE_MIDDLE -> (holder as RecentViewHolder).bindData(item)
VIEW_TYPE_LAST -> (holder as EndViewHolder).bindData(item)
}
}
}
override fun getItemViewType(position: Int): Int {
return when (position) {
0 -> VIEW_TYPE_FIRST
1 -> VIEW_TYPE_MIDDLE
else -> VIEW_TYPE_LAST
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when(viewType) {
VIEW_TYPE_FIRST -> StartViewHolder(ItemCurveStartBinding.inflate(LayoutInflater.from(parent.context),parent,false)){
getItem(it)?.let { clicked.invoke(it) }
}
VIEW_TYPE_LAST -> EndViewHolder(ItemCurveEndBinding.inflate(LayoutInflater.from(parent.context),parent,false)){
getItem(it)?.let { clicked.invoke(it) }
}
else -> RecentViewHolder(ItemRecentBinding.inflate(LayoutInflater.from(parent.context),parent,false)){
getItem(it)?.let { clicked.invoke(it) }
}
}
}
companion object {
private val recentDiffUtil = object : DiffUtil.ItemCallback<RecentBO>() {
override fun areItemsTheSame(oldItem: RecentBO, newItem: RecentBO): Boolean {
return oldItem.localPath == newItem.localPath && oldItem.imageUrl == newItem.imageUrl
}
override fun areContentsTheSame(oldItem: RecentBO, newItem: RecentBO): Boolean {
return oldItem == newItem
}
}
}
}
If there is only one item in list then all sides should be rounded.otherwise above image and last item should be rounded also.
StartViewHolder.kt
class StartViewHolder(val itemBinding: ItemCurveStartBinding,
val clicked:(pos:Int) -> Unit): RecyclerView.ViewHolder(itemBinding.root) ,
View.OnClickListener{
init {
itemView.setOnClickListener(this)
}
fun bindData(recentBO: RecentBO,onlyOneItem:Boolean){
itemBinding.item = recentBO
val radius: Float = 30.0f//getResources().getDimension(R.dimen.default_corner_radius)
if(onlyOneItem){
itemBinding.ivPhoto.shapeAppearanceModel = itemBinding.ivPhoto.shapeAppearanceModel
.toBuilder()
.setTopLeftCorner(CornerFamily.ROUNDED,radius)
.setBottomLeftCorner(CornerFamily.ROUNDED,radius)
.setTopRightCorner(CornerFamily.ROUNDED,radius)
.setBottomRightCorner(CornerFamily.ROUNDED,radius)
.build();
}else {
itemBinding.ivPhoto.shapeAppearanceModel = itemBinding.ivPhoto.shapeAppearanceModel
.toBuilder()
.setTopLeftCorner(CornerFamily.ROUNDED,radius)
.setBottomLeftCorner(CornerFamily.ROUNDED,radius)
.build();
}
}
override fun onClick(p0: View?) {
clicked.invoke(absoluteAdapterPosition)
}
}
I think something mistake in below code
(holder as StartViewHolder).bindData(item,itemCount == 1)
there are 2 items in RecyclerView but itemCount shows 1
What am i missing or doing wrong??
When I added an item to it First item remain fully rounded.
Code for initializing adapter
private val recentPagerAdapter by lazy { RecentPagerAdapter{
openPreview(PhotoBO(it.localPath))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mViewModel.getRecentPhotos().observe(viewLifecycleOwner)
{
recentPagerAdapter
.submitData(viewLifecycleOwner.lifecycle,it)
recentPagerAdapter.notifyDataSetChanged()
}
}
Please See Edit
itemCount and snapshot().items.size is 1 where as I have submitData 2 items to PagingDataAdapter
How to know total item submitted to PagingDataAdapter inside onBindViewHolder ?

How to fix java.lang.IndexOutOfBoundsException: Index: 0, Size: 0 on onbindviewholder

I have this app that I am working on and I keep getting this error when I swipe through the viewpager java.lang.IndexOutOfBoundsException: Index: 0, Size: 0 This is where the crash point to. I have tried searching but can't find a solution.
This is how my adapter class looks like: I have a feeling it might be my wrong positioning since after debugging it says the position on this line when (expenseData[position][0].expenseDate) { is 1 yet I pass 0?
class TransactionAdapter(
private val activity: Activity,
private val expenseData: ArrayList<ArrayList<ExpenseData>>,
private val totalExpenseAmount: Double)
: RecyclerView.Adapter<GeneralViewHolder>(), TransactionItemListener {
open class GeneralViewHolder(view: View?) : RecyclerView.ViewHolder(view!!)
internal class CustomViewHolder(val binding: TransactionsListBinding) : GeneralViewHolder(binding.root) {
fun setTotalExpenseDate(date: String?) {
binding.totalExpenseDate.text = date
}
fun setTotalExpenseAmount(amount: String?) {
binding.totalExpenseAmount.text = amount
}
}
override fun getItemViewType(position: Int): Int {
return if (position == 0) -1 else position
}
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): GeneralViewHolder {
val holder: GeneralViewHolder
val view: View
if (viewType == -1) {
view = LayoutInflater.from(activity)
.inflate(R.layout.month_summary_card, viewGroup, false)
holder = MonthSummaryCard(view)
} else {
val view1 = TransactionsListBinding.inflate(LayoutInflater.from(activity))
holder = CustomViewHolder(view1)
}
return holder
}
override fun onBindViewHolder(holder: GeneralViewHolder, position: Int) {
when (expenseData[position][0].expenseDate) {
SimpleDateFormat("MMM dd, yyyy",
Locale.US).format(Date()) -> holder1.setTotalExpenseDate("Today")
else -> holder1.setTotalExpenseDate(expenseData[position][0].expenseDate)
}
expenseData[position].forEach { expenseData ->
if (expenseData.expenseType == "Expense") totalExpense += expenseData.expenseAmount else totalIncome += expenseData.expenseAmount
}
val amount = totalIncome - totalExpense
if (amount < 0) holder1.setTotalExpenseAmount(expenseData[position][0].currency + " " +
java.lang.Double.valueOf(amount * -1).toString()) else holder1.setTotalExpenseAmount(expenseData[position][0].currency + " " + String.format(Locale.US, "%1$,.2f", amount))
}
} catch (e: NullPointerException) {
Timber.d("Currency null")
}
}
override fun getItemCount(): Int {
return expenseData.size
}
}
Will appreciate feedback..
In onBindViewHolder, you're accessing expenseData[position][0].expenseDate without checking if expenseData[position] has a non-zero size. It doesn't- you don't have any data there. Why your data is wrong is impossible to tell based on the code you've given.

Invisible items becomes visible on scroll in RecyclerView

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.

RecyclerView changing image incorrectly onNotifyItemChanged

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".

Categories

Resources