From b1ad5977bc8178b6d350ebe9099daded4c1ef603 Mon Sep 17 00:00:00 2001 From: Dianne Hackborn Date: Mon, 2 Aug 2010 17:30:33 -0700 Subject: New two-pane mode for PreferenceActivity. This introduces a whole new way to use PreferenceActivity, as a container for PreferenceFragments that the user can switch between from a list of headers. Change-Id: I1c79b7c78b86790dc460a1414a999aba5de80628 --- .../android/preference/PreferenceActivity.java | 522 +++++++++++++++++---- .../android/preference/PreferenceFragment.java | 30 +- 2 files changed, 462 insertions(+), 90 deletions(-) (limited to 'core/java/android/preference') diff --git a/core/java/android/preference/PreferenceActivity.java b/core/java/android/preference/PreferenceActivity.java index 4686978..fdc874b 100644 --- a/core/java/android/preference/PreferenceActivity.java +++ b/core/java/android/preference/PreferenceActivity.java @@ -16,71 +16,96 @@ package android.preference; -import android.app.Activity; +import com.android.internal.util.XmlUtils; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.app.Fragment; import android.app.ListActivity; +import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.util.Xml; +import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; import android.view.View.OnClickListener; +import android.widget.ArrayAdapter; import android.widget.Button; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; /** - * Shows a hierarchy of {@link Preference} objects as - * lists, possibly spanning multiple screens. These preferences will - * automatically save to {@link SharedPreferences} as the user interacts with - * them. To retrieve an instance of {@link SharedPreferences} that the - * preference hierarchy in this activity will use, call - * {@link PreferenceManager#getDefaultSharedPreferences(android.content.Context)} - * with a context in the same package as this activity. - *

- * Furthermore, the preferences shown will follow the visual style of system - * preferences. It is easy to create a hierarchy of preferences (that can be - * shown on multiple screens) via XML. For these reasons, it is recommended to - * use this activity (as a superclass) to deal with preferences in applications. - *

- * A {@link PreferenceScreen} object should be at the top of the preference - * hierarchy. Furthermore, subsequent {@link PreferenceScreen} in the hierarchy - * denote a screen break--that is the preferences contained within subsequent - * {@link PreferenceScreen} should be shown on another screen. The preference - * framework handles showing these other screens from the preference hierarchy. - *

- * The preference hierarchy can be formed in multiple ways: - *

  • From an XML file specifying the hierarchy - *
  • From different {@link Activity Activities} that each specify its own - * preferences in an XML file via {@link Activity} meta-data - *
  • From an object hierarchy rooted with {@link PreferenceScreen} - *

    - * To inflate from XML, use the {@link #addPreferencesFromResource(int)}. The - * root element should be a {@link PreferenceScreen}. Subsequent elements can point - * to actual {@link Preference} subclasses. As mentioned above, subsequent - * {@link PreferenceScreen} in the hierarchy will result in the screen break. - *

    - * To specify an {@link Intent} to query {@link Activity Activities} that each - * have preferences, use {@link #addPreferencesFromIntent}. Each - * {@link Activity} can specify meta-data in the manifest (via the key - * {@link PreferenceManager#METADATA_KEY_PREFERENCES}) that points to an XML - * resource. These XML resources will be inflated into a single preference - * hierarchy and shown by this activity. - *

    - * To specify an object hierarchy rooted with {@link PreferenceScreen}, use - * {@link #setPreferenceScreen(PreferenceScreen)}. - *

    - * As a convenience, this activity implements a click listener for any - * preference in the current hierarchy, see - * {@link #onPreferenceTreeClick(PreferenceScreen, Preference)}. + * This is the base class for an activity to show a hierarchy of preferences + * to the user. Prior to {@link android.os.Build.VERSION_CODES#HONEYCOMB} + * this class only allowed the display of a single set of preference; this + * functionality should now be found in the new {@link PreferenceFragment} + * class. If you are using PreferenceActivity in its old mode, the documentation + * there applies to the deprecated APIs here. + * + *

    This activity shows one or more headers of preferences, each of with + * is associated with a {@link PreferenceFragment} to display the preferences + * of that header. The actual layout and display of these associations can + * however vary; currently there are two major approaches it may take: + * + *

    + * + *

    Subclasses of PreferenceActivity should implement + * {@link #onBuildHeaders} to populate the header list with the desired + * items. Doing this implicitly switches the class into its new "headers + * + fragments" mode rather than the old style of just showing a single + * preferences list. + * + * + *

    Sample Code

    + * + *

    The following sample code shows a simple preference activity that + * has two different sets of preferences. The implementation, consisting + * of the activity itself as well as its two preference fragments is:

    + * + * {@sample development/samples/ApiDemos/src/com/example/android/apis/preference/PreferenceWithHeaders.java + * activity} * - * @see Preference - * @see PreferenceScreen + *

    The preference_headers resource describes the headers to be displayed + * and the fragments associated with them. It is: + * + * {@sample development/samples/ApiDemos/res/xml/preference_headers.xml headers} + * + * See {@link PreferenceFragment} for information on implementing the + * fragments themselves. */ public abstract class PreferenceActivity extends ListActivity implements PreferenceManager.OnPreferenceTreeClickListener { + private static final String TAG = "PreferenceActivity"; private static final String PREFERENCES_TAG = "android:preferences"; + private static final String EXTRA_PREFS_SHOW_FRAGMENT = ":android:show_fragment"; + + private static final String EXTRA_PREFS_NO_HEADERS = ":android:no_headers"; + // extras that allow any preference activity to be launched as part of a wizard // show Back and Next buttons? takes boolean parameter @@ -92,12 +117,26 @@ public abstract class PreferenceActivity extends ListActivity implements private static final String EXTRA_PREFS_SET_NEXT_TEXT = "extra_prefs_set_next_text"; private static final String EXTRA_PREFS_SET_BACK_TEXT = "extra_prefs_set_back_text"; - private Button mNextButton; + // --- State for new mode when showing a list of headers + prefs fragment + + private final ArrayList

    mHeaders = new ArrayList
    (); + + private HeaderAdapter mAdapter; + + private View mPrefsContainer; + + private boolean mSinglePane; + + // --- State for old mode when showing a single preference list private PreferenceManager mPreferenceManager; private Bundle mSavedInstanceState; + // --- Common state + + private Button mNextButton; + /** * The starting request code given out to preference framework. */ @@ -116,12 +155,128 @@ public abstract class PreferenceActivity extends ListActivity implements } }; + private class HeaderViewHolder { + ImageView icon; + TextView title; + TextView summary; + } + + private class HeaderAdapter extends ArrayAdapter
    { + private LayoutInflater mInflater; + + public HeaderAdapter(Context context, List
    objects) { + super(context, 0, objects); + mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + HeaderViewHolder holder; + View view; + + if (convertView == null) { + view = mInflater.inflate(com.android.internal.R.layout.preference_list_item, + parent, false); + holder = new HeaderViewHolder(); + holder.icon = (ImageView)view.findViewById( + com.android.internal.R.id.icon); + holder.title = (TextView)view.findViewById( + com.android.internal.R.id.title); + holder.summary = (TextView)view.findViewById( + com.android.internal.R.id.summary); + view.setTag(holder); + } else { + view = convertView; + holder = (HeaderViewHolder)view.getTag(); + } + + Header header = getItem(position); + if (header.icon != null) holder.icon.setImageDrawable(header.icon); + else if (header.iconRes != 0) holder.icon.setImageResource(header.iconRes); + if (header.title != null) holder.title.setText(header.title); + if (header.summary != null) holder.summary.setText(header.summary); + + return view; + } + } + + /** + * Description of a single Header item that the user can select. + */ + public static class Header { + /** + * Title of the header that is shown to the user. + */ + CharSequence title; + + /** + * Optional summary describing what this header controls. + */ + CharSequence summary; + + /** + * Optional icon resource to show for this header. + */ + int iconRes; + + /** + * Optional icon drawable to show for this header. (If this is non-null, + * the iconRes will be ignored.) + */ + Drawable icon; + + /** + * Full class name of the fragment to display when this header is + * selected. + */ + String fragment; + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(com.android.internal.R.layout.preference_list_content); + mPrefsContainer = findViewById(com.android.internal.R.id.prefs); + boolean hidingHeaders = onIsHidingHeaders(); + mSinglePane = hidingHeaders || !onIsMultiPane(); + String initialFragment = getIntent().getStringExtra(EXTRA_PREFS_SHOW_FRAGMENT); + + if (initialFragment != null && mSinglePane) { + // If we are just showing a fragment, we want to run in + // new fragment mode, but don't need to compute and show + // the headers. + getListView().setVisibility(View.GONE); + mPrefsContainer.setVisibility(View.VISIBLE); + switchToHeader(initialFragment); + + } else { + // We need to try to build the headers. + onBuildHeaders(mHeaders); + + // If there are headers, then at this point we need to show + // them and, depending on the screen, we may also show in-line + // the currently selected preference fragment. + if (mHeaders.size() > 0) { + mAdapter = new HeaderAdapter(this, mHeaders); + setListAdapter(mAdapter); + if (!mSinglePane) { + mPrefsContainer.setVisibility(View.VISIBLE); + switchToHeader(initialFragment != null + ? initialFragment : onGetInitialFragment()); + } + + // If there are no headers, we are in the old "just show a screen + // of preferences" mode. + } else { + mPreferenceManager = new PreferenceManager(this, FIRST_REQUEST_CODE); + mPreferenceManager.setOnPreferenceTreeClickListener(this); + } + } + + getListView().setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY); + // see if we should show Back/Next buttons Intent intent = getIntent(); if (intent.getBooleanExtra(EXTRA_PREFS_SHOW_BUTTON_BAR, false)) { @@ -163,45 +318,165 @@ public abstract class PreferenceActivity extends ListActivity implements } } } + } + + /** + * Called to determine if the activity should run in multi-pane mode. + * The default implementation returns true if the screen is large + * enough. + */ + public boolean onIsMultiPane() { + Configuration config = getResources().getConfiguration(); + if ((config.screenLayout&Configuration.SCREENLAYOUT_SIZE_MASK) + == Configuration.SCREENLAYOUT_SIZE_XLARGE + && config.orientation == Configuration.ORIENTATION_LANDSCAPE) { + return true; + } + return false; + } + + /** + * Called to determine whether the header list should be hidden. The + * default implementation hides the list if the activity is being re-launched + * when not in multi-pane mode. + */ + public boolean onIsHidingHeaders() { + return getIntent().getBooleanExtra(EXTRA_PREFS_NO_HEADERS, false); + } + + /** + * Called to determine the initial fragment to be shown. The default + * implementation simply returns the fragment of the first header. + */ + public String onGetInitialFragment() { + return mHeaders.get(0).fragment; + } + + /** + * Called when the activity needs its list of headers build. By + * implementing this and adding at least one item to the list, you + * will cause the activity to run in its modern fragment mode. Note + * that this function may not always be called; for example, if the + * activity has been asked to display a particular fragment without + * the header list, there is no need to build the headers. + * + *

    Typical implementations will use {@link #loadHeadersFromResource} + * to fill in the list from a resource. + * + * @param target The list in which to place the headers. + */ + public void onBuildHeaders(List

    target) { + } + + /** + * Parse the given XML file as a header description, adding each + * parsed Header into the target list. + * + * @param resid The XML resource to load and parse. + * @param target The list in which the parsed headers should be placed. + */ + public void loadHeadersFromResource(int resid, List
    target) { + XmlResourceParser parser = null; + try { + parser = getResources().getXml(resid); + AttributeSet attrs = Xml.asAttributeSet(parser); + + int type; + while ((type=parser.next()) != XmlPullParser.END_DOCUMENT + && type != XmlPullParser.START_TAG) { + } + + String nodeName = parser.getName(); + if (!"PreferenceHeaders".equals(nodeName)) { + throw new RuntimeException( + "XML document must start with tag; found" + + nodeName + " at " + parser.getPositionDescription()); + } + + int outerDepth = parser.getDepth(); + while ((type=parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + nodeName = parser.getName(); + if ("Header".equals(nodeName)) { + Header header = new Header(); + + TypedArray sa = getResources().obtainAttributes(attrs, + com.android.internal.R.styleable.PreferenceHeader); + header.title = sa.getText( + com.android.internal.R.styleable.PreferenceHeader_title); + header.summary = sa.getText( + com.android.internal.R.styleable.PreferenceHeader_summary); + header.iconRes = sa.getResourceId( + com.android.internal.R.styleable.PreferenceHeader_icon, 0); + header.fragment = sa.getString( + com.android.internal.R.styleable.PreferenceHeader_fragment); + sa.recycle(); + + target.add(header); + + XmlUtils.skipCurrentTag(parser); + } else { + XmlUtils.skipCurrentTag(parser); + } + } + + } catch (XmlPullParserException e) { + throw new RuntimeException("Error parsing headers", e); + } catch (IOException e) { + throw new RuntimeException("Error parsing headers", e); + } finally { + if (parser != null) parser.close(); + } - mPreferenceManager = onCreatePreferenceManager(); - getListView().setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY); } @Override protected void onStop() { super.onStop(); - mPreferenceManager.dispatchActivityStop(); + if (mPreferenceManager != null) { + mPreferenceManager.dispatchActivityStop(); + } } @Override protected void onDestroy() { super.onDestroy(); - mPreferenceManager.dispatchActivityDestroy(); + + if (mPreferenceManager != null) { + mPreferenceManager.dispatchActivityDestroy(); + } } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); - final PreferenceScreen preferenceScreen = getPreferenceScreen(); - if (preferenceScreen != null) { - Bundle container = new Bundle(); - preferenceScreen.saveHierarchyState(container); - outState.putBundle(PREFERENCES_TAG, container); + if (mPreferenceManager != null) { + final PreferenceScreen preferenceScreen = getPreferenceScreen(); + if (preferenceScreen != null) { + Bundle container = new Bundle(); + preferenceScreen.saveHierarchyState(container); + outState.putBundle(PREFERENCES_TAG, container); + } } } @Override protected void onRestoreInstanceState(Bundle state) { - Bundle container = state.getBundle(PREFERENCES_TAG); - if (container != null) { - final PreferenceScreen preferenceScreen = getPreferenceScreen(); - if (preferenceScreen != null) { - preferenceScreen.restoreHierarchyState(container); - mSavedInstanceState = state; - return; + if (mPreferenceManager != null) { + Bundle container = state.getBundle(PREFERENCES_TAG); + if (container != null) { + final PreferenceScreen preferenceScreen = getPreferenceScreen(); + if (preferenceScreen != null) { + preferenceScreen.restoreHierarchyState(container); + mSavedInstanceState = state; + return; + } } } @@ -214,13 +489,76 @@ public abstract class PreferenceActivity extends ListActivity implements protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); - mPreferenceManager.dispatchActivityResult(requestCode, resultCode, data); + if (mPreferenceManager != null) { + mPreferenceManager.dispatchActivityResult(requestCode, resultCode, data); + } } @Override public void onContentChanged() { super.onContentChanged(); - postBindPreferences(); + + if (mPreferenceManager != null) { + postBindPreferences(); + } + } + + @Override + protected void onListItemClick(ListView l, View v, int position, long id) { + super.onListItemClick(l, v, position, id); + + if (mAdapter != null) { + onHeaderClick(mHeaders.get(position), position); + } + } + + /** + * Called when the user selects an item in the header list. The default + * implementation will call either {@link #startWithFragment(String)} + * or {@link #switchToHeader(String)} as appropriate. + * + * @param header The header that was selected. + * @param position The header's position in the list. + */ + public void onHeaderClick(Header header, int position) { + if (mSinglePane) { + startWithFragment(header.fragment); + } else { + switchToHeader(header.fragment); + } + } + + /** + * Start a new instance of this activity, showing only the given + * preference fragment. When launched in this mode, the header list + * will be hidden and the given preference fragment will be instantiated + * and fill the entire activity. + * + * @param fragmentName The name of the fragment to display. + */ + public void startWithFragment(String fragmentName) { + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setClass(this, getClass()); + intent.putExtra(EXTRA_PREFS_SHOW_FRAGMENT, fragmentName); + intent.putExtra(EXTRA_PREFS_NO_HEADERS, true); + startActivity(intent); + } + + /** + * When in two-pane mode, switch the fragment pane to show the given + * preference fragment. + * + * @param fragmentName The name of the fragment to display. + */ + public void switchToHeader(String fragmentName) { + Fragment f; + try { + f = Fragment.instantiate(this, fragmentName); + } catch (Exception e) { + Log.w(TAG, "Failure instantiating fragment " + fragmentName, e); + return; + } + openFragmentTransaction().replace(com.android.internal.R.id.prefs, f).commit(); } /** @@ -246,27 +584,24 @@ public abstract class PreferenceActivity extends ListActivity implements } /** - * Creates the {@link PreferenceManager}. - * - * @return The {@link PreferenceManager} used by this activity. - */ - private PreferenceManager onCreatePreferenceManager() { - PreferenceManager preferenceManager = new PreferenceManager(this, FIRST_REQUEST_CODE); - preferenceManager.setOnPreferenceTreeClickListener(this); - return preferenceManager; - } - - /** * Returns the {@link PreferenceManager} used by this activity. * @return The {@link PreferenceManager}. + * + * @deprecated This function is not relevant for a modern fragment-based + * PreferenceActivity. */ + @Deprecated public PreferenceManager getPreferenceManager() { return mPreferenceManager; } private void requirePreferenceManager() { if (mPreferenceManager == null) { - throw new RuntimeException("This should be called after super.onCreate."); + if (mAdapter == null) { + throw new RuntimeException("This should be called after super.onCreate."); + } + throw new RuntimeException( + "Modern two-pane PreferenceActivity requires use of a PreferenceFragment"); } } @@ -274,8 +609,14 @@ public abstract class PreferenceActivity extends ListActivity implements * Sets the root of the preference hierarchy that this activity is showing. * * @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy. + * + * @deprecated This function is not relevant for a modern fragment-based + * PreferenceActivity. */ + @Deprecated public void setPreferenceScreen(PreferenceScreen preferenceScreen) { + requirePreferenceManager(); + if (mPreferenceManager.setPreferences(preferenceScreen) && preferenceScreen != null) { postBindPreferences(); CharSequence title = getPreferenceScreen().getTitle(); @@ -291,16 +632,27 @@ public abstract class PreferenceActivity extends ListActivity implements * * @return The {@link PreferenceScreen} that is the root of the preference * hierarchy. + * + * @deprecated This function is not relevant for a modern fragment-based + * PreferenceActivity. */ + @Deprecated public PreferenceScreen getPreferenceScreen() { - return mPreferenceManager.getPreferenceScreen(); + if (mPreferenceManager != null) { + return mPreferenceManager.getPreferenceScreen(); + } + return null; } /** * Adds preferences from activities that match the given {@link Intent}. * * @param intent The {@link Intent} to query activities. + * + * @deprecated This function is not relevant for a modern fragment-based + * PreferenceActivity. */ + @Deprecated public void addPreferencesFromIntent(Intent intent) { requirePreferenceManager(); @@ -312,7 +664,11 @@ public abstract class PreferenceActivity extends ListActivity implements * preference hierarchy. * * @param preferencesResId The XML resource ID to inflate. + * + * @deprecated This function is not relevant for a modern fragment-based + * PreferenceActivity. */ + @Deprecated public void addPreferencesFromResource(int preferencesResId) { requirePreferenceManager(); @@ -322,7 +678,11 @@ public abstract class PreferenceActivity extends ListActivity implements /** * {@inheritDoc} + * + * @deprecated This function is not relevant for a modern fragment-based + * PreferenceActivity. */ + @Deprecated public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { return false; } @@ -333,7 +693,11 @@ public abstract class PreferenceActivity extends ListActivity implements * @param key The key of the preference to retrieve. * @return The {@link Preference} with the key, or null. * @see PreferenceGroup#findPreference(CharSequence) + * + * @deprecated This function is not relevant for a modern fragment-based + * PreferenceActivity. */ + @Deprecated public Preference findPreference(CharSequence key) { if (mPreferenceManager == null) { diff --git a/core/java/android/preference/PreferenceFragment.java b/core/java/android/preference/PreferenceFragment.java index f85bc9e..ac61574 100644 --- a/core/java/android/preference/PreferenceFragment.java +++ b/core/java/android/preference/PreferenceFragment.java @@ -37,6 +37,17 @@ import android.widget.ListView; * {@link PreferenceManager#getDefaultSharedPreferences(android.content.Context)} * with a context in the same package as this fragment. *

    + * Furthermore, the preferences shown will follow the visual style of system + * preferences. It is easy to create a hierarchy of preferences (that can be + * shown on multiple screens) via XML. For these reasons, it is recommended to + * use this fragment (as a superclass) to deal with preferences in applications. + *

    + * A {@link PreferenceScreen} object should be at the top of the preference + * hierarchy. Furthermore, subsequent {@link PreferenceScreen} in the hierarchy + * denote a screen break--that is the preferences contained within subsequent + * {@link PreferenceScreen} should be shown on another screen. The preference + * framework handles showing these other screens from the preference hierarchy. + *

    * The preference hierarchy can be formed in multiple ways: *

  • From an XML file specifying the hierarchy *
  • From different {@link Activity Activities} that each specify its own @@ -61,24 +72,21 @@ import android.widget.ListView; * As a convenience, this fragment implements a click listener for any * preference in the current hierarchy, see * {@link #onPreferenceTreeClick(PreferenceScreen, Preference)}. - *

    - * See {@link PreferenceActivity} for more details. * * *

    Sample Code

    * - *

    The following sample code shows the use if a PreferenceFragment to - * embed preferences in a larger activity and switch between them. The content - * layout of the activity is:

    + *

    The following sample code shows a simple preference fragment that is + * populated from a resource. The resource it loads is:

    * - * {@sample development/samples/ApiDemos/res/layout/fragment_preferences.xml layout} + * {@sample development/samples/ApiDemos/res/xml/preferences.xml preferences} * - *

    The code using this layout consists of an activity and three fragments. - * One of the fragments is a list of categories the user can select; the other - * two are the different preference options for the categories.

    + *

    The fragment implementation itself simply populates the preferences + * when created. Note that the preferences framework takes care of loading + * the current values out of the app preferences and writing them when changed:

    * - * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentPreferences.java - * activity} + * {@sample development/samples/ApiDemos/src/com/example/android/apis/preference/FragmentPreferences.java + * fragment} * * @see Preference * @see PreferenceScreen -- cgit v1.1