I have added an animation to the recyclerview to show the below transition.
When I long press on an item it shows the radio button and the item card moves to the right. The issue is that after the initial selection when ever I click or select other items, item6 and the items below animates again.
Can someone explain why this is happening and how I can fix this.
ListAdapter.kt:
class ListItemAdapter(private val values: List<PlaceholderContent.PlaceholderItem>
) : RecyclerView.Adapter<ListItemAdapter.ItemViewHolder>() {
private lateinit var itemClick: OnItemClick
private var selectedIndex: Int = -1
private var selectedItems: SparseBooleanArray = SparseBooleanArray()
private var isActive: Boolean = false
private var activateAnimation: Boolean = false
fun setItemClick(itemClick: OnItemClick) {
this.itemClick = itemClick
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): ListItemAdapter.ItemViewHolder {
val view = LayoutInflater.from(parent.context).inflate(
R.layout.fragment_item,
parent,
false
)
return ItemViewHolder(view)
}
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
holder.itemView.apply {
findViewById<TextView>(R.id.item_number).text = values[position].id
findViewById<TextView>(R.id.content).text = values[position].content
}
holder.itemView.findViewById<CardView>(R.id.list_item).setOnClickListener {
itemClick.onItemClick(values[position], position)
}
holder.itemView.findViewById<CardView>(R.id.list_item).setOnLongClickListener {
itemClick.onLongPress(values[position], position)
true
}
toggleIcon(holder, position)
}
override fun getItemCount(): Int {
return values.size
}
private fun itemTransition(holder: ItemViewHolder){
val animator = ObjectAnimator.ofFloat(holder.itemView.findViewById(R.id.list_item), View.TRANSLATION_X, 150f)
animator.start()
}
private fun itemTransitionBack(holder: ItemViewHolder){
val animator = ObjectAnimator.ofFloat(holder.itemView.findViewById(R.id.list_item), View.TRANSLATION_X, 0f)
animator.start()
}
fun toggleIcon(holder: ItemViewHolder, position: Int){
val checkBox = holder.itemView.findViewById<RadioButton>(R.id.is_selected)
if(selectedItems.get(position, false)){
checkBox.isGone = false
checkBox.isChecked = true
}
else{
checkBox.isGone = true
checkBox.isChecked = false
}
if(isActive) checkBox.isGone = false
if(activateAnimation){
itemTransition(holder)
}
else
itemTransitionBack(holder)
if(selectedIndex == position) selectedIndex = - 1
}
fun selectedItemCount() = selectedItems.size()
fun toggleSelection(position: Int){
selectedIndex = position
if (selectedItems.get(position, false)){
selectedItems.delete(position)
}else {
selectedItems.put(position, true)
}
notifyItemChanged(position)
isActive = selectedItems.isNotEmpty()
activateAnimation = selectedItems.isNotEmpty()
notifyDataSetChanged()
}
fun clearSelection(){
selectedItems.clear()
notifyDataSetChanged()
}
interface OnItemClick {
fun onItemClick(item: PlaceholderContent.PlaceholderItem, position: Int)
fun onLongPress(item: PlaceholderContent.PlaceholderItem, position: Int)
}
inner class ItemViewHolder(itemView: View): RecyclerView.ViewHolder(itemView){
}
}
ItemFragment.kt
adapter = ListItemAdapter(PlaceholderContent.ITEMS)
val recyclerViewList = view.findViewById<RecyclerView>(R.id.list)
recyclerViewList.adapter = adapter
recyclerViewList.layoutManager = LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false)
val myHelper = ItemTouchHelper(myCallback)
myHelper.attachToRecyclerView(recyclerViewList)
adapter.setItemClick(object : ListItemAdapter.OnItemClick{
override fun onItemClick(
item: PlaceholderContent.PlaceholderItem,
position: Int
) {
if(adapter.selectedItemCount() > 0)
toggleSelection(position)
}
override fun onLongPress(
item: PlaceholderContent.PlaceholderItem,
position: Int
) {
toggleSelection(position)
}
})
private fun toggleSelection(position: Int){
adapter.toggleSelection(position)
}
You're calling notifyDataSetChanged() inside your adapter.toggleSelection(position), regardless of whether this position was updated or not, this is re-binding all the visible views (and running the animations again).
Update
As stated in the comments, the reason why the 6th item is animated is likely due to the default ViewPool that the RecyclerView keeps (5 items). The 6th view is not part of that so it gets re-bound, re-displayed, and... re-animated.
What I would do is:
Could I get rid of the notifyDataSetChanged()? Why are you calling that?
Could I leverage RecyclerView-Selection since it's a Google library and what they suggest we use? It would have the benefit of less "custom" code.
Other than this, you could try to increase the RecycledViewPool as suggested by Pawel in the comments. Keep in mind this would likely be considered a code smell because different resolution, densities, screen sizes, etc., may affect how this behaves at runtime; this would be flaky and prone to fail, but depending on your particular use-case, may allow you go get away with it for now.
You can override getItemViewType(int position) this will returning the id of the view which is always unique.
#Override
public int getItemViewType(int position) {
return position;
}
or
#Override
public int getItemViewType(final int position) {
return R.layout.fragment_item;
}
Since the Android system stores a static reference to each layout as an Integer in the “R” (resources) class, we can simply return the layout resource id to be used in the onCreateViewHolder() method.
This is kind of a hack, but have you consider using?
holder.setIsRecyclable(false);
If the list would not be large and if it solves the issue, it can be used as a quick solution.
Instead of position try & use holder.getAdapterPosition() same for all the click action your are performing in to the bind method of the adapter,
holder.itemView.findViewById<CardView>(R.id.list_item).setOnClickListener {
itemClick.onItemClick(values[position], holder.getAdapterPosition())
}
holder.itemView.findViewById<CardView>(R.id.list_item).setOnLongClickListener {
itemClick.onLongPress(values[position], holder.getAdapterPosition())
true
}
toggleIcon(holder, holder.getAdapterPosition())
Related
I have a RecyclerView with a checkbox for every item. When checkbox pressed it is moved to the bottom of the list. When i check any item besides the first one everything is OK but if i check the first one, it auto-scroll to the new item location.
I redesigned the adapter using a recent codelab but still the same issue
class ItemsAdapter(val clickListener: ItemAdapterListener):
ListAdapter<UiQueryItem<Item>, ItemsAdapter.ViewHolder>(asyncDifferConfig) {
companion object {
private val diffCallback = object : UiQueryItemDiffCallback<Item>() {}
private val executors = AppExecutors()
private val asyncDifferConfig =
AsyncDifferConfig.Builder<UiQueryItem<Item>>(diffCallback)
.setBackgroundThreadExecutor(executors.cpuExecutorService)
.build()
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
holder.bind(item,clickListener)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder.from(parent)
}
class ViewHolder private constructor(val binding: ListItemBinding): RecyclerView.ViewHolder(binding.root){
fun bind(item: UiQueryItem<Item>, clickListener: ItemAdapterListener) {
binding.item = item.item
binding.clickListener = clickListener
binding.isSelected = item.isFlag1
binding.executePendingBindings()
}
companion object {
fun from(parent: ViewGroup): ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = ListItemBinding.inflate(layoutInflater, parent, false)
return ViewHolder(binding)
}
}
}
}
I think i found a workaround.
I noticed the next github issue solution:
Automatic scrolling when new data set #224
It did not work in my case but after tweaking a bit i found an answer. Please note that it is only a workaround and might make some bugs if you are trying to implement your own auto-scroll solution. The Workaround simply checkes if an item changed position from first place and if so, gets back to first position
Also note that #elihart is suggesting other solutions (check the link)
private val dataObserver = object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
super.onItemRangeMoved(fromPosition, toPosition, itemCount)
if (fromPosition == 0) {
binding.itemRv.scrollToPosition(0)
}
}
}
override fun onResume() {
super.onResume()
adapter.registerAdapterDataObserver(dataObserver)
}
override fun onPause() {
super.onPause()
adapter.unregisterAdapterDataObserver(dataObserver)
}
So am having this recyclerview which will contain holders of multiple types one of which could be a scrollable horizontal list of edge to edge images, that are being scrolled automatically and have a current item indicator. so for this i used a viewholder which will itself contain another recyclerview and a dots indicator( which itself is another recycler view, so basically recyclerview = a list of vh , where one of the vh = 2 horizontal recyclerview).
title
[A,B,C,D...]
[+ ---]
title
[A,B,C,D...]
[+ --]
title
[A,B,C,D...]
[+ --]
title
[A,B,C,D...]
[+ --]
My innermost recylerview of horizontal images is created something like this:
class ImageAdapter : RecyclerView.Adapter<ImageVH>() {
var imageResList = mutableListOf<Int>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ImageVH(parent, viewType)
override fun onBindViewHolder(holder: ImageVH, pos: Int)
= holder.bindData(imageResList[pos % imageResList.size])
override fun getItemCount() = Int.MAX_VALUE
}
class ImageVH(v: View) : RecyclerView.ViewHolder(v) {
constructor(parent: ViewGroup, viewtype: Int) : this(
LayoutInflater.from(parent.context).inflate(R.layout.item_image, parent, false)
)
fun bindData(imageRes: Int) {
Glide.with(itemView.context).load("").error(imageRes).into(itemView.ivImage)
}
}
it is basically fooling the adapter to think as if i have a million images but will actually have just a few images. this creates an impression of circular scroll.
Next i will need something to change the dots indicator of the second recyclerview. for this i went into the parent of this recyclerview and attached an onScrollListener . The onScrollListener gives me 2 function: onScrolled and onScrollStateChanged.
with onScrolled , i determine when to change the next dots recyclerview's state to show the new dot. i do this via linear layout manager. when it gives findFirstCompletelyVisibleItemPosition as positive number .
with onScrollStateChanged(), i run a kind of recursion, where whenever i get the state as SCROLL_STATE_IDLE, I post a handler to scroll the recyclerview to next item after 2 seconds. after 2 seconds, it will automatically smooth scroll and again fire the same event, causing the handler to fire the same action again.
so the code looks something like this:
data class Rails(val title: String, val images: MutableList<Int>,val autoscroll:Boolean =false)
class RailsAdapter : RecyclerView.Adapter<RailVH>() {
var railsList = mutableListOf<Rails>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = RailVH(parent, viewType)
override fun onBindViewHolder(holder: RailVH, pos: Int) = holder.bindData(railsList[pos])
override fun getItemCount() = railsList.size
}
class RailVH(v: View) : RecyclerView.ViewHolder(v) {
constructor(parent: ViewGroup, viewtype: Int) : this(
LayoutInflater.from(parent.context).inflate(R.layout.item_rails, parent, false)
)
private var autoscrollImages = false
fun bindData(rails: Rails) {
autoscrollImages = rails.autoscroll
with(itemView) {
tvTitle?.text = rails.title
rvImagers?.apply {
adapter = ImageAdapter().also {
it.imageResList = rails.images
it.notifyDataSetChanged()
}
PagerSnapHelper().attachToRecyclerView(this)
isNestedScrollingEnabled = false
onFlingListener = null
addOnScrollListener(onScrollListener)
}
}
if(autoscrollImages){
bannerChangerHandler.postDelayed(bannerChangerRunnable,bannerChangerDelayMilllis)
}
}
private val onScrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
//super.onScrolled(recyclerView, dx, dy)
val bannerLLManager = itemView.rvImagers?.layoutManager as? LinearLayoutManager
bannerLLManager?.let { linearLayoutManager ->
val bannerCurrentPos = linearLayoutManager.findFirstCompletelyVisibleItemPosition()
if (bannerCurrentPos >= 0) {
val rvDotsDataListSize = 5
val positionInRange = bannerCurrentPos % rvDotsDataListSize
Toast.makeText(
itemView.context,
"highlight dot #$positionInRange",
Toast.LENGTH_SHORT
).show()
}
}
}
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
//super.onScrollStateChanged(recyclerView, newState)
when (newState) {
RecyclerView.SCROLL_STATE_IDLE -> {
if(autoscrollImages){
Log.e(">>a>>", "RecyclerView.SCROLL_STATE_IDLE!")
bannerChangerHandler.postDelayed(bannerChangerRunnable, bannerChangerDelayMilllis
)
}
}
RecyclerView.SCROLL_STATE_DRAGGING -> {
Log.e(">>a>>", "RecyclerView.SCROLL_STATE_DRAGGING!")
bannerChangerHandler.removeCallbacks(bannerChangerRunnable)
}
else -> {
}
}
}
}
private val bannerChangerHandler: Handler = Handler()
private val bannerChangerRunnable = Runnable {
itemView.rvImagers?.apply {
val bannerManager = layoutManager as? LinearLayoutManager
bannerManager?.let {
val bannerCurrentPos = it.findFirstCompletelyVisibleItemPosition()
smoothScrollToPosition(bannerCurrentPos + 1)
}
}
}
private var bannerChangerDelayMilllis = 2000L
}
for brevity, assume whenever the toast is occuring, its going to scroll the 2nd dots indicator recyclerview .
This all seems to work in principle, but after sometimes the handler seems to fire twice or thrice , causing bad ux. sometimes it even goes berserks and stops showing any logs or anything and just makes the rails run infinetely very fast, like handler firing an autoscroll runner every millisecond.
handlers firing 2-3 times
So any help with this? i am assuming something is wrong at the implementation level, like firing handler events could be handled better?
Update:
thanks to #ADM , I got this working. I tweaked it as per my requirements, and had to forgo of circular scroll support in the reverse direction, but the given solution was enough to answer my query. thanks!
Handler is not an issue here its the Runnable. you are using and posting same Runnable each time thats why its getting piled up . You can not remove the previous call because you do not have a Tag or token to this delayed call . take a look at some of Handler's method like sendMessageDelayed these might help .
After giving it some thought i think you can move the Auto scroll part to SnapHelper. Not a full prove solution but i think it will work. You might have to put few checks in SnapHelper . Give it a try and let me know . i haven't tested it.
class AutoPagedSnapHelper(private var autoScrollInterval: Long) : PagerSnapHelper() {
private var recyclerView: RecyclerView? = null
private var currentPage = 0
private var isHold = false
private val autoScrollRunnable = Runnable {
recyclerView?.let {
if (recyclerView?.scrollState != RecyclerView.SCROLL_STATE_DRAGGING && !isHold) {
if (it.adapter != null) {
val lastPageIndex = (recyclerView?.adapter!!.itemCount - 1)
var nextIndex: Int
nextIndex = currentPage + 1
if (currentPage == lastPageIndex) {
nextIndex = 0
}
it.post {
val linearSmoothScroller = object : LinearSmoothScroller(recyclerView?.context) {
override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics): Float {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi
}
}
linearSmoothScroller.targetPosition = nextIndex
(recyclerView?.layoutManager as LinearLayoutManager).startSmoothScroll(linearSmoothScroller)
}
}
} else {
postNextPage()
}
}
}
override fun attachToRecyclerView(recyclerView: RecyclerView?) {
super.attachToRecyclerView(recyclerView)
if (this.recyclerView === recyclerView) {
return
}
if (autoScrollInterval != 0L) {
this.recyclerView = recyclerView
this.recyclerView?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (newState == RecyclerView.SCROLL_STATE_IDLE || newState == RecyclerView.SCROLL_STATE_SETTLING) {
val itemPosition = (recyclerView.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition()
if (itemPosition != -1) {
currentPage = itemPosition
postNextPage()
}
}
}
})
postNextPage()
recyclerView?.addOnItemTouchListener(object : RecyclerView.OnItemTouchListener {
override fun onInterceptTouchEvent(rv: RecyclerView, event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
isHold = true
}
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> {
isHold = false
}
}
return false
}
override fun onTouchEvent(rv: RecyclerView, event: MotionEvent) {}
override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {}
})
}
}
fun postNextPage() {
recyclerView?.handler?.removeCallbacks(autoScrollRunnable)
recyclerView?.postDelayed(autoScrollRunnable, autoScrollInterval)
}
companion object {
private const val MILLISECONDS_PER_INCH = 75f //default is 25f (bigger = slower)
}
}
This should take care of auto change page. You do not have to use scrollListener in Adapter. Give it a try.
Actually I am using recycler view and adding a layout in the rows and I am using flip animation on cardviews(when clicked on it). The problem is when I add multiple items in the recycler the flip animation works only with the first item. I used toast to make sure that click function is working with other items or not, turns out it's working but flip animation is not working with any other items.Can any one help me out here
This is my code
override fun onCardClick(item: PacketModel, position: Int) {
val scale = this.resources.displayMetrics.density
frontCard.cameraDistance= 8000 * scale
backCard.cameraDistance = 8000 * scale
front_anim = AnimatorInflater.loadAnimator(context, R.animator.front_animator) as AnimatorSet
back_anim = AnimatorInflater.loadAnimator(context, R.animator.back_animator) as AnimatorSet
if (isFront){
front_anim.setTarget(frontCard)
back_anim.setTarget(backCard)
front_anim.start()
back_anim.start()
isFront = false
}else
{
front_anim.setTarget(backCard)
back_anim.setTarget(frontCard)
back_anim.start()
front_anim.start()
isFront = true
}
Toast.makeText(context, item.Name , Toast.LENGTH_SHORT).show()
}
}
This is the adapter Class
class PacketAdapter (val packetList: ArrayList<PacketModel> , var clickListener2: onPacketItemClickListener): RecyclerView.Adapter<PacketAdapter.ViewHolder>(){
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val a = LayoutInflater.from(parent?.context).inflate(R.layout.packet, parent, false)
return ViewHolder(a)
}
override fun getItemCount(): Int {
return packetList.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val packet : PacketModel = packetList[position]
holder.intialize(packet, clickListener2)
}
class ViewHolder(itemView : View) : RecyclerView.ViewHolder(itemView)
{
val packetTime = itemView.findViewById<TextView>(R.id.packetTime)
val timeMessage = itemView.findViewById<TextView>(R.id.timeMessage)
fun intialize(item: PacketModel, action: onPacketItemClickListener){
packetTime.text = item.Name
timeMessage.text = item.Age
itemView.setOnClickListener {
action.onCardClick(item, adapterPosition)
}
}
}
interface onPacketItemClickListener{
fun onCardClick (item: PacketModel, position: Int)
}
}
You should place your card flipping code inside your recyclerview adapter so that recyclerview can recycle it as it should be. You can place your card flipping code inside itemview onClicklistener:
itemView.setOnClickListener {
// Place your flipping code here
action.onCardClick(item, adapterPosition)
}
Remove flipping code from onCardClick callback. Let me know if it works fine.
I'm new to Android development (and Kotlin).
I'm trying to implement a RecyclerView (which works fine) and when I click on a specific row it opens a new activity (Intent).
However, whenever I've press/click on one of the rows, I'm only able to get the value "-1" returned.
I've tried a number of different approaches (you should see the number of tabs in my browser).
This seems like it should be a fairly straightforward occurrence for something as common as a RecyclerView, but for whatever reason I'm unable to get it working.
Here is my RecyclerView Adapter file:
class PNHLePlayerAdapter (val players : ArrayList<PNHLePlayer>, val context: Context) : RecyclerView.Adapter<ViewHolder>() {
var onItemClick: ((Int)->Unit) = {}
// Gets the number of items in the list
override fun getItemCount(): Int {
return players.size
}
// Inflates the item views
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val itemView = LayoutInflater.from(context).inflate(
R.layout.pnhle_list_item,
parent,
false
)
val viewHolder = ViewHolder(itemView)
itemView.setOnClickListener {
onItemClick(viewHolder.adapterPosition)
}
return ViewHolder(itemView)
}
// Binds each item in the ArrayList to a view
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.tvPlayerName?.text = players[position].Name
holder.tvPlayerRank?.text = position.toString()
holder.tvPNHLe?.text = players[position].PNHLe.toString()
holder.tvTeam?.text = players[position].Team
holder.ivLeague?.setImageResource(leagueImageID)
}
}
class ViewHolder (view: View) : RecyclerView.ViewHolder(view) {
val linLayout = view.hor1LinearLayout
val ivTeam = view.teamImageView
val tvPlayerName = view.playerNameTextView
val tvPlayerRank = view.rankNumTextView
val tvPNHLe = view.pnhleTextView
val tvTeam = view.teamTextView
val ivLeague = view.leagueImageView
}
As you can see, there is a class property "onItemClick" which uses a lambda as the click callback.
I setOnClickListener in the onCreateViewHolder method after the view is inflated.
Next, in my Activity I add the list to my Adapter and set the call back.
However, every time I 'Toast' the position it is displayed as '-1'.
val adapter = PNHLePlayerAdapter(list, this)
adapter.onItemClick = { position ->
Toast.makeText(this, position.toString(),Toast.LENGTH_SHORT).show()
var intent = Intent(this, PlayerCardActivity::class.java)
//startActivity(intent)
}
rv_player_list.adapter = adapter
Perhaps I'm not thinking about this properly, but shouldn't the position represent the row number of the item out of the RecyclerView???
Ideally, I need to use the position so that I can obtain the correct item from the 'list' (ArrayList) so that I can pass information to my next Activity using the Intent
I found the issue.
Change this line in onCreateViewHolder:
return ViewHolder(itemView)
to this one:
return viewHolder
I would reorganize the adapter like this:
class PNHLePlayerAdapter : androidx.recyclerview.widget.RecyclerView.Adapter<Adapter.ViewHolder>() {
interface AdapterListener {
fun onItemSelected(position: Int?)
}
var players: List<Player> = listOf()
set(value) {
field = value
this.notifyDataSetChanged()
}
var listener: AdapterListener? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_car_selector, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(position)
}
override fun getItemCount(): Int {
return brands.size
}
inner class ViewHolder(view: View): androidx.recyclerview.widget.RecyclerView.ViewHolder(view) {
private var position: Int? = null
private val baseView: LinearLayout? = view.findViewById(R.id.baseView) as LinearLayout?
...
init {
baseView?.setOnClickListener {
listener?.onManufacturerSelected(position)
}
}
fun bind(position: Int) {
this.position = position
...
}
}
}
And from your activity/fragment set the listener as adapter.listener = this, and implement the onItemSelected(position: Int?)
override fun onItemSelected(position: Int?) {
...
}
I have a view and I want to change its size on click.
I have following layout for test:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<pro.labster.coloringbook.ui.view.ColorView
android:id="#+id/colorView"
android:layout_width="#dimen/colorSize"
android:layout_height="#dimen/colorSize"
android:layout_centerInParent="true"
app:normalSize="#dimen/colorSize"
app:selectedSize="#dimen/selectedColorSize" />
</RelativeLayout>
And the following code:
val colorView = findViewById<ColorView>(R.id.colorView)
colorView.setBackgroundColor(Color.RED)
colorView.setOnClickListener {
isSelected = !isSelected
colorView.setColorSelected(isSelected)
}
Size change code:
fun setColorSelected(isSelected: Boolean) {
if (isColorSelected != isSelected) {
if (isSelected) {
setCurrentSize(selectedSize.toInt())
} else {
setCurrentSize(normalSize.toInt())
}
}
isColorSelected = isSelected
}
private fun setCurrentSize(size: Int) {
if (layoutParams.height != size || layoutParams.width != size) {
layoutParams.width = size
layoutParams.height = size
requestLayout()
}
}
It works good:
https://www.youtube.com/watch?v=Ft8xcX5Qxbg
But if I add this view to RecyclerView, it lags on size change:
class ColorsAdapter(colorsHex: List<String>) : RecyclerView.Adapter<ColorsAdapter.ViewHolder>() {
private val colors = mutableListOf<Int>()
private var selectedPosition: Int = 0
init {
colorsHex.forEach {
colors.add(Color.parseColor(it))
}
}
override fun getItemCount() = colors.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val color = colors[position]
holder.colorView.setBackgroundColor(color)
holder.colorView.tag = position
holder.colorView.setColorSelected(position == selectedPosition)
}
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent?.context)
val view = inflater.inflate(R.layout.view_item_color, parent, false)
return ViewHolder(view)
}
inner class ViewHolder(itemView: View?) : RecyclerView.ViewHolder(itemView) {
val colorView: ColorView = itemView!!.findViewById(R.id.colorView)
init {
colorView.setOnClickListener {
val oldPosition = selectedPosition
selectedPosition = colorView.tag as Int
if (oldPosition != selectedPosition) {
notifyItemChanged(oldPosition)
notifyItemChanged(selectedPosition)
}
}
}
}
}
view_item_color.xml:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="#dimen/selectedColorSize"
android:layout_height="#dimen/selectedColorSize">
<pro.labster.coloringbook.ui.view.ColorView
android:id="#+id/colorView"
android:layout_width="#dimen/colorSize"
android:layout_height="#dimen/colorSize"
android:layout_gravity="center"
app:normalSize="#dimen/colorSize"
app:selectedSize="#dimen/selectedColorSize" />
</FrameLayout>
https://www.youtube.com/watch?v=m8g6zpj9aDg
As I can see, it also tries to animate size change — is it true?
And how to fix this lag?
As seen in the docs of DefaultItemAnimator:
This implementation of RecyclerView.ItemAnimator provides basic animations on remove, add, and move events that happen to the items in a RecyclerView. RecyclerView uses a DefaultItemAnimator by default.
If you want to remove those animations, then null out the default animator:
recyclerview.itemAnimator = null
You are using notifyItemChanged(position: Int), RecyclerView doesn't know what exactly changed - maybe item was replaced with another item all together, so new ViewHolder gets bound in adapters onBindViewHolder(holder: ViewHolder, position:Int) and replaces old one using default animation.
If you know explicitly what changed you can use notifyItemChanged(position: Int, payload: Any) to supply change payload for adapter, then RecyclerView will only perform partial update of a ViewHolder in adapters onBindViewHolder(holder: ViewHolder, position:Int, payloads: MutableList<Any>) and will not replace it.
For example you can change in your ViewHolder:
init {
colorView.setOnClickListener {
val oldPosition = selectedPosition
selectedPosition = colorView.tag as Int
if (oldPosition != selectedPosition) {
notifyItemChanged(oldPosition, false) // include payloads
notifyItemChanged(selectedPosition, true)
}
}
}
Then override in your Adapter:
override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
if(payloads.size < 1){
//if there are no payloads perform full binding
onBindViewHolder(holder, position)
return
}
// if viewHolder can consume incremental updates iterate over payloads
// otherwise it's enough to grab the last update
// cast as Boolean is safe because only Booleans are passed as payloads
holder.colorView.setColorSelected(payloads.last() as Boolean)
}
This is also a good place to start your own animations by running them on ViewHolders views (remember to override adapters onViewRecycled(holder: ViewHolder) and cancel/undo their state there)
You can do this by Item Animator and initialize them item animator