I am trying to make a recycler view like tumblr app. You can see here: https://streamable.com/s/gpyec/kxvjnz
My question is how to add a video (or any clickable) below recyclerview? I added an item decoration implementation as follows:
class RecyclerViewAdItemDecoration(private val func:() -> Unit) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
super.getItemOffsets(outRect, view, parent, state)
val position = parent.getChildLayoutPosition(view)
val mLayoutManager = parent.layoutManager
if (mLayoutManager is GridLayoutManager) {
setGridParams(view, position, parent)
} else if (mLayoutManager is LinearLayoutManager) {
setLinearParams(view, position, parent)
}
}
private fun setGridParams(view: View, position: Int, parent: RecyclerView) {
val p = view.layoutParams as ViewGroup.MarginLayoutParams
if (position == 0) {
p.setMargins(0,0,0, 0)
} else if (position >= 10 && (position % 10 == 0 || position % 11 == 0)) {
p.setMargins(0,0,0, parent.height)
func()
} else {
p.setMargins(0,0,0, 0)
}
}
private fun setLinearParams(view: View, position: Int, parent: RecyclerView) {
val p = view.layoutParams as ViewGroup.MarginLayoutParams
if (position == 0) {
p.setMargins(0,0,0, 0)
} else if (position >= 10 && (position % 10 == 0)) {
p.setMargins(0,0,0, parent.height)
func()
} else {
p.setMargins(0,0,0, 0)
}
}
}
This way I could add enough space for background view but it's not clickable now. I also couldn't find any library for such implementation. Appreciated for any help.
Edit:
To clarify, I want to show background video (or any view) right after every 10th item in recycler view. Like it's seen in video in the link, there is a space between every 10 item in recycler view, which also triggers to play the video in the background (below recycler view)
show background video (or any view) right after every 10th item in
recycler view.
If the background video is after every 10th item, it means there is an item (11th), which is transparent.
What you actually want here is recyclerview with multiple view types.
Use a RelativeLayout for activity_main.xml which allows to place views
on top of others (in Z axis).
ex: RecyclerView is the top most view here.
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:focusable="true">
<View
android:id="#+id/ad"
android:background="#color/colorAccent"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rv_posts"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</RelativeLayout>
Create two item layouts for two types of recycler view types
ex:
item_normal.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="100dp">
<TextView
android:id="#+id/tv_post"
tools:text="Post"
android:background="#android:color/white"
android:textSize="32sp"
android:gravity="center_horizontal|center_vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
item_transparent.xml (where layout background is transparent that allow to see the view below the surface area)
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#android:color/transparent"/>
Setting android:clickable=false in item_transparent does not stop triggering the click event on transparent item, so use communication flow using interfaces, to bring the other view(ad) to the front when clicked on transparent item.
MainActivity.kt
class MainActivity : AppCompatActivity(), RvAdpater.OnItemClick {
private lateinit var adView: View
private lateinit var rvPosts: RecyclerView
override fun onClick() {
bringAdFront()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
rvPosts = findViewById(R.id.rv_posts)
rvPosts.layoutManager = LinearLayoutManager(this)
val rvAdpater = RvAdpater()
rvAdpater.setListener(this)
rvPosts.adapter = rvAdpater
}
private fun bringAdFront() {
adView = findViewById<View>(R.id.ad)
adView.bringToFront()
}
override fun onBackPressed() {
// to go back to the normal recycler view when back button is pressed
val parent = rvPosts.parent as ViewGroup
parent.removeAllViews()
parent.addView(adView, 0)
parent.addView(rvPosts, 1)
}
}
RvAdapter.kt
const val TAG = "RvAdpater"
class RvAdpater : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private lateinit var listener:OnItemClick
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val viewNormal = LayoutInflater.from(parent.context).inflate(R.layout.item_normal, parent, false)
val viewTransparent = LayoutInflater.from(parent.context).inflate(R.layout.item_transparent, parent, false)
return when(viewType){
0 -> NormalViewHolder(viewNormal)
2 -> TransparentViewHolder(viewTransparent)
else -> NormalViewHolder(viewNormal)
}
}
override fun getItemCount(): Int = 10
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when(getItemViewType(position)){
0 -> {
val normalHolder = holder as NormalViewHolder
normalHolder.tv.text = "Post"
normalHolder.itemView.setOnClickListener {
Log.d(TAG, "Clicked on Normal item")
}
}
2 -> {
val transparentHolder = holder as TransparentViewHolder
transparentHolder.itemView.setOnClickListener {
listener.onClick()
}
}
}
}
fun setListener(onItem:OnItemClick){
listener = onItem
}
interface OnItemClick{
fun onClick()
}
override fun getItemViewType(position: Int): Int = position % 2 * 2
class NormalViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
val tv:TextView = itemView.findViewById(R.id.tv_post)
}
class TransparentViewHolder(itemView: View): RecyclerView.ViewHolder(itemView)
}
Checkout the repo for an working example
For handling multiple view types, you may use Epoxy library.
Related
Using ItemTouchHelper class we can DRAG, DROP, & SWIPE items in the recycler view; but how to just merge two items when dragged & dropped on one another?
Is it possible to do using ItemTouchHelper (or) is there any other API for that?
You could override onMove() in your ItemTouchHelper, it is called when you drag item A over item B. It gets called with the parameters viewHolder: RecyclerView.ViewHolder and target: RecyclerView.ViewHolder where viewHolder is the viewHolder of item A, and target is item B.
Have some variable of type ViewHolder, that you set to target in onMove, to always have a reference to the item below item A.
override clearView() to detect when the item is dropped, update your model in the background, so itemA now is merged with target, then call notifyItemChanged(itemA.adapterPosition) and notifyItemRemoved(itemB.adapterPosition) to animate a "merge"
class MainActivity : AppCompatActivity() {
companion object{
val fruit = arrayListOf("apple", "pear", "orange", "banana", "grape", "pineapple")
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView1 = findViewById<RecyclerView>(R.id.testRecycler)
val layoutManager = LinearLayoutManager(this)
recyclerView1.layoutManager = layoutManager
val adapter = FruitAdapter()
recyclerView1.adapter = adapter
val itemTouchHelper = ItemTouchHelper(
object : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.UP or ItemTouchHelper.DOWN,
0
) {
#SuppressLint("StaticFieldLeak")
var target: RecyclerView.ViewHolder? = null
#SuppressLint("StaticFieldLeak")
var moving: RecyclerView.ViewHolder? = null
override fun clearView(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
) {
if(target!=null && moving != null){
val targetPos = target!!.adapterPosition
val sourcePos = moving!!.adapterPosition
fruit[targetPos] += "\n\n" + fruit[sourcePos]
fruit.removeAt(sourcePos)
target = null
moving = null
adapter.notifyDataSetChanged()
}
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder
): Boolean {
this.target = target
this.moving = viewHolder
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
TODO("Not yet implemented")
}
})
itemTouchHelper.attachToRecyclerView(recyclerView1)
}
}
class FruitAdapter: RecyclerView.Adapter<FruitAdapter.FruitViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FruitViewHolder {
val itemView = LayoutInflater.from(parent.context)
.inflate(R.layout.item, parent, false)
return FruitViewHolder(itemView)
}
override fun onBindViewHolder(holder: FruitViewHolder, position: Int) {
holder.itemView.findViewById<TextView>(R.id.fruitNameTextView).text = MainActivity.fruit[position]
}
override fun getItemCount(): Int {
return MainActivity.fruit.size
}
class FruitViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){}
}
item.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:padding="20px"
android:layout_margin="20px"
android:background="#color/teal_200"
android:layout_height="wrap_content">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/fruitNameTextView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:id="#+id/testRecycler"
android:layout_height="match_parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
I am trying to creating a animation in a recyclerview. The kind of animation that I am looking for is the one which is available in Telegram android app.
When you open a chat in telegram and long press on the message, the recyclerview multi select option comes with checkbox. I am trying to create the same effect.
My current status:
ListItemAdapter.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
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: ListItemAdapter.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.setOnClickListener {
itemClick.onItemClick(values[position], position)
}
holder.itemView.setOnLongClickListener {
itemClick.onLongPress(values[position], position)
true
}
toggleIcon(holder, position)
}
override fun getItemCount(): Int {
return values.size
}
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(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()
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){
}
}
fragment_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:id="#+id/list_item"
>
<RadioButton
android:id="#+id/is_selected"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_margin="16dp"
/>
<TextView
android:id="#+id/item_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="#dimen/text_margin"
android:textAppearance="?attr/textAppearanceListItem" />
<TextView
android:id="#+id/content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="#dimen/text_margin"
android:textAppearance="?attr/textAppearanceListItem" />
</LinearLayout>
MainActivity.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)
}
})
return view
}
private fun toggleSelection(position: Int){
adapter.toggleSelection(position)
}
To add an animated transition, the simplest way is by adding
animateLayoutChanges="true"
to the individual item root(since that's where you'd like it animated), which in your case would be:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:id="#+id/list_item"
animateLayoutChanges="true" <!-- Around here -->
>
...
</LinearLayout>
To have it look exactly like in telegram, you'd need a bit of a different design to start with, but that's another issue(and a matter of perseverance in a way :P);
Also if you'd like different types of animations, you'd need to go at it with a different way, you could do it by adding animate to the view: ( alpha goes from 0f to 1f usually((invisible-visible)), and you'd need to change the view visibility paremeter if needed to visible/invisible/gone so that it doesn't register click events on that area, depending on your needs. In any case, this should be trial & error to get your desired behavior)
is_selected.animate().alpha(1f).setInterpolator(AccelerateDecelerateInterpolator())
is_selected.visibility = View.VISIBLE
You could also read up a little on available android interpolators, since we can't be quite sure whether they will remove some(unlikely)/add new ones.
I have vertical RecyclerView with child(nested) horizontal RecyclerView.
For second recycler I use layout manager:
LinearLayoutManager(itemView.context, LinearLayoutManager.HORIZONTAL, false).apply {
isItemPrefetchEnabled = true
initialPrefetchItemCount = 4
}
But nested recycler builds only first two visible items.
Full code:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recycler = findViewById<RecyclerView>(R.id.recycler)
recycler.layoutManager = LinearLayoutManager(this)
recycler.adapter = MainAdapter(buildElements())
}
private fun buildElements(): List<Parent> {
return (0..2).map { pCount -> createParent(pCount) }
}
private fun createParent(index: Int): Parent {
return Parent(text = "$index",
list = (0..30).map { Child("item $it") }
)
}
}
Main adapter
class MainAdapter(private val list: List<Parent>) : RecyclerView.Adapter<MainAdapter.CustomHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomHolder {
val inflater = LayoutInflater.from(parent.context)
return CustomHolder(inflater.inflate(R.layout.item, parent, false))
}
override fun getItemCount(): Int = list.size
override fun onBindViewHolder(holder: CustomHolder, position: Int) {
holder.bind(list[position])
}
class CustomHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val recycler = itemView.findViewById<RecyclerView>(R.id.recyclerView)
private val title = itemView.findViewById<TextView>(R.id.title)
init {
recycler.layoutManager =
LinearLayoutManager(itemView.context, LinearLayoutManager.HORIZONTAL, false).apply {
isItemPrefetchEnabled = true
initialPrefetchItemCount = 4
}
}
fun bind(data: Parent) {
title.text = data.text
recycler.adapter = ChildAdapter(data.list)
}
}
}
Main recycler element
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="#+id/title"
android:layout_width="wrap_content"
android:layout_height="16dp"/>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerView"
android:layout_marginTop="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</FrameLayout>
Child adapter
class ChildAdapter(private val list: List<Child>) : RecyclerView.Adapter<ChildAdapter.ChildHolder>() {
companion object {
private val TAG = ChildAdapter::class.java.simpleName
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChildHolder {
val inflater = LayoutInflater.from(parent.context)
return ChildHolder(inflater.inflate(R.layout.child, parent, false))
}
override fun getItemCount(): Int = list.size
override fun onBindViewHolder(holder: ChildHolder, position: Int) {
holder.bind(list[position])
}
class ChildHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val title = itemView.findViewById<TextView>(R.id.childText)
fun bind(data: Child) {
title.text = data.text
Log.d(TAG, "bind child element: ${data.text}")
}
}
}
And child element:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="275dp"
android:padding="8dp"
android:layout_height="100dp">
<TextView
android:id="#+id/childText"
android:layout_width="wrap_content"
android:layout_gravity="center"
android:layout_height="wrap_content"/>
</FrameLayout>
In logs is see only this:
D/ChildAdapter: bind child element: item 0
D/ChildAdapter: bind child element: item 1
D/ChildAdapter: bind child element: item 0
D/ChildAdapter: bind child element: item 1
D/ChildAdapter: bind child element: item 0
D/ChildAdapter: bind child element: item 1
Logs mean that only the first and second elements was build, but I want prebuilt also third and forth elements.
Also I try use this, but it also not working
prefetching working only for scrolling, not for the first initial items creation.
Finally I use custom layout manager with override getExtraLayoutSpace
and return 100000. My solution potentially dangerous, but I use it, because I know that item count can't be more than 10 items.
I am using a RecyclerView with GridLayoutManager. Users can toggle the span count between 2 and 4, which would result in an animation that runs the inbuilt translate animation for each cell to it's new position. Code I have been using thus far is:
TransitionManager.beginDelayedTransition(moviesGridRecycler);
gridLayoutManager.setSpanCount(NEW_SPAN_COUNT);
adapter.notifyDataSetChanged();
This has been working fine for me, but now I need to have a different layout for each span count. To support this, I have 2 view types in the RecyclerView and since the view type is changed when moving to a new span count, RecyclerView is unable to see that it's the "same" content, and the default translate animation is not run.
If I enable layout transitions, I get view entry animations and not view position change animations. Cell views for both span count are having a width and height as match_parent and wrap_content.
As an alternative, I tried dropping the new view type all together, and just having one view type. I have a FrameLayout, which holds views for both span counts. In onBindViewHolder(), I simply toggle visibility of child views. This too results in no animations.
I followed this thread and this one, but could not resolve my issue.
Full Code:
class ItemFragment : Fragment() {
private var spanCount = 2
lateinit var recyclerView: RecyclerView
lateinit var button: Button
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_item_list, container, false)
recyclerView = view.findViewById(R.id.listView)
recyclerView.apply {
adapter = MyItemRecyclerViewAdapter(DummyContent.ITEMS, spanCount)
layoutManager = GridLayoutManager(context, spanCount)
addItemDecoration(RecyclerGridDecoration(context))
}
button = view.findViewById(R.id.toggle)
button.setOnClickListener { onToggle() }
return view
}
fun onToggle() {
spanCount = if (spanCount == 2) 4 else 2
TransitionManager.beginDelayedTransition(recyclerView)
(recyclerView.layoutManager as GridLayoutManager).spanCount = spanCount
(recyclerView.adapter!! as MyItemRecyclerViewAdapter).apply {
span = spanCount
notifyDataSetChanged()
}
}
}
class MyItemRecyclerViewAdapter(
private val mValues: List<DummyItem>,
var span: Int)
: RecyclerView.Adapter<MyItemRecyclerViewAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
if (viewType == 2) {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.items_two, parent, false)
return TwoViewHolder(view)
} else {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.items_four, parent, false)
return FourViewHolder(view)
}
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = mValues[position]
if (holder is TwoViewHolder) {
holder.textTwo.text = item.content
} else if (holder is FourViewHolder) {
holder.textFour.text = item.content
}
}
override fun getItemCount(): Int = mValues.size
abstract inner class ViewHolder(mView: View) : RecyclerView.ViewHolder(mView)
inner class TwoViewHolder(mView: View) : ViewHolder(mView) {
val image: ImageView = mView.imageTwo
val textTwo: TextView = mView.textTwo
}
inner class FourViewHolder(mView: View) : ViewHolder(mView) {
val textFour: TextView = mView.textFour
}
override fun getItemViewType(position: Int): Int {
return span
}
}
I have a solution for your problem change your toggle function to this:
fun onToggle() {
spanCount = if (spanCount == 2) 4 else 2
(recyclerView.adapter!! as MyItemRecyclerViewAdapter).apply {
span = spanCount
notifyDataSetChanged()
}
recyclerView.post(object:Runnable {
public override fun run() {
TransitionManager.beginDelayedTransition(recyclerView)
(recyclerView.layoutManager as GridLayoutManager).spanCount = spanCount
}
})
}
In the above code we are first changing the viewType of recyclerView and then we are changing spanCount of GridLayoutManager on RecyclerView UI Handler Queue, so to achieve the two UI operations serially.
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