Fast scoll disabled after changing list view adapter - java

I have a ListView with fastscroll enabled:
mList.setFastScrollEnabled(true);
This works very well. But when I change the adapter, I cannot fast scroll anymore.
mList.setAdapter(mAdapter);
My adapter has getCount() properly implemented. Calling
mList.setFastScrollEnabled(true);
after setting the new adapter doesn't work either nor adding android:fastScrollEnabled="true" to the ListView XML works.
Is there any way to reenable fast scrolling?

After digging in the depths of the Android sourcecode, I finally found a solution:
mAdapter = new ArrayAdapter....
mList.setAdapter(mAdapter);
// We have to post notifyDataSetChanged() here, so fast scroll is set correctly.
// Without it, the fast scroll cannot get any child views as the adapter is not yet fully
// attached to the view and getChildCount() returns 0. Therefore, fast scroll won't
// be enabled.
// notifyDataSetChanged() forces the listview to recheck the fast scroll preconditions.
mList.post(new Runnable() {
#Override
public void run() {
mAdapter.notifyDataSetChanged();
}
});
FastScroller.java has the following method:
public void onItemCountChanged(int totalItemCount) {
final int visibleItemCount = mList.getChildCount();
....
updateLongList(visibleItemCount, totalItemCount);
}
When setAdapter() is called, FastScroller re-checks the conditions for enabling fast scrolling. But since the new adapter is not fully shown yet, mList.getChildCount() returns zero and updateLongList() won't enable fast scrolling.
With my fix, the ListView gets notified that the underlaying data was changed after the new adapter was fully attached to the view and the preconditions for fast scrolling are now satisfied.

Related

Removing item from list in the OnBindViewHolder method for RecyclerView Android

Ok so it is a bit complicated, I have a custom RecyclerView Adapter and in the OnBindViewHolder method I would like to remove the current item from the recyclerview depending on some different variables but when I remove the item from the ArrayList and call notifyDataSetChanged(); I get :
java.lang.IllegalStateException: Cannot call this method while RecyclerView is computing a layout or scrolling.
How can I remove the item from the RecyclerView in the onBindViewHolder ? , I do not want to do it before setting the adapter because each item in the recyclerview has a sublist and I want to remove the item if the sublist is empty. Thanks for any ideas and sorry for bad english.
Please follow the method used below as shown in this answer.
private List<DetectedIssue> issues = new ArrayList<DetectedIssue>();
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
// - get element from your dataset at this position
// - replace the contents of the view with that element
holder.button.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
try {
issues.remove(position);
notifyItemRemoved(position);
//this line below gives you the animation and also updates the
//list items after the deleted item
notifyItemRangeChanged(position, getItemCount());
} catch (SQLException e) {
e.printStackTrace();
}
}
});
}
#Override
public int getItemCount() {
return issues.size();
}
PS: As can be seen from the documentation it is bad practice to use notifyDataSetChanged() if only an item is added, moved or removed as unnecessary processes are run.
This event does not specify what about the data set has changed, forcing any observers to assume that all existing items and structure may no longer be valid. LayoutManagers will be forced to fully rebind and relayout all visible views.
If you want the underlying arraylist in some activity be changed then you will have to use an interface to communicate with the activity to change the arraylist.
I know its a bit late (4 years, heh) but I'm in the same solution. As the error states it is actually impossible to call methods such as adapter.notifyItemRemoved(position); or notifyItemRangeChanged(position, getItemCount()); and even the more intensive notifyDataSetChanged(); from within the onBindViewHolder method unless it is within an onClickListener whereby it will be called after the layout is built and not during the build.
A simple workaround for me was to call datalist.remove(position); so if the layout is rebuilt the view is omitted, then simply call holder.itemView.setVisibility(View.GONE);. It's not as clean, but it gets the job done for views you wish to hide.

RecyclerView item animator problems

I am trying to remove items from a RecyclerView from my adapters onBindViewHolder.
When I call this...
public void removeDropFromView(int position) {
data.remove(position);
notifyItemRemoved(position);
}
...my animation shows, but it will not allow the adapter position to update (the new position 0 becomes position 1).
When I call this...
public void removeDropFromView(int position) {
data.remove(position);
notifyItemRemoved(position);
notifyDataSetChanged();
}
...the item is removed, the position of all my items are updated, but it completely skips the animation.
Some have said my troubles are coming from this not being possible in the onBindViewHolder, but I have tried all of this in the ViewHolder's onClick with the same results.
How can I get the animation to show, while also keeping all of the data in proper order?
Thanks for the help!
Before deleting the data, get its viewholder, then call the setIsRecyclable(false) of the viewholder. Check this, line 102. It uses swipe to delete.

How i can load new elements when my ListView is scrolled to bottom?

You can find my code here:
How to correctly build table with data using onPostExecute and ListView
I need to do loading data from the server when my ListView is scroll to the bottom. I tried to looking for solution on Stackoverflow, but it is not helpful for me.
Also if it's not difficult i like to know how it's work for understand all.
Thank's to all
You have 2 solutions:
1) With OnScrollListener
You must have a class that extends ListView and implements OnScrollListener.
When you initialise the view, set it as the scroll listener :
setOnScrollListener(this);
Implement the method onScroll. It's called when you scroll with the arguments firstVisibleItem, visibleItemCount and totalItemCount.
When firstVisibleItem+visibleItemCount==totalItemCount you reached the bottom of the list, you can call your AsyncTask again to load the next items.
2) With a custom adapter
In the method getView that you must overwrite, you have access to the position of the item being rendered, i.e. about to be visible on the screen.
Let's say you store your items in a List items you know when you reached the bottom of the list when position == items.size()-1. You can then call your AsyncTask.
Warning
Be careful with these 2 solutions, if all the items of the list fit in the screen, your AsyncTask may be called very often and for no reason. You must do the necessary checks for that before starting it.
Use the Scroll state change listener in your program...I hope It will help you definitely ....
listStudies.setOnScrollListener(new OnScrollListener() {
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (Logic Condition) {
//Here also You can do your Logic here and then you can achieve your wishes.....
}
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
if (Logic Condition) {
//You just do your Logic here and then you can achieve your wishes.....
}
}
});

ViewPager + Adapter in Fragment => laggy swiping

I have a ViewPager with some fragments. Each fragment has a ListView in a SlidingDrawer (=invisible before swiping) with an ArrayAdapter.
Adapter is set on onCreateView(), that slows down swiping, because 30 list items have to load each time I swipe, because new fragments are being created.
My Question is, whether it is possible to set the adapter after swiping when it ViewPager is idle? Or is there a better way? The List needs to be already loaded when the SlidingDrawer is expanded.
I had a similar problem... I used listeners. Still, when you swipe two pages back to back it was laggy... I did something like this that improved the experience....
viewpager.setOnPageChangeListener(new OnPageChangeListener() {
int positionCurrent;
boolean dontLoadList;
#Override
public void onPageScrollStateChanged(int state) {
if(state == 0){ // the viewpager is idle as swipping ended
new Handler().postDelayed(new Runnable() {
public void run() {
if(!dontLoadList){
//async thread code to execute loading the list...
}
}
},200);
}
}
}
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
positionCurrent = position;
if( positionOffset == 0 && positionOffsetPixels == 0 ) // the offset is zero when the swiping ends{
dontLoadList = false;
}
else
dontLoadList = true; // To avoid loading content for list after swiping the pager.
}
}
If you take a few milli seconds to load the list that comes as supplement to the viewpager, its ok in terms of UX rather than giving a bad swiping experience... So, the idea is to wait for 400ms in the thread before loading the list and making sure that you actually dont load content when the user is trying to swipe fast to see the viewpager content...
My Question is, wether it is possible to set the Adapter after swiping
when it Pager is idle?
There is the OnPageChangeListener that you could set on the ViewPager to monitor the swipe gestures. You could then use the onPageSelected()(or the onPageScrollStateChanged() to monitor the current state) method to get notified when a new page has been selected and start from that method the loading of data.
Also, make sure the ListView are responsible for the lag and not some other part of your code.

ListView selection remains persistent after exiting choice mode

I have a ListView subclass that I allow selections on when the context action bar (CAB) is active. The CAB is set as a callback to the onItemLongClick event:
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
// Inflate a menu resource providing context menu items
MenuInflater inflater = mode.getMenuInflater();
inflater.inflate(context_menu, menu);
getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);
return true;
}
This is fine, and the ListView works as expected, with the currently selected item staying highlighted when touched.
When I close the CAB, I want the ListView to return to normal (i.e. Touch mode). The problem is that the last selected item remains highlighted indefinitely, regardless of what methods I try to clear it:
public void onDestroyActionMode(ActionMode mode) {
//Unselect any rows
ListView lv = getListView();
lv.clearChoices(); // Has no effect
lv.setChoiceMode(ListView.CHOICE_MODE_NONE); // Has no effect on the highlighted item
lv.setFocusable(false); // Has no effect
lv.setSelection(0); // Has no effect
mActionMode = null;
}
Any suggestions?
The main reason for the problem is that once the ListView selection mode is switched to CHOICE_MODE_NONE, the framework optimizes out the clear operation as it is no longer supporting 'selections'. I have improved the above workarounds a bit by clearing the selection state manually and then setting the mode in a delayed manner so the framework will have its turn to clear the state before turning the mode to CHOICE_MODE_NONE.
final ListView lv = getListView();
lv.clearChoices();
for (int i = 0; i < lv.getCount(); i++)
lv.setItemChecked(i, false);
lv.post(new Runnable() {
#Override
public void run() {
lv.setChoiceMode(ListView.CHOICE_MODE_NONE);
}
});
I faced the same issue and since requesting layout doesn't solve the problem for me either I implemented a little hack which works for me. Maybe this is the same issue because I'm switching between CHOICE_MODE_SINGLE and CHOICE_MODE_NONE.
When the action mode ends I'm calling this code snippet. clearChoices makes sure that all items are not checked anymore (internally). The iteration over the views makes sure that all currently visible views are reset and not checked anymore.
mListView.clearChoices();
for (int i = 0; i < mListView.getChildCount(); i++) {
((Checkable) mListView.getChildAt(i)).setChecked(false);
}
mListView.setChoiceMode(ListView.CHOICE_MODE_NONE);
Looking at the ListView sourcecode, the only way to work around this is to set the ListView to CHOICE_MODE_NONE, then re-assign the ListAdapter (which clears the internal selection list regardless of choice mode)
i.e. in a ListFragment/ListActivity
getListView().setChoiceMode(ListView.CHOICE_MODE_NONE);
getListView().setAdapter(getListAdapter())
I was having this issue in API Level 17 and solved it by doing:
listView.clearChoices();
listView.invalidateViews();
For me, it seems the accepted answer is not working for invisible items, and it's no need to call
for (int i = 0; i < lv.getCount(); i++)
lv.setItemChecked(i, false);
instead, just call
lv.requestLayout();
To completely solve my issue, I call
lv.clearChoices();
lv.requestLayout();
in onDestroyActionMode()
and call
lv.setItemChecked(position, false)
in onItemClick() when it's not in ActionMode
However, I did not confirm whether call setItemChecked() will result some performance issues
This has been logged as an AOSP bug, but marked as obsolete for whatever reason.
Normally you would expect this to work:
getListView().clearChoices();
getListView().setChoiceMode(ListView.CHOICE_MODE_NONE);
Unfortunately it does not. Deferring setting choice mode to none in the next layout pass would work:
getListView().clearChoices();
getListView().post(new Runnable() {
#Override
public void run() {
getListView().setChoiceMode(ListView.CHOICE_MODE_NONE);
}
});
I had tried all the approaches discussed above but none of them work for me. Finally, I decide to apply the following workaround. The key idea is that,
During multimode, instead of reusing the "cached" view, we will create a completely new view. Not efficient, but at least "partially" solve my problem.
Here is the code of my customized ArrayAdapter
#Override
public View getView(int position, View convertView, ViewGroup parent) {
// Key to solve this problem. When we are in multimode, we will not reusing the cached view.
View rowView = this.multimode ? null : convertView;
if (rowView == null) {
LayoutInflater inflater = activity.getLayoutInflater();
rowView = inflater.inflate(R.layout.watchlist_row_layout, null);
ViewHolder viewHolder = new ViewHolder();
viewHolder.textView0 = (TextView) rowView.findViewById(R.id.text_view_0);
viewHolder.textView1 = (TextView) rowView.findViewById(R.id.text_view_1);
viewHolder.textView2 = (TextView) rowView.findViewById(R.id.text_view_2);
rowView.setTag(viewHolder);
}
Also, I feel safer to have the following code in ActionMode.Callback, although I'm not sure how much it helps.
#Override
public void onDestroyActionMode(ActionMode mode) {
MyFragment.this.myArrayAdapter.setMultimode(false);
// http://stackoverflow.com/questions/9754170/listview-selection-remains-persistent-after-exiting-choice-mode
// Using View.post is the key to solve the problem.
final ListView listView = MyFragment.this.getListView();
listView.clearChoices();
for (int i = 0, ei = listView.getChildCount(); i < ei; i++) {
listView.setItemChecked(i, false);
}
listView.post(new Runnable() {
#Override
public void run() {
listView.setChoiceMode(ListView.CHOICE_MODE_NONE);
}
});
actionMode = null;
}
Side Note
Using MultiChoiceModeListener couple with CHOICE_MODE_MULTIPLE_MODAL will make this bug gone. However, for device below API level 11 will not able to use this solution.
I know this has been answered, but above answers still gave me problems with the cached/recycled views that ListView maintains, that didn't update it's state when scrolled back into view.
So, the above solution changes slightly to:
lv.clearChoices();
ArrayList<View> list = new ArrayList<View>();
lv.reclaimViews(list);
for (View view : list) {
((Checkable) view).setChecked(false);
}
lv.setChoiceMode(lv.CHOICE_MODE_NONE);
This is better than using getChildAt(i) because that method jusg gives you the currently visble views and does not account for the internal cached views, that are not visible.
I have found that the only two methods that work here (API 19) are:
Resetting the list adapter, which is undesirable because it goes back to the top of the list;
Setting the choice mode to CHOICE_MODE_NONE in a new Runnable
If the choice mode is changed without using listView.post(new Runnable()), it doesn't work. Can anyone explain to me why this is?
Apologies for not commenting; I have no reputation.
Thanks.
Not sure if this is too late just wanted to share. I created an intent to the same page so that once the clicked data is captured it recreates a fresh page without any clicked persistence.
Is not a bug. That behavior is required to support multiple HID for Android.
So to show the selection state you only need set the choice mode of the listview and a background to support the selected state for the "list item layout", like:
android:background="?android:attr/activatedBackgroundIndicator"
FYI: http://android-developers.blogspot.mx/2008/12/touch-mode.html

Categories