I am using RecyclerView to list Items and In each single list displaying an image which will be Visible/Gone dynamically. I am using View.GONE to hide the view.
In a condition where the image should hide is not working always. It is still showing in screen,and also in debug mode i have checked that and when getting the
image.getVisiblity() it is giving me int value "8" which means the view is Gone,But still i can see that image in that list.
It happens only sometimes.
And i tried to use View.INVISIBLE and it is working all the time but it is taking the space in layout which is as expected
I am using sparseArray to store all the holders classes.I have written a method in Adapter and calling this from activity.I am trying to hide the replayIcon view
public void handleReplayButton(int pos,Boolean isDisplay) {
Holder holder = holderSparseArray.get(pos);
if(holder != null) {
if (isDisplay != null && isDisplay == true) {
holder.playIcon.setVisibility(View.GONE);
holder.pauseIcon.setVisibility(View.GONE);
holder.replayIcon.setVisibility(View.VISIBLE);
} else if(isDisplay != null && isDisplay == false) {
holder.playIcon.setVisibility(View.VISIBLE);
holder.pauseIcon.setVisibility(View.GONE);
holder.replayIcon.setVisibility(View.GONE);
} else {
holder.playIcon.setVisibility(View.GONE);
holder.pauseIcon.setVisibility(View.VISIBLE);
holder.replayIcon.setVisibility(View.GONE);
}
}
}
Here it is going to the last else statement what i want and it is setting the view to GONE.and when i call holder.replayIcon.getVisibility() it is giving me int 8 but,still i can see the icon
Try calling invisible at the end of one statement which makes it visible and vice versa.
Or
You can also try to put notifydatasetchanged().
You will have to call notifyDataSetChanged() to refresh the list in the recycler view.
But since you have to remove an item, you can also use notifyItemRemoved
Also, if you are using setVisibility() method to HIDE the view, then make sure you also set the view as VISIBLE for valid items, because the items are reused in a recycler view.
For more : https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter
If you will call notifyDataSetChanged() - it will update all the items in the list.
Don't do that if you need to update special items by index because it will take a lot of memory to redraw the all views.
Instead like the guys wrote before you should use notifyItemChanged(), notifyItemInserted() or notifyItemRemoved().
If you want to update couple views use can use notifyItemRangeChanged(), notifyItemRangeRemoved() or notifyItemRangeInserted().
You can read more about it here
Also there is one way to it. You can use DiffUtils callbacks.
Pretty good approach that work with animation already.
DiffUtils Calbacks
Related
I have a RecyclerView and I'm trying to implement delete functionality. In order to get the correct positions of the itemViews, I call notifyItemRangeChanged() after deleting. However when I do that the cool animation of the itemView sliding to the right and being deleted is now gone. It looks like the notifyItemRangeChanged() cuts off the animation created by notifyItemRemoved(). Is there any way I can have the animation and the items delete properly?
Activity Code:
circuitsObject?.circuits?.remove(circuitsObject?.circuits?.get(position))
recyclerView.adapter?.notifyItemRemoved(position)
recyclerView.adapter?.notifyItemRangeChanged(position, circuitsObject?.circuits!!.size)
if (recyclerView.adapter?.itemCount!! == 0) {
loadEmptyUI()
}
Adapter Code:
class CircuitViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
...
itemView.setOnClickListener { clickListener(circuit) }
itemView.setOnLongClickListener {
onLongClickListener(position)
true
}
The parameter "position" comes straight from the ViewHolder. To compensate for the fact that the position doesn't match the index of my data I am using "notifyItemRangeChanged". However, doing so gets rid of the animation. So I'm wondering if there's any way to bypass this such that I can still have the proper position logic as well as the animation for delete.
You don't need to call notifyItemRangeChanged(...) after calling notifyItemRemoved(...).
My outer RecyclerView crashes either with
IllegalArgumentException: Scrapped or attached views may not be recycled. isScrap:false isAttached:true...
or
IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
Like the title suggests I have an RecyclerView in the list item layout of the first RecyclerView. This layout is used to display messages and the
inner RecyclerView to display attachments that come with the message. The inner RecyclerViews visibility is set to either GONE or VISIBLE depending whether the message has any attachments or not. The simplified outer list item layout looks like this
ConstraintLayout
TextView
TextView
TextView
RecyclerView
And the part of the adapter that handles the inner RecyclerView looks like this
private fun bindFiles(message: Message?) = with(itemView) {
if (message != null && message.attachments.isNotEmpty())
{
sent_message_attachments.setAsVisible()
sent_message_attachments.layoutManager = GridLayoutManager(this.context,Math.min(message.attachments.size,3))
sent_message_attachments.adapter = AttachmentAdapter(message.attachments)
sent_message_attachments.itemAnimator = null
sent_message_attachments.setHasFixedSize(true)
}
else{
sent_message_attachments.setAsGone()
sent_message_attachments.adapter = null
sent_message_attachments.layoutManager = null
}
}
The bug has something to do with the way I fetch the attachments in the inner adapter since once I disable the part that start the download process, everything is fine. There's no problem when loading images from the device, but once I start the download process, everything goes to hell. This is the part that handles images and kicks off the download process in the inner adapter. I have functions for videos and for other file types that are pretty much the same exact thing but use slightly different layout.
private fun bindImage(item: HFile?) = with(itemView) {
if (item != null)
{
if (item.isOnDevice && !item.path.isNullOrEmpty())
{
if (item.isGif)
{
attachment_image.displayGif(File(item.path))
}
else
{
attachment_image.displayImage(File(item.path))
}
}
else
{
//TODO: Add option to load images manually
FileHandler(item.id).downloadFileAsObservable(false)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ progress ->
//TODO: Show download process
},
{ error ->
error.printStackTrace()
//TODO: Enable manual retry
},
{ notifyItemChanged(adapterPosition)} //onComplete
)
}
}
}
I use the same structure as above in my DiscussionListAdapter to load discussion portraits (profile pictures etc.) and it does not have the same issue.
These are the extensions functions used to inflate the viewHolders and to display the images
fun ViewGroup.inflate(layoutRes: Int): View
{
return LayoutInflater.from(context).inflate(layoutRes, this, false)
}
fun ImageView.displayGif(file:File){
GlideApp.with(context).asGif().load(file).transforms(CenterCrop(), RoundedCorners(30)).into(this)
}
fun ImageView.displayImage(file:File){
GlideApp.with(context).load(file).transforms(CenterCrop(), RoundedCorners(30)).into(this)
}
I've been on this for the past couple of days and just can't get my head around it. Any help in any direction is greatly appreciated. I know my explanations can be a bit all over the place so just ask for clarification when needed :)
UPDATE
I have now been able to produce this with a GridLayout as well as with RecyclerView. It's safe to assume that the nested RecyclerViews were not the culprit here. I even tried to ditch the Rx-piece that handled loading the images and created an IntentService for the process, but the same crashes still occur.
With GridLayout I mean that instead of having another adapter to populate the nested RecyclerView I use only one adapter to populate the message and to inflate and populate views for the attachments as well and to attach those views to the nested GridLayout.
The crash happens when I start to download a file and then scroll the view, that is supposed to show the downloaded file, out of the screen. That view should get recycled but for some reason the download process (which in my test cases only takes around 100ms-400ms) causes the app to throw one of the two errors mentioned in the original question. It might be worth noting that I'm using Realm and the adapter takes in a RealmResults<Message> list as it's dataset. My presenter looks for changes in the list and then notifies the adapter when needed (changed due to the implementation of IntentService).
This is how I'm capable to reproduce this time and time again:
Open a discussion that has messages with attachments
Start to scroll upwards for more messages
Pass a message with an attachment and scroll it off screen while it's still loading
Crash
There is no crash if I stop and wait for the download to complete and everything works as intended. The image/video/file gets updated with a proper thumbnail and the app wont crash if I scroll that out of view.
UPDATE 2
I tried swapping the nested ViewGroup for a single ImageView just to see is the problem within the nestedness. Lo and behold! It still crashes. Now I'm really confused, since the DiscussionListAdapter I mentioned before has the same exact thing in it and that one works like a charm... My search continues. I hope someone, some day will benefit from my agony.
UPDATE 3
I started to log the parent of every ViewHolder in the onBindViewHolder() function. Like expected I got nulls after nulls after nulls, before the app crashed and spew this out.
04-26 21:54:50.718 27075-27075/com.hailer.hailer.dev D/MsgAdapter: Parent of ViewHolder: android.view.ViewOverlay$OverlayViewGroup{82a9fbc V.E...... .......D 0,0-1440,2168}
There's a method to my madness after all! But this just poses more questions. Why is ViewOverlay used here? As a part of RecyclerView or as a part of the dark magicians plans to deprive me of my sanity?
Sidenote
I went digging into RecyclerViews code to check if I could find a reason for the ViewOverlaymystery. I found out that RecyclerView calls the adapters onCreateViewHolder() function only twice. Both times providing itself as the parent argument for the function. So no luck there... What the hell can cause the item view to have the ViewOverlay as it's parent? The parent is an immutable value, so the only way for the ViewOverlay to be set as the parent, is for something to construct a new ViewHolder and supply the ViewOverlay as the parent object.
UPDATE 4
Sometimes I amaze myself with my own stupidity. The ViewOverlay is used because the items are being animated. I didn't even consider this to be an option since I've set the itemAnimator for the RecyclerView as null, but for some odd reason that does not work. The items are still being animated and that is causing this whole charade. So what could be the cause of this? (How I chose to ignore the moving items, I do not know, but the animations became very clear when I forced the app to download same picture over and over again and the whole list went haywire.)
My DiscussionInstanceFragment contains the RecyclerView in question and a nested ConstraintLayout that in turn contains an EditText for user input and a send button.
val v = inflater.inflate(R.layout.fragment_discussion_instance, container, false)
val lm = LinearLayoutManager(context)
lm.reverseLayout = true
v.disc_instance_messages_list.layoutManager = lm
v.disc_instance_messages_list.itemAnimator = null
v.disc_instance_messages_list.adapter = mPresenter.messageAdapter
This is the piece that handles the initialization of the RecyclerView. I'm most definitely setting the itemAnimator as null, but the animations just wont stop! I've tried setting the animateLayoutChanges xml attribute on the root ConstraintLayout and on the RecyclerView but neither of them worked. It's worth mentioning that I also checked whether the RecyclerView had an itemAnimator in different states of the program, and every time I check the animator, it is null. So what is animating my RecyclerView?!
I have faced the same issue
Try this in your child RecyclerView it works for me
RecyclerView childRC = itemView.findViewById(R.id.cmol_childRC);
layoutManager = new LinearLayoutManager(context);
childRC.setItemAnimator(null);
childRC.setLayoutManager(layoutManager);
childRC.setNestedScrollingEnabled(false);
childRC.setHasFixedSize(true);
now set your Adapter like this
ArrayList<Model> childArryList = new ArrayList<>();
childArryList.addAll(arrayList.get(position).getArrayList());
ChildOrderAdapter adapter = new ChildOrderAdapter(context, childArryList);
holder.childRC.swapAdapter(adapter, true);
hope this helps
I finally figured out what was causing this. In my DiscussionInstanceView I have a small view that is animated into and out of view with ConstraintLayout keyframe animations. This view only shows the download progress of the chat history and is used only once, when the discussion is first opened. BUT since I had a call to hiding that view every time my dataset got updated, I was forcing the ConstraintLayout to fire of an animation sequence thus making everything animate during the dataset update. I just added a simple check whether I was downloading the history or not and this problem got fixed.
I am using FirebaseListAdapter to populate ListView. It is working fine for the data populated directly from Firebase. Here, I am trying to display/hide a tick image based on the status field. On initial load it is working fine, but when I scroll up and down the tick mark is getting set for some of the other items incorrectly. Need help! Here is my code inside populateView.
nameTextView.setText(appointment.getName());
timeTextView.setText(appointment.getTime());
if (appointment.getAppointmentStatus() == APPOINTMENT_COMPLETED) {
appStatusImage.setImageResource(R.drawable.ic_done_black_24dp);
} else {
appStatusImage.setVisibility(View.INVISIBLE);
}
This happens probably because you are using a RecyclerView, that meaning that the Views are reused instead of each object having it's own View. In other words, when you write:
if (appointment.getAppointmentStatus() == APPOINTMENT_COMPLETED) {
appStatusImage.setImageResource(R.drawable.ic_done_black_24dp);
}
Initially, you will set your View with the given image if it meets your if statement. But if the View itself is reused for another object that does not meet the if statement, the image resource will remain from the previous object property. What you should do is:
if (appointment.getAppointmentStatus() == APPOINTMENT_COMPLETED) {
appStatusImage.setImageResource(R.drawable.ic_done_black_24dp);
appStatusImage.setVisibility(View.VISIBLE);
} else {
appStatusImage.setVisibility(View.INVISIBLE);
}
And everything should work as you expect
I'm creating kind of music player which has a listview with songs (having progress bar near currently playing song)
What is important is that I have an Item with views which can be changed from outside (in some handler)
public View getView(int i, View convertView, ViewGroup viewGroup) {
RelativeLayout result;
if (convertView == null) {
result = (RelativeLayout) inflater.inflate(R.layout.list_item, viewGroup, false);
} else {
result = (RelativeLayout) convertView;
}
...
ProgressBar progressBar = result.findViewById(R.id.progressBar)
...
if (i == currentSong) {
// saving to instance variable
this.currentlyPlayingProgressBar = progressBar;
} else {
progressBar.setVisibility(View.GONE);
}
...
return result;
}
(Code was changed to focus on my problem)
Btw currentSong can be changed from outside, adapter.notifyDataSetChanged() is being called in this case.
It seems like I'm using listView incorrectly, but I don't know the better way.
The main problem is that I need to have links to views to change them.
And the only way where I can get them is in getView method which reuses those view in a way only google developers can explain=(
First problem
This is all happening in Fragment which is just a part of a viewPager. when user scrolls of this fragment and then scrolls back then getView method is being called with some strange objects inside.. And I override currentlyPlayingProgressBar with this invalid value. Which causes the freeze of my statusbar. (it starts updating wrong view)
And I have no idea which view is it..
Second problem
I am reusing list items and this means that when user scrolls list view down - then sometimes he gets actually the same list item with the same progressBar.
This progressBar must be invisible but it's not (I think it's all because of my usage of currentlyPlayingProgressBar from outside)
Thanks in advance.
You can do this in two ways:
1) notifyDataSetChanged, which just resets entire ListView and assigns all visible list items again to views. notifyDataSetChanged is very expensive, since it makes entire list and view hierarchy to be recreated, layouts are measured, etc, which is slow. For frequent update of single list item, such as progress bar change, this is overkill.
2) Find view of particular listview item, and update only that one. We'll focus on this approach.
First you need to somehow identify which list view contains which list item. Common approach is to use View.setTag in your Adapter.getView, where setTag parameter is Object of your choice, may be same item as you return for Adapter.getItem.
So later you know which ListView child view has which item assigned.
Assuming you want to update particular item displayed in ListView, you have to iterate through ListView child views to find which view displays your item:
Object myItem = ...;
for(int i = list.getChildCount(); --i>=0; ){
View v = list.getChildAt(i);
Object tag = v.getTag();
if(tag==myItem) {
// found, rebind this item
bindItemToView(myItem, v);
break;
}
}
You must expect that ListView currently may not display your list item (is scrolled away).
From code above you see that it calls bindItemToView, which is your function to bind item to list view. You'd probably call same function to setup the item view in Adapter.getView.
You may also optimize it further, assuming you want to update only ProgressBar, then don't call bindItemToView, but update only ProgressBar in your listitem view (findViewById, setup values).
Hint: you can make it even more optimal by using ViewHolder approach. Then setTag would not contain your item object, but your ViewHolder object.
#Pointer null has also given very usefull aproach, but in your case I think u are updating the list which is not visible, in this case you have to set the tag from the adapter just like the list index and curresponding check if the list item exist between the last visible item or first visible item then update it else donot update..
I'm having a bit of trouble preserving the scroll position of a list view when changing it's adapter's data.
What I'm currently doing is to create a custom ArrayAdapter (with an overridden getView method) in the onCreate of a ListFragment, and then assign it to its list:
mListAdapter = new CustomListAdapter(getActivity());
mListAdapter.setNotifyOnChange(false);
setListAdapter(mListAdapter);
Then, when I receive new data from a loader that fetches everything periodically, I do this in its onLoadFinished callback:
mListAdapter.clear();
mListAdapter.addAll(data.items);
mListAdapter.notifyDataSetChanged();
The problem is, calling clear() resets the listview's scroll position. Removing that call preserves the position, but it obviously leaves the old items in the list.
What is the proper way to do this?
As you pointed out yourself, the call to 'clear()' causes the position to be reset to the top.
Fiddling with scroll-position, etc. is a bit of a hack to get this working.
If your CustomListAdapter subclasses from ArrayAdapter, this could be the issue:
The call to clear(), calls 'notifyDataSetChanged()'. You can prevent this:
mListAdapter.setNotifyOnChange(false); // Prevents 'clear()' from clearing/resetting the listview
mListAdapter.clear();
mListAdapter.addAll(data.items);
// note that a call to notifyDataSetChanged() implicitly sets the setNotifyOnChange back to 'true'!
// That's why the call 'setNotifyOnChange(false) should be called first every time (see call before 'clear()').
mListAdapter.notifyDataSetChanged();
I haven't tried this myself, but try it :)
Check out: Maintain/Save/Restore scroll position when returning to a ListView
Use this to save the position in the ListView before you call .clear(), .addAll(), and . notifyDataSetChanged().
int index = mList.getFirstVisiblePosition();
View v = mList.getChildAt(0);
int top = (v == null) ? 0 : v.getTop();
After updating the ListView adapter, the Listview's items will be changed and then set the new position:
mList.setSelectionFromTop(index, top);
Basically you can save you position and scroll back to it, save the ListView state or the entire application state.
Other helpful links:
Save Position:
How to save and restore ListView position in Android
Save State:
Android ListView y position
Regards,
Please let me know if this helps!
There is one more use-case I came across recently (Android 8.1) - caused by bug in Android code. If I use mouse-wheel to scroll list view - consecutive adapter.notifyDataSetChanged() resets scroll position to zero. Use this workaround until bug gets fixed in Android
listView.onTouchModeChanged(true); // workaround
adapter.notifyDataSetChanged();
More details is here: https://issuetracker.google.com/u/1/issues/130103876
In your Expandable/List Adapter, put this method
public void refresh(List<MyDataClass> dataList) {
mDataList.clear();
mDataList.addAll(events);
notifyDataSetChanged();
}
And from your activity, where you want to update the list, put this code
if (mDataListView.getAdapter() == null) {
MyDataAdapter myDataAdapter = new MyDataAdapter(mContext, dataList);
mDataListView.setAdapter(myDataAdapter);
} else {
((MyDataAdapter)mDataListView.getAdapter()).refresh(dataList);
}
In case of Expandable List View, you will use
mDataListView.getExpandableListAdapter() instead of
mDataListView.getAdapter()