Conditional Preference Headers
Android 3.0 introduced the new, two-tier form of PreferenceActivity
.
You supply header+fragment pairs. Android shows the user the list of
headers, and upon clicking on a header, Android shows the fragment.
However, on phone-sized devices, it will do those one a time: the list
of fragments fills the screen, and then the chosen fragment fills the
screen. On larger devices, PreferenceActivity
shows them both
side by side. This is a classic rendition of
the master-detail pattern.
However, it does tend to steer developers in the direction of displaying headers all of the time. For many apps, that is rather pointless, because there are too few preferences to collect to warrant having more than one header.
One alternative approach is to use the headers on larger devices,
but skip them on smaller devices. That way, the user does not have to
tap past a single-item ListFragment
just to get to the actual
preferences to adjust.
This is a wee bit tricky to implement.
The basic plan is to have smarts in onBuildHeaders()
to handle this.
onBuildHeaders()
is the callback that Android invokes on our
PreferenceActivity
to let us define the headers to use in the
master-detail pattern. If we want to have headers, we would supply
them here; if we want to skip the headers, we would instead fall
back to the classic (and, admittedly, deprecated)
addPreferencesFromResource()
method to load up some preference
XML.
There is an isMultiPane()
method on PreferenceActivity
, starting
with API Level 11, that will tell you if the activity will render
with two fragments (master+detail) or not. In principle, this would
be ideal to use. Unfortunately, it does not seem to be designed to
be called from onBuildHeaders()
. Similarly, addPreferencesFromResource()
does not seem to be callable from onBuildHeaders()
. Both are due
to timing: onBuildHeaders()
is called in the middle of the
PreferenceActivity
onCreate()
processing.
So, we have to do some fancy footwork.
By examining the source code to PreferenceActivity
,
you will see that the logic that drives the single-pane vs. dual-pane
UI decision boils down to:
onIsHidingHeaders() || !onIsMultiPane()
If that expression returns true
, we are in single-pane mode; otherwise,
we are in dual-pane mode. onIsHidingHeaders()
will normally return
false
, while onIsMultiPane()
will return either true
or false
based upon screen size. Specifically, onIsMultiPane()
looks at an
internal boolean resource (com.android.internal.R.bool.preferences_prefer_dual_pane
),
which has different definitions based upon screen size. At present, it
will be true
for -sw720dp
devices, false
otherwise.
So, we can leverage this information in a PreferenceActivity
to
conditionally load our headers:
public class EditPreferences extends SherlockPreferenceActivity {
private boolean needResource=false;
@SuppressWarnings("deprecation")
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (needResource
|| Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
addPreferencesFromResource(R.xml.preferences);
}
}
@Override
public void onBuildHeaders(List<Header> target) {
if (onIsHidingHeaders() || !onIsMultiPane()) {
needResource=true;
}
else {
loadHeadersFromResource(R.xml.preference_headers, target);
}
}
}
Here, if we are in dual-pane mode, onBuildHeaders()
populates the
headers as normal. If, though, we are in single-pane mode, we skip
that step and make note that we need to do some more work in onCreate()
.
Then, in onCreate()
, if we did not load our headers, or if we are
on API Level 10 or below, we use the classic addPreferencesFromResource()
method.
The net result is that on Android 3.0+ tablets, we get the dual-pane, master-detail look with our one header, but on smaller devices (regardless of version), we roll straight to the preferences themselves.