RecyclerView Messing Up Views When Scrolling - With Picture - android

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

Related

How to deselect items in recycler view and validate it?

I am beginner in Android Kotlin , I am developing app which requires a selected data from recycler view to be in an arraylist. selection work fine ,if i selected item it will validate the value is null or not. if value not null button will enable. the problem is when i unselected item, the button is still enable and item still not removed from the selected data arrayList(data from API). if someone could help me to solve the issue, will be very glad. Thanks a lot. here is my code
private var checkPosition = -1
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GridViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_pul, parent, false)
return GridViewHolder(view)
}
override fun onBindViewHolder(holder: GridViewHolder, position: Int) {
holder.bindPul(listPul[position], listener)
}
override fun getItemCount(): Int {
return listpul.size
}
inner class GridViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
fun bindPul(pul: Model, listener: OnItemClickListener) = with(itemView){
val binding = ItemPBinding.bind(itemView)
binding.lblPayAmount.text = p.ModelValue.toDouble().currencyFormatter()
when (checkPosition) {-1 -> {
binding.itemPul.setCardBackgroundColor(Color.WHITE)
binding.itemPul.strokeColor = Color.parseColor("#DDEBF9")
binding.lblCheck.setVisible(false)
}else -> when(checkPosition){
absoluteAdapterPosition -> {
binding.itemPul.setCardBackgroundColor(Color.parseColor("#F5FAFF"))
binding.itemPul.strokeColor = Color.TRANSPARENT
binding.lblCheck.setVisible(true)
}else -> {
binding.itemP.setCardBackgroundColor(Color.WHITE)
binding.itemP.strokeColor = Color.parseColor("#DDEBF9")
binding.lblCheck.setVisible(false)
}
}
}
setOnClickListener {
binding.itemPul.setCardBackgroundColor(Color.parseColor("#F5FAFF"))
binding.itemPul.strokeColor = Color.TRANSPARENT
binding.lblCheck.setVisible(true)
if (checkPosition != absoluteAdapterPosition){
notifyItemChanged(checkPosition)
checkPosition = absoluteAdapterPosition
}
else{
binding.itemPul.setCardBackgroundColor(Color.WHITE)
binding.itemPul.strokeColor = Color.parseColor("#DDEBF9")
binding.lblCheck.setVisible(false)
}
listener.onItemClick(pul)
}
}
}
fun updateChecked(item: Model){
listp.mapIndexed { _, Model -> Model.isSelected = Model.ModelValue == item.ModelValue}
notifyItemChanged(checkPosition)
notifyDataSetChanged()
}
interface OnItemClickListener {
fun onItemClick(pul: Model)
}
}

kotlin notifyDataSetChanged does not update the view

I've begun to use kotlin a few days ago and I'm getting a problem with a recyclerView. When I do call the notifiyDataSetChanged method of my adapter the view does not update. (I used the debugger and my variable "usersList" is updated but not the view. Here's my Adapter class :
private class RankingAdapter : RecyclerView.Adapter<RankingViewHolder>() {
var usersList : MutableList<User?>? = mutableListOf()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RankingViewHolder {
val v = LayoutInflater.from(parent.context)
.inflate(R.layout.ranking_item_layout, parent,false) as LinearLayout
return RankingViewHolder(v)
}
override fun getItemCount(): Int {
return usersList?.size ?: 0
}
override fun onBindViewHolder(holder: RankingViewHolder, position: Int) {
val user : User? = usersList?.get(position)
holder.rank.text = (usersList?.indexOf(user)).toString()
holder.name.text = user?.firstName
holder.points.text = user?.score.toString()
}
fun setUsers(users : List<User?>) {
usersList?.clear()
usersList?.addAll(users)
notifyDataSetChanged()
}
}
and the method where setUsers(users) is called
private fun displayRanking(users : List<User>) {
visibleLayout.visibility = View.VISIBLE
errorImageView.visibility = View.GONE
progressBar.visibility = View.GONE
val logedInUser = SaveSharedPreference.getLogedInUser(context)
userRank.text = (adapter.usersList?.indexOf(logedInUser)).toString()
userName.text = logedInUser.firstName
userPoints.text = logedInUser.score.toString()
adapter.setUsers(users)
}
EDIT 1: Try this
fun setUsers(users : List<User?>) {
usersList?.clear()
usersList?.addAll(users)
}
Where you set recyclerView Adapter:
recyclerView.layoutManager = LinearLayoutManger(this, LinearLayoutManager.Vertical, false)
recyclerView.setAdapter(adapter)
And in your displayRanking function:
private fun displayRanking(users : List<User>) {
visibleLayout.visibility = View.VISIBLE
errorImageView.visibility = View.GONE
progressBar.visibility = View.GONE
val logedInUser = SaveSharedPreference.getLogedInUser(context)
userRank.text = (adapter.usersList?.indexOf(logedInUser)).toString()
userName.text = logedInUser.firstName
userPoints.text = logedInUser.score.toString()
adapter.setUsers(users)
adapter.notifyDataSetChanged()
}
Try calling adapter.notifyDataSetChanged() in your displayRanking function after calling adapter.setUsers(users) and delete notifyDataSetChanged() in your Adapter class.

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 keeps scrolling after reaching end of list

RecyclerView keeps scrolling
I am making a Media Library app. When I use the recyclerview list it scrolls over the same
items again and again. It does not stop when it reaches the bottom of the file list.
class MediaListAdapter(val mediaList: ArrayList<Media>): RecyclerView.Adapter<MediaListAdapter.MediaViewHolder>(){
private var context: Context? = null
fun updateMediaList(newMediaList: List<Media>){
mediaList.clear()
mediaList.addAll(newMediaList)
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaViewHolder {
context = parent.context
val inflater = LayoutInflater.from(parent.context)
val view = DataBindingUtil.inflate<ItemMediaBinding>(inflater, R.layout.item_media, parent, false)
return MediaViewHolder(view)
}
override fun onBindViewHolder(holder: MediaViewHolder, position: Int) {
holder.view.btnPlayVideo.visibility = View.INVISIBLE
holder.view.mediaItem = mediaList[position]
if(mediaList[position].mediaUrl.contains(".mp4")){
holder.view.btnPlayVideo.visibility = View.VISIBLE
holder.view.btnPlayVideo.setOnClickListener {
context!!.startActivity(Intent(context, VideoViewActivity::class.java).putExtra("videoUrl", mediaList[position].mediaUrl))
}
}
}
override fun getItemCount():Int {
return mediaList.size
}
class MediaViewHolder(var view: ItemMediaBinding) : RecyclerView.ViewHolder(view.root)
}
That is my Adapter class.
The media information is being pulled from a database containing the image urls and other information related to the information.
I have compared my code to a similar solution that works how I want and can't find any reason for the bug.
val media = MutableLiveData<List<Media>>()
val loading = MutableLiveData<Boolean>()
private val mediaList = ArrayList<String>()
fun fetchFromDatabase() {
loading.value = true
loadImages()
launch {
storeMedia(mediaList)
val media = MediaDatabase(getApplication()).mediaDao().getAllMedia()
mediaRetrieved(media)
}
}
private fun mediaRetrieved(mediaList: List<Media>) {
media.value = mediaList
loading.value = false
}
private fun storeMedia(list: List<String>) {
var found = false
launch {
val dao = MediaDatabase(getApplication()).mediaDao().getAllMedia()
for(item in list){
found = false
for(media in dao){
if(item == media.mediaLocation){
found = true
break
}
}
if(!found){
val media = Media(mediaUrl = item)
MediaDatabase(getApplication()).mediaDao().insert(media)
}
}
}
}
That is the code from the viewholder were I am adding data to the recyclerview adapter
Check the size of items that are available is your db/api from wherever you are retrieving media information(May be you are receiving the repeated data).

How to make delete button to delete recyclerview items in kotlin?

I made selectDelete button on the main activity instead of RecyclerView.
When I click that button, I want to delete the items which are checked.
There is an error that checks the RecyclerView item checkbox and unchecks the item when scrolling.
Activity
class CartViewActivity : AppCompatActivity(), SwipeRefreshLayout.OnRefreshListener {
private val tag = this::class.java.simpleName
lateinit var adapter: CartItemRecyclerAdapter
var itemList: MutableList<CartItemDataVo> = arrayListOf()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_cart_view)
selectDelete.setOnClickListener {
if (checkBox.isChecked) {
*adapter.removeItems()*
}
}
swipeRefreshLo.setOnRefreshListener(this)
itemList.add(CartItemDataVo("item1", 1, 16800, "cart_doll", false))
itemList.add(CartItemDataVo("item2", 1, 16800, "cart_cup", false))
itemList.add(CartItemDataVo("item3", 1, 30000, "cart_perfume", false))
itemList.add(CartItemDataVo("item4", 1, 16800, "cart_fan", false))
adapter = CartItemRecyclerAdapter(this, this, itemList)
recycler_view.adapter = adapter
recycler_view.layoutManager =
androidx.recyclerview.widget.LinearLayoutManager(applicationContext)
}
override fun onRefresh() {
swipeRefreshLo.isRefreshing = false
}
}
Adapter:
class CartItemDataVo(
title: String,
itemNumber: Int,
pointValue: Int,
imageView: String,
CheckBox: Boolean
) {
var title: String = title
var itemNumber: Int = itemNumber
var pointValue: Int = pointValue
var image: String = imageView
var isChecked: Boolean = CheckBox
}
class CartItemRecyclerAdapter(
val context: Context,
private var activity: Activity,
private var dataList: MutableList<CartItemDataVo>
) : RecyclerView.Adapter<CartItemRecyclerAdapter.Holder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
val view = LayoutInflater.from(context).inflate(R.layout.cart_item_list, parent, false)
return Holder(view)
}
override fun onBindViewHolder(holder: Holder, position: Int) {
holder?.bind(dataList[position], context)
}
override fun getItemCount(): Int = dataList.size
*#SuppressLint("NewApi")
fun removeItems() {
dataList.removeIf { it.isChecked }
notifyDataSetChanged()
}*
fun toggleItems() {
for (item: CartItemDataVo in dataList) {
var state = item.isChecked
item.isChecked = state.not()
}
notifyDataSetChanged()
}
inner class Holder(itemView: View?) : RecyclerView.ViewHolder(itemView!!) {
var titleText = itemView?.findViewById(R.id.titleText) as TextView
var temNumerTextt = itemView?.findViewById(R.id.textViewItemNumer) as TextView
var pointValueText = itemView?.findViewById(R.id.pointValueText) as TextView
var imageView = itemView?.findViewById(R.id.imageView) as ImageView
var checkBox = itemView?.findViewById(R.id.checkBox) as CheckBox
fun bind(data: CartItemDataVo, context: Context) {
if (data.image != "") {
val resourceId =
context.resources.getIdentifier(data.image, "drawable", context.packageName)
imageView?.setImageResource(resourceId)
} else {
imageView.setImageResource(R.mipmap.ic_launcher)
}
titleText?.text = data.title
temNumerTextt?.text = data.itemNumber.toString()
pointValueText?.text = data.pointValue.toString() + "P"
if (data.isChecked) {
checkBox.buttonDrawable =
checkBox.context.getDrawable(R.drawable.check_box_active_cs)
val layout = activity?.findViewById(R.id.layoutOrder) as LinearLayout
layout.visibility = View.VISIBLE
} else {
checkBox.buttonDrawable = checkBox.context.getDrawable(R.drawable.check_box_no)
val layout = activity?.findViewById(R.id.layoutOrder) as LinearLayout
layout.visibility = View.GONE
}
checkBox?.setOnClickListener {
if (checkBox.isChecked == data.isChecked) {
checkBox.buttonDrawable = it.context.getDrawable(R.drawable.check_box_active_cs)
val layout = activity?.findViewById(R.id.layoutOrder) as LinearLayout
layout.visibility = View.VISIBLE
} else {
checkBox.buttonDrawable = it.context.getDrawable(R.drawable.check_box_no)
val layout = activity?.findViewById(R.id.layoutOrder) as LinearLayout
layout.visibility = View.GONE
}
}
}
}
}
First of all, replace
var itemList: MutableList<CartItemDataVo> = arrayListOf()
with
val itemList: MutableList<CartItemDataVo> = arrayListOf()
You don't want mutable property, which is also mutable collection at the same time. This is very bad practice.
Same situation in adapter
private var dataList : MutableList<CartItemDataVo>
replace with
private val dataList : MutableList<CartItemDataVo>
Then remove private var activity : Activity from your adapter's constructor. Don't put any references to Activity or Fragment in adapter!
Adapter should be only responsible for displaying list.
If in any case, you need communication between Activity or Fragment and adapter, use interface instead. This is not ViewHolder responsibility to hold reference to parent layout and manipulate it!
val layout = activity?.findViewById(R.id.layoutOrder) as LinearLayout
All lines like below should be removed from ViewHolder. If you need set something which belongs to Activity, based on action on list, use interface.
Finally, in adapter add method which will remove checked items from it:
fun removeItems() {
itemList.removeIf { it.isChecked }
notifyDataSetChanged()
}
Add the following line right after checkBox?.setOnClickListener { (as first line in listener)
data.isChecked = !data.isChecked
And replace
selectDelete.setOnClickListener {
if(checkBox.isChecked){
}
}
with
selectDelete.setOnClickListener {
if(checkBox.isChecked){
adapter?.removeItems()
}
}
Bonus:
read about data class, and use it for CartItemDataVo (don't use CheckBox in constructor),
updateData method can be optimized using DiffUtil for RecyclerView,
it can be improved by modifying data in Activity, not in adapter, so responsible would be moved to better place,
read about SOLID principles,
read about MVVM and MVP design patterns.

Categories

Resources