Issues with dual-pane preference screens Issues with dual-pane preference screens android android

Issues with dual-pane preference screens


Problem Solved
With how popular this question has become, I decided to revisit this issue again and see if I could find a resolution...and I did. Found a nice little work around that solves the single pane showing instead of dual pane and ensuring a header is always pre-selected when in dual pane mode.

If you don't care about an explanation, you can just skip on down to the code. If you don't care about ICS, a lot of the header tracking code can be removed as JB added a getter for the headers array list.

Dual Pane Issue
When viewing the preference header list in single pane mode or dual pane mode, there is only ever one PreferenceActivity created and it's the same activity for both cases. As a result, there's never a problem in handling screen rotations that will switch the pane mode.

However, in single pane mode when you click on a header, the corresponding fragment is attached to a NEW PreferenceActivity. This new fragment containing PreferenceActivity never invokes onBuildHeaders(). And why would it? It doesn't need to display them. This lie ins the problem.

When rotating that fragment into a dual pane mode, it doesn't have any header list to show so it just continues to show the fragment only. Even if it did show the header's list, you'll have some backstack issues as you would now have two copies of the PreferenceActivity showing headers. Keep clicking enough headers and you'll get quite a lengthy stack of activities for the user to navigate back through. As a result, the answer is simple. Just finish() the activity. It'll then load the original PreferenceActivity which DOES have the header list and will properly show the dual pane mode.

Auto Selecting Header
The next issue that needed tackling was that switching between single to dual pane mode with the new fix didn't auto select a header. You were left with a headers list and no details fragment loaded. This fix isn't quite as simple. Basically you just have to keep track of which header was last clicked and ensure during PreferenceActivity creation...a header is always selected.

This ends up being a bit annoying in ICS since the API does not expose a getter for the internally tracked headers list. Android does already persist that list and you could technically retrieve it by using the same privately stored internal string key however that's just a bad design choice. Instead, I suggest manually persisting it again yourself.

If you don't care about ICS, then you can just use the getHeaders() method exposed in JB and not worry about any of this saved/restore state stuff.

Code

public class SettingsActivity extends PreferenceActivity {private static final String STATE_CUR_HEADER_POS = "Current Position";private static final String STATE_HEADERS_LIST   = "Headers List";private int mCurPos = AdapterView.INVALID_POSITION;  //Manually track selected header position for dual pane modeprivate ArrayList<Header> mHeaders;  //Manually track headers so we can select one. Required to support ICS.  Otherwise JB exposes a getter instead.@Overridepublic void onBuildHeaders(List<Header> target) {    loadHeadersFromResource(R.xml.preference, target);    mHeaders = (ArrayList<Header>) target;  //Grab a ref of the headers list}@Overrideprotected void onCreate(Bundle savedInstanceState) {    super.onCreate(savedInstanceState);    //This is the only code required for ensuring a dual pane mode shows after rotation of a single paned preference screen    if (onIsMultiPane() && onIsHidingHeaders()) {        finish();    }}@Overridepublic boolean onIsMultiPane() {    //Override this if you want dual pane to show up on smaller screens    return getResources().getBoolean(R.bool.pref_prefer_dual_pane);}@Overrideprotected void onListItemClick(ListView l, View v, int position, long id) {    super.onListItemClick(l, v, position, id);    //Intercept a header click event to record its position.    mCurPos = position;}@Overrideprotected void onRestoreInstanceState(Bundle state) {    super.onRestoreInstanceState(state);    //Retrieve our saved header list and last clicked position and ensure we switch to the proper header.    mHeaders = state.getParcelableArrayList(STATE_HEADERS_LIST);    mCurPos = state.getInt(STATE_CUR_HEADER_POS);    if (mHeaders != null) {        if (mCurPos != AdapterView.INVALID_POSITION) {            switchToHeader(mHeaders.get(mCurPos));        } else {            switchToHeader(onGetInitialHeader());        }    }}@Overrideprotected void onSaveInstanceState(Bundle outState) {    super.onSaveInstanceState(outState);    //Persist our list and last clicked position    if (mHeaders != null && mHeaders.size() > 0) {        outState.putInt(STATE_CUR_HEADER_POS, mCurPos);        outState.putParcelableArrayList(STATE_HEADERS_LIST, mHeaders);    }}}


The key idea behind the code below came from the Commonsware blog entry linked in the question, so it feels relevant. I specifically had to extend the concept to deal with an orientation change issue that sounds very similar to the one in the question, so here's hoping it gives you a start.

The Settings class should not have any bearing on the orientation issue, but including it anyway to be clear.

Per my code comment, see if the checkNeedsResource call in onCreate will help at all:

public class SettingsActivityextends     PreferenceActivity{@SuppressWarnings("deprecation")@Overridepublic void onCreate(Bundle savedInstanceState) {    super.onCreate(savedInstanceState);    // Show settings without headers for single pane or pre-Honeycomb. Make sure to check the    // single pane or pre-Honeycomb condition again after orientation change.    if (checkNeedsResource()) {        MyApp app = (MyApp)getApplication();        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);        Settings settings = new Settings();        addPreferencesFromResource(R.xml.prefs_api);        settings.setupPreference(findPreference(MyApp.KEY_USERNAME), prefs.getString(MyApp.KEY_USERNAME, null), true);        settings.setupPreference(findPreference(MyApp.KEY_API_URL_ROOT), prefs.getString(MyApp.KEY_API_URL_ROOT, null), true);        if (this.isHoneycomb) {            // Do not delete this. We may yet have settings that only apply to Honeycomb or higher.            //addPreferencesFromResource(R.xml.prefs_general);        }        addPreferencesFromResource(R.xml.prefs_about);        settings.setupPreference(findPreference(MyApp.KEY_VERSION_NAME), app.getVersionName());    }}@TargetApi(Build.VERSION_CODES.HONEYCOMB)@Overridepublic void onBuildHeaders(List<Header> target) {    super.onBuildHeaders(target);    // This check will enable showing settings without headers for single pane or pre-Honeycomb.     if (!checkNeedsResource()) {        loadHeadersFromResource(R.xml.pref_headers, target);    }}private boolean checkNeedsResource() {    // This check will enable showing settings without headers for single pane or pre-Honeycomb.     return (!this.isHoneycomb || onIsHidingHeaders() || !onIsMultiPane());}private boolean isHoneycomb = (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB);

}

public class Settings {public Settings() {}public void setupPreference(Preference pref, String summary, boolean setChangeListener) {    if (pref != null) {        if (summary != null) {            pref.setSummary(summary);        }        pref.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {            @Override            public boolean onPreferenceChange(Preference pref, Object newValue) {                pref.setSummary(newValue.toString());                return true;            }        });    }}public void setupPreference(Preference pref, String summary) {    setupPreference(pref, summary, false);}

}