ViewPager + FragmentStatePagerAdapter: ViewPager stops working when screen orientation changes - android

i am using ActionBarSherlock (which is basically an extension of the Android Support Package).
What i'm trying to do is the following:
I have a FragmentActivity which hosts just a single ViewPager. This ViewPager has a FragmentStatePagerAdapter (because there will be many items in the future). But for now it is just loaded with 2 items for testing.
Everything is working just fine while i am in portrait orientation. But when i change so landscape orientation it switches back to the first item in the adapter (which is fine since everything is reloaded etc), but i am unable to swype to the next item. There is just nothing happening.
From debugging i can see that the Loader return the two items just fine. getItem(...) is also called with position 0 and 1. So basicall everything looks fine, except it isn't ;)
Btw: the same thing is happening vice versa when i start in landscape orienation and switch to portrait orientation.
Any ideas what might be wrong here?
Here is some of my code:
public class QuotesStatePagerAdapter extends FragmentStatePagerAdapter {
private List<Quote> mQuotes;
public QuotesStatePagerAdapter(FragmentManager fm, List<Quote> quotes) {
super(fm);
mQuotes = quotes;
}
#Override
public Fragment getItem(int position) {
Bundle arguments = new Bundle();
arguments.putSerializable("quote", mQuotes.get(position));
QuoteFragment fragment = new QuoteFragment();
fragment.setArguments(arguments);
return fragment;
}
#Override
public int getCount() {
return mQuotes.size();
}
}
public QuotesFragment() {
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
public void updateOrdering(ORDERING newOrdering) {
mOrdering = newOrdering;
getLoaderManager().getLoader(0).startLoading();
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.quotes, container, false);
}
#Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mViewPager = (ViewPager) view.findViewById(R.id.viewpager);
mViewPager.setOnPageChangeListener(this);
}
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
getLoaderManager().initLoader(0, null, this);
}
#Override
public Loader<List<Quote>> onCreateLoader(int id, Bundle args) {
return new QuotesLoader(getActivity(), mCategoryId);
}
#Override
public void onLoadFinished(Loader<List<Quote>> loader, List<Quote> data) {
mQuotes = data;
mViewPager.setAdapter(new QuotesStatePagerAdapter(
getSupportFragmentManager(), mQuotes));
}

android:configChanges="orientation" worked like a charm, but i saw that it's not recommended by android team (only last case resource)... See here http://developer.android.com/guide/topics/manifest/activity-element.html#config
I found out that putting:
setRetainInstance(true); on the onCreateView of each fragment retained the instance do the trick. (please not that the onActivityCreated will be called again)

Try adding android:configChanges="orientation"to manifest inside Activity tag

Related

ViewPager crashes on orientation change - fragments stack up and cause OutOfMemoryException

My project structure looks like this:
MainActivity->PagerFragment->PagerViewAdapter which holds multiple PagerImageFragments
I'm getting an OutOfMemoryException after a couple of orientation changes but the pager displays bitmaps "efficiently" (I used AsyncTask/LruCache). Then I noticed that the memory used by app multiplies itself after each orientation change and from logs I can see that onCreate() in PagerFragment is called twice each time. I reckon that because of that my fragments get recreated and stack, causing the issue. How do I stop that from happening?
I tried setting
#Override
protected void onSaveInstanceState(final Bundle outState) {
// super.onSaveInstanceState(outState);
}
Which did nothing for me. Then I tried:
setRetainInstance(true);
Which made my PagerFragment recreate only once, but that's still not what I wanted.
EDIT: I changed MainActivity a little so it only does the initialization stuff if there is no saved instance and deletes all fragments when starting a new one. I deleted setRetainInstance(true). I can now change orientation numerous times (it will still end in OOM exception, but it takes much longer to trigger it). But now there is another problem - if I leave PagerFragment for another fragment and come back it crashes.
MainActivity:
public class MainActivity extends AppCompatActivity {
private final String TAG = "LOG";
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (savedInstanceState==null) {
...
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
PagerFragment fragment = new PagerFragment();
fragmentTransaction.add(R.id.fragment_container, fragment);
fragmentTransaction.commit();
...
switchContent(new PagerFragment());
}
}
public void switchContent(Fragment fragment) {
FragmentManager fm = getSupportFragmentManager();
List<Fragment> fragmentList = fm.getFragments();
if (fragmentList != null) {
for (Fragment frag : fragmentList ) {
Log.d(TAG, "switchContent: destroying "+frag);
fm.beginTransaction()
.remove(frag).commit();
}
}
fm.beginTransaction()
.replace(R.id.fragment_container, fragment)
.commit();
}
Inside PagerFragment:
PagerAdapter adapter = new PagerViewAdapter(getFragmentManager(), pdfFactory.getPageCount());
ViewPager pager = (ViewPager) getActivity().findViewById(R.id.pager);
pager.setAdapter(adapter);
pager.setPageMargin((int) getResources().getDimension(R.dimen.activity_horizontal_margin));
pager.setCurrentItem(position);
pager.setOffscreenPageLimit(2);
PagerViewAdapter:
public class PagerViewAdapter extends FragmentStatePagerAdapter {
private final int size;
public PagerViewAdapter(FragmentManager fm, int size) {
super(fm);
this.size = size;
}
#Override
public int getCount() {
return size;
}
#Override
public Fragment getItem(int position) {
return PagerImageFragment.newInstance(position);
}
}
PagerImageFragment:
public class PagerImageFragment extends Fragment {
private PhotoView photoView;
int position;
private MyCache<Integer, Bitmap> myCache;
public static PagerImageFragment newInstance(Integer pos) {
final PagerImageFragment f = new PagerImageFragment();
final Bundle args = new Bundle();
args.putInt("position", pos);
f.setArguments(args);
Log.d("DISPLAY", "Bundling...");
return f;
}
public PagerImageFragment() {}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//Initialize the cache
myCache = new MyCache<>( 5 * 1024 * 1024 );
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate and locate the main ImageView
final View v = inflater.inflate(R.layout.pager_element, container, false);
photoView = (PhotoView) v.findViewById(R.id.pager_image);
Bitmap image = myCache.get(position);
if (image != null) {
photoView.setImageBitmap(image);
}
else {
new SetImageTask(photoView, myCache).execute(position);
}
return v;
}
Exception(after edit):
java.lang.NullPointerException: Attempt to write to field 'int android.support.v4.app.Fragment.mNextAnim' on a null object reference
at android.support.v4.app.BackStackRecord.run(BackStackRecord.java:770)
at android.support.v4.app.FragmentManagerImpl.execPendingActions(FragmentManager.java:1677)
at android.support.v4.app.FragmentManagerImpl$1.run(FragmentManager.java:536)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:155)
at android.app.ActivityThread.main(ActivityThread.java:5696)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1028)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:823)
add this in your activity in manifest
android:configChanges="orientation|keyboardHidden|screenSize"
<activity android:name="MainActivity"
android:configChanges="orientation|keyboardHidden|screenSize">
</activity>
onConfigurationChanged will be called when orientation changes but your activity will not be recreated but instead onConfigurationChanged will be called and you have to manage the orientation changes if there are any. So it can prevent OutOfMemoryException problem due to recreation of everything on orientation change.
#Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// Handle orientation changes here
}
First of all, you shouldn't use getFragmentManager for your Pager Adapter. Instead of it, use getChildFragmentManager
PagerAdapter adapter = new PagerViewAdapter(getChildFragmentManager(), pdfFactory.getPageCount());
If it won't help, add Leak Canary library to build.gradle dependencies to detect memory leaks https://github.com/square/leakcanary
Turns out it's a bug in FragmentStateViewPager - it doesn't delete the views. The fix is to use this instead, although not perfect but best thing I could find https://github.com/adamsp/FragmentStatePagerIssueExample/blob/master/app/src/main/java/com/example/fragmentstatepagerissueexample/app/FixedFragmentStatePagerAdapter.java

How to make ViewPager destroy items right after I've scrolled to next page

If we have 4 pages in a View pager and swipe from positions 0 do 3
Item at position 0 is destroyed when we get to position 2, item at position 1 is destroyed when we get to position 4.
I need them to be destroyed right away, because I want the view to be recreated if I go back to it, which doesnt happen at the moment.
public class ViewPagerTutorialAdapter extends FragmentStatePagerAdapter {
public ViewPagerTutorialAdapter(FragmentManager fm) {
super(fm);
}
#Override
public Fragment getItem(int position) {
Bundle args = new Bundle();
args.putInt("page", position);
TutorialPageFragment fragment = new TutorialPageFragment();
fragment.setArguments(args);
return fragment;
}
#Override
public int getCount() {
return 4;
}
}
public class TutorialPageFragment extends android.support.v4.app.Fragment {
private ImageView ivTutorialPage;
private TransitionDrawable myTransitionDrawable;
private View rootView;
public TutorialPageFragment() {
}
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
rootView = inflater.inflate(R.layout.fragment_tutorial_page, container, false);
return rootView;
}
#Override
public void onResume() {
super.onResume();
ivTutorialPage = (ImageView) rootView.findViewById(R.id.ivTutorialPage);
ivTutorialPage.setImageResource(R.drawable.fadein);
myTransitionDrawable = (TransitionDrawable) ivTutorialPage.getDrawable();
new Timer().schedule(new TimerTask() {
#Override
public void run() {
getActivity().runOnUiThread(new Runnable() {
#Override
public void run() {
myTransitionDrawable.startTransition(1000);
}
});
}
}, 1000);
}
}
While this is not exactly answer to asked question, a better practice is not recreate Views if not necessary because it can be expensive to do on more complicated layouts + cause stuttering / extra lag which makes user experience worse.
In this case (running animations when fragment becomes visible) it's better to override setUserVisibleHint in fragment with code to run animation which will trigger when user visits the screen. The main advantage is of course no need to recreate Views and allows Android to perform it's internal optimization naturally.
Try viewPager.setOffscreenPageLimit(1);. This should save only one page near selected one. There is not way to only load selected item at a time according to this.

WebView: load image and fit to screen width

I've already read several threads about this problem but I got another strange misbehavior:
I've created a ViewPager that has an initial Page count of 5. If the page count-2 is reached,
pagerAdapter.notifyDataSetChanged();
gets called.
The fragments all get their own id, counting up from 1. So the first fragment got the number one, second one the number two and so on.
Each fragment loads an image out of a webserver, depending on their id:
webView.loadUrl("http://myserver/" + id + ".jpg");
Now it appears, that sometimes the images are larger than the device's screen width. That's why I added these lines:
webView.getSettings().setLoadWithOverviewMode(true);
webView.getSettings().setUseWideViewPort(true);
The first 5 elements seem fitted perfectly, but when trying to swipe to the next fragment, you've got to first swipe the webview about 3px to the left, after that swipe again, to get to the next fragment.
I don't know why this is happening , but after the initial 5 images, it works correctly. Maybe it has something to do with the fact that in the beginning I tell the pageradapter that there are 5 fragments to show, and after scrolling more fragments are added. These following fragments are not affected by this problem.
This was the first problem.
The second problem is, when swiping through the completely correctly shown images, sometimes an image wil not be fitted to the screen. I've got to turn the screen to landscape and back to portrait, only then the image is fitted correctly.
How can this happen, why does it work when the screen gets redrawn, but not on the first try? And it only appears to happen sometimes.
These are my two problems, the first one with the minimal wron fitting of the first 5 fragments and the second one the sometimes non-fitting images when swiping, but fit when screen gets redrawn
Here's my main activity:
public class MyActivity extends ActionBarActivity {
public static int NUM_PAGES = 5;
private ViewPager viewPager;
private CustomPagerAdapter pagerAdapter;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my);
viewPager = (ViewPager)findViewById(R.id.activity_my_viewpager);
pagerAdapter = new CustomPagerAdapter(getSupportFragmentManager());
viewPager.setAdapter(pagerAdapter);
viewPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
#Override
public void onPageScrolled(int i, float v, int i2) {
}
#Override
public void onPageSelected(int i) {
if(NUM_PAGES - i <= 2) {
NUM_PAGES+=3;
}
pagerAdapter.notifyDataSetChanged();
}
#Override
public void onPageScrollStateChanged(int i) {
}
});
}
private class CustomPagerAdapter extends FragmentStatePagerAdapter {
public CustomPagerAdapter(FragmentManager fm) {
super(fm);
}
#Override
public Fragment getItem(int position) {
return ScreenFragment.create(position);
}
#Override
public int getCount() {
return NUM_PAGES;
}
}
}
And here's the fragment class:
public class ScreenFragment extends Fragment {
public static final String ARG_PAGE = "ARG_PAGE";
private int pageNumber;
public static ScreenFragment create(int pageNumber) {
ScreenFragment fragment = new ScreenFragment();
Bundle args = new Bundle();
args.putInt(ARG_PAGE, pageNumber);
fragment.setArguments(args);
return fragment;
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
pageNumber = getArguments().getInt(ARG_PAGE)+1;
}
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
ViewGroup viewGroup = (ViewGroup)inflater.inflate(R.layout.fragment_screen, container, false);
WebView webView = (WebView)viewGroup.findViewById(R.id.fragment_screen_webview);
webView.setWebViewClient(new WebViewClient());
webView.getSettings().setLoadWithOverviewMode(true);
webView.getSettings().setUseWideViewPort(true);
webView.loadUrl("http://someserver/" + Integer.toString(number) + ".jpg");
return viewGroup;
}
}
Thanks in advance
You should try inflating an ImageView layout instead of using a WebView.
When inflating a ImageView for each page you can use a AsyncTask to download the image for every page.
See here: https://stackoverflow.com/a/2472175/3064486
You can have a layout holding only an ImageView that has android:scaleType="fitXY" or "fitCenter" then in your onCreateView inside your adapter you can just inflate this layout and find the ImageView inside it using findViewById. Then you can use img.setImageBitmap just as this answer suggests.
maybe this will help you,
getWindow().setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
);

Multiple calls to FragmentTransaction.replace() - only one works after orientation change

I am using the following code to populate my UI with 2 fragments, the containers are FrameLayout's defined in XML. This first time this code is called i.e. when the app starts, it works fine, and both my fragments are displayed as expected. However after a configuration change(specifically, orientation), only the first fragment in the transaction is shown.
I don't think it's an issue with the fragments themselves, because if I reverse the code so one replace is called before the other or vice versa, that fragment will be displayed. So for example with the snippet from below as a guide, if I swap the mSummary and mDetails replace calls, then mDetails will be displayed and mSummary won't.
It's always the second one in the block that is missing.
// Using tablet layout
} else {
FragmentManager fm = super.getFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.replace(R.id.summary_container, mSummaryFragment);
ft.replace(R.id.details_container, mDetailsFragment);
ft.commit();
}
I'm saving the fragments in onSaveInstanceState and restoring them from the Bundle savedInstanceState when the activity is recreated. I also tried breaking the transaction into two pieces by calling commit() and then getting another FragmentTransaction object but no joy there.
So for anyone coming across this at a later stage...
I finally manage to fix this by creating a new instance of the fragment and restoring it's state using a Fragment.SavedState object. So:
if (mSummaryFragment.isAdded() && mDetailsFragment.isAdded()) {
Fragment.SavedState sumState = getSupportFragmentManager().saveFragmentInstanceState(mSummaryFragment);
Fragment.SavedState detState = getSupportFragmentManager().saveFragmentInstanceState(mDetailsFragment);
mSummaryFragment = new SummaryFragment();
mSummaryFragment.setInitialSavedState(sumState);
mDetailsFragment = new DetailsFragment();
mDetailsFragment.setInitialSavedState(detState);
}
FragmentTransaction ft = mFragmentManager.beginTransaction();
ft.add(R.id.summary_container, mSummaryFragment);
ft.add(R.id.details_container, mDetailsFragment);
ft.commit();
I do not understand why this works and the old method doesn't, however this may be helpful for someone else.
this should work and orientation change will not affect the fragment., if you face any problem just let me know.
public class MainActivity extends FragmentActivity {
Fragment fragment = new Fragment1();
Fragment fragment2=new Fragment2();
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
FragmentManager fm = super.getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.replace(R.id.frame1, fragment);
ft.replace(R.id.frame2, fragment2);
ft.commit();
}
public void onSaveInstanceState(Bundle outState){
getSupportFragmentManager().putFragment(outState,"fragment1",fragment);
getSupportFragmentManager().putFragment(outState,"fragment2",fragment2);
}
public void onRetoreInstanceState(Bundle inState){
fragment = getSupportFragmentManager().getFragment(inState,"fragment1");
fragment2 = getSupportFragmentManager().getFragment(inState,"fragment2");
}
class Fragment1 extends Fragment{
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
}
ListView listView;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view=inflater.inflate(R.layout.summary_view,container,false);
return view;
}
#Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
}
}
class Fragment2 extends Fragment{
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
}
ListView listView;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view=inflater.inflate(R.layout.detail_view,container,false);
return view;
}
#Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
}
}
}

Updating ListView (in Fragment) after Dialog dismiss

I've read some question about this kind of problem but i can't solve it.
I've had this:
public class Classwork extends FragmentActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_homework);
mViewPager = (ViewPager) findViewById(R.id.pager);
mViewPager.setAdapter(mSectionsPagerAdapter);
}
public class SectionsPagerAdapter extends FragmentStatePagerAdapter {
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
}
#Override
public Fragment getItem(int position) {
// getItem is called to instantiate the fragment for the given page.
// Return a DummySectionFragment (defined as a static inner class
// below) with the page number as its lone argument.
Fragment fragment = new DummySectionFragment();
Bundle args = new Bundle();
args.putInt(DummySectionFragment.ARG_SECTION_NUMBER, position + 1);
args.putInt("posizione", position);
fragment.setArguments(args);
return fragment;
}
#Override
public int getCount() {
//code for number of pages
}
#Override
public CharSequence getPageTitle(int position) {
//code for return the title
}
}
public class DummySectionFragment extends Fragment {
public static final String ARG_SECTION_NUMBER = "section_number";
public DummySectionFragment() {
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
//query to populate ListView
//ListView with CustomAdapter
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
public void onItemClick(AdapterView<?> myAdapter, View myView, int myItemInt, long mylng) {
//Start new activity DialogDeleteAddGradeCopy
}
});
myDbHelper.close();
return rootView;
}
}
}
I've created it with the Eclipse wizard.
A DummyFragment contains a ListView, that is populated (correctly) with a SQLite query.
With a click on an item of the ListView a "dialog" (actually is an activity with a dialog style) is displayed.
Dialog:
public class DialogDeleteAddGradeCopy extends Activity {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//retrive data form Intent for the update in the db
button.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
//update the db
finish();
}
}
});
}
This dialog allows the user to update (with db query) one of the values of the clicked item.
All is working fine, except the update of the Fragment. The update query is ok (db is up-to-date when the dialog is closed), but to update the Fragment i need to swipe right or left two times and turn back (in this way the fragment is re-created so all works fine).
How can i force updating/re-creating the Fragment?
Thanks in advance.
Sorry for my bad English...
Normally when using multiple instances of Fragment (which I'm guessing you are based on the swipe left/swipe right comment) you don't want to update the content using an Intent as you would with an activity but using an Interface.
If the data is coming from another Fragment hosted by the same activity, say in a ViewPager or tab set up, you would set up an interface between the information sending fragment and the host activity and then call a public method to update the reciving fragment from within the host activity. The basics of using an interface to update fragments can be found here. I don't know the specifics of your whole project but I'd look into this.
As for what's posted:
You're code is not behaving properly because getItem is not proper in this context. When you are updating information by passing an intent and need to retrieve a new Bundle of information you should use
#Override
protected void onNewIntent(Intent intent) {
// TODO Auto-generated method stub
super.onNewIntent(intent);
}
You may have to set an activity flag to get this to work (for example FLAG_ACTIVITY_SINGLE_TOP) but basicly the idea is onNewIntent will be updated when a new intent is passed to your activity. Using getItem as you have it now will only update when the fragment is initially created and the args are set.

Categories

Resources