Data Binding inflation is very slow - android

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.

Related

View Binding Both Activity and Bottom Bar

I am setting up View Binding in my project, and I ran into an issue. I tried to look for documentation regarding this under without luck.
I have my activity, and I have set up View Binding for this:
class AeropressActivity : AppCompatActivity() {
private lateinit var binding: ActivityAeropressBinding
private lateinit var bottomSheetBehavior: BottomSheetBehavior<ConstraintLayout>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityAeropressBinding.inflate(layoutInflater)
setContentView(binding.root)
setupBottomSheet()
onClickListeners()
}
private fun setupBottomSheet() {
bottomSheetBehavior = BottomSheetBehavior.from(findViewById(R.id.layout_bottom_sheet))
// OnClickListener for bottomSheetBehavior
bottomSheetBehavior.addBottomSheetCallback(
object : BottomSheetBehavior.BottomSheetCallback() {
override fun onSlide(bottomSheet: View, slideOffset: Float) {
}
override fun onStateChanged(bottomSheet: View, newState: Int) {
}
}
)
}
After setting this up, I want to use View Binding for my BottomSheet, as it has some buttons and a Chronometer that I want to add onClickListeners to.
I have added the
lateinit var bindingBottomSheet: LayoutBottomSheetBinding
but inflating this does nothing:
bindingBottomSheet = LayoutBottomSheetBinding.inflate(layoutInflater)
What am I missing to get this to work? Is this even possible?
Edit:
The BottomSheet layout looks like this:
<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:id="#+id/layout_bottom_sheet"
android:layout_width="match_parent"
android:layout_height="140dp"
app:behavior_peekHeight="50dp"
tools:context=".BottomSheetActivity"
app:behavior_hideable="false"
android:background="#drawable/background_bottom_bar"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<ImageView
android:id="#+id/image_view_button_home"
android:src="#drawable/ic_home"
android:layout_width="40dp"
android:layout_height="35dp"
android:layout_marginStart="24dp"
android:layout_marginTop="10dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="#+id/image_view_button_timer"
android:layout_width="40dp"
android:layout_height="35dp"
android:layout_marginTop="10dp"
android:src="#drawable/ic_stopwatch"
app:layout_constraintLeft_toRightOf="#id/image_view_button_home"
app:layout_constraintRight_toLeftOf="#id/image_view_button_info"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:src="#drawable/ic_info"
android:id="#+id/image_view_button_info"
android:layout_width="40dp"
android:layout_height="35dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="24dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintLeft_toRightOf="#id/image_view_button_timer"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="#+id/button_bottom_reset"
android:layout_width="100dp"
android:layout_height="50dp"
android:text="Reset"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/image_view_button_info" />
<Chronometer
android:gravity="center_horizontal"
android:textSize="48sp"
android:textColor="#color/white"
android:format="%s"
android:id="#+id/chronometer_bottom_bar"
android:layout_width="0dp"
android:paddingStart="6dp"
android:paddingEnd="6dp"
android:layout_height="60dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="#id/button_bottom_start"
app:layout_constraintStart_toEndOf="#id/button_bottom_reset"
app:layout_constraintTop_toBottomOf="#id/image_view_button_timer" />
<Button
android:id="#+id/button_bottom_start"
android:layout_width="100dp"
android:layout_height="40dp"
android:text="Start"
app:layout_constraintBottom_toTopOf="#id/button_bottom_stop"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="#id/image_view_button_home" />
<Button
android:id="#+id/button_bottom_stop"
android:layout_width="100dp"
android:layout_height="40dp"
android:text="Stop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="#id/button_bottom_start" />
</androidx.constraintlayout.widget.ConstraintLayout>
The BottomSheet has its own layout which is added to the activity's layout by using the .
I want to use View Binding for my BottomSheet, as it has some buttons and a Chronometer that I want to add onClickListeners to.
So, you can't access the underlying views of the BottomSheet from the AeropressActivity
First make sure that the BottomSheet layout is wrapped in <layout></layout> tag.
Then make sure to have an id to the <include> so that it allows you access the BottomSheet layout using data binding.
AeropressActivity layout:
<layout>
.
.
<include
android:id="#+id/bottom_sheet"
layout="#layout/bottom_sheet" />
</layout>
Then to access buttons in the BottomSheet layout:
bindingBottomSheet.bottomSheet.myButtonId
Assuming that the button id is: my_button_id

OnClickListener is not working inside my adapter class

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

recyclerView won't scroll smoothly for first time

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

Horizontal recyclerview with snaphelper, how to center first and last element?

I've made a little sample of something I'm trying to do in a real app, and I can't figure out how to solve my issue. The current solution looks like this.
So I have a vertical list of movie categories, where within each genre, I have a horizontal list of movies. The first row, is showing The Exorcist as the first element, but it's not centered. The second row in Action, shows how it looks when I've scrolled a bit to the right. Last row is showing how the end of the row looks like.
I'd like to have the first and last of the rows, to be centered as well when they're "selected".
My main activity looks like this:
<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"
android:background="#android:color/white">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/moviesListRecyclerView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="#layout/movie_category_item_view" />
</androidx.constraintlayout.widget.ConstraintLayout>
The movie_category_item_view looks like this:
<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:id="#+id/movieListLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#android:color/white">
<androidx.appcompat.widget.AppCompatTextView
android:id="#+id/movieCategoryTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="40dp"
android:textColor="#android:color/black"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Horror" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/moviesRecyclerView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:orientation="horizontal"
android:visibility="visible"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/movieCategoryTitle"
tools:listitem="#layout/movie_horizontal_item_view" />
</androidx.constraintlayout.widget.ConstraintLayout>
And the movie_horizontal_item_view like this:
<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="wrap_content"
android:layout_height="wrap_content"
android:background="#android:color/white"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
android:id="#+id/movieTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textColor="#android:color/black"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Horror" />
<androidx.appcompat.widget.AppCompatImageView
android:id="#+id/image"
android:layout_width="237dp"
android:layout_height="209dp"
android:background="#android:color/black"
android:src="#drawable/ic_launcher_foreground"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/movieTitle"
tools:src="#drawable/ic_launcher_foreground" />
</androidx.constraintlayout.widget.ConstraintLayout>
Hopefully what I'm asking for makes sense!
In case you want to try it out for yourself and see what I mean, it can be found on github here
use carouselview.CarouselView
<alirezat775.lib.carouselview.CarouselView
android:id="#+id/carousel_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"/>
the adapter
class MyAdapter(var ctx: Context) : CarouselAdapter() {
private var vh: CarouselViewHolder? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CarouselViewHolder {
val inflater = LayoutInflater.from(parent.context)
val v = inflater.inflate(R.layout.home_list_item, parent, false)
vh = MyViewHolder(v)
return vh as MyViewHolder
}
override fun onBindViewHolder(holder: CarouselViewHolder, position: Int) {
}
inner class MyViewHolder(itemView: View) : CarouselViewHolder(itemView) {
}
}
the activity
class MainActivity : AppCompatActivity() {
var carousel: Carousel? = null
val adapter = MyAdapter(this)
override fun onCreate(savedInstanceState: Bundle?) {
carousel = Carousel(this, carousel_view, adapter)
carousel!!.setOrientation(CarouselView.HORIZONTAL, false)
// carousel!!.autoScroll(true, 5000, true)
// carousel!!.scaleView(true)
}
}

Prevent RecyclerView from swallowing touch events without creating custom ViewGroup

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

Categories

Resources