Keeping Parent Image when Dragging and Dropping in Kotlin - android

I am trying to drag an item but want to keep the parent image after dragging. I have a code that drags the item but it does not keep the parent item. I have tried commenting out the owner.removeView(v) but the program crashes if I do that. Ill take any help and Thank You in advance.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val chip = binding.chip
val card = binding.lltop
val card2 = binding.llbottom
card.setOnDragListener (dragListener)
card2.setOnDragListener(dragListener)
chip.setOnLongClickListener(){
val clipText = " this is our clipdata"
val item = ClipData.Item(clipText)
val mimeTypes = arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN)
val data = ClipData(clipText,mimeTypes, item)
val dragShadowBuilder = View.DragShadowBuilder(it)
it.startDragAndDrop(data, dragShadowBuilder, it,0)
it.visibility = View.VISIBLE
true
}
}
val dragListener = View.OnDragListener { view, event ->
when (event.action) {
DragEvent.ACTION_DRAG_STARTED -> {
event.clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)
}
DragEvent.ACTION_DRAG_ENTERED -> {
view.invalidate()
true
}
DragEvent.ACTION_DRAG_LOCATION -> true
DragEvent.ACTION_DRAG_EXITED -> {
view.invalidate()
true
}
DragEvent.ACTION_DROP -> {
val item = event.clipData.getItemAt(0)
val dragData = item.text
view.invalidate()
val v = event.localState as View
val owner = v.parent as ViewGroup
owner.removeView(v)
val destination = view as LinearLayout
destination.addView(v)
v.visibility = View.VISIBLE
true
}
DragEvent.ACTION_DRAG_ENDED -> {
view.invalidate()
true
}
else -> false
}
}
}

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

How to add items to RecyclerView from EditText, ImageView with onClickListener?

so I want to make an app, where you can add data such as Image, Video, description and Title from PopupWindow, then by clicking AddButton it will add provided data to recyclerView. I know I need to use notifyItemInserted but i don't know how. The Image will be a thumbnail, Video will be not visible, it will work only on the next fragment with player (i will do it later).
here is a preview of popupWindow and recyclerView behind it.
RecyclerAdapter.kt
class RecyclerAdapter: RecyclerView.Adapter<RecyclerAdapter.MyViewHolder>() {
private var dataList = emptyList<ListData>()
class MyViewHolder(val binding: CardLayoutBinding): RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
return MyViewHolder(CardLayoutBinding.inflate(LayoutInflater.from(parent.context), parent,false))
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.binding.tvTitle.text = dataList[position].title
holder.binding.tvDescription.text = dataList[position].description
holder.binding.tvThumbnail // what to do next?
}
override fun getItemCount(): Int {
return dataList.size
}
}
ListData.kt
data class ListData(
val id: Int,
val title: String,
val description: String,
val image: ImageView,
val Video: VideoView
)
MainActivity.kt
class MainActivity : AppCompatActivity() {
private var dialogView: View? = null
private val getPreviewImage = registerForActivityResult(ActivityResultContracts.GetContent(), ActivityResultCallback {
it?.let { uri ->
dialogView?.findViewById<ImageView>(R.id.imageChange)?.setImageURI(it)
}?:run {
Log.e("MainActivity", "URI not present")
}
})
private val getPreviewVideo = registerForActivityResult(ActivityResultContracts.GetContent(), ActivityResultCallback {
it?.let { uri ->
dialogView?.findViewById<VideoView>(R.id.videoChange)?.setVideoURI(it)
}?: run{
Log.e("MainActivity", "URI not present")
}
})
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
supportActionBar?.hide()
bottomNavigationView.background = null
bottomNavigationView.menu.findItem(R.id.placeholder).isEnabled = false
replaceFragment(HomeFragment())
bottomNavigationView.setOnItemSelectedListener {
when (it.itemId) {
R.id.home -> replaceFragment(HomeFragment())
R.id.player -> replaceFragment(PlayerFragment())
R.id.profile -> replaceFragment(ProfileFragment())
R.id.settings -> replaceFragment(SettingsFragment())
}
true
}
popupAddButton.setOnClickListener {
showDialog()
}
}
private fun replaceFragment(fragment: Fragment) {
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.fragment_container, fragment)
transaction.commit()
}
private fun showDialog() { //this is for popupWindow
dialogView = layoutInflater.inflate(R.layout.popup, null)
val dialog = Dialog(this)
val titleEditText = dialogView?.findViewById<EditText>(R.id.titleEdit) //popUp edit field title
val descEditText = dialogView?.findViewById<EditText>(R.id.description) //popUp edit field description
dialogView?.addImage?.setOnClickListener {
getPreviewImage.launch("image/*")
}
dialogView?.addVideo?.setOnClickListener {
getPreviewVideo.launch("video/*")
}
dialogView?.addButton?.setOnClickListener {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
if (titleEditText?.text?.isEmpty() == true || descEditText?.text?.isEmpty() == true){
Toast.makeText(applicationContext, "add required data", Toast.LENGTH_SHORT).show()
}else{
Toast.makeText(applicationContext, "Added", Toast.LENGTH_SHORT).show()
}
}
dialog.setContentView(dialogView!!)
dialog.show()
}
}
Don't see any new activity here so i will just drop a code examples so ppl who might encounter this question have some clue for start doing research of their own.
In the fragment where you have your recycler view you might set a listener for fragment result from specific child fragment using a TAG.
This is an example of mine:
childFragmentManager.setFragmentResultListener(
VisitanteVehiculoFormDialog.TAG,
viewLifecycleOwner
) { key, bundle ->
val result =
bundle.getSerializable(VisitanteVehiculoFormDialog.TAG_RESULT) as VisitanteVehiculoSerializale
_visitanteVehiculoAdapter.insertItem(
AutomovilVisitante(
1,
MODELO = result.MODELO,
PLACA_TEXTO = result.PLACA_TEXTO,
ID_VISITANTE_AUTORIZADO = result.ID_VISITANTE_AUTORIZADO,
ID_AUTOMOVIL_VISITANTE = result.ID_AUTOMOVIL_VISITANTE
)
)
In my dialog fragment i have the following:
binding?.buttonCrearVisitanteAutomovil?.setOnClickListener {
val b = Bundle()
b.putSerializable(
TAG_RESULT, VisitanteVehiculoSerializale(
MODELO = binding?.etModeloVehiculo2?.text.toString(),
PLACA_TEXTO = binding?.etPlacaVehiculo?.text.toString(),
ID_VISITANTE_AUTORIZADO = 0,
ID_AUTOMOVIL_VISITANTE = 0
)
)
Handler(Looper.getMainLooper()).postDelayed({
setFragmentResult(TAG, b)
dismiss()
}, 1000L)
}
hope someone find this examples useful.

Implement SearchView inside recyclerview header

I wanted to add a SearchView to my recyclerview. I wanted it to be at the top and scrollable with the items. To achieve this, I created separate adapter for my header and it contains the Searchview as well. Then I used a ConcatAdapter to combine this header adapter with the contents below it.
Initially I want all the items to be visible under the SearchView from _onBoardingState which is a MutableStateFlow and when user searches for a tag then the results for it get added to _onSearch which is also a MutableStateFlow.
I have this MutableStateFlow, _onBoardingState inside my ViewModel that gets the value from Firestore in the init of ViewModel. The number of results is less (~ 20) so there is no pagination implemented and all items get loaded at once.
Now, whenever user wants to search an item by a tag, the SearchView returns a Flow of the typed value and also a Flow that updates about if the SearchView is still open or closed. I used these extension functions for this:
fun SearchView.getQueryTextChangeStateFlow(onSubmit: ()-> Unit): StateFlow<String> {
val query = MutableStateFlow("")
setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
onSubmit()
return true
}
override fun onQueryTextChange(newText: String): Boolean {
query.value = newText
return true
}
})
return query
}
fun SearchView.getActiveStateFlow(): StateFlow<Boolean> {
val isOpen = MutableStateFlow(false)
setOnSearchClickListener {
isOpen.value = true
}
setOnCloseListener {
isOpen.value = false
false
}
return isOpen
}
Inside my ViewModel I have
...
private val _onBoardingState: MutableStateFlow<Model?> = MutableStateFlow(null)
private val _onSearch: MutableStateFlow<Model?> = MutableStateFlow(null)
private val _isActive: MutableStateFlow<Boolean> = MutableStateFlow(false)
fun toggleSearchViewState(isActive: Boolean) {
_isActive.value = isActive
}
val cuurentFlow: Flow<Model?> =
_isActive.flatMapLatest { isActive ->
if (isActive) {
_onSearch
} else {
_onBoardingState
}
}
...
Now the issue here is, whenever the recyclerview is scrolled down, the SearchView gets recycled and hence the setOnCloseListener gets called for it. This causes the _isActive value to be set to false by the Header's Adapter so the value of cuurentFlow gets toggled which should not be happening.
I thought of a solution as to set the setOnCloseListener of SearchView inside the header adapter's onViewRecycled() to null, but this didn't help. Below is code for my Header Adapter as well if needed.
class OnBoardingHeaderAdapter(
private val context: Context,
) : RecyclerView.Adapter<OnBoardingHeaderAdapter.HeaderViewHolder>() {
private var queryTextListener: ((StateFlow<String>) -> Unit)? = null
private var searchViewListener: ((StateFlow<Boolean>) -> Unit)? = null
inner class HeaderViewHolder(binding: OnboardingHeaderItemBinding) :
RecyclerView.ViewHolder(binding.root) {
private val root = binding.headerRoot
val search = binding.search
fun bind(headerMetaData: HeaderMetaData) {
root.visibility =
if (headerMetaData.shouldShow)
View.VISIBLE
else
View.GONE
val searchEditText: EditText =
search.findViewById(androidx.appcompat.R.id.search_src_text)
searchEditText.setHintTextColor(context.resources.getColor(R.color.white))
searchEditText.setTextColor(context.resources.getColor(R.color.white))
}
}
fun setQueryTextListener(listener: (StateFlow<String>) -> Unit) {
this.queryTextListener = listener
}
fun setSearchViewListener(listener: (StateFlow<Boolean>) -> Unit) {
this.searchViewListener = listener
}
private val RECYCLER_COMPARATOR = object : DiffUtil.ItemCallback<HeaderMetaData>() {
override fun areItemsTheSame(oldItem: HeaderMetaData, newItem: HeaderMetaData) =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: HeaderMetaData, newItem: HeaderMetaData) =
oldItem == newItem
}
val headerDiffer = AsyncListDiffer(this, RECYCLER_COMPARATOR)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeaderViewHolder {
val binding = OnboardingHeaderItemBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return HeaderViewHolder(binding)
}
override fun onBindViewHolder(holder: HeaderViewHolder, position: Int) {
//holder.setIsRecyclable(false)
if (position < 1) {
val header = headerDiffer.currentList[position]
holder.bind(header)
}
queryTextListener?.let {
it(holder.search.getQueryTextChangeStateFlow() {
val imm = context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
val view: View = holder.search
imm.hideSoftInputFromWindow(view.windowToken,0)
})
}
searchViewListener?.let {
it(holder.search.getActiveStateFlow())
}
}
override fun getItemCount(): Int = headerDiffer.currentList.size
override fun onViewRecycled(holder: HeaderViewHolder) {
super.onViewRecycled(holder)
holder.search.setOnCloseListener(null)
}
}
I wanted to know what is the best approach to solve this issue, I think even if i use a recyclerview with multiple view types here for the header then still the recycling issue will be there.

A list of 10+ items are retrieved from firebase database, after 9th item 10th item is affecting 1st and 11th item is affecting 2nd item and so on

I'm retrieving a list of items from the firebase database and displaying them using RecyclerView in my android app. The problem is when I'm retrieving more than 9 items and updating some data in the 1st item (while running) as you can see in the image below the 10th item also got affected and vice-versa same with 2nd and 11th item and so on.
Like when I'm adding and fixing the quantity of the 1st item , the same happens with the 10th item (the 10th item also get added with the same quantity as of 1st item).
MenuFragment.kt
class MenuFragment : Fragment() {
private var dishList: MutableList<DishModel> = mutableListOf()
private lateinit var myRef: DatabaseReference
lateinit var list: RecyclerView
lateinit var proceedToCartLayout: RelativeLayout
lateinit var addToCartBtn: Button
private var selectedCategory = ""
companion object {
fun newInstance(): Fragment {
return MenuFragment()
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_menu, container, false)
//retrieve id
val bundle = this.arguments
selectedCategory = bundle!!.getString("CATEGORY_ID")!!
list = view.findViewById(R.id.recyclerMenu)
myRef = FirebaseDatabase.getInstance().getReference("Category")
proceedToCartLayout = view.findViewById(R.id.ProceedToCart)
addToCartBtn = view.findViewById(R.id.btn_cart)
return view
}
override fun onResume() {
if (ConnectionManager().checkConnectivity(activity as Context)) {
fetchMenu()
} else {
val alterDialog = androidx.appcompat.app.AlertDialog.Builder(activity as
Context)
alterDialog.setTitle("No Internet")
alterDialog.setMessage("Connect to internet to continue")
alterDialog.setIcon(R.drawable.nointernet)
alterDialog.setPositiveButton("Open Settings") { _, _ ->
val settingsIntent = Intent(Settings.ACTION_SETTINGS)//open wifi settings
startActivity(settingsIntent)
}
alterDialog.setNegativeButton("Exit") { _, _ ->
ActivityCompat.finishAffinity(activity as Activity)
}
alterDialog.setCancelable(false)
alterDialog.create()
alterDialog.show()
}
super.onResume()
}
private fun fetchMenu() {
myRef.child(selectedCategory).addValueEventListener(object : ValueEventListener {
override fun onCancelled(p0: DatabaseError) {
Toast.makeText(context, "$p0", Toast.LENGTH_SHORT).show()
}
override fun onDataChange(p0: DataSnapshot) {
if (p0.exists()) {
dishList.clear()
for (i in p0.children) {
val plan = i.getValue(DishModel::class.java)
dishList.add(plan!!)
}
val adapter = MenuAdapter(
context!!,
R.layout.menu_list_item,
dishList,
proceedToCartLayout,
addToCartBtn, selectedCategory
)
list.adapter = adapter
}
}
})
}
}
MenuAdapter.kt
class MenuAdapter(
private val ctx: Context,
private val layoutResId: Int,
private val dishList:
List<DishModel>,
private val proceedToCartPassed: RelativeLayout,
private val buttonProceedToCart: Button,
private val categoryName: String
) : RecyclerView.Adapter<MenuAdapter.MyViewHolder>() {
private lateinit var proceedToCart: RelativeLayout
private var itemSelectedCount: Int = 0
private var itemsSelectedId = arrayListOf<String>()
private var itemList = arrayListOf<DishModel>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MenuAdapter.MyViewHolder {
val layoutInflater: LayoutInflater = LayoutInflater.from(ctx)
val view: View = layoutInflater.inflate(layoutResId, null)
return MyViewHolder(view)
}
override fun onBindViewHolder(holder: MenuAdapter.MyViewHolder, position: Int) {
var btnClick = false
var no = 0
val range = 1..5
val dish = dishList[position]
holder.dishName.text = dish.dishName
holder.dishPrice.text = "Rs " + dish.cost + " /-"
Picasso.get().load(dish.image).error(R.drawable.defaultdish).into(holder.dishImage)
holder.increaseBtn.setOnClickListener {
if (btnClick) {
no += 1
if (no in range) {
holder.quantity.text = no.toString()
dish.qty = no
}
}
}
holder.decreaseBtn.setOnClickListener {
if (btnClick) {
no -= 1
if (no in range) {
holder.quantity.text = no.toString()
dish.qty = no
}
}
}
proceedToCart = proceedToCartPassed
buttonProceedToCart.setOnClickListener {
val intent = Intent(ctx, CartActivity::class.java)
intent.putExtra(
"categoryId",
categoryName
)
intent.putExtra(
"selectedItemsId",
itemsSelectedId
)
intent.putExtra("itemList", itemList)
ctx.startActivity(intent)
}
holder.buttonAddToCart.setOnClickListener {
if (holder.buttonAddToCart.text.toString() == "Remove") {
itemSelectedCount--//unselected
itemsSelectedId.remove(holder.buttonAddToCart.tag.toString())
itemList.remove(dish)
holder.buttonAddToCart.text = "Add"
holder.buttonAddToCart.setBackgroundResource(R.drawable.rounded_corners)
btnClick = false
no = 0
dish.qty = no
holder.quantity.text = no.toString()
} else {
itemSelectedCount++//selected
itemsSelectedId.add(holder.buttonAddToCart.tag.toString())
itemList.add(dish)
holder.buttonAddToCart.text = "Remove"
holder.buttonAddToCart.setBackgroundResource(R.drawable.rounded_corners_yellow)
btnClick = true
no = 1
dish.qty = no
holder.quantity.text = no.toString()
}
if (itemSelectedCount > 0) {
proceedToCart.visibility = View.VISIBLE
} else {
proceedToCart.visibility = View.GONE
}
}
holder.buttonAddToCart.tag = dishList.indexOf(dish) + 1
}
override fun getItemCount(): Int {
return dishList.size
}
inner class MyViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val dishName: TextView = view.findViewById(R.id.txtDishName)
val dishPrice: TextView = view.findViewById(R.id.txtDishPrice)
val dishImage: ImageView = view.findViewById(R.id.dishImage)
val buttonAddToCart: Button = view.findViewById(R.id.add_dish)
val increaseBtn: Button = view.findViewById(R.id.increase)
val decreaseBtn: Button = view.findViewById(R.id.decrease)
val quantity: TextView = view.findViewById(R.id.quantity)
}
}
Please Add
holder.quantity.text = dish.qty.toString() after or before as you wish Picasso.get().load(dish.image).error(R.drawable.defaultdish).into(holder.dishImage)
Something like below.
Picasso.get().load(dish.image).error(R.drawable.defaultdish).into(holder.dishImage)
holder.quantity.text = dish.qty.toString()
holder.increaseBtn.setOnClickListener
Because I can see you are not setting holder.quantity.text thinking your recyclerview item view have the default as 0 but as you know in recyclerview the views are been reused and when they. are been reused the default value is not 0 for that but the value last view which is going to be reused, which in your case is every 10th item.
The golden rule if you are working with recylerview and thinking of using default value of any view from the XML view itself, so you will be surprised when you start scrolling you will notice that that is not the case and the default value is something other than that.
Always set the default value in onBind so next time the view been reused it takes the default value you want
if you are having an if condition to update something do write it's else as well because the same caching will make you wonder why your items are behaving differently.
Important point, make sure your setting default value in 1st point is outside any clicklistener or any view listener you set up because we have to make sure the setter gets executed every time the onBind is getting called
I have my fair share of experience working with checkbox, edittext , radio button etc in items and giving me surprising result on scrolling hence I always make sure to follow the above points when working with recylerviews.
I added the below code in my adapter (override getItemViewType) and it worked !
override fun getItemViewType(position: Int): Int {
return position
}

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.

Categories

Resources