I have tried using an extended FragmentStatePagerAdapter and an extended FragmentPagerAdapter to achieve what I'm trying and neither seem to work. I am sending down a JSON object from my server to fill an object model on the android client. On first load, everything works perfectly, I can swipe through pages just fine from beginning to end. Here is my object model:
ContestEntriesModel.java
public class ContestEntriesModel {
public int ContestId;
public String ContestName;
public ContestEntryModel[] entries;
}
As you can see, there is an array of ContestEntryModel. Each of those models looks like this:
ContestEntryModel.java
public class ContestEntryModel {
public long ContestEntryId;
public int UserId;
public String FullName;
public int Comments;
public string ImageUrl;
}
As stated above, when the first data payload comes down from the server, I am filling one fragment per item in the ContestEntryModel[] array with data from that model (including an image from an internet resource (ImageUrl)), and that works perfectly. I can swipe until my fingers bleed and all is good in the world.
I'm trying to now send another payload down from the server which has the items in the array in a different order (I will ultimately add new items too, but one step at a time). I have searched high and low on StackOverflow for a solution to this and posted a question yesterday which got me close to where I need to be (thanks Reinier), but I'm still struggling with getting all of my fragments to reorder and have the adapter completely refresh with the new order. The adapter seems to cache a few of the slides (the ones adjacent to the one I'm currently viewing) and they don't "refresh" until I've slid a few images past them and then return. Yesterday's trouble was getting the currently viewed slide to not "refresh" when the new data comes down from the server which I have working...sort of. Let's say I have 7 items in my array and I swipe to slide #3 (index 2) on the first load. I then push new data from the server which is just ordering the array in reverse. The currently viewed slide (slide #3) should then move to index position 4 and I should only be able to swipe to the right 2 times (index position 5 and 6). Instead what is happening is that it's remembering the current index position for that slide (index 2) and keeping it there. As I said above, slide #2 (index 1) and slide #4 (index 3) are also being maintained. My expected behavior is that those slides should reverse positions around slide #3 (index 2).
Here is my implementation of my PagerAdapter (this is the version that is a FragmentStatePagerAdapter not a FragmentPagerAdapter:
ScreenSlidePagerAdapter:
public class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter {
public ContestEntriesModel entries;
public ScreenSlidePagerAdapter(FragmentManager fm, ContestEntriesModel Entries) {
super(fm);
this.entries = Entries;
}
public long getItemId(int position) {
return entries.entries[position].ContestEntryId;
}
#Override
public int getItemPosition(Object object) {
android.support.v4.app.Fragment f = (android.support.v4.app.Fragment)object;
for(int i = 0; i < getCount(); i++){
android.support.v4.app.Fragment fragment = getItem(i);
if(f.equals(fragment)){
//Log.d("currentPosition",Integer.toString(i));
return i;
}
}
return POSITION_NONE;
}
#Override
public android.support.v4.app.Fragment getItem(int position) {
long entryId = getItemId(position);
//Log.d("EntryIds",Integer.toString(entryId));
if(mItems.get(entryId) != null) {
return mItems.get(entryId);
}
Fragment f = ScreenSlidePageFragment.create(position);
mItems.put(entryId, f);
return f;
}
#Override
public int getCount() {
return NUM_PAGES;
}
}
currently I'm setting a static property on my activity for the adapter (I use it in a few other "external" classes) like so:
MainActivity:
public class MainActivity extends FragmentActivity {
public static int NUM_PAGES = 0;
public static ViewPager mPager;
public static PagerAdapter mPagerAdapter;
public static ContestEntriesModel Entries;
public static HashMap<Long, android.support.v4.app.Fragment> mItems = new HashMap<Long, android.support.v4.app.Fragment>();
public static Long CurrentEntryId;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.entries_layout);
GetEntriesTask task = new GetEntriesTask(43);
task.execute();
}
public void LoadEntries()
{
mPager = (ViewPager) findViewById(R.id.pager);
mPagerAdapter = new ScreenSlidePagerAdapter(getSupportFragmentManager(), Entries);
mPager.setAdapter(mPagerAdapter);
mPager.setPageTransformer(true, new MyPageTransformer());
mPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
#Override
public void onPageSelected(int position) {
ContestEntries.CurrentEntryId = ContestEntries.Entries.entries[position].ContestEntryId;
Log.d("currentEntry",Long.toString(ContestEntries.CurrentEntryId));
invalidateOptionsMenu();
}
});
}
//the rest is omitted as it's not pertinent to my issue.
When a new dataload comes down from the sever, I am just setting my static property (Entries) to the new version. I also tried accessing the public property in the PagerAdapter (entries) to modify that directly, but that is not accessible for some reason (even though the class and the property both have a public accessor). I assume that is where this is going wrong because it's that instance that I need to be modifying, but honestly, my brain is fried at this point and I'm just not sure. Here is the code I'm using for the update:
UpdateCode:
#Override
protected void onPostExecute(ContestEntriesModel entries) {
Fragment currentFragment = MainActivity.mItems.get(MainActivity.CurrentEntryId);
Log.d("fromUpdate",Long.toString(MainActivity.CurrentEntryId));
MainActivity.Entries = entries;
MainActivity.NUM_PAGES = MainActivity.Entries.entries.length;
//MainActivity.mItems.clear();
MainActivity.mPagerAdapter.notifyDataSetChanged();
MainActivity.mPager.setCurrentItem(MainActivity.mPagerAdapter.getItemPosition(currentFragment));
}
So using that, it does keep me on the current slide (which is what I need), but that slide is not repositioned correctly as mentioned above, nor are the other adjacent slides "refreshed". Again, assuming I'm on slide #3, if i swipe to the left once the old slide #2 is still there. If I swipe to the left twice, slide #1 is updated to the "new" slide (slide #7 from the first load). If I then swipe back to the right once, slide #2 is still there (should be slide #6). If I then swipe to the right once more, the "original" slide that I was on that I need maintained, is now replaced with slide #5 (again that number is from the original load). If I keep swiping to the right all the way to the "new" slide #5, I see the original again. That is where it needs to be on the new data load, in that position.
I sincerely apologize for this wall of text, but I thought if I could provide as much code as possible, this would make more sense. I've read several different approaches to this problem but I can't wrap my head around any that suit my needs specifically.
Any suggestions / code help would be greatly appreciated.
You were working along the right lines. Your getItemPosition would never work because it is always instantiating a new Fragment object. You need to maintain your own ordering list and receive return POSITION_UNCHANGED if that objects view is still in the correct position. I will update my answer with a code example soon.
If all Fragments are of the same class you could just update the Fragments with new data intead oof reorder them.
MyFragment:
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
DataProvider.getInstance().addOnDataChangedListener(this);
if (mRoot == null) {
mRoot = inflater.inflate(R.layout.entitlement, container, false);
}
updateDetails();
return mRoot;
}
#Override
public void onDestroyView() {
DataProvider.getInstance().removeOnDataChangeListener(this);
super.onDestroyView();
}
public void dataChanged() {
updateDetails();
}
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
mContext = activity;
}
#Override
public void onDetach() {
super.onDetach();
mContext = null;
}
The DataProvider simply calls dataChanged() on registered listeners. Important: If you use getActivity() getResource() or similar in updateDetails() this may crash, since the Activity may still be null although OnAttach() has been called. To avoid this I use my own Context-Object to do these things, which is set in OnAttach() and removed in OnDetach().
My adapter just takes care of the page titles, since the fragments refresh on their own:
protected LinkedList<CharSequence> mPageTitles = new LinkedList<CharSequence>();
#Override
public CharSequence getPageTitle(int position) {
try {
return mPageTitles.get(position);
} catch (Exception ex) {
return null;
}
}
protected final void setData(List data) {
mPageTitles.clear();
if (list != null) {
for (int idx = 0; idx < list.size(); ++idx) {
mPageTitles.add(DataProvider.getInstance().getPageTitle(list.get(idx)));
}
}
}
#Override
public void notifyDataSetChanged() {
setData(DataProvider.getInstance().getData());
super.notifyDataSetChanged();
}
The time you update your DataProvider you also have to call myAdapter.notifyDataSetChanged().
This is probably not the best way of doing it, but you could give it a try.
This solution actually produces a hiccup as it's goal is to display new data instantly.
If your currentSlide should not be updated, you should mark it and take care of it in updateDetails()`:
private boolean sticky = false;
public void setSticky() {
sticky = true;
}
public void forceUpdate() {
sticky = false;
updateDetails();
}
protected void updateDetails() {
if (sticky) {
return;
}
// Do your update
}
Related
I have a ViewPager whose getCount(); changes based on a list of objects passed through the constructor. Using the adapter.notifyDataSetChanged(); I expect to see another page added. This seems to work correctly but when I remove an item from the mentioned list (used to calculate the getCount()) the ViewPager only removes the page successfully if its outside the offsetLimit.
Exemple: I have two pages and through a button I remove an item from the list. Then I call notifyDataSetChanged() and expect to have only one page left, but... the second "Page" is still there even thought I cannot swipe to it!!!!
How can I come around this? I want to avoid re-instantiating the whole ViewPager with the fragments.
CODE (simplification)
public class Adapter extends FragmentStatePagerAdapter {
private final List<String> list;
public Adapter(FragmentManager fragmentManager, List<String> list) {
super(fragmentManager);
this.list = list;
}
#Override
public int getCount() {
return list.size();
}
}
And this is inside the Activity:
adapter = new Adapter(getSupportFragmentManager(), items);
final ViewPager pager = findViewById(R.id.pager);
pager.setAdapter(adapter);
findViewById(R.id.add).setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
items.add("test");
adapter.notifyDataSetChanged();
}
});
findViewById(R.id.remove).setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
items.remove(items.size() - 1);
adapter.notifyDataSetChanged();
}
});
With this I can add items from the Activity and update de adapter (it works) and remove ... its doesn't work. What can I do?
You can override getItemPosition() & return POSITION_NONE in it.
getItemPosition() :
Called when the host view is attempting to determine if an item's position has changed. Returns POSITION_UNCHANGED if the position of the given item has not changed or POSITION_NONE if the item is no longer present in the adapter.
The default implementation assumes that items will never change position and always returns POSITION_UNCHANGED.
I'm writing an application where I need to show some 100 page in ViewPager. There are facility to jump from one index to direct any random index page. Here Every page it self contain list of item.
Now if I just to user ViewPager.current(index) to jump from one to any random index page..then UI get stuck for a moment and then display.
So to avoid this I thought to implement only 7 adapter page view..where these 100(getCount()) page will reuses these 7 page.
But Im'm getting
E/AndroidRuntime(25961): java.lang.IllegalStateException: Fragment already added: ArrayListFragment{24dad9b6 #0 id=0x7f060009}
E/AndroidRuntime(25961): at android.support.v4.app.FragmentManagerImpl.addFragment(FragmentManager.java:1192)
E/AndroidRuntime(25961): at android.support.v4.app.BackStackRecord.run(BackStackRecord.java:616)
E/AndroidRuntime(25961): at android.support.v4.app.FragmentManagerImpl.execPendingActions(FragmentManager.java:1484)
E/AndroidRuntime(25961): at android.support.v4.app.FragmentManagerImpl.executePendingTransactions(FragmentManager.java.
I'm using FragmentStatePagerAdapter for adapter implementation.
Following snap of my code I'm using
static final int NUM_ITEMS = 100;
private int MAX_AVAILABLE_COUNT = 7;
private ArrayList<ArrayListFragment> listFragment = new ArrayList();
public class MyAdapter extends FragmentStatePagerAdapter {
public MyAdapter(FragmentManager fragmentManager) {
super(fragmentManager);
for (int i = 0; i < MAX_AVAILABLE_COUNT; i++) {
listFragment.add(ArrayListFragment.newInstance(i));
}
}
#Override
public int getCount() {
return NUM_ITEMS;
}
#Override
public Fragment getItem(int position) {
original = position;
if (position >= MAX_AVAILABLE_COUNT - 1) {
int newPosition = position % MAX_AVAILABLE_COUNT;
position = newPosition;
}
Fragment fragment = listFragment.get(position);
return fragment;
}
}
Any suggestion what I'm doing wrong here. Or any other suggestion to achieve it!!
Your error is because you are manipulating with position in getItem() method. You can't do like that because at some point you will return fragment tied to other page and you will get exception like above. And storing references to fragments is also bad practice, always return new instance of fragment object.
In one of my apps, I need to add Fragments on both sides of the ViewPager. First of all, I will get a constant of 5 feeds, and my ViewPager will show feed at index 2 i.e. my current displayed Fragment will contain data present at index 2. So overall my ViewPager will show center of 5 feeds at start and that i have achieved by just setting the ViewPager current Item as 2 like this
viewPager.setCurrentItem(2);
Now user can swipe both sides, when he will swipe left from center position, I will look for next feed i.e fetch from server and add feed at zero index of my ViewPager like this
feedsList.add(0, modelClassObject); // feedsList will contain all feeds and has been used by adapter to show data in fragments.
adapter.notifyDataSetChanged();
and when i swipe right from center position, i will add feed at the last simply like this
feedsList.add(modelClassObject);
adapter.notifyDataSetChanged();
Now the problem is if i only add feeds at the right i.e at the end of the feedsList, everything works fine, but problem comes when i add feeds at zero index. My adapter is not showing that new feed that has been added to zero position instead it is repeating one of the existing feed and that too on the right side but not on the left. I Have tried everything, but nothing is going right way. Here is my adapter code.
private class HorizontalPagerAdapter extends FragmentStatePagerAdapter {
public HorizontalPagerAdapter(FragmentManager fm) {
super(fm);
}
#Override
public Fragment getItem(int arg0) {
return FeedUserProfileFragment.newInstance(feedsList.get(arg0),
arg0);
}
#Override
public int getCount() {
return feedsList.size();
}
}
I have also used this
#Override
public int getItemPosition(Object object) {
return POSITION_NONE;
}
but no results.. :(
So in severe need, If anyone had done that earlier and faced the same issue, please let me know what i am doing wrong. I only need to add fragment at zero index of my ViewPager.
I faced a similar problem before, and my solution was :
at first the list is declared in the adapter itself, so that when creating an instance of that adapter I can have it's own list then.
modified the method getItem(int arg0) in the adapter class so that it returns a specific item from the list depending on that item position.
when creating a new fragment, use instantiate method to create it, and after that add it to your fragments.
So, the complete solution would be :
adapter class:
private class HorizontalPagerAdapter extends FragmentStatePagerAdapter {
public List<Fragment> feedsList;
public HorizontalPagerAdapter(FragmentManager fm) {
super(fm);
}
#Override
public Fragment getItem(int position) {
return feedsList.get(position);
}
#Override
public int getCount() {
return feedsList.size();
}
}
and when creating the adapter:
public static YourPageAdapter adapter_obj; // make sure it's static object
adapter_obj = new YourPageAdapter(getSupportFragmentManager());
adapter_obj.feedsList = new ArrayList<Fragment>();
// then add the list of fixed fragments to it(the 5 in the beginning)
adapter_obj.feedsList = fragments_list; // an ArrayList contains the 5 fragments
and when want to create a new fragment:
adapter_obj.feedsList.add(0, Fragment.instantiate(mContext, ViewPager_Container_Class.class.getName(), page));
adapter_obj.notifyDataSetChanged();
FragmentStatePagerAdapter can't handle it right when you add a new fragment in front.
The workaround is this:
Add a new fragment at the end.
Call notifyDataSetChanged();
Bring the fragment to front.
Call notifyDataSetChanged();
BTW, getItemPosition() should return correct positions all along:
public int getItemPosition(Object object)
{
return feedsList.indexOf( object );
}
So, with your code, it should be:
newFrag = Fragment.instantiate(...);
feedsList.add( newFrag );
adapter_obj.notifyDataSetChanged();
feedsList.remove( feedsList.size() - 1 );
feedsList.add( 0, newFrag );
adapter_obj.notifyDataSetChanged();
I guess the implementation of FragmentStatePagerAdapter doesn't expect both adding a new fragment and changing position happen at the same time.
I asked this question over a year ago and got some good feedback about how to implement it but that project was abandoned. It has now resurfaced and I'm rekindling this fire.
The closest answer I got is here: Android ViewPager: Move any page to end programatically?
Essentially what I'm trying to do is reorder view fragments (and add new view fragments) based on data that I retrieve from a web service. The solution I posted the link to above works with a FragmentPagerAdapter but does not appear to work with a FragmentStatePagerAdapter. When I call notifyDataSetChanged() after rebuilding my data set, when using FragmentStatePagerAdapter, I get outofbounds exceptions because the size of my Array is not large enough. If I change everything up to use FragmentPagerAdapter, I do not get that exception, but I also am not seeing the fragments ordered correctly.
here is my fragmentstatepageradapter:
public class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter {
public ScreenSlidePagerAdapter(FragmentManager fm) {
super(fm);
}
public int getItemId(int position) {
//
return Entries.contestEntries.get(position).entry_id;
}
#Override
public int getItemPosition(Object object) {
android.support.v4.app.Fragment f = (android.support.v4.app.Fragment)object;
for(int i = 0; i < getCount(); i++){
android.support.v4.app.Fragment fragment = getItem(i);
if(f.equals(fragment)){
return i;
}
}
return POSITION_NONE;
}
#Override
public android.support.v4.app.Fragment getItem(int position) {
int entryId = getItemId(position);
if(Entries.mItems.get(entryId) != null) {
return Entries.mItems.get(entryId);
}
Fragment f = ScreenSlidePageFragment.create(position);
Entries.mItems.put(entryId, f);
return f;
}
#Override
public int getCount() {
return Entries.contestEntries.size();
}
}
as you can see, I'm just referencing static properties on my activity (Entries) because when I tried pushing those into members of the adapter, it wouldn't work at all.
here are the relevant properties from that activity:
public static ViewPager mPager;
public static PagerAdapter mPagerAdapter;
public static HashMap<Integer, android.support.v4.app.Fragment> mItems = new HashMap<Integer, android.support.v4.app.Fragment>();
public static int CurrentEntryId;
public static ArrayList<ContestEntryModel> contestEntries;
When the activity receives a new order, I'm currently just doing an mItems.clear() before I call notifyDataSetChanged(). This works fine but I have two issues with it: 1) it causes the screen to flash as it rebuilds the current fragment and 2) this is using more resources that I probably need to be using as I should be able to just use fragments that have already been built.
the way I'm resetting contestEntries is by receiving a JSON string from the web service then gson'ing it to my model, then resetting the contestEntries ArrayList to that new value.
I've also tried reordering the HashMap before calling notifyDataSetChanged() and that didn't work.
I have no problems changing this solution to use FragmentPagerAdapter() from FragmentStatePagerAdapter() if that's what's required to make this work, but as I said above, in the testing I did yesterday, it was not reordering the fragments using the solution posted above.
any suggestions would be greatly appreciated.
TIA
I have a ViewPager set up that is drawing the data for its pages (views) from data passed down from a server. On occasion, the server will send down new data that will re-order the views (and sometimes add new views) and I need to make that happen seamlessly on my android device while the user is currently viewing a particular fragment.
I have it set to call notifyDataSetChanged() when the new data is pulled down from the server, but that seems to keep a cached version of the slides to the left and right of the currently viewed slide which potentially will change with the reorder. I have looked at this topic here: ViewPager PagerAdapter not updating the View and implemented the first solution which works fine, other than it reloads the current slide that is being viewed which won't work for my purposes. I took a shallow-dive look into implementing the setTag() method proposed on that same page in the second answer and that would work for updating information on the slides, but it won't help me for reordering them.
is there some way I can reorder all of the slides behind the scenes w/o causing any burps in the currently viewed slide?
TIA
EDIT: adding code and asking for some further clarification
This is my Adapter class.
private class ScreenSlidePagerAdapter extends FragmentPagerAdapter {
ContestEntriesModel entries;
public ScreenSlidePagerAdapter(FragmentManager fm, ContestEntriesModel Entries) {
super(fm);
this.entries = Entries;
}
#Override
public long getItemId(int position) {
return entries.entries[position].ContestEntryId;
}
#Override
public int getItemPosition(Object object) {
android.support.v4.app.Fragment f = (android.support.v4.app.Fragment)object;
for(int i = 0; i < getCount(); i++){
android.support.v4.app.Fragment fragment = getItem(i);
if(f.equals(fragment)){
return i;
}
}
return POSITION_NONE;
}
#Override
public android.support.v4.app.Fragment getItem(int position) {
long entryId = getItemId(position);
//Log.d("EntryIds",Integer.toString(entryId));
if(mItems.get(entryId) != null) {
return mItems.get(entryId);
}
Fragment f = ScreenSlidePageFragment.create(position);
mItems.put(entryId, f);
return f;
}
#Override
public int getCount() {
return NUM_PAGES;
}
}
And here is what I'm using when a new payload comes down from the server:
#Override
protected void onPostExecute(ContestEntriesModel entries) {
Fragment currentFragment = ContestEntries.mItems.get(ContestEntries.CurrentEntryId);
Log.d("fromUpdate",Long.toString(ContestEntries.CurrentEntryId));
ContestEntries.Entries = entries;
ContestEntries.NUM_PAGES = ContestEntries.Entries.entries.length;
ContestEntries.mPagerAdapter.notifyDataSetChanged();
ContestEntries.mPager.setCurrentItem(ContestEntries.mPagerAdapter.getItemPosition(currentFragment));
}
Check my answer here. The key is to override both getItemId(int) and getItemPosition(Object) in your adapter.
getItemId(int) is used to identify your pages uniquely within your dataset. getItemPosition(Object) is used to fetch the (updated) position for this unique page in your dataset.
If you want to keep focus on the same page before and after updating (adding/removing pages), you should call viewpager.setCurrentItem(int) for the new position of your page after calling .notifyDataSetChanged() on your adapter.