Im using my Custom FragmentAdapter to hold the fragment into a List
The Process i want:
1) User click in a list item and it create a new Activity within arguments and than create the fragment using the TagsPageAdapter.addPage(fragment);
2) This created fragment has a "id" int inside his arguments
3) When i hit the addPage, it verify all the fragments inside the List to check if any fragments has the id, if yes, restore it, if not, than create new and add to the list
this 3 steps are working perfeclty, the problem is when i try to remove one (error after the adapter code below)
adapter:
private class TabsPagerAdapter extends FragmentPagerAdapter {
private Bundle bundle;
private String titulo;
private TabsPagerAdapter(FragmentManager fm) {
super(fm);
}
#Override
public LeiAberta getItem(int position) {
current = fragments.get(position);
return current;
}
private void addPage(LeiAberta page) {
boolean found = false;
for (int i = 0; i < fragments.size(); i++) {
leiAberta = fragments.get(i);
if (leiAberta != null) {
if (leiAberta.getArguments() != null) {
if (leiAberta.getArguments().getInt("id") == page.getArguments().getInt("id")) {
viewPager.setCurrentItem(i);
found = true;
}
}
}
}
if (!found) {
fragments.add(page);
notifyDataSetChanged();
viewPager.setCurrentItem(fragments.size() - 1);
}
}
#Override
public int getCount() {
return fragments.size();
}
#Override
public CharSequence getPageTitle(int position) {
this.bundle = fragments.get(position).getArguments();
if (this.bundle != null) {
this.titulo = bundle.getString("titulo");
}
return titulo;
}
}
what im using to remove the fragment from the list:
int current_position = viewPager.getCurrentItem();
if(current_position == 0) {
viewPager.setCurrentItem(current_position +1);
} else {
viewPager.setCurrentItem(current_position -1);
}
fragments.remove(current_position);
adapter.notifyDataSetChanged();
if i remove the current tab/fragment, and then open another item in the list that will add another tab, no matter what item, it just restores the old fragment in the same position, for some reason the position of the removed fragment in the List keep filled in the shadows, and the strange thing is: the list.size(); decrease when i remove, so WHY? From where is this old fragment restored? how can i really destroy it, so the addPage doesnt find it in the "shadows"
i had tried:
Update ViewPager dynamically?
Removing fragments from FragmentStatePagerAdapter
Remove Fragment Page from ViewPager in Android
How to remove pages in Android ViewPager?
and many others
apparently the only solution was this framework:
https://github.com/inloop/UpdatableFragmentStatePagerAdapter?utm_source=android-arsenal.com&utm_medium=referral&utm_campaign=4851
using this override
#Override
public int getItemPosition(Object object) {
if (fragments.contains(object)) return fragments.indexOf(object);
else return POSITION_NONE;
}
Related
I got a viewPager inside it I have several fragment.
providerViewPager = (ViewPager) findViewById(R.id.viewPager);
userRequest.add(1);
fragmentPagerAdapter = new FragmentStatePagerAdapter(HomeActivity.this.getSupportFragmentManager()) {
#Override
public int getCount() {
return userRequest.size();
}
#Override
public Fragment getItem(int position) {
return MyFragment.newInstance(position, userRequest.get(position));
}
};
providerViewPager.setAdapter(fragmentPagerAdapter);
userRequest is a arrayList of int, for each item in this list I create a MyFragment.
MyFragment contain the display view for each page.
view.findViewById(R.id.mybutton).setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
ViewPager viewPager = ((HomeActivity)getActivity()).getViewPager();
FragmentStatePagerAdapter adapter = ((HomeActivity)getActivity()).getAdapter();
ArrayList<Integer> ints = ((HomeActivity)getActivity()).getRequest();
viewPager.removeView(view);
ints.remove(getArguments().getInt("position"));
adapter.notifyDataSetChanged();
}
});
This is a onClickListener who is inside my fragment, I would like to remove this current fragment when I'm click on it.
As you can see I get the viewPager, the adapter and the arrayList from my Activity, and I try some method to remove this fragment, this work but I got some bug, for exemple if I remove an element at the middle of the view Pager I can't reach the element at the end. I would like to know if they are something much simple for remove fragment.
I solve it with this :
public void removeUserRequest(int position) {
if (position > 0) {
removeViewOnSwipe(position - 1);
} else {
removeViewOnSwipe(position);
}
}
private void removeViewOnSwipe(int position) {
userRequest.remove(position);
if (userRequest.size() == 0) {
providerViewPager.setVisibility(View.GONE);
fragmentPagerAdapter = null;
} else {
fragmentPagerAdapter = new FragmentStatePagerAdapter(HomeActivity.this.getSupportFragmentManager()) {
#Override
public int getCount() {
return userRequest.size();
}
#Override
public Fragment getItem(int position) {
return ProviderSwipeListFragment.newInstance(position, userRequest.get(position));
}
};
providerViewPager.setAdapter(fragmentPagerAdapter);
providerViewPager.setCurrentItem(position);
}
}
removeUserRequest is the method call in the Fragment.
removeViewOnSwipe : we remove the item in the userRequest list. And after that I create a new Adapter. And finally set the correct new position with
providerViewPager.setCurrentItem(position);
I try do to it without recreat a new adapter with using notifyDataSetChanged instead, but I got some bug, the previous fragment can be visible if the user scroll a lot on the end of the swipe list
I was just wondering if it is the normal behaviour of viewpager and its adapter to always call the getItem() method for index 0 and 1, even if I immediately set a current position.
Here is my code:
mNewsPagerAdapter = new NewsDetailPagerAdapter(getChildFragmentManager());
mNewsPagerAdapter.updateNewsList(news);
mViewPager = (ViewPager) mView.findViewById(R.id.horizontal_view_pager);
mViewPager.setPageMargin(2);
mViewPager.setPageMarginDrawable(R.color.black);
mViewPager.setAdapter(mNewsPagerAdapter);
mViewPager.setCurrentItem(mCurrentPositionPager, false);
If I switch from my overview activity to my detail activity with this viewpager, the adapter always calls the getItem() method for position 0 and 1 and after that the getItem() method for the position of mOriginalPosition and its neighbors. I was wondering if this is the correct behaviour or if I missed something to implement it in a right way. Thanks for your help :)
Edit: Added my adapter code
public class NewsDetailPagerAdapter extends FragmentStatePagerAdapter {
private SparseArray<Fragment> mPageReferenceMap = new SparseArray<Fragment>();
private ArrayList<News> mNewsList;
public NewsDetailPagerAdapter(FragmentManager fm) {
super(fm);
}
/**
* Setzt die neuen News.
**/
public void updateNewsList(ArrayList<News> list) {
mNewsList = list;
}
#Override
public Fragment getItem(int position) {
Log.d("debug", "getItem position:" + position);
News newsItem = mNewsList.get(position);
NavigationFragment fragment = new NavigationFragment();
mPageReferenceMap.put(position, fragment);
return fragment;
}
#Override
public int getCount() {
return mNewsList.size();
}
#Override
public int getItemPosition(Object object) {
return POSITION_NONE;
}
public Fragment getFragment(int position) {
return mPageReferenceMap.get(position);
}
}
It is normal (and intelligent in my opinion).
ViewPager class has one property named mOffscreenPageLimit with default value of 1. This number determines how many pages on the left and on the right of the current page that the Viewpager will preload. For instance, you have 10 pages, current position is 5 and mOffcreenPageLimit is 1, the page at position 4 and 6 will be loaded.
You could change this property by calling this method
viewpager. setOffscreenPageLimit(int)
If you pass in an integer that is smaller than 1, it has no effect.
Yes, this is the normal behaviour of the ViewPager, because it will always try to stay ahead of the user by rendering tabs that limit with the drawing area. I personally don't recommend creating a custom ViewPager as you are almost sure to break functionality unless you really know what you are doing. Your adapter class should look something like this:
public class YourCustomPagerAdapter extends FragmentStatePagerAdapter {
private List<Fragment> fragmentList = new ArrayList<>();
private List<String> titleList = new ArrayList<>();
public WizardPagerAdapter(FragmentManager fm) {
super(fm);
}
public void addFragment(Fragment fragment, String title) {
fragmentList.add(fragment);
titleList.add(title);
}
#Override
public Fragment getItem(int position) {
return fragmentList.get(position);
}
#Override
public CharSequence getPageTitle(int position) {
super.getPageTitle(position);
return titleList.get(position);
}
#Override
public int getCount() {
return fragmentList.size();
}
}
and you should add your fragments as such:
#Override
public void onCreate(Bundle savedInstanceState) {
...
YourCustomPagerAdapter adapter = new YourCustomPagerAdapter (getSupportFragmentManager());
adapter.addFragment(FragmentOne.newInstance(), "Frag 1");
adapter.addFragment(FragmentTwo.newInstance(), "Frag 2");
viewPager.setAdapter(adapter);
...
}
Actually this is the normale behavior. In fact, as soos as you associate the ViewPager with the adapter, the adapter creates the first visibile layout (index 0) end the next one (index 1). This is done by default in the "setAdapter". Then, when you set a different position, the adapter will instantiate the fragment at the selected index, the previous one and the next one.
This is the usual ViewPager setAdapter code:
public void setAdapter(PagerAdapter adapter) {
if (mAdapter != null) {
mAdapter.setViewPagerObserver(null);
mAdapter.startUpdate(this);
for (int i = 0; i < mItems.size(); i++) {
final ItemInfo ii = mItems.get(i);
mAdapter.destroyItem(this, ii.position, ii.object);
}
mAdapter.finishUpdate(this);
mItems.clear();
removeNonDecorViews();
mCurItem = 0;
scrollTo(0, 0);
}
final PagerAdapter oldAdapter = mAdapter;
mAdapter = adapter;
mExpectedAdapterCount = 0;
if (mAdapter != null) {
if (mObserver == null) {
mObserver = new PagerObserver();
}
mAdapter.setViewPagerObserver(mObserver);
mPopulatePending = false;
final boolean wasFirstLayout = mFirstLayout;
mFirstLayout = true;
mExpectedAdapterCount = mAdapter.getCount();
if (mRestoredCurItem >= 0) {
mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);
setCurrentItemInternal(mRestoredCurItem, false, true);
mRestoredCurItem = -1;
mRestoredAdapterState = null;
mRestoredClassLoader = null;
} else if (!wasFirstLayout) {
populate();
} else {
requestLayout();
}
}
if (mAdapterChangeListener != null && oldAdapter != adapter) {
mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter);
}
}
In order to change the ViewPager behavior, you could extend the classic ViewPager overriding the setAdapter method and set the mCurrItem to the desired position.
I hope it helped
Edit:
After different tests, we found a solution.
If the ViewPager adapter is set after ViewPager layout become visible, items 0 and 1 are load.
If you want to avoid this behavior but you can't set the adapter before the layout become visible (because you are waiting for data), than you can use this workaround:
1) Set the ViewPager visibility initially to GONE
2) After you receive all the data, you update the adapter and you set the current item value
3) Finally you set the ViewPager visibility to VISIBLE
Here you can find an example:
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.detail_overview_fragment, container, false);
final int position = getArguments().getInt("position");
final ViewPager viewPager = (ViewPager) v.findViewById(R.id.viewpager);
viewPager.setVisibility(View.GONE);
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
#Override
public void run() {
viewPager.setAdapter(new PagerAdapter(getChildFragmentManager()));
viewPager.setCurrentItem(position);
viewPager.setVisibility(View.VISIBLE);
}
},5000);
return v;
}
i think the error is the adapter:
/**
* Setzt die neuen News.
**/
public void updateNewsList(ArrayList<News> list) {
//mNewsList = list;
mNewsList.clear();
mNewsList.addAll(list);
/**
* Notifies the attached observers that the underlying data has been changed
* and any View reflecting the data set should refresh itself.
*/
this.notifyDataSetChanged();
}
error reason :this list is diffent entity for that adapter.
I am currentky using a PagerTitleStrip in my application
(Doc: http://developer.android.com/reference/android/support/v4/view/PagerTitleStrip.html)
Everything works fine, and I have set the title like this:
public class PageAdapter extends FragmentPagerAdapter {
private List<Fragment> fragments;
public PageAdapter(FragmentManager fm, List<Fragment> fragments) {
super(fm);
this.fragments = fragments;
}
#Override
public Fragment getItem(int position) {
return this.fragments.get(position);
}
#Override
public int getCount() {
return this.fragments.size();
}
String[] titles = { "1", "2", "3", "4","5" };
#Override
public CharSequence getPageTitle(int position) {
return titles[position];
}
}
My concern now, is that once the first Fragment has finished loading, I have to change the title of the strip.
I have already tried to get the FragmentPagerAdapter and tried to manipulate it, but never succeed to change a title (text)
There is indeed a getPageTitle method: mPagerAdapter.getPageTitle(position) but no setPageTitle ...
Any idea or suggestion?
If I understand problem correctly - you get data later and then want to update titles array and trigger refresh of titles.
If that's the case just trigger PagerAdapter.notifyDataSetChanged(); after that method is invoked Fragments will call getPageTitle method and update themselves.
Kind of ugly solution but working : iterate to the visual tree and set the text yourself.
Also, you have to be aware that there is a first transparent/empty item on the left.
Here is the code (Xamarin here) to update the first title.
var firstHidedOneFound = false;
for (int i = 0; i < _tabstrip.ChildCount; ++i)
{
var child = _tabstrip.GetChildAt(i) as TextView;
if (child == null) { continue; }
if (!firstHidedOneFound)
{
firstHidedOneFound = true;
continue;
}
child.Text = ViewModel.MyShowPanelTitle;
break;
}
I have a viewpagerindicator by Jake Wharton and in that viewpager i have 6 pages.All of the pages contains listviews and i'm loading them with an AsyncTask.Sometimes viewpager shows wrong pages at wrong indexes.Here is my viewpager adapter:
public class TestFragmentAdapter extends FragmentPagerAdapter{
protected static final String[] CONTENT = new String[] { "a","b","c","d","e","f" };
private int mCount = CONTENT.length;
Fragment fragment1,fragment2,fragment3,fragment4,fragment5,fragment6;
public TestFragmentAdapter(FragmentManager fm) {
super(fm);
fragment1 = new CategoryListView();
fragment2 = CustomizedListviewRecent.newInstance(41);
fragment3 = CustomizedListviewMostRecents.newInstance(0);
fragment4 = CustomizedListviewPopulars.newInstance();
fragment5 = CustomizedListviewRecent.newInstance(42);
fragment6 = CustomizedListviewRecent.newInstance(43);
}
#Override
public Fragment getItem(int position) {
if(position == 0)
return fragment1;
else if(position == 1)
return fragment2;
else if(position == 2)
return fragment3;
else if(position == 3)
return fragment4;
else if(position == 4)
return fragment5;
else
return fragment6;
}
#Override
public int getCount() {
return mCount;
}
#Override
public CharSequence getPageTitle(int position) {
return TestFragmentAdapter.CONTENT[position % CONTENT.length];
}
public void setCount(int count) {
if (count > 0 && count <= 10) {
mCount = count;
notifyDataSetChanged();
}
}
}
Here is how i create my viewpager:
public class SampleTitlesDefault extends BaseSampleActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mAdapter = new TestFragmentAdapter(getSupportFragmentManager());
mPager = (ViewPager)findViewById(R.id.pager);
mPager.setAdapter(mAdapter);
mIndicator = (TitlePageIndicator)findViewById(R.id.indicator);
mIndicator.setViewPager(mPager);
mIndicator.setCurrentItem(1);
}
}
What can be problem here?How can i solve this?Thanks for your help
I know its too late but may be some one need it.
I have also faced the similar problem regarding wrong position and some time it shows same results on two fragments due to wrong positon.I solved my problem by replacing the FragmentPagerAdapter with FragmentStatePagerAdapter
And I also passed index postion from onPageScrolled method using OnPageChangeListener inteface
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
#Override
public void onPageSelected(int position) {
bundle = new Bundle();
bundle.putInt("position", position);
primaryFragment.setCurrentPostionAndData(position);
}
#Override
public void onPageScrollStateChanged(int state) {
}
});
Like the docs say, think about it this way. If you were to do an application like a book reader, you will not want to load all the fragments into memory at once. You would like to load and destroy Fragments as the user reads. In this case you will use FragmentStatePagerAdapter. If you are just displaying 3 "tabs" that do not contain a lot of heavy data (like Bitmaps), then FragmentPagerAdapter might suit you well. Also, keep in mind that ViewPager by default will load 3 fragments into memory. The first Adapter you mention might destroy View hierarchy and re load it when needed, the second Adapter only saves the state of the Fragment and completely destroys it, if the user then comes back to that page, the state is retrieved.
Difference between FragmentPagerAdapter and FragmentStatePagerAdapter
Please don't "cache" the Fragments yourself because it already does that for you.
That said, you should newInstance them inside the getItem method, and not when creating the adapter. Example:
public TestFragmentAdapter(FragmentManager fm) {
super(fm);
}
#Override
public Fragment getItem(int position) {
if(position == 0)
return new CategoryListView();
else if(position == 1)
return CustomizedListviewRecent.newInstance(41);
else if(position == 2)
return CustomizedListviewMostRecents.newInstance(0);
else if(position == 3)
return CustomizedListviewPopulars.newInstance();
else if(position == 4)
return CustomizedListviewRecent.newInstance(42);
else
return CustomizedListviewRecent.newInstance(43);
}
Common reasons to do that are:
People think it's more efficient to do it that way; or
A reference to the Fragment may be needed later on.
For (1), as I said, you don't need to worry about it (unless you want to learn it and tweak later, but for now let it be). And if you need (2), then you can do this (originally seen here).
I have created an activity that uses FragmentStatePagerAdapter to provide small gallery. However, I can't get it to refresh when activity resumes (after coming back from other activity, for example). Every time first two pictures will be blank, and only after i swipe two pictures to the side, they get refreshed. None of the answers I've found work (especially overriding getItemPosition())
I set it up like this:
mPagerAdapter = new PhotosPagerAdapter(getSupportFragmentManager());
mPager = (ViewPager) findViewById(R.id.photosViewPager);
mPager.setAdapter(mPagerAdapter);
Then I have FragmentStatePagerAdapter class:
private class PhotosPagerAdapter extends FragmentStatePagerAdapter{
public PhotosPagerAdapter(FragmentManager fm) {
super(fm);
}
#Override
public int getCount() {
return photos.size();
}
#Override
public Fragment getItem(int position) {
ImageFragment f = new ImageFragment(position);
return f;
}
#Override
public int getItemPosition(Object object) {
throw new RuntimeException();
//return POSITION_NONE;
}
}
As you probably noticed, I throw RuntimeException in getItemPosition, because I wanted to check when it's called. And it isn't called until I add something to list containing my pictures. Then ImageFragment class:
public class ImageFragment extends Fragment{
int position;
Bitmap mBitmap;
int width;
int height;
ImageView img;
public ImageFragment(){
}
public ImageFragment(int position){
this.position = position;
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
img = new ImageView(container.getContext());
img.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
width = container.getWidth();
height = container.getHeight();
loadBitmap();
return img;
}
public void loadBitmap(){
if (img == null){
return;
}
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(photos.get(position), options);
options.inSampleSize = calculateInSampleSize(options, width/2, height/2);
options.inJustDecodeBounds = false;
mBitmap = BitmapFactory
.decodeFile(photos.get(position), options);
img.setImageBitmap(mBitmap);
}
#Override
public void onDestroyView() {
mBitmap.recycle();
super.onDestroyView();
}
}
Code is kinda messy after I tried to fix it... But: removing onDestroyView() doesn't work. I have put mPagerAdapter.notifyDataSetChanged() in several places that must be called (like onResume()), with no result. I'm getting kinda desperate with that.
Dealing with fragment pager adapters can be a PITA.
Here are a few helpful tips:
ViewPager PagerAdapter not updating the View
Update ViewPager dynamically?
Generally speaking this one works 99% of the time...
Override getItemPosition in your PagerAdapter like this:
#Override
public int getItemPosition(Object object) {
return POSITION_NONE;
}
Sometimes even those don't work, I have to use the 'brute force' method and recreate the entire view again (from onCreate onwards)...
I was having a similar problem like your, after some research i created a nice script that avoid recreating fragments that are already there but at the same time allow you to update all visible/initiated fragment.
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.view.ViewGroup;
import java.util.WeakHashMap;
/**
* Created by TheCobra on 9/17/15.
*/
public abstract class FragmentAdvanceStatePagerAdapter extends FragmentStatePagerAdapter
{
private WeakHashMap<Integer, Fragment> mFragments;
public FragmentAdvanceStatePagerAdapter(FragmentManager fm)
{
super(fm);
mFragments = new WeakHashMap<Integer, Fragment>();
}
#Override
public Fragment getItem(int position)
{
Fragment item = getFragmentItem(position);
mFragments.put(Integer.valueOf(position), item);
return item;
}
#Override
public void destroyItem(ViewGroup container, int position, Object object)
{
super.destroyItem(container, position, object);
Integer key = Integer.valueOf(position);
if (mFragments.containsKey(key))
{
mFragments.remove(key);
}
}
#Override
public void notifyDataSetChanged()
{
super.notifyDataSetChanged();
for (Integer position : mFragments.keySet())
{
//Make sure we only update fragments that should be seen
if (position != null && mFragments.get(position) != null && position.intValue() < getCount())
{
updateFragmentItem(position, mFragments.get(position));
}
}
}
#Override
public int getItemPosition(Object object)
{
//If the object is a fragment, check to see if we have it in the hashmap
if (object instanceof Fragment)
{
int position = findFragmentPositionHashMap((Fragment) object);
//If fragment found in the hashmap check if it should be shown
if (position >= 0)
{
//Return POSITION_NONE if it shouldn't be display
return (position >= getCount()? POSITION_NONE : position);
}
}
return super.getItemPosition(object);
}
/**
* Find the location of a fragment in the hashmap if it being view
* #param object the Fragment we want to check for
* #return the position if found else -1
*/
protected int findFragmentPositionHashMap(Fragment object)
{
for (Integer position : mFragments.keySet())
{
if (position != null &&
mFragments.get(position) != null &&
mFragments.get(position) == object)
{
return position;
}
}
return -1;
}
public abstract Fragment getFragmentItem(int position);
public abstract void updateFragmentItem(int position, Fragment fragment);
}
Copy that code into a file name "FragmentAdvanceStatePagerAdapter.java". Now in your adapter, extend from this one and override "getFragmentItem()" & "updateFragmentItem()". Whenever you call notifydatachange(), updateFragmentItem() will be called with all the fragment already created. When the adapter need to create a new fragment, getFragmentItem() will be called.
I hope that save and help a lot of people :)
Good Luck and happy Programming!!!
P.S. your adapter "getItem()" should be "getFragmentItem()" if you use this class.
Instead of returning POSITION_NONE and creating all fragments again, you can do as I suggested here: Update ViewPager dynamically?
Never just use POSITION_NONE use
if(fragmentManager.getFragments().contains(object))
return POSITION_NONE;
else
return POSITION_UNCHANGED;
to avoid Fatal Exception: java.lang.IllegalStateException
Fragment {} is not currently in the FragmentManager