I have a case where a view within RecyclerView items needs to be animated using the Lottie library. Each recycler view item is clickable and contains a liking Lottie animation.
I defined a custom RecyclerView.ItemAnimator like this:
class SampleItemAnimator : DefaultItemAnimator() {
override fun animateChange(
oldHolder: RecyclerView.ViewHolder,
newHolder: RecyclerView.ViewHolder,
preInfo: ItemHolderInfo,
postInfo: ItemHolderInfo
): Boolean {
val holder = newHolder as BindingViewHolder<ItemSampleBinding>
val animator = lottieAnimatorListener {
dispatchAnimationFinished(holder)
holder.binding.sampleAnimation.removeAllAnimatorListeners()
}
holder.binding.sampleAnimation.addAnimatorListener(animator)
if (preInfo is SampleItemHolderInfo) {
if (preInfo.isItemLicked) {
holder.binding.sampleAnimation.playAnimation()
} else {
resetAnimation(holder.binding.sampleAnimation)
}
return true
}
return super.animateChange(oldHolder, newHolder, preInfo, postInfo)
}
private fun resetAnimation(lottieAnimationView: LottieAnimationView) {
lottieAnimationView.progress = 0f
lottieAnimationView.cancelAnimation()
}
override fun recordPreLayoutInformation(
state: RecyclerView.State,
viewHolder: RecyclerView.ViewHolder,
changeFlags: Int,
payloads: MutableList<Any>
): ItemHolderInfo {
if (changeFlags == FLAG_CHANGED) {
return produceItemHolderInfoOrElse(payloads.firstOrNull() as? Int) {
super.recordPreLayoutInformation(state, viewHolder, changeFlags, payloads)
}
}
return super.recordPreLayoutInformation(state, viewHolder, changeFlags, payloads)
}
private fun produceItemHolderInfoOrElse(value: Int?, action: () -> ItemHolderInfo) =
when (value) {
LIKE_ITEM -> SampleItemHolderInfo(true)
UNLIKE_ITEM -> SampleItemHolderInfo(false)
else -> action()
}
override fun canReuseUpdatedViewHolder(viewHolder: RecyclerView.ViewHolder) = true
override fun canReuseUpdatedViewHolder(
viewHolder: RecyclerView.ViewHolder,
payloads: MutableList<Any>
) = true
}
lottieAnimatorListener is just a function that creates Animator.AnimatorListener to tell RecyclerView when the animation is canceled or ended by calling dispatchAnimationFinished(holder).
Everything works except that sometimes the liking animation can randomly play on items with no likes, especially while scrolling RecyclerView too fast.
As far as I understand, it happens because the ItemAnimator re-uses the same view holders and either uses outdated ItemHolderInfo or does not notify the RecyclerView about the end of the animation correctly.
That is how I pass a payload to the adapter to tell what has changed using DiffUtil.Callback.
class SampleListDiffCallback : DiffCallback<SampleItem> {
override fun areContentsTheSame(oldItem: SampleItem, newItem: SampleItem) =
oldItem.markableItem == newItem.markableItem
override fun areItemsTheSame(oldItem: SampleItem, newItem: SampleItem) =
oldItem.identifier == newItem.identifier
override fun getChangePayload(
oldItem: SampleItem,
oldItemPosition: Int,
newItem: SampleItem,
newItemPosition: Int
): Any? = createPayload(oldItem.markableItem, newItem.markableItem)
private fun createPayload(
oldItem: MarkableItem,
newItem: MarkableItem
) = when {
! oldItem.isLiked && newItem.isLiked -> LIKE_ITEM
oldItem.isLiked && ! newItem.isLiked -> UNLIKE_ITEM
else -> null
}
}
That is how I define a ViewHolder using the FastAdapter library:
class SampleItem(
val markableItem: MarkableItem,
private val onClickItem: (Item) -> Unit
) : AbstractBindingItem<ItemSampleBinding>() {
override val type: Int = R.layout.item
override var identifier: Long = markableItem.item.hashCode().toLong()
override fun createBinding(inflater: LayoutInflater, parent: ViewGroup?) =
ItemSampleBinding.inflate(inflater, parent, false)
override fun bindView(binding: ItemSampleBinding, payloads: List<Any>) {
super.bindView(binding, payloads)
with(binding) {
itemName.text = markableItem.item.name
itemImage.setContent(markableItem.item, IMAGE_SIZE)
likeAnimation.progress = if(markableItem.isClicked) 1f else 0f
root.setThrottleClickListener { onClickItem(markableItem.item) }
}
}
}
UPD: The liking animation's duration is 2 seconds.
Does anybody know if there is any way to fix it?
Related
So, I am creating a cocktail app, based on the https://www.thecocktaildb.com/ api. Thus far, I have only created a screen to display options based on the ingredient I put in the search bar (search bar is not done yet). Yet when I run the app, only the first entry is displayed
By putting Log.e("TAG", "$position") inside of my onBindViewHolder, of the adapter, I saw that the position variable never increases from 0
class CocktailsAdapter: RecyclerView.Adapter<CocktailsAdapter.CocktailsViewHolder>() {
inner class CocktailsViewHolder(val binding: ItemCocktailPreviewBinding) :
RecyclerView.ViewHolder(binding.root)
private val differCallback = object : DiffUtil.ItemCallback<CocktailsByBaseDto>() {
override fun areItemsTheSame( oldItem: CocktailsByBaseDto, newItem: CocktailsByBaseDto): Boolean {
return oldItem.drinks[0].idDrink == newItem.drinks[0].idDrink
}
override fun areContentsTheSame(oldItem: CocktailsByBaseDto, newItem: CocktailsByBaseDto): Boolean {
return oldItem.drinks[0] == newItem.drinks[0]
}
}
val differ = AsyncListDiffer(this, differCallback)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CocktailsViewHolder {
return CocktailsViewHolder(
ItemCocktailPreviewBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: CocktailsViewHolder, position: Int) {
val binding = holder.binding
val cocktail = differ.currentList[position]
holder.itemView.apply {
Glide.with(this).load(cocktail.drinks[position].strDrinkThumb).into(binding.imgCocktailsMainRecyclerViewImage)
binding.tvCocktailsMainRecyclerViewTitle.text = cocktail.drinks[position].strDrink
Log.e("TAG", "$position")
setOnClickListener {
onItemClickListener?.let { it(cocktail) }
}
}
}
override fun getItemCount(): Int {
return differ.currentList.size
}
private var onItemClickListener: ((CocktailsByBaseDto) -> Unit)? = null
fun setOnItemClickListener(listener: (CocktailsByBaseDto) -> Unit) {
onItemClickListener = listener
}
I have tried both position and 0 (which makes more sense) inside val cocktail = differ.currentList[position], but neither gave me a different result
Fixed it by changing the class I had passed to DiffUtil.ItemCallback
I am building an app to be able to drag an item from one recycle view to another and I still have to keep the option to re-order inside a single recycler view.
So I have defined a Reorder Callback already
class ReorderHelperCallback(val adapter : ItemTouchHelperAdapter): ItemTouchHelper.Callback() {
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN or
ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
return makeMovementFlags( dragFlags, 0)//swipeFlags )
}
override fun onMove(
recyclerView: RecyclerView,
source: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
adapter.onItemMove(source.getAdapterPosition(),
target.adapterPosition)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
//Not use for Drag N Drop
}
}
also an interface:
interface OnStartDragListener {
fun onStartDrag(viewHolder: RecyclerView.ViewHolder?)
}
and a touchhelper:
interface ItemTouchHelperAdapter {
fun onItemMove(fromPosition: Int, toPosition: Int): Boolean
fun onItemDismiss(position: Int)
}
to allow the reorder to work, I had to update the Recycler view adapter as below:
class Adapter(
private var context: Context?,
val dragStartListener : OnStartDragListener
): RecyclerView.Adapter<Adapter.ViewHolder>(), ItemTouchHelperAdapter {
var arrayItems : ArrayList<Data?> = ArrayList()
fun setData(array : MutableList<Data?>){
array.toCollection(arrayItems)
notifyDataSetChanged()
}
override fun getItemCount(): Int {
return arrayItems.size
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Adapter.ViewHolder {
val binding = DashboardTileLayoutBinding
.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding, dragStartListener)
}
override fun onBindViewHolder(holder: Adapter.ViewHolder, position: Int) {
holder.setData(arrayItems[position])
}
inner class ViewHolder(val binding: LayoutBinding,
val dragStartListener : OnStartDragListener? = null)
: RecyclerView.ViewHolder(binding.root) {
val tileLayout = binding.tileLayout
fun setData(data: Data?) {
....
tileLayout.setOnDragListener { view, dragEvent ->
when(dragEvent.action) {
ACTION_DRAG_STARTED -> {
dragStartListener?.onStartDrag(this)
true
}
else -> false
}
}
}
}
override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
Collections.swap(arrayItems, fromPosition, toPosition)
notifyItemMoved(fromPosition, toPosition)
return true
}
override fun onItemDismiss(position: Int) {
TODO("Not yet implemented")
}
}
and the fragment which contain the rv, I have updated the adapter init:
list1adapter?.let { adapter ->
adapter.setData(list)
val callback: ItemTouchHelper.Callback = ReorderHelperCallback(adapter)
mItemTouchHelperSelected = ItemTouchHelper(callback)
mItemTouchHelperSelected?.attachToRecyclerView(selectedLayout)
}
---
override fun onStartDrag(viewHolder: RecyclerView.ViewHolder?) {
viewHolder?.let {
mItemTouchHelperSelected?.startDrag(it)
}
}
But my fragment contain 2 recycler views. list1 is working fine to user drag and drop to re-order the item but now, I would like to also be able to move an item from my rv list1 to the list2 and vice versa
Any idea, how to make it Kotlin ? I tried an sample code, byt I am losing the re-ordering.
Thanks
Hey I am working in android kotlin. I am working in reyclerview. I want to do single selection in my items. I tried some code which is working fine.
OptionsViewAdapter.kt
class OptionsViewAdapter : ListAdapter<ProductVariant, OptionsViewHolder>(PRODUCT_COMPARATOR) {
private var selectedItemPosition: Int = 0
companion object {
private val PRODUCT_COMPARATOR = object : DiffUtil.ItemCallback<ProductVariant>() {
override fun areItemsTheSame(
oldItem: ProductVariant,
newItem: ProductVariant
): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(
oldItem: ProductVariant,
newItem: ProductVariant
): Boolean {
return oldItem == newItem
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OptionsViewHolder {
return OptionsViewHolder.bindView(parent)
}
override fun onBindViewHolder(holder: OptionsViewHolder, position: Int) {
holder.binding.root.setOnClickListener {
selectedItemPosition = holder.bindingAdapterPosition
notifyAdapter()
}
val drawableColor = if (selectedItemPosition == position)
R.drawable.options_item_selected_background
else
R.drawable.options_item_default_background
holder.binding.root.background =
ContextCompat.getDrawable(holder.binding.root.context, drawableColor)
holder.bindItem(getItem(position), position)
}
fun notifyAdapter() {
notifyDataSetChanged()
}
}
OptionsViewHolder.kt
class OptionsViewHolder(
val binding: OptionsItemLayoutBinding,
) : RecyclerView.ViewHolder(binding.root) {
companion object {
fun bindView(parent: ViewGroup): OptionsViewHolder {
return OptionsViewHolder(
OptionsItemLayoutBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
}
fun bindItem(item: ProductVariant?, position: Int) {
}
}
Video for single click is working fine.
When I move onBindViewHolder code inside bindItem it not working. Can someone guide me why this is happening.
vs
OptionsViewAdapter.kt
class OptionsViewAdapter : ListAdapter<ProductVariant, OptionsViewHolder>(PRODUCT_COMPARATOR) {
companion object {
private val PRODUCT_COMPARATOR = object : DiffUtil.ItemCallback<ProductVariant>() {
override fun areItemsTheSame(
oldItem: ProductVariant,
newItem: ProductVariant
): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(
oldItem: ProductVariant,
newItem: ProductVariant
): Boolean {
return oldItem == newItem
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OptionsViewHolder {
return OptionsViewHolder.bindView(parent)
}
override fun onBindViewHolder(holder: OptionsViewHolder, position: Int) {
holder.bindItem(getItem(position), position ,::notifyAdapter)
}
fun notifyAdapter() {
notifyDataSetChanged()
}
}
OptionsViewHolder.kt
class OptionsViewHolder(
val binding: OptionsItemLayoutBinding,
) : RecyclerView.ViewHolder(binding.root) {
private var selectedItemPosition: Int = 0
companion object {
fun bindView(parent: ViewGroup): OptionsViewHolder {
return OptionsViewHolder(
OptionsItemLayoutBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
}
fun bindItem(
item: ProductVariant?,
position: Int,
onAdapterChange: () -> Unit
) {
binding.root.setOnClickListener {
selectedItemPosition = position
onAdapterChange()
}
val drawableColor = if (selectedItemPosition == position)
R.drawable.options_item_selected_background
else
R.drawable.options_item_default_background
binding.root.background =
ContextCompat.getDrawable(binding.root.context, drawableColor)
}
}
The video for single click is not working.
You moved the selectedItemPosition into the ViewHolder class, so you have a separate copy of this property for every instance of the ViewHolder class so you are no longer affecting other items in the list when any one list item is clicked.
This would be much easier to implement by making the view holder class an inner class of the Adapter so it can directly modify the adapter’s selectedItemPosition property. And I would give the property a custom setter so you can automatically notify the adapter of the change instead of having to work with a separate function call. You can also make it notify the adapter specifically of which two row items changed instead of the whole data set (more efficient and by doing it in the property setter you have access to the old and new row positions in one place easily—field and value).
private var selectedItemPosition: Int = 0
set(value) {
val oldPos = field
field = value
notifyItemChanged(oldPos)
notifyItemChanged(value)
}
Even though you are passing the method reference notifyAdapter() in bindItem() function you are not calling it in the OnClickListener that is why it is not working.
So i have this strange behaviour where my RecyclerView only Displays data when i start the App and then restart the activity via Android Studio. Or make a change in XML, then undo it and restart.
The then displayed data is correct and also updates when i add things, but it is not visible after starting the app.
Can anyone help me out? I am really clueless why that would be.
Fragment:
#AndroidEntryPoint
class HistoryFragment : Fragment(R.layout.fragment_history) {
private val viewModel: PurchaseViewmodel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = FragmentHistoryBinding.bind(view)
val exampleAdapter = ExampleAdapter()
binding.apply{
recyclerView.apply{
layoutManager = LinearLayoutManager(requireContext())
adapter = exampleAdapter
setHasFixedSize(true)
}
ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false /// We dont need any interaction with Drag&Drop we only want swipe left&right
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val receipt = exampleAdapter.currentList[viewHolder.adapterPosition]
viewModel.onSwipe(receipt)
}
}).attachToRecyclerView(recyclerView)
}
setFragmentResultListener("add_receipt_request"){_,bundle ->
val result = bundle.getInt("add_receipt_request")
viewModel.onAddResult(result)
}
viewModel.receipts.observe(viewLifecycleOwner){ /// New Items get passed to the List
exampleAdapter.submitList(it)
}
viewLifecycleOwner.lifecycleScope.launchWhenStarted { //as soon as we close our app the events will be suspended, but not deleted and will remain after restart
viewModel.addTaskEvent.collect { event->
when(event){
is PurchaseViewmodel.TasksEvent.ShowUndoDelete -> {
Snackbar.make(requireView(),"Tasks deleted", Snackbar.LENGTH_LONG)
.setAction("UNDO"){
viewModel.unDoDeleteClick(event.receipts)
}.show()
}
}
}
}
}
}
Adapter:
class ExampleAdapter : ListAdapter<Receipts,ExampleAdapter.ExampleViewHolder>(DiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ExampleViewHolder {
val binding = ReceiptsBinding.inflate(LayoutInflater.from(parent.context),parent,false)
return ExampleViewHolder(binding)
}
override fun onBindViewHolder(holder: ExampleViewHolder, position: Int) {
val currentItem = getItem(position)
holder.bind(currentItem)
}
override fun getItemCount(): Int {
return super.getItemCount()
}
class ExampleViewHolder(private val binding: ReceiptsBinding) : RecyclerView.ViewHolder(binding.root){ //Examples One Row in our list
fun bind (receipts: Receipts) {
binding.apply {
storeHistory.text = receipts.store
amountHistory.text = receipts.total
dateHistory.text = receipts.date
}
}
}
class DiffCallback : DiffUtil.ItemCallback<Receipts>() {
override fun areItemsTheSame(oldItem: Receipts, newItem: Receipts) =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Receipts, newItem: Receipts) =
oldItem == newItem
}
}
You should compare corresponding fields of old/new Receipts in areContentsTheSame() instead of comparing the entire old/new objects
override fun areContentsTheSame(oldItem: Receipts, newItem: Receipts) =
oldItem.store == newItem.store
&& oldItem.total == newItem.total
&& oldItem.date == newItem.date
How to solve the child adapter image that changes and show wrong images after scrolling?
this is my code
Parent Data Class
data class Review(
val author: String,
val date: String,
val rating: Float,
val comment: String,
val images: List<Image>)
Child Data Class
For author and comment, I only take it from the parent data class
data class Image(
var author: String? = null,
var comment: String? = null,
val large: String,
val thumbnail: String)
ParentAdapter
class ReviewAdapter(private val callback: ProductReviewImageAdapterCallback) :
PagingDataAdapter<Review, ReviewAdapter.ListViewHolder>(DIFF_CALLBACK) {
private val viewPool = RecyclerView.RecycledViewPool()
private val imageAdapter: ProductReviewImageAdapter by lazy {
ProductReviewImageAdapter(callback)
}
companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Review>() {
override fun areItemsTheSame(oldItem: Review, newItem: Review): Boolean {
return oldItem.author == newItem.author
}
override fun areContentsTheSame(oldItem: Review, newItem: Review): Boolean {
return oldItem.comment == newItem.comment
}
}
}
inner class ListViewHolder(itemBinding: ItemProductReviewBinding) :
RecyclerView.ViewHolder(itemBinding.root) {
val binding = ItemProductReviewBinding.bind(itemBinding.root)
fun bind(data: Review, position: Int) {
with(binding) {
if (data.images.isEmpty()) {
rvImageReview.gone()
} else {
setupImagesRecyclerView(rvImageReview)
rvImageReview.visible()
imageAdapter.differ.submitList(data.images)
}
tvNameReviewer.text = data.author
tvReviewDesc.text = data.comment
tvDateReview.text = data.date
ratingBarReview.rating = data.rating
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder =
ListViewHolder(
ItemProductReviewBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
override fun onBindViewHolder(holder: ListViewHolder, position: Int) {
val review = getItem(position)
if (review != null) {
holder.bind(review, position)
}
}
private fun setupImagesRecyclerView(recyclerView: RecyclerView) {
recyclerView.apply {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
adapter = imageAdapter
setRecycledViewPool(viewPool)
setItemViewCacheSize(20)
}
}
}
ChildAdapter
class ProductReviewImageAdapter(private val callback: ProductReviewImageAdapterCallback) : RecyclerView.Adapter<ProductReviewImageAdapter.ListViewHolder>() {
private val diffCallback = object : DiffUtil.ItemCallback<Image>() {
override fun areItemsTheSame(
oldItem: Image,
newItem: Image
): Boolean {
return oldItem.author == newItem.author
}
override fun areContentsTheSame(
oldItem: Image,
newItem: Image
): Boolean {
return oldItem.comment == newItem.comment
}
}
val differ = AsyncListDiffer(this, diffCallback)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder =
ListViewHolder(
ItemImageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
override fun getItemCount(): Int = differ.currentList.size
override fun onBindViewHolder(holder: ListViewHolder, position: Int) {
holder.bind(differ.currentList[position], position)
}
inner class ListViewHolder(itemBinding: ItemImageBinding) :
RecyclerView.ViewHolder(itemBinding.root) {
private val itemBinding = ItemImageBinding.bind(itemBinding.root)
fun bind(data: Image, position: Int) {
with(itemBinding) {
image.loadImageRoundedCorner(data.thumbnail)
image.setOnClickListener {
callback.onProductReviewImageClicked(position, differ.currentList)
}
}
}
}
}
Before scrolling
on first load, the first position data displays the appropriate image
before scrolling
After a bit scrolling
the image in the first position data changes to the image in the next data
after a bit scrolling
Scroll to last data/page
the last image shows an unsuitable image
scroll to last data/page
*Note
there is only 5 data, and only first and last data contains image
The image in the first position data should be a person image and the last position image should be a pink image
I use Coil as the image loader and have used the ImageView.clear() method before loading the image
Thanks in advance, sorry if my english is bad. I'm not a native speaker
Update
Fixed :
it turned out that it was only wrong at the time of adapter initialization, the adapter should be initialized in onBindViewHolder, not in the parent adapter so that not all items have the same adapter which causes all data to change