Is there a way to call notifyDataSetChanged() on a custom adapter without refreshing the list or disturbing the UI?
I have a ListView with a custom Adapter behind it, using a List of Guest objects as its dataset. When a Guest marks his attendance by tapping on his name, a tick is supposed to appear next to the guest's name in the UI. This I can do, but when I call notifyDataSetChanged(), the list of names is pushed all the way to the top, presumably because the list "refreshes".
If I don't call notifyDataSetChanged(), however, the tick disappears when I scroll past the updated entry and scroll back again. This is due to the ListView's "recycling" of Views as I understand, but it sure doesn't make my job any easier.
How would one call notifyDataSetChanged() without making the entire ListView refresh itself?
Better to have one boolean field in your Guest Class :isPresent.
whenever user taps on list item you can get the selected item using adapter.getItemAtPosition().
update the value isPresent to true. and make show the tick mark.
In your adapter class. check for isPresent value. If it is marked to true then show the tick mark else hide it.
This is how you can achieve the both. Show Tick Mark on ListItem click and if you scroll the listview and come back to the same item you tickmark show/hide will be taken care by Adapter.
you could retain the position like this
// save index and top position
int index = mList.getFirstVisiblePosition();
View v = mList.getChildAt(0);
int top = (v == null) ? 0 : v.getTop();
// ...
// restore
mList.setSelectionFromTop(index, top);
Actually possible if you don't want to make a "selected item" here is the code
public void updateItem(ListView listView, Activity activity) {
if (mData == null) return;
DebugLog.i("A", "firstCell: " + listView.getFirstVisiblePosition() + " lastCell: " + listView.getLastVisiblePosition());
for (int firstCell = listView.getFirstVisiblePosition(); firstCell <= listView.getLastVisiblePosition(); firstCell++) {
final DataItem item = (DataItem) getItem(firstCell); // in this case I put the this method in the Adapter and call it from Activity where the adapter is global varialbe
View convertView = listView.getChildAt(firstCell);
if (convertView != null) {
final TextView titleTextView = (TextView) convertView.findViewById(R.id.item_title);
// here is the most important to do; you have to use Main UI thread to update the view that is why you need activity parameter in the method
mActivity.runOnUiThread(new Runnable() {
#Override
public void run() {
titleTextView.setText( item + " updated");
}
});
}
}
}
Related
I have a simple recyclerview with items (tips) and a loading spinner at the bottom.
here's how the item count and item view type methods look:
#Override
public int getItemViewType(int position) {
if (position == getItemCount() - 1) { // last position
return LOADING_FOOTER_VIEW_TYPE;
}
else {
return TIP_VIEW_TYPE;
}
}
#Override
public int getItemCount() {
return tips.size() + 1; // + 1 for the loading footer
}
basically, i just have a loading spinner under all my items.
I create the adapter once like so:
public TipsListAdapter(TipsActivity tipsActivity, ArrayList<Tip> tips) {
this.tipsActivity = tipsActivity;
this.tips = tips;
}
and then once i have fetched additional items, i call add like so:
public void addTips(List<Tip> tips) {
// hide the loading footer temporarily
isAdding = true;
notifyItemChanged(getItemCount() - 1);
// insert the new items
int insertPos = this.tips.size(); // this will basically give us the position of the loading spinner
this.tips.addAll(tips);
notifyItemRangeInserted(insertPos, tips.size());
// allow the loading footer to be shown again
isAdding = false;
notifyItemChanged(getItemCount() - 1);
}
What's odd here is that when i do that, the scroll position goes to the very bottom. It almost seems like it followed the loading spinner. This only happens on the first add (i.e. when there is only the loading spinner showing initally). subsequent adds maintains the proper scroll position (the position where the items were inserted).
This doesn't happen if i change notifyItemRangeInserted() to notifyItemRangeChanged() like so:
public void addTips(List<Tip> tips) {
// hide the loading footer temporarily
isAdding = true;
notifyItemChanged(getItemCount() - 1);
// insert the new items
int insertPos = this.tips.size(); // this will basically give us the position of the loading spinner
this.tips.addAll(tips);
notifyItemRangeChanged(insertPos, tips.size());
// allow the loading footer to be shown again
isAdding = false;
notifyItemChanged(getItemCount() - 1);
}
Nor does it happen if i simply call notifyDataSetChanged() like so:
public void addTips(List<Tip> tips) {
this.tips.addAll(tips);
notifyDataSetChanged();
}
Here's the code for setting the adapter in my Activity:
public void setAdapter(#NonNull ArrayList<Tip> tips) {
if (!tips.isEmpty()) { // won't be empty if restoring state
hideProgressBar();
}
tipsList.setAdapter(new TipsListAdapter(this, tips));
}
public void addTips(List<Tip> tips) {
hideProgressBar();
getAdapter().addTips(tips);
restorePageIfNecessary();
}
private TipsListAdapter getAdapter() {
return (TipsListAdapter) tipsList.getAdapter();
}
Note:
I don't manually set scroll position anywhere.
I call setAdapter() in onResume()
addTips() is called after I fetch items from the server
Let me know if you need any additional parts of my code.
This only happens on the first add (i.e. when there is only the loading spinner showing initally). subsequent adds maintains the proper scroll position (the position where the items were inserted).
RecyclerView has built-in behavior when calling the more-specific dataset change methods (like notifyItemRangeInserted() as opposed to notifyDataSetChanged()) that tries to keep the user looking at "the same thing" as before the operation.
When the data set changes, the first item the user can see is prioritized as the "anchor" to keep the user looking at approximately the same thing. If possible, the RecyclerView will try to keep this "anchor" view visible after the adapter update.
On the very first load, the first item (the only item) is the loading indicator. Therefore, when you load the new tips and update the adapter, this behavior will prioritize keeping the loading indicator on-screen. Since the loading indicator is kept at the end of the list, this will scroll the list to the bottom.
On subsequent loads, the first item is not the loading indicator, and it doesn't move. So the RecyclerView will not appear to scroll, since it doesn't have to do so to keep the "anchor" on-screen.
My recommendation is to check insertPos and see if it is zero. If it is, that means this is the first load, so you should update the adapter by calling notifyDataSetChanged() in order to avoid this anchoring behavior. Otherwise, call notifyItemRangeInserted() as you're currently doing.
Remove the setAdapter code from onResume ASAP as you are setting new TipsListAdapter(this, tips);
Every time a new reference of the adapter is created...make field mAdapter and then set it in onCreate . RecyclerView doesnt remember the scrolled position because everytime a new reference of adapter is being created.. onResume gets called infinitely when activity is in running state..
So either you setAdapter in onCreate using new operator to create reference for adapter or,
in onResume use mAdapter field variable reference..
In my app I am using recycler view.I want to show and hide view on particular condition.But when I scroll recycler views I am not getting expected behaviour.When I Visible a view it gets visible for other rows as well randomly.
What I understand is when it recycles it reuses view and when previous view when it gets recycled it finds the visibility of that view.How can I hide view on particular condition? here is my adapter code
#Override
public void onBindViewHolder(UrduRhymesViewHolder holder, int position) {
RhymesModel current = mUrduRhymesList.get(position);
AppUtility.setCustomFont(mContext, holder.tvUrduRhymesName, Constants.HANDLEE_REGULAR);
holder.tvUrduRhymesName.setText(current.getRhymeName());
holder.ivUrduRhymesLogo.setImageUrl(current.getThumbnailUrl(), mImageRequest);
int status = AppUtility.getFavouriteStatus(mContext, current.getRhymeName(), new UrduRhymesDb(mContext));
if (status == 0)
holder.btnFavourite.setBackgroundResource(R.mipmap.btn_star_unactive);
else
holder.btnFavourite.setBackgroundResource(R.mipmap.btn_star);
ProgressbarDetails progressbarDetails = ProgressbarDetails.getProgressDetail(current.getRhymeName());
if (progressbarDetails == null) {
progressbarDetails = new ProgressbarDetails();
progressbarDetails.prgProgressBar = holder.pbRhymeDownload;
progressbarDetails.download_btn_settings = holder.downloadButtonLayout;
} else {
progressbarDetails.prgProgressBar = holder.pbRhymeDownload;
progressbarDetails.download_btn_settings = holder.downloadButtonLayout;
holder.pbRhymeDownload.setProgress(progressbarDetails.progress);
}
ProgressbarDetails.addUpdateProgressDetail(current.getRhymeName(), progressbarDetails);
if (progressbarDetails != null && progressbarDetails.isDownloading) {
Log.e("test","downloading foe position "+position );
holder.downloadButtonLayout.setBackgroundResource(R.mipmap.btn_download);
holder.pbRhymeDownload.setVisibility(View.VISIBLE);
holder.pbRhymeDownload.setProgress(progressbarDetails.progress);
} else {
Log.e("test","should not be visible for position "+position);
holder.pbRhymeDownload.setVisibility(View.GONE);
}
}
Here progressbarDetails.isDownloading (having value true) is the criteria when I want to show my view but is else clause it is not hiding my view
Edit: Here ProgressbarDetails (Singleton )is a class keeping reference of every row of adapter's progress bar.
No direct way of hiding and unhiding recylerview childitems.
Solution:
Let us assume that the recyclerview adapter is ArrayList
Now make another arraylist (temp_list)
Scenarios:
Hide: iterate through your adapter items and remove the ones that you want to hide. Put each of these into temp_list. After iteration is over, call notifyDataSetChanged()
Show: iterate through your temp_list items and remove the ones that you want to show. Put each of these into adapter. After iteration is over, call notifyDataSetChanged()
You should add a flag in your viewHolder that indicates if this view should be displayed or not . and check this flag every time in the onBindViewHolder.
because the recyclerView reuses the same views you should make a decision depending on something special for every view in you viewHolder.
do u mean when your data has been changed?
and your layout want to change ?
adapter.notifyDataSetChanged();
I am retrieving some data in an Async class called from a Custom ArrayAdapter. When i add a new comment, i update the comment text view and that works ok, but after i reload the entire list the updates don't appear anymore. I can see in the logcat that there is a new comment nr, but not on the UI.
Shouldn't this : answersListView.invalidateViews be enough? I am trying to update that single row from the listview, to escape the issue with not updating the comment nr after a while.
private void updateView(int index) {
System.out.println("index: " + index);
View v = answersListView.getChildAt(index - answersListView.getFirstVisiblePosition());
if (v == null)
return;
final TextView nrComments = (TextView) v.findViewById(com.dub.mobile.R.id.showCommentsTxt);
if (nrComments != null) {
if (nrComments.getText().toString().trim().length() == 0) {
// first comment
nrComments.setText("1 comments");
} else {
// comments exist already
int newNr = Integer.parseInt(nrComments.getText().toString().trim()
.substring(0, nrComments.getText().toString().trim().indexOf("comments")).trim()) + 1;
nrComments.setText(newNr + " comments");
}
System.out.println("final nr of comments: " + nrComments.getText());
nrComments.setVisibility(View.VISIBLE);
}
answersListView.getAdapter().getView(position, v, answersListView);
v.setBackgroundColor(Color.GREEN);
answersListView.invalidateViews();
}
And :
updateView(position);
adapter.notifyDataSetChanged();
The answer is pretty much what #Rami said in a comment to your question. You don't update directly the views, you just need to update the data. In your adapter, you override the getView method, in there is where you make all this changes, you don't need the updateViews method.
Let me try to explain how it works.
The listView uses an Adapter.
The List view ask the Adapter "give me the view in X position" with the getView method.
The adapter creates that view, is returned to the ListView, and that what is shown.
The Adapter itself, should contain a List with the data you want to show.
Those views (rows) are created and destroyed everytime one of those views become visible or invisible in the screen, or if you call the notifyDataSetChanged method.
So now, the thing is, if you change, let's say, the object in the position 5, of the adapter List for a different object with new data, then next time the ListView ask the Adapter to give me the view in the position 5, the adapter is going to create the View (row) with the new data. That's it.
you need to update the data in the UI thread.
if you have context in your Async class use
runOnUiThread(new Runnable() {
#Override
public void run() {
//update here
}
});
or
android.os.Handler handler = new android.os.Handler();
handler.post(new Runnable() {
#Override
public void run() {
//update here
}
});
That's not how you change data for an item in a list view. You must have used some array of strings to fill data in textview in the getView function of the adapter. You only need to change that array and then call notifydatasetchanged on adapter. Views are recycled by adapter so above does not make sense
getView (...)
{
TextView tv = new TextView(context);
tv.setText(myData[index]); // You need to change myData array contents
}
I have a list of Items that are "seen" or "not seen" in ArrayList<Item>. If they're not seen I change the background color of the ListView item in my CustomArrayAdapter like this :
if(item.is_seen == null || item.is_seen == 0) {
row.setBackgroundResource(R.color.yellow);
} else {
row.setBackgroundResource(R.color.transparent);
}
Now what I want to do is set all items background to transparent after 3 seconds spent on the page.
I already tried to do something like this:
mScheduledExecutor.schedule(new Runnable() {
#Override
public void run() {
for(int i=0; i<mItems.size(); i++) {
final Item n = mItems.get(i);
if(n.is_seen == null || n.is_seen == 0) {
// update value in db
int isSeen = 1;
updateItem(n._id, isSeen);
// change the color of backgrounds
View view = listViewItem.getChildAt(i);
if(view != null) {
view.setBackgroundResource(R.color.red);
}
}
}
Updating the value in the DB works, but the rest doesn't. But I'd like to avoid to reload the data. I just need the color to change.
I don't have errors, it just does nothing.
I look everywhere for an answer and didn't find one.
Am I wrong since the beginning? Is what I want to achieve even possible?
I thank you in advance for all the help you could give me.
Instead of changing the color of the view like youre doing,
// change the color of backgrounds
View view = listViewItem.getChildAt(i);
if(view != null) {
view.setBackgroundResource(R.color.red);
}
(code above will not work, because the views are recycled in the ListAdapter) update the DATA off which you build your list - add a property to the class you are passing into your ListAdapter, then grab that instance from the list and update that property, you have the position at which it needs to be updated already, so that's easy. Then, call notifyDataSetChanged() on the list. It will not redraw the list if you didn't ADD/REMOVE items from list, but it will update correct view for you. This is the only way to do it - absolutely NO WAY to get to a view corresponding to a specific element in a list after list has been drawn already. Only way is to refresh/redraw the list with notifyDataSetChanged(), followed by refreshDrawableState().
I'm using a ListView with a custom ArrayAdapter.
The List is an infinite scroll of tweets.
Updates to the list are inserted from the top.
I want to obtain an effect as the Twitter application. I'm not talking about the "scroll to update", but to maintain the position after the update.
I've just implemented some code that works in that way. Here it is:
// get the position of the first visible tweet.
// pausedCounter traces the number of tweets in the waiting line
final int idx = listView.getFirstVisiblePosition() + pausedCounter;
View first = listView.getChildAt(0);
int position = 0;
if (first != null)
position = first.getTop();
// here I update the listView with the new elements
for (Tweet[] tweets1 : pausedTweets)
super.updateTweets(tweets1);
final int finalPosition = position;
// this code maintain the position
listView.post(new Runnable() {
#Override
public void run() {
listView.setSelectionFromTop(idx, finalPosition);
}
});
The problem of this code is that for an instant the listView goes to the first element of the list, then kick in the setSelectionFromTop and it goes to the correct position.
This sort of "flickering" is annoying, and I want to remove it.
I found out only this solution:
// add the new elements to the current ArrayAdapter
for (Tweet[] tweets1 : pausedTweets)
super.updateTweets(tweets1);
// create a NEW ArrayAdapter using the data of the current used ArrayAdapter
// (this is a custom constructor, creates an ArrayAdapter using the data from the passed)
TweetArrayAdapter newTweetArrayAdapter =
new TweetArrayAdapter(context, R.layout.tweet_linearlayout, (TweetArrayAdapter)listView.getAdapter());
// change the ArrayAdapter of the listView with the NEW ArrayAdapter
listView.setAdapter(newTweetArrayAdapter);
// set the position. Remember to add as offset the number of new elements inserted
listView.setSelectionFromTop(idx, position);
In this way I have no "flickering" at all!