I have a FragmentStatePagerAdapter with a Listview in each page. The FragmentStatePagerAdapter is being refreshed once every second with new data for some of his pages (remember that each page is a listview).
I set the content of the listview with setAdapter:
StableArrayAdapter adapter = new StableArrayAdapter((MainActivity) getActivity(), R.layout.list_view_item, ((MainActivity) getActivity()).getData(mPage));
listView.setAdapter(adapter);
The problem is that some pages has a lot of content and a vertical scroll, so every second when listView.setAdapter(adapter);
is being called, the scroll of the listview is forced to his upper position. It is a very abnormal and annoying behaviour.
I find this on Stack Overflow: notifyDataSetChanged() makes the list refresh and scroll jumps back to the top
The problem is that these solutions are designed for a normal ViewPager or normal Adapter and can't work with FragmentStatePageAdapter or something, because after trying them I still have the same problem.
This is my adapter for the FragmentStatePagerAdapter:
public class CollectionPagerAdapter extends FragmentStatePagerAdapter {
public CollectionPagerAdapter(FragmentManager fm) {
super(fm);
}
@Override
public Fragment getItem(int position) {
return ObjectFragment.newInstance(position);
}
@Override
public int getCount() {
return infoTitlesArray.length;
}
@Override
public CharSequence getPageTitle(int position) {
return infoTitlesArray[position];
}
}
The ViewPager which has the problem:
<android.support.v4.view.ViewPager
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v4.view.PagerTitleStrip
android:id="@+id/pager_title_strip"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:paddingTop="4dp"
android:paddingBottom="4dp"
style="@style/CustomPagerTitleStrip"/>
</android.support.v4.view.ViewPager>
Layout of the fragment:
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/listview"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
</ListView>
The Java code for the fragment:
public static class ObjectFragment extends Fragment implements DataUpdateListener {
private int mPage;
private ListView listView;
public static ObjectFragment newInstance(int page) {
ObjectFragment objectFragment = new ObjectFragment();
Bundle args = new Bundle();
args.putInt("page", page);
objectFragment.setArguments(args);
return objectFragment;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mPage = getArguments().getInt("page");
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_collection_object, container, false);
((MainActivity) getActivity()).addDataUpdateListener(this);
listView = (ListView) rootView;
StableArrayAdapter adapter = new StableArrayAdapter((MainActivity) getActivity(), R.layout.list_view_item, ((MainActivity) getActivity()).getData(mPage));
listView.setAdapter(adapter);
return rootView;
}
@Override
public void onDestroyView() {
((MainActivity) getActivity()).removeDataUpdateListener(this);
super.onDestroyView();
}
//DataUpdateListener interface methods
@Override
public int getPage() {
return mPage;
}
@Override
public void onDataUpdated(ArrayList<String> data) {
StableArrayAdapter adapter = new StableArrayAdapter((MainActivity) getActivity(), R.layout.list_view_item, data);
listView.setAdapter(adapter);
}
}
When the content of the fragment (which is a listview) needs to be updated, the method onDataUpdated is called with the new content received in the parameter.
Adding adapter which generates a crash, using the code samples from Budius:
private static class StableArrayAdapter extends ArrayAdapter<String> {
HashMap<String, Integer> mIdMap = new HashMap<String, Integer>();
public StableArrayAdapter(Context context, int textViewResourceId, List<String> objects) {
super(context, textViewResourceId, objects);
for (int i = 0; i < objects.size(); ++i) {
mIdMap.put(objects.get(i), i);
}
}
public void setData(List<String> objects){
mIdMap.clear();
for (int i = 0; i < objects.size(); ++i) {
mIdMap.put(objects.get(i), i);
}
}
@Override
public long getItemId(int position) {
String item = getItem(position);
return mIdMap.get(item);
}
@Override
public boolean hasStableIds() {
return true;
}
}
New adapter trying to use Budius solution. But it does not work. The content is not being refreshed.
private static class StableArrayAdapter extends ArrayAdapter<String> {
List<String> objects;
public StableArrayAdapter(Context context, int textViewResourceId, List<String> objects) {
super(context, textViewResourceId, objects);
this.objects = new ArrayList<>();
this.objects.addAll(objects);
}
public void setData(List<String> objects){
this.objects.clear();
this.objects.addAll(objects);
}
@Override
public long getItemId(int position) {
return Math.abs(objects.get(position).hashCode());
}
@Override
public boolean hasStableIds() {
return true;
}
}
calling setAdapter(adapter)
and the list returning to the top is normal and a "expected" behavior. Afterall, it's a new adapter and the framework should not assume that the new adapter have any relation to the old one.
Said that, your solution should still be around notifyDataSetChanged()
and the appropriate usage of stableIds
. With that in mind, what you should do is simply switch the ArrayList<String> data
instead of creating a new one. Something like this:
@Override
public void onDataUpdated(ArrayList<String> data) {
adapter.setData(data); // internally replaces the old data with this new one
adapter.notifyDataSetChanged();
}
and then on your implementation of StableArrayAdapter you must override hasStableIds
to return true and override getItemId
to return a meaningfull value. Considering your data are simply strings, it's a bit tricky but something like Math.abs(value.hashCode)
should be enough.
PS.: I stronly recommend using RecyclerView
instead of ListView
. Personally I don't understand why Google didn't deprecated ListView yet.
edit:
as per comment, sometihng like this:
public class StableArrayAdapter extends /* whatever you're extending */ {
... your code as usual
@Override public boolean hasStableIds() { return true; }
@Override public long getItemId(int position) {
return Math.abs(data.get(position).hashCode());
}
}
with that you're telling the system that the ID
is direct related to the data you got and you returning ID that is connected to your data (even if losely connected). Meaning, it will be one ID for one data and a different ID for a different data.
That way on notify the ListView knows to keep te position based on the ID.
edit 2:
I found your error. Your adapter extends from ArrayAdapter
. This already have a List<String>
internally and when you call getItem(position);
, the data comes from that internal List.
The fix is simple.
List<String> objects;
and use the internal List.To update the data you call add
, addAll
and clear
methods from ArrayAdapter (https://developer.android.com/reference/android/widget/ArrayAdapter.html):
something like:
@Override
public void onDataUpdated(ArrayList<String> data) {
adapter.clear();
adapter.addAll(data);
adapter.notifyDataSetChanged();
}