I'm currently working on a UI that looks like this
The blue part is a ConstraintLayout while the purple part is a RecyclerView inside it (it's a RecyclerView because it's content are dynamic based on service response).
I'm setting onClick handler on the ConstraintLayout that would take the user to another page. The problem is the RecyclerView is consuming the clicks and not forwarding it to its parent. Thus onClick handler works for the blue area, but not for the purple area.
I tried setting android:clickable="false" and android:focusable="false" in the RecyclerView but it still won't propagate the clicks to its parent.
One solution I came across is to extend from ConstraintLayout and override onInterceptTouchEvent() to return true. However I have a strict requirement in my project to not create custom widgets, so I cannot use this solution.
Is there a way to tell RecyclerView to stop consuming touch events?
Activity layout:
<FrameLayout 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"
tools:context=".MainActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="300dp"
android:layout_margin="16dp"
android:background="#42d7f4"
android:onClick="navigate"
android:padding="16dp">
<TextView
android:id="#+id/headerText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="FRUITS"
android:textSize="36sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/itemsList"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="#9f41f2"
android:clickable="false"
android:focusable="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>
Item layout:
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/itemText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:clickable="false"
android:focusable="false" />
Activity.kt:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val rcView = findViewById<RecyclerView>(R.id.itemsList)
rcView.layoutManager = LinearLayoutManager(this)
val items = listOf("Apple", "Banana", "Oranges", "Avocado")
rcView.adapter = ItemAdapter(items)
}
fun navigate(view: View) {
Toast.makeText(this, "Navigating to details page", Toast.LENGTH_SHORT)
.show()
}
}
class ItemAdapter(private val data: List<String>) : RecyclerView.Adapter<ItemViewHolder>() {
override fun getItemCount(): Int = data.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item, parent, false)
return ItemViewHolder(view)
}
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
holder.bind(data[position])
}
}
class ItemViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val itemTv: TextView = view.findViewById(R.id.itemText)
fun bind(item: String) {
itemTv.text = item
}
}
If you freeze the layout after you've set the adapter it no longer swallows clicks:
recyclerView.isLayoutFrozen = true // kotlin
recyclerView.setLayoutFrozen(true); // java
Just keep in mind if you need to change the data in the adapter you have to unfreeze the layout before you call notifyDataSetChanged and then re-freeze the layout. I don't love this solution but it's the only one that worked for me.
One way to approach this is to set the RecyclerView row with a click listener and to disable clicks and long clicks on the children of the RecyclerView row as follows:
ConstraintLayout: `android:onClick="navigate"`
For the item layout: `android:onClick="navigate"`
TextView: android:clickable="false"
android:longClickable="false"
(etc. for all children of the row)
I think what is missing is to make the TextView not long clickable.
You can set your RecyclerView focus to false.
recyclerView.setFocusable(false);
And/Or set its rows view.setFocusable(false);
EDIT
If you want to give the ConstraintLayout the focus
layout.setFocusable(true);
layout.setClickable(true);
//
// add click listener and event ....
//
For more information please refer to the official documentation provided by Google
Hope this helps, cheers
Probably the easiest way to completely block interaction with anything inside a single view is to put a transparent view over it that intercepts all touch events. You can set click on that view manage all the functionality through that view.
For example like this
<FrameLayout 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"
tools:context=".MainActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="300dp"
android:layout_margin="16dp"
android:background="#42d7f4"
android:padding="16dp">
<TextView
android:id="#+id/headerText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="FRUITS"
android:textSize="36sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/itemsList"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="#9f41f2"
android:clickable="false"
android:focusable="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<View
android:id="#+id/clickView"
android:layout_width="0dp"
android:layout_height="0dp"
android:onClick="navigate"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:clickable="true"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>
Now you can functionality perform through that view instead through ConstraintLayout.
Moreover, you look at this answer
Related
Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 1 year ago.
Improve this question
I'm trying to get a popup to show up when I press a button. I wanted to make sure it even shows up before working on the details but the app just crashes when i press it. The commented block is what I originally intended the button to do (just adding an item to the recyclerview) but I decided I wanted to do something a bit different. It only had the problem of crashing when I started adding the popup window stuff. The app still runs fine even if I remove the popup window stuff and leave the commented block commented out. So I don't know if the listadapter class is relevant but ill just include it.
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var listAdapter: ListAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
listAdapter = ListAdapter(mutableListOf())
recyclerView.adapter = listAdapter
recyclerView.layoutManager = LinearLayoutManager(this)
btn_add.setOnClickListener {
/*
//get text from textbox
val itemText = et_reminder.text.toString()
if (itemText.isNotEmpty()){
//create item with text
val item = Item (itemText)
//add to list
listAdapter.addItem((item))
//clear textbox
et_reminder.text.clear()
}*/
val view : View = LayoutInflater.from(applicationContext).inflate (R.layout.popup_s,null)
val popup: PopupWindow = PopupWindow(view, 250, 250,true)
popup.showAtLocation(view, Gravity.NO_GRAVITY, 0,0)
}
}
}
ListAdapter.kt
class ListAdapter(
private val items: MutableList<Item>
): RecyclerView.Adapter<ListAdapter.ListViewHolder>()
{
class ListViewHolder(itemView: View):RecyclerView.ViewHolder(itemView)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder {
val itemView: View =
LayoutInflater.from(parent.context).inflate (R.layout.list_item, parent, false)
return ListViewHolder(itemView)
}
override fun onBindViewHolder(holder: ListViewHolder, position: Int) {
val curr_item = items [position]
holder.itemView.apply {
tv_reminder.text = curr_item.text;
cb_check.isChecked = curr_item.checked
cb_check.setOnCheckedChangeListener { _, isChecked ->
//changing status of checked
curr_item.checked = isChecked
for (i in items.indices.reversed()) {
if (items[i].checked) {
items.removeAt(i)
notifyItemRemoved(i)
}
}
}
}
}
override fun getItemCount(): Int {
return items.size
}
fun addItem (new_item: Item){
items.add (new_item)
notifyItemInserted(items.size - 1)
}
}
activity_main.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"
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:id="#+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="#color/basically_black"
app:layout_constraintBottom_toTopOf="#+id/et_reminder"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="#+id/et_reminder"
android:layout_width="0dp"
android:layout_height="45dp"
android:hint="Reminder"
android:textSize="20sp"
android:textColorHint="#color/white_purple"
android:textColor="#color/white_purple"
android:background="#color/basically_black"
android:paddingStart="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="#+id/btn_add"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/recyclerView"/>
<Button
android:id="#+id/btn_add"
android:layout_width="45dp"
android:layout_height="wrap_content"
android:text="+"
android:textSize="20sp"
android:background="#color/basically_black"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
list_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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="55dp"
android:paddingStart="10dp">
<TextView
android:id="#+id/tv_reminder"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingTop="0dp"
android:text="temp"
android:textColor="#color/white_purple"
android:textSize="20sp"
android:maxLines="1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="#+id/cb_check"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<CheckBox
android:id="#+id/cb_check"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
popup_s.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="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true">
<EditText
android:id="#+id/et_r"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:hint="Reminder"
android:textColor="#color/white_purple"
android:textColorHint="#color/gray"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1.0">
</EditText>
</androidx.constraintlayout.widget.ConstraintLayout>
I think that happen because you are trying to pass applicationContext it will give you Unable to add window -- token null is not valid; is your activity running?
try to pass the current activity context instead cause PopupWindow can only be attached to an Activity
val view : View = LayoutInflater.from(this).inflate (R.layout.popup_s,null)
val popup = PopupWindow(view, 250, 250,true)
popup.showAtLocation(view, Gravity.NO_GRAVITY, 0,0)
I looked at many solutions posted online but they couldn't solve my problem. Probably the adapter position is returning -1 but why?
java.lang.ArrayIndexOutOfBoundsException: length=10; index=-1
at java.util.ArrayList.get(ArrayList.java:439)
at
com.firebase.ui.common.BaseObservableSnapshotArray.getSnapshot(BaseObservableSnapshotArray.java:70)
at com.example.twitterclone.adapters.MyAdapter$TweetViewHolder.<init>(MyAdapter.kt:36)
at com.example.twitterclone.adapters.MyAdapter.onCreateViewHolder(MyAdapter.kt:50)
at com.example.twitterclone.adapters.MyAdapter.onCreateViewHolder(MyAdapter.kt:21)
at androidx.recyclerview.widget.RecyclerView$Adapter.createViewHolder(RecyclerView.java:7078)
at
RecyclerViewAdapterCode:
class MyAdapter(
options: FirestoreRecyclerOptions<Tweet>,
private val clickInterface: ClickInterface
):FirestoreRecyclerAdapter<Tweet, MyAdapter.TweetViewHolder>(options) {
inner class TweetViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val profile =
view.findViewById<de.hdodenhof.circleimageview.CircleImageView>(R.id.userProfile)
val tweet = view.findViewById<TextView>(R.id.tweet)
val like = view.findViewById<TextView>(R.id.totalLikes)
val thumbsUp = view.findViewById<ImageView>(R.id.thumbsUp)
val name=view.findViewById<TextView>(R.id.tweetUserName)
init {
val tweetId=snapshots.getSnapshot(adapterPosition).get("tweetId")
thumbsUp.setOnClickListener {
clickInterface.clickLike(tweetId.toString())
}
}
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): TweetViewHolder {
val viewHolder = TweetViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.post_tweets_item, parent, false)
)
return viewHolder
}
override fun onBindViewHolder(
holder: TweetViewHolder,
position: Int,
model: Tweet
) {
holder.tweet.text = model.content.toString()
holder.like.text = model.likes.toString()
UserDao().getUser(model.uid!!).get().addOnSuccessListener {
holder.name.text = it.get("name").toString()
Glide.with(holder.profile.context).load(it.get("profileUrl").toString())
.into(holder.profile)
}
}
}
LayoutCode
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical"
android:layout_margin="10dp"
android:layout_height="wrap_content">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="2dp"
app:cardCornerRadius="5dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<de.hdodenhof.circleimageview.CircleImageView
android:id="#+id/userProfile"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginTop="8dp"
android:layout_marginLeft="8dp" />
<TextView
android:id="#+id/tweetUserName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:textColor="#android:color/black"
android:layout_marginTop="8dp" />
</LinearLayout>
<TextView
android:id="#+id/tweet"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="13dp"
android:textColor="#android:color/black" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="80dp"
android:orientation="vertical"
android:padding="8dp">
<ImageView
android:layout_width="30dp"
android:id="#+id/thumbsUp"
android:clickable="true"
android:layout_height="30dp"
android:src="#drawable/ic_baseline_thumb_up_24" />
<TextView
android:id="#+id/totalLikes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="80dp"
android:orientation="vertical"
android:padding="8dp">
<ImageView
android:clickable="true"
android:layout_width="30dp"
android:layout_height="30dp"
android:id="#+id/comments"
android:src="#drawable/ic_baseline_comment_24" />
<TextView
android:id="#+id/totalComments"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
adapterPosition in the ViewHolder's init block is returning -1. So we just need to call adapterPosition in the listener which will solve the issue, since it would retrieve the position when the view holder is created and attached to the recyclerview. Setting any kind of listeners in the init block is a good approach since the onCreateViewHolder called only once to create a view holder but the onBindViewHolder called multiple times for the same view holder, so setting listeners in the onBindViewHolder would be redundant.
init {
thumbsUp.setOnClickListener {
val tweetId=snapshots.getSnapshot(adapterPosition).get("tweetId")
clickInterface.clickLike(tweetId.toString())
}
}
Attache you onClickListener in onBindViewHolder
override fun onBindViewHolder(
holder: TweetViewHolder,
position: Int,
model: Tweet
) {
//your code of attaching data to view
holder.thumbsUp.setOnClickListener{
clickInterface.clickLike(model.tweetId.toString())
}
}
Reconsider your application architecture. As asking DAO for data inside RecyclerView.Adapter is quite strange approach
Yeah, I'd expect adapterPosition in your getSnapshotcall in the ViewHolder's init block is returning -1. As far as I'm aware, all the adapter position getters return the position of the last requested item for display. But when you're first creating ViewHolders, nothing has bound to the adapter yet (it's all still in the setup phase), so its default position is probably -1.
Basically the way RecyclerViews work is this - you define these ViewHolder objects which are basically things that display data for each item. Instead of having one for each item in the list, the adapter creates a handful of them (enough to fill the visible area and a couple either side), and then recycles them by moving them around when they're out of view, and updates their contents to represent a different item.
onCreateViewHolder is what the adapter calls to create those container objects. It's where you inflate the layout and usually do findViewById on the views so you have a reference to them, and you can just set a TextView's text instead of having to fnd the view every time you want to update it.
onBindViewHolder is what gets called when you need to update a ViewHolder with a different item's info. This is where you actually do all the setting of data, changing images and whatever, updating click listeners if necessary.
Basically you shouldn't be using adapterPosition in your ViewHolder's init block, because that gets called once, when the object is constructed. Not only do you not have a meaningful position at that point, but isn't that something you'll want to update often, as the position changes? That's what onBindViewHolder is for! It has the position passed in and everything. That's the method where you set state
I have a recyclerView inside a fragment, this fragment is located in a viewpager2.
I read data from server and convert them to my ModelClass, them create an ArrayList and pas this array list to recyclerView.
here is my recyclerView item layout:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:background="#drawable/main_icon_background"
android:elevation="5dp"
android:layoutDirection="rtl"
android:orientation="horizontal"
android:padding="10dp">
<ImageView
android:id="#+id/image"
android:layout_width="120dp"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="#color/black" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.6"
android:orientation="vertical">
<TextView
android:id="#+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="#style/vazir_font"
android:textColor="#color/black" />
<TextView
android:id="#+id/description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:ellipsize="end"
android:maxLines="3"
android:textAppearance="#style/vazir_font"
android:textColor="#color/gray"
android:textSize="13sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="1.5dp"
android:layout_marginStart="20dp"
android:layout_marginTop="5dp"
android:layout_marginEnd="20dp"
android:layout_marginBottom="8dp"
android:alpha="0.5"
android:background="#color/gray" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="#+id/source"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:layout_weight="0.8"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="#style/vazir_font"
android:textColor="#color/colorPrimaryDark"
android:textSize="13sp" />
<TextView
android:id="#+id/date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:gravity="end"
android:textAppearance="#style/vazir_font"
android:textColor="#color/colorAccent"
android:textSize="13sp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
and here is my recyclerView adapter:
class NewsRecyclerViewAdapter : RecyclerView.Adapter<NewsRecyclerViewAdapter.ViewHolder>() {
private val items = ArrayList<NewsAdapterModel>()
lateinit var onClick: NewsTutorialClickListener
fun add(list: ArrayList<NewsAdapterModel>) {
items.clear()
items.addAll(list)
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view =
LayoutInflater.from(parent.context).inflate(R.layout.news_recycler_model, parent, false)
return ViewHolder(view)
}
override fun getItemCount(): Int = items.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.binding(items[position])
}
override fun getItemId(position: Int): Long {
return items[position].id.hashCode().toLong()
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun binding(model: NewsAdapterModel) {
itemView.setOnClickListener {
onClick.onClick(model.id)
}
itemView.title.text = model.title
itemView.description.text = model.description
itemView.date.text = Arrange().persianConverter(model.date)
itemView.source.text = model.source
Glide.with(itemView.context)
.load(model.image)
.placeholder(R.mipmap.logo)
.into(itemView.image)
}
}
}
I used this recycler view in my fragment just like this:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
adapter.onClick = this
binding.recyclerView.layoutManager = LinearLayoutManager(context)
binding.recyclerView.adapter = adapter
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.setItemViewCacheSize(20)
scope.launch {
withContext(Dispatchers.Default) {
fetchNews()
}
}
}
my problem is : when I scroll recycler view for first time it is not smooth and it seems some frames are jumped, but when I reach end of recyclerView and scroll to up then scroll down it is very smooth and find. how can I solve this issue?
I also should mention that Items of recyclerView are not much, it's around 10 items.
remove
recyclerView.setHasFixedSize(true)
actually it can cause no harm and indeed improves performance, because recyclerView won't bother calculating size of viewHolders evrytime inside onBindViewHolder. But you have all your TextViews with wrap_content height which will most certainly cause some broken UI in future.
remove
recyclerView.setItemViewCacheSize(20)
This function forces RecyclerView to prematurely inflate and bind 20 extra ViewHolders, so that it can use them straight away without calling OncreateViewHolder and OnBindViewHolder. You should'nt play around with this and just go with the defaults. Although 20 extra viewholders shouldn't have been a big deal, i guess maybe RecyclerView got confused because you forced it create extra 20 on top of existing 10, while you list only contains 10 items. It was taking time to create those extra viewHolders blocking ui thread, but after they were created it seemed smooth.
You don't need to add these lines
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.setItemViewCacheSize(20)
Please remove these lines
I'm currently using DataBinding with a RecyclerView, and I'm getting pretty severe lag when a list first loads. However, after I scroll past a page and it stops creating new viewholders, everything is fine.
During creation, the only thing I'm doing is inflating a layout using DataBindingUtils, so I'm wondering if there are parts that can be improved.
Attached below are relevant code snippets. I'm currently using a library, FastAdapter, so the signatures will not match exactly
When creating a viewholder, I do the following:
override fun createView(ctx: Context, parent: ViewGroup?): View {
val start = System.nanoTime()
val binding: ViewDataBinding = DataBindingUtil.inflate(
LayoutInflater.from(ctx),
layoutRes, parent,
false,
null
)
L.d { "Create view ${(System.nanoTime() - start) / 1000000}" }
return binding.root
}
It appears that for my more complex layout, viewholders take around 30-60 ms each, resulting in notable lag.
Binding and unbinding are like so:
final override fun bindView(holder: ViewHolder, payloads: MutableList<Any>) {
super.bindView(holder, payloads)
val binding = DataBindingUtil.getBinding<Binding>(holder.itemView) ?: return
binding.bindView(holder, payloads)
binding.executePendingBindings()
}
open fun Binding.bindView(holder: ViewHolder, payloads: MutableList<Any>) {
setVariable(BR.model, data)
}
final override fun unbindView(holder: ViewHolder) {
super.unbindView(holder)
val binding = DataBindingUtil.getBinding<Binding>(holder.itemView) ?: return
binding.unbindView(holder)
binding.unbind()
}
open fun Binding.unbindView(holder: ViewHolder) {}
final override fun getViewHolder(v: View): ViewHolder = ViewHolder(v, layoutRes)
Basically, I usually have a single data model that I set, and I implement unbinding myself per viewholder. There doesn't seem to be a problem there.
Reading other articles, it appears that most developers have the same viewholder creation method as I do. Is DataUtilBinding not meant to be done upon creation, or is there something I'm missing?
As an addition, here is my relevant layout xml:
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<data>
<variable
name="model"
type="github.fragment.ShortRepoRowItem" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:windowBackground"
android:foreground="?selectableItemBackground"
android:paddingStart="#dimen/kau_activity_horizontal_margin"
android:paddingTop="#dimen/kau_padding_small"
android:paddingEnd="#dimen/kau_activity_horizontal_margin"
android:paddingBottom="#dimen/kau_padding_small"
tools:context=".activity.MainActivity">
<TextView
android:id="#+id/repo_name"
style="#style/TextAppearance.AppCompat.Medium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="#{model.name}"
android:textColor="?android:textColorPrimary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="#tools:sample/full_names" />
<TextView
android:id="#+id/repo_desc"
style="#style/TextAppearance.AppCompat.Caption"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="2"
android:text="#{model.description}"
android:textColor="?android:textColorSecondary"
app:goneFlag="#{model.description}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/repo_name"
tools:text="#tools:sample/lorem/random" />
<com.google.android.material.chip.Chip
android:id="#+id/repo_stars"
style="#style/RepoChips"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:chipIcon="#drawable/ic_star_border"
app:compactNumberText="#{model.stargazers.totalCount}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/repo_desc"
app:layout_constraintWidth_percent="0.12"
tools:text="123" />
<com.google.android.material.chip.Chip
android:id="#+id/repo_forks"
style="#style/RepoChips"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:chipIcon="#drawable/ic_fork"
app:compactNumberText="#{model.forkCount}"
app:layout_constraintStart_toEndOf="#id/repo_stars"
app:layout_constraintTop_toBottomOf="#id/repo_desc"
app:layout_constraintWidth_percent="0.12"
tools:text="123" />
<com.google.android.material.chip.Chip
android:id="#+id/repo_issues"
style="#style/RepoChips"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:chipIcon="#drawable/ic_issues"
app:compactNumberText="#{model.issues.totalCount}"
app:layout_constraintStart_toEndOf="#id/repo_forks"
app:layout_constraintTop_toBottomOf="#id/repo_desc"
app:layout_constraintWidth_percent="0.12"
tools:text="1.5k" />
<com.google.android.material.chip.Chip
android:id="#+id/repo_prs"
style="#style/RepoChips"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:chipIcon="#drawable/ic_pull_requests"
app:compactNumberText="#{model.pullRequests.totalCount}"
app:layout_constraintStart_toEndOf="#id/repo_issues"
app:layout_constraintTop_toBottomOf="#id/repo_desc"
app:layout_constraintWidth_percent="0.12"
tools:text="123" />
<com.google.android.material.chip.Chip
android:id="#+id/repo_language"
style="#style/RepoChips"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="#{model.primaryLanguage.name}"
app:chipIcon="#drawable/ic_language"
app:languageColor="#{model.primaryLanguage.color}"
app:layout_constraintEnd_toStartOf="#id/repo_date"
app:layout_constraintStart_toEndOf="#id/repo_prs"
app:layout_constraintTop_toBottomOf="#id/repo_desc"
app:layout_constraintWidth_percent="0.25"
tools:text="JavaScript" />
<com.google.android.material.chip.Chip
android:id="#+id/repo_date"
style="#style/RepoChips"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:chipIcon="#drawable/ic_time"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="#id/repo_language"
app:layout_constraintTop_toBottomOf="#id/repo_desc"
app:relativeDateText="#{model.pushedAt}"
app:textStartPadding="4dp"
tools:text="#tools:sample/date/mmddyy" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
I've tried doing the same thing with two linear layouts instead of ConstraintLayout, but it doesn't seem to make much of a difference.
Code snapshot: https://github.com/AllanWang/GitDroid/tree/f802c991580d70470b422186fc43f46b9cfe2465
I did some more measurements and found out that the culprit was actually the layout inflation (ie 60ms) instead of just the binding (ie 3ms).
To verify, inflate the view as is without DataBindingUtil, then call bind on the view.
From there, the problem was also the use of MaterialChips.
After switching 6 material chips to textviews, inflation time went down 3 folds to around 10-30ms.
I am using a NestedScrollView and inside that, I am having some edit text and buttons and then a RecyclerView. When I open the activity all the items start fetching from the server using the PageKeyedDataSource and the RecyclerView scrolls very slowly and in the end the app show ANR.
I have looked this issue in the GitHub repo of android-architecture-components and found that SmartNestedScrollView can solve the problem but when I use SmartNestedScrollView then only a small portion of my recycler view scrolls.
https://github.com/googlesamples/android-architecture-components/issues/215
LayoutFile.xml
<?xml version="1.0" encoding="utf-8"?>
<com.magtappofficial.app.utilities.SmartNestedScrollView
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"
android:fillViewport="true"
android:layout_margin="16dp"
tools:context=".ui.Home">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:focusable="true"
android:focusableInTouchMode="true"
android:layout_width="0px"
android:layout_height="0px"/>
<EditText.../>
<ImageView.../>
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/newsProgress"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="#+id/exclusiveForYou"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"/>
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/newsRecycler"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="#+id/exclusiveForYou"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.magtappofficial.app.utilities.SmartNestedScrollView>
Can Anyone Please tell me what else can I do?
Note: This is not the best practice, it's just a workaround and if someone has any better answer please please please share.
For those of you who are still looking for an answer, I have a simple fix.
I have just moved the entire view that I wanted to show above the recycler view inside the recycler view, i.e, made that view as the header of Recycler View.
Inside the Recycler View :
const val HEADER = 0
const val REAL_ITEM = 1
The getItemViewType method which will return the view type to onCreateViewHolder.
override fun getItemViewType(position: Int): Int {
return when (position) {
0 -> HEADER
else -> REAL_ITEM
}
}
Inside the onCreateViewHolder method:
override fun onCreateViewHolder(p0: ViewGroup, p1: Int): NewsViewHolder {
val view = when (p1) {
HEADER -> {
LayoutInflater.from(ctx).inflate(R.layout.rv_header, p0, false)
}
else -> LayoutInflater.from(ctx).inflate(R.layout.xyz, p0, false)
}
return SomeViewHolder(view)
}
Inside the onBindViewHolder you can get the view type and bind data as per the view requires.
override fun onBindViewHolder(p0: NewsViewHolder, p1: Int) {
val viewType = getItemViewType(p1)
}