USE CASE
I have a RecyclerView whose ViewHolder contains a TextInputLayout. For some situations I want it to display a hint and for other situations I don't.
ISSUE
When the RecyclerView recycles a ViewHolder that has previously displayed a hint but on the new bind does not require a hint. It will display an empty hint instead. See picture:
Image for NOT recycled ViewHolder
Image for recycled ViewHolder
The problem seems to happen ONLY if the new binding has NO hint. If I set a hint to every ViewHolder with different values, it recycles and displays correctly.
Snippet
Adapter.kt
override fun onBindViewHolder(holder: FormViewHolder, position: Int) {
holder.onBind(getItem(position))
}
ViewHolder.kt
override fun onBind(model: Model) {
binding.inputLayout.isHintEnabled = model.hasHint
binding.inputLayout.isHintAnimationEnabled = model.hasHint
binding.inputLayout.hint = model.hintValue
}
I tried:
Updating to latest version ATM: com.google.android.material:material:1.8.0
i don't see problems in your code, so, of course it sounds like a bug, but you can try:
set focus.
set text color.
invoke invalidate() on text input layout in onBind viewholder method.
and see how the system reacts.
TextInputLayout will only recalculate the cutout dimensions if the hint is enabled and not empty, so as soon as we try to set either of those values, it can never reach the method necessary to close the cutout. The simplest straightforward solution might just be to create a new TextInputLayout/EditText instance whenever you need to clear the hint. Otherwise, we can do something with reflection
Is there a way to redraw a TextInputLayout’s OutlinedBox?
Thanks to #Mike.M for the answer
Related
Let's have some View in fragment (e.g. LinearLayout or ImageView). If I change property of this View programatically (e.g. change backgroundColor) to same property that is already set, is view redrawn/rerendered?
Example of double set:
binding.someLinearLayout.setBackgroundColor(redColor)
binding.someLinearLayout.setBackgroundColor(grayColor);
binding.someLinearLayout.setBackgroundColor(grayColor);
binding.someLinearLayout.setBackgroundColor(grayColor);
LinearLayout will be redrawn twice or 4 times? If 4 times, should I implement something like this to prevent it?
fun setBackgroundColor(ll: LinearLayout, color: Int){
val current = binding.progressBarChunk4.background as ColorDrawable
if(current.getColor() != color)
ll.setBackgroundColor(color)
}
setBackgroundColor(binding.someLinearLayout, redColor); // Color set
setBackgroundColor(binding.someLinearLayout, grayColor); // Color set
setBackgroundColor(binding.someLinearLayout, grayColor); // ignored
setBackgroundColor(binding.someLinearLayout, grayColor); // ignored
So basically I am asking if multiple set of same property affects performance, because view gets redrawn everytime.
If you call setBackgroundColor four times in a row like that, it doesn't cause four redraws. The view is only redrawn one time on the next loop of the main thread, after your current function has returned.
Changing these properties usually does invalidate the view, so your first call to setBackgroundColor will trigger a redraw on the next loop of the main thread, even if it matches the existing color. I can't guarantee that's true of every existing property in the core Views, because I haven't checked them all. It is definitely true for setBackgroundColor and setText.
Your defensive check of the current color could potentially prevent an unnecessary redraw, but only if you ultimately don't mutate any views in this iteration of your main thread. I can't think of a case where it would be worth worrying about. If this is in onCreate() or onViewCreated(), the view hasn't been drawn yet anyway. If this is in a click listener, your view is getting redrawn anyway because your button's visual state is changing.
It might be worth doing defensive checks for changes that affect the size of the view, because then you could be preventing a re-layout. But probably only if this happening during some animation, because otherwise the savings would not be noticeable.
I have a RecyclerView.Adapter together with a list of items. At some point I will make some changes to this list and then I will call notifyDataSetChanged() (or another similar method). How do I know how many ViewHolders will be created prior to making this call? Or how many times onBindViewHolder will be called? Ideally I should know one of these numbers before calling this notify method, but I could also manage with receiving a callback when the adapter finishes to make all the work (inflations, bindings). From my experiments, the adapter is not done with the work right after calling notifyDataSetChanged.
I need to know how many items will be or were inflated. Ideally 'will be'. Some ideas on how I can achieve this?
UPDATE
Stumbled upon: Is there a callback for when RecyclerView has finished showing its items after I've set it with an adapter?, which gave me an answer to the second thing I asked: "I could also manage with receiving a callback when the adapter finishes to make all the work (inflations, bindings)."
I made some modifications to the accepted answer and the following Log only displays after all the recycler's items were inflated (at least in my app):
RecyclerView(context).apply {
layoutManager = object: LinearLayoutManager(context, VERTICAL, false) {
override fun onLayoutCompleted(state: RecyclerView.State?) {
super.onLayoutCompleted(state)
val firstVisibleItemPosition = findFirstVisibleItemPosition()
val lastVisibleItemPosition = findLastVisibleItemPosition()
if(firstVisibleItemPosition == -1 || lastVisibleItemPosition == -1)
return
Log.d("RECYCLER", "all sync inflations done")
}
}
}
How do I know how many ViewHolders will be created prior to making this call? Or how many times onBindViewHolder will be called?
Sorry, there is no way to know that. It will depend on all sorts of variables which are not known until rendering is being done.
For example, suppose that your RecyclerView is a vertically-scrolling list of rows, and each row shows some text in a TextView. How many rows will be needed will depend on:
What the text is, which could be short or long
What the row layout is and how it calls for that text to be rendered (font, font scale, padding, margin, etc.)
What the font scale is
What the RecyclerView size is (which in turn varies based on things like window size, which in turn varies based on things like screen size and whether we are in split-screen or multi-window mode, etc.)
And, in terms of how many ViewHolder objects will be instantiated, that will also depend on whether this is the first time the RecyclerView has been shown or whether you are updating an existing one with new data.
And that is just for very simple rows. More complex rows would add more dependencies.
Ideally I should know one of these numbers before calling this notify method
That is unrealistic. My guess it that is unnecessary. You might consider asking a separate Stack Overflow question where you explain why you feel that you need this, and perhaps we can suggest alternative approaches to solving your perceived problem. IOW, your question feels like an XY problem, where you have guessed at a solution to some other problem.
In my bindRow method of my RecyclerView adapter I am checking to see if an EditText has been modified such that its value falls below 100. If so, I change the color of the text to red.
However the text does not update immediately unless I rotate the screen.
How do I get it to change the color immediately?
From my experience, this isn't the place where I would be doing this. I have had a similar concern where the ViewHolders in my Adapter weren't accurate - and then rotating the screen 'fixed' it.
My solution involved updating my Adapter's data List<> and then calling swapAdapter(...)
I'm dynamically adding Views to my items in a RecyclerView. These added Views should only be related to the item which they're added to, but I'm having a problem when I scroll. It seems the View is recycled and a new item is loaded, but those previously added views are still there, only now on the wrong item.
I'm assuming that it's just because the ViewHolder is being reused, so the added items show up again with a new item, when loaded.
How would one go about solving this?
This was an old question of mine. A bounty was placed on it, hence the surge of popularity and the multiple new and irrelevant answers.
As stated in both my comment to this answer and #CQM's comment below my original question, the answer is to override the onViewRecycled() method and perform any needed operations there. This method is called when a view is recycled, and any cleanup operations can be done here.
Documentation on this method can be found here.
In my case, it was a matter of deleting the invisible TextView's attached to the view. The text itself had been deleted, but the view remained. If many invisible TextView's accumulate on the view and aren't properly recycled when scrolling, the scroll will begin to lag.
You need to track what views have been added based on the backing data. I would probably add any necessary extra views in onBindViewHolder(), and remove any that might be present in onViewRecycled(). Then when you want to make one appear dynamically, change whatever variable you have tracking whether it should be visible, and call notifyItemChanged().
Based on this:
but those previously added Views are still there, but now on the wrong item.
Basically, as per the RecyclerView documentation, You have to reset the views everytime inside the onBindViewHolder() method,
so let say, you have a method that sets a view param if its your profile, so the code for the same goes as follows,
if (list.get(position).getId()==PreferenceManager.getUserID())
{
// do some view change here
setViewParam(true);
}else
{
// reset the view change here
setViewParam(false);
}
So what you're doing here is giving recycled ViewHolder a chance to reset.
Do comment if you need help!
You can use this! setItemViewCacheSize(int size)
Check here RecyclerViewDocumentation.
The offscreen view cache stays aware of changes in the attached adapter, allowing a LayoutManager to reuse those views unmodified without needing to return to the adapter to rebind them.
First of all, can you share some more code please?
Second, why would you want to dynamically add new views on fly? Why don't you use different VIEWTYPE or just have those view already on your layout and just make them visible/invisible or visible/gone? (I believe it will be more efficient this way).
Let me remind you something about RecyclerView, yes when user is scrolling viewHolder are being reused (few of them can be created, even more than it needs to fill the screen). So if it happened that you added some views on "item A" and user scroll to "item Z", that viewHolder can be reused for that "item Z", hence the show up of the previously added views.
How can you solve that?
Well always check on every items if you need to add new views, if yes add them if not already added, else always remove those views (if present) to return to default viewHolder state (or whatever you call it).
Hope this will help you.
Save Information by tags for items with new child each time the Add newView operation occur. (In shared preference for example)
Tag: create with item position onBindViewHolder.
...
SharedPreference sharedPref = getSharedPreference("text" + position, context);
SharedPreference.Editor editor = sharedPref.edit();
editor.putString("view", "ImageView");
...
when load Adapter get this value and put default as null.
I am not sure about its efficiency but i will work.
...
String viewType = sharedPref.getString("view", null);
//it will return ImageView
if you know some possible viewTypes for example always going to be ImageView & TextView so with some if statement it will be ok.
if(viewType.equals("ImageVIew")){
item(position).addView(new ImageVIew(context));
}
Good Luck
In your adapter class of your recyclerView,
in the onBindViewHolder method,
create another adapter and do the same methods for your new adapter.
The hierarchy will be,
mainRecyclerView -> item1(->childRecyclerView1) , item2(->childRecyclerView2), item3(->childRecyclerView3)
This way you can achieve what you want without wrong values to be viewed on wrong items.
You should take any Empty Layout like Linearlayout in your child item layout XML and then add views into that LinearLayout of your particular item in this way when you scroll List all of you child views which you have added to LinearLayout also scroll with that item .
after using setSelection(int, false) like suggested here because i had troubles using the default setSelection(int) for initial setup it turns out that using the two param version messes up the spinner layout till the first manual selection takes place, details see image below.
Is there a way to "update" the spinner layout?
Okay i got it. I extedned the Spinner Class, added a var for saveing that this is the "first" pass and have overrwitten the onDraw method. after super.OnDraw() is called i can be sure that the layout has been drawn the first time and all data is passed to the spinner so to following requestLayout() will fix any layout errors. so i just test if this is the first onDraw with my var, if so i call requestLayout() and set the var to false. it's not the best way and maybe there is another event i could use that is run bevore the draw happens, but it's good enough for my needs.