package com.netfluke.sergey.catpicker; import android.app.Fragment; import android.app.FragmentManager; import android.app.FragmentTransaction; import android.content.Context; import android.content.SharedPreferences; import android.content.res.Configuration; import android.graphics.drawable.GradientDrawable; import android.media.AudioManager; import android.media.SoundPool; import android.os.Build; import android.os.PersistableBundle; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; import android.view.View; import java.util.List; /** * This example is different from the Android Guide's * https://developer.android.com/training/basics/fragments/index.html * in that the intent here is to programmatically present different views in * different screen orientations: one fragment in portrait, two side-by-side in landscape. * * In this example, fragments are added programmatically, via FragmentTransactions and * inflation; they are not included in any XML layouts and must be manually inflated * and attached. * * Fragments also could be added statically via XML, see CatPickerStatic. * * Given that fragments are created programmatically, we must inhibit automatic * restoration of Views on flip, because after the flip we need a _different_ set of Views. * Hence onSaveInstanceState(..) and onRestoreInstanceState(..), which normally * automatically save and restore the views hierarchy, are overridden, so that the calls * to their super's methods are suppressed. Then the only views created on a flip are * the ones we create ourselves, programmatically. Not overriding these functions leads * to duplication and accumulation of fragments not visible on the screen. Check this * with "adb shell dumpsys activity com.netfluke.sergey.catpicker" and look for * fragments reported. * * So in this code is all fragments and views are destroyed on flip, * and nothing gets restored automatically; our code is in full control. * We thus rely on the activity getting destroyed on flip, and the only knowledge * of its prior Views being contained in the savedInstanceState bundle passed to onCreate(..), * which we suppress. See CatPickerWithConfigChange for how it could work when * activities and fragments are _not_ destroyed on flip. */ /* NOTE: I started out using tags in FragmentTransaction replace() when adding fragments. I thought to use these tags to retrieve these fragments when I needed them, to update their contents or to remove them. I believed that findFragmentByTag(..) would give me the active visible fragment with that tag. That way I would not need to keep extra global references to fragments. Unfortunately, this failed. I would occasionally get fragments that have already been removed and were waiting to be destroyed, rather than the added visible fragment added under the same tag. I confirmed it by running "adb shell dumpsys activity com.netfluke.sergey.catpicker"; the output showed fragments with mRemoving=true mAdded=false alongside the visible fragments. Apparently, findFragmentByTag(..) returned these. I tried forcefully committing pending transactions with getFragmentManager().executePendingTransactions(); but it did not eliminate the removed fragments from findFragmentByTag's output. See also the code snippet marked with "debug", which shows this is the case. Hence I opted to track active fragments explicitly in private class members; I left tags and code to show occasional defunct fragments in findFragmentByTag(..) results. */ public class MainActivity extends AppCompatActivity { private int catId; // Always start with the cat chosen at the previous run, even // after the app has been quit. private SharedPreferences prefs; // keep the fragments explicitly for onChangeConfiguration private ImageFragment im; // full screen or null in Portrait, always present in Landscape private PickerFragment pf; // there can be only one, but can go on backstack in Portrait mode @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.d("MAIN", "onCreate( " + ((savedInstanceState == null) ? "null" : savedInstanceState.toString() + " )")); // extract saved preferences prefs = getPreferences(Context.MODE_PRIVATE); catId = prefs.getInt("CAT_ID", R.drawable.pusheen1); // manually attach all the fragments we need, based on orientation attachFragments(); } private boolean isPortrait() { Configuration o = getResources().getConfiguration(); return o.orientation == Configuration.ORIENTATION_PORTRAIT; } private void attachFragments() { FragmentManager fmgr = getFragmentManager(); if( isPortrait() ) { setContentView(R.layout.activity_portrait); // create and add a Picker (vertical buttons) pf = new PickerFragment(); FragmentTransaction t = fmgr.beginTransaction(); t.replace(R.id.content_frame, pf, "picker"); t.commit(); } else { // landscape setContentView(R.layout.activity_landscape); // add both new picker and image FragmentTransaction t = fmgr.beginTransaction(); pf = new PickerFragment(); im = new ImageFragment(); im.catId = catId; t.replace(R.id.content_frameA, pf, "picker"); t.replace(R.id.content_frameB, im, "image"); t.commit(); // this sometimes runs *before* onCreateView of the fragment, // so looking up image by id in loadPicture(..) fails to find it: // // im.loadPicture(catId); // Calling // fmgr.executePendingTransactions(); // right after commit does not actually help: although the // ImageFragment gets attached right away, views would still take // some time to load and may still be absent when looked for. // // By contrast, setting the property instead and having the fragment // load the picture when its views are ready in onResume() worked. } } @Override protected void onPause() { super.onPause(); // save the cat's state, to start with the same cat when app is next launched in landscape SharedPreferences.Editor ed = prefs.edit(); ed.putInt("CAT_ID", catId); ed.commit(); } /** * Calls to super purposefully disabled. We don't want the Views saved * and restored, we want to re-create them manually. Using these super calls * results in fragment duplication (see that with * "abd shell dumpsys activity "). * So we save only what we want to save. Cf. * https://www.intertech.com/Blog/saving-and-retrieving-android-instance-state-part-1/ * https://www.intertech.com/Blog/saving-and-retrieving-android-instance-state-part-2/ * * Compare the contents of the savedInstanceState bundle * on entry to onCreate(..) when these functions are and are not called. */ @Override public void onSaveInstanceState(Bundle outState) { //super.onSaveInstanceState(outState); Log.d("STATE", "onSaveInstanceState"); outState.putInt("CAT_ID", catId); } // See above. @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { //super.onRestoreInstanceState(savedInstanceState); Log.d("STATE", "onRestoreInstanceState" + savedInstanceState.toString()); catId = savedInstanceState.getInt("CAT_ID"); } /* This function is called in response to all three button clicks in Picker. * It finds out which button was clicked by checking the ID of the View * passed in, i.e., the View that accepted the click. */ public void click(View v) { // which button? switch (v.getId()) { case R.id.button1: catId = R.drawable.manul1; break; case R.id.button2: catId = R.drawable.manul2; break; case R.id.button3: catId = R.drawable.manul3; break; default: Log.d("MAIN", "no such cat"); catId = R.drawable.pusheen1; } // debug: Finding fragment by tag will occasionally return a fragement // already removed, and waiting to be destroyed. Thus finding fragments // by tag is not always useful: it may return a "dead" fragment detached // from an activity; a live fragment with the same tag could be hidden. // Hence this activity keeps explicit references to it fragments. Fragment imgByTag = getFragmentManager().findFragmentByTag("image"); if (imgByTag != null && imgByTag.isRemoving() ) { Log.d("FRAG", "ImageFragment retrieved by tag is in removing stage"); } // end debug // We want to do different things in Portrait and Landscape: // portrait: push PickerFragment on backstack, replace content frame with new ImageFragment // landscape: set image in existing ImageFragment if (isPortrait()) { // there should be no active ImageFragment // im will retain a pointer to a previous one called from Picker, though. im = new ImageFragment(); im.catId = catId; FragmentTransaction t = getFragmentManager().beginTransaction(); t.replace(R.id.content_frame, im, "image"); // but keep the obscured PickerFragment on the backstack, so that // the "Back" button brings us back to it (so we can select more cats!) t.addToBackStack(null); t.commit(); } else { // Landscape . Image fragment should exist and be visible if (im == null) Log.d("MAIN", "no image fragment in Landscape!"); else if (im.isVisible()) im.loadPicture(catId); else { Log.d("MAIN", "image fragment is not visible. This should not happen."); } } } }