PreferenceFragmentCompat.java revision 6904f67c96a28a0e5966b4fb6d37a0ad5f136858
1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
15 */
16
17package android.support.v7.preference;
18
19import android.content.Context;
20import android.content.res.TypedArray;
21import android.os.Bundle;
22import android.os.Handler;
23import android.os.Message;
24import android.support.annotation.Nullable;
25import android.support.annotation.XmlRes;
26import android.support.v4.app.DialogFragment;
27import android.support.v4.app.Fragment;
28import android.support.v7.widget.LinearLayoutManager;
29import android.support.v7.widget.RecyclerView;
30import android.util.TypedValue;
31import android.view.ContextThemeWrapper;
32import android.view.LayoutInflater;
33import android.view.View;
34import android.view.ViewGroup;
35
36/**
37 * Shows a hierarchy of {@link Preference} objects as
38 * lists. These preferences will
39 * automatically save to {@link android.content.SharedPreferences} as the user interacts with
40 * them. To retrieve an instance of {@link android.content.SharedPreferences} that the
41 * preference hierarchy in this fragment will use, call
42 * {@link PreferenceManager#getDefaultSharedPreferences(android.content.Context)}
43 * with a context in the same package as this fragment.
44 * <p>
45 * Furthermore, the preferences shown will follow the visual style of system
46 * preferences. It is easy to create a hierarchy of preferences (that can be
47 * shown on multiple screens) via XML. For these reasons, it is recommended to
48 * use this fragment (as a superclass) to deal with preferences in applications.
49 * <p>
50 * A {@link PreferenceScreen} object should be at the top of the preference
51 * hierarchy. Furthermore, subsequent {@link PreferenceScreen} in the hierarchy
52 * denote a screen break--that is the preferences contained within subsequent
53 * {@link PreferenceScreen} should be shown on another screen. The preference
54 * framework handles showing these other screens from the preference hierarchy.
55 * <p>
56 * The preference hierarchy can be formed in multiple ways:
57 * <li> From an XML file specifying the hierarchy
58 * <li> From different {@link android.app.Activity Activities} that each specify its own
59 * preferences in an XML file via {@link android.app.Activity} meta-data
60 * <li> From an object hierarchy rooted with {@link PreferenceScreen}
61 * <p>
62 * To inflate from XML, use the {@link #addPreferencesFromResource(int)}. The
63 * root element should be a {@link PreferenceScreen}. Subsequent elements can point
64 * to actual {@link Preference} subclasses. As mentioned above, subsequent
65 * {@link PreferenceScreen} in the hierarchy will result in the screen break.
66 * <p>
67 * To specify an object hierarchy rooted with {@link PreferenceScreen}, use
68 * {@link #setPreferenceScreen(PreferenceScreen)}.
69 * <p>
70 * As a convenience, this fragment implements a click listener for any
71 * preference in the current hierarchy, see
72 * {@link #onPreferenceTreeClick(Preference)}.
73 *
74 * <div class="special reference">
75 * <h3>Developer Guides</h3>
76 * <p>For information about using {@code PreferenceFragment},
77 * read the <a href="{@docRoot}guide/topics/ui/settings.html">Settings</a>
78 * guide.</p>
79 * </div>
80 *
81 * <a name="SampleCode"></a>
82 * <h3>Sample Code</h3>
83 *
84 * <p>The following sample code shows a simple preference fragment that is
85 * populated from a resource.  The resource it loads is:</p>
86 *
87 * {@sample development/samples/ApiDemos/res/xml/preferences.xml preferences}
88 *
89 * <p>The fragment implementation itself simply populates the preferences
90 * when created.  Note that the preferences framework takes care of loading
91 * the current values out of the app preferences and writing them when changed:</p>
92 *
93 * {@sample development/samples/ApiDemos/src/com/example/android/apis/preference/FragmentPreferences.java
94 *      fragment}
95 *
96 * @see Preference
97 * @see PreferenceScreen
98 */
99public abstract class PreferenceFragmentCompat extends Fragment implements
100        PreferenceManager.OnPreferenceTreeClickListener,
101        PreferenceManager.OnDisplayPreferenceDialogListener,
102        PreferenceManager.OnNavigateToScreenListener,
103        DialogPreference.TargetFragment {
104
105    /**
106     * Fragment argument used to specify the tag of the desired root
107     * {@link android.support.v7.preference.PreferenceScreen} object.
108     */
109    public static final String ARG_PREFERENCE_ROOT =
110            "android.support.v7.preference.PreferenceFragmentCompat.PREFERENCE_ROOT";
111
112    private static final String PREFERENCES_TAG = "android:preferences";
113
114    private static final String DIALOG_FRAGMENT_TAG =
115            "android.support.v7.preference.PreferenceFragment.DIALOG";
116
117    private PreferenceManager mPreferenceManager;
118    private RecyclerView mList;
119    private boolean mHavePrefs;
120    private boolean mInitDone;
121
122    private Context mStyledContext;
123
124    private int mLayoutResId = R.layout.preference_list_fragment;
125
126    /**
127     * The starting request code given out to preference framework.
128     */
129    private static final int FIRST_REQUEST_CODE = 100;
130
131    private static final int MSG_BIND_PREFERENCES = 1;
132    private Handler mHandler = new Handler() {
133        @Override
134        public void handleMessage(Message msg) {
135            switch (msg.what) {
136
137                case MSG_BIND_PREFERENCES:
138                    bindPreferences();
139                    break;
140            }
141        }
142    };
143
144    final private Runnable mRequestFocus = new Runnable() {
145        public void run() {
146            mList.focusableViewAvailable(mList);
147        }
148    };
149
150    /**
151     * Interface that PreferenceFragment's containing activity should
152     * implement to be able to process preference items that wish to
153     * switch to a specified fragment.
154     */
155    public interface OnPreferenceStartFragmentCallback {
156        /**
157         * Called when the user has clicked on a Preference that has
158         * a fragment class name associated with it.  The implementation
159         * should instantiate and switch to an instance of the given
160         * fragment.
161         */
162        boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref);
163    }
164
165    /**
166     * Interface that PreferenceFragment's containing activity should
167     * implement to be able to process preference items that wish to
168     * switch to a new screen of preferences.
169     */
170    public interface OnPreferenceStartScreenCallback {
171        /**
172         * Called when the user has clicked on a PreferenceScreen item in order to navigate to a new
173         * screen of preferences.
174         * @param caller The fragment requesting navigation.
175         * @param pref The preference screen to navigate to.
176         */
177        boolean onPreferenceStartScreen(PreferenceFragmentCompat caller, PreferenceScreen pref);
178    }
179
180    @Override
181    public void onCreate(Bundle savedInstanceState) {
182        super.onCreate(savedInstanceState);
183        final TypedValue tv = new TypedValue();
184        getActivity().getTheme().resolveAttribute(R.attr.preferenceTheme, tv, true);
185        final int theme = tv.resourceId;
186        if (theme <= 0) {
187            throw new IllegalStateException("Must specify preferenceTheme in theme");
188        }
189        mStyledContext = new ContextThemeWrapper(getActivity(), theme);
190
191        mPreferenceManager = new PreferenceManager(mStyledContext);
192        mPreferenceManager.setOnNavigateToScreenListener(this);
193        final Bundle args = getArguments();
194        final String rootKey;
195        if (args != null) {
196            rootKey = getArguments().getString(ARG_PREFERENCE_ROOT);
197        } else {
198            rootKey = null;
199        }
200        onCreatePreferences(savedInstanceState, rootKey);
201    }
202
203    /**
204     * Called during {@link #onCreate(Bundle)} to supply the preferences for this fragment.
205     * Subclasses are expected to call {@link #setPreferenceScreen(PreferenceScreen)} either
206     * directly or via helper methods such as {@link #addPreferencesFromResource(int)}.
207     *
208     * @param savedInstanceState If the fragment is being re-created from
209     *                           a previous saved state, this is the state.
210     * @param rootKey If non-null, this preference fragment should be rooted at the
211     *                {@link android.support.v7.preference.PreferenceScreen} with this key.
212     */
213    public abstract void onCreatePreferences(Bundle savedInstanceState, String rootKey);
214
215    @Override
216    public View onCreateView(LayoutInflater inflater, ViewGroup container,
217            Bundle savedInstanceState) {
218
219        TypedArray a = mStyledContext.obtainStyledAttributes(null,
220                R.styleable.PreferenceFragmentCompat,
221                R.attr.preferenceFragmentStyle,
222                0);
223
224        mLayoutResId = a.getResourceId(R.styleable.PreferenceFragmentCompat_layout,
225                mLayoutResId);
226
227        a.recycle();
228
229        final View view = inflater.inflate(mLayoutResId, container, false);
230
231        final View rawListContainer = view.findViewById(R.id.list_container);
232        if (!(rawListContainer instanceof ViewGroup)) {
233            throw new RuntimeException("Content has view with id attribute 'R.id.list_container' "
234                    + "that is not a ViewGroup class");
235        }
236
237        final ViewGroup listContainer = (ViewGroup) rawListContainer;
238
239        final RecyclerView listView = onCreateRecyclerView(inflater, listContainer,
240                savedInstanceState);
241        if (listView == null) {
242            throw new RuntimeException("Could not create RecyclerView");
243        }
244
245        mList = listView;
246        listContainer.addView(mList);
247        mHandler.post(mRequestFocus);
248        return view;
249    }
250
251    @Override
252    public void onActivityCreated(Bundle savedInstanceState) {
253        super.onActivityCreated(savedInstanceState);
254
255        if (mHavePrefs) {
256            bindPreferences();
257        }
258
259        mInitDone = true;
260
261        if (savedInstanceState != null) {
262            Bundle container = savedInstanceState.getBundle(PREFERENCES_TAG);
263            if (container != null) {
264                final PreferenceScreen preferenceScreen = getPreferenceScreen();
265                if (preferenceScreen != null) {
266                    preferenceScreen.restoreHierarchyState(container);
267                }
268            }
269        }
270    }
271
272    @Override
273    public void onStart() {
274        super.onStart();
275        mPreferenceManager.setOnPreferenceTreeClickListener(this);
276        mPreferenceManager.setOnDisplayPreferenceDialogListener(this);
277    }
278
279    @Override
280    public void onStop() {
281        super.onStop();
282        mPreferenceManager.setOnPreferenceTreeClickListener(null);
283        mPreferenceManager.setOnDisplayPreferenceDialogListener(null);
284    }
285
286    @Override
287    public void onDestroyView() {
288        mList = null;
289        mHandler.removeCallbacks(mRequestFocus);
290        mHandler.removeMessages(MSG_BIND_PREFERENCES);
291        super.onDestroyView();
292    }
293
294    @Override
295    public void onSaveInstanceState(Bundle outState) {
296        super.onSaveInstanceState(outState);
297
298        final PreferenceScreen preferenceScreen = getPreferenceScreen();
299        if (preferenceScreen != null) {
300            Bundle container = new Bundle();
301            preferenceScreen.saveHierarchyState(container);
302            outState.putBundle(PREFERENCES_TAG, container);
303        }
304    }
305
306    /**
307     * Returns the {@link PreferenceManager} used by this fragment.
308     * @return The {@link PreferenceManager}.
309     */
310    public PreferenceManager getPreferenceManager() {
311        return mPreferenceManager;
312    }
313
314    /**
315     * Sets the root of the preference hierarchy that this fragment is showing.
316     *
317     * @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy.
318     */
319    public void setPreferenceScreen(PreferenceScreen preferenceScreen) {
320        if (mPreferenceManager.setPreferences(preferenceScreen) && preferenceScreen != null) {
321            onUnbindPreferences();
322            mHavePrefs = true;
323            if (mInitDone) {
324                postBindPreferences();
325            }
326        }
327    }
328
329    /**
330     * Gets the root of the preference hierarchy that this fragment is showing.
331     *
332     * @return The {@link PreferenceScreen} that is the root of the preference
333     *         hierarchy.
334     */
335    public PreferenceScreen getPreferenceScreen() {
336        return mPreferenceManager.getPreferenceScreen();
337    }
338
339    /**
340     * Inflates the given XML resource and adds the preference hierarchy to the current
341     * preference hierarchy.
342     *
343     * @param preferencesResId The XML resource ID to inflate.
344     */
345    public void addPreferencesFromResource(@XmlRes int preferencesResId) {
346        requirePreferenceManager();
347
348        setPreferenceScreen(mPreferenceManager.inflateFromResource(mStyledContext,
349                preferencesResId, getPreferenceScreen()));
350    }
351
352    /**
353     * Inflates the given XML resource and replaces the current preference hierarchy (if any) with
354     * the preference hierarchy rooted at {@code key}.
355     *
356     * @param preferencesResId The XML resource ID to inflate.
357     * @param key The preference key of the {@link android.support.v7.preference.PreferenceScreen}
358     *            to use as the root of the preference hierarchy, or null to use the root
359     *            {@link android.support.v7.preference.PreferenceScreen}.
360     */
361    public void setPreferencesFromResource(@XmlRes int preferencesResId, @Nullable String key) {
362        requirePreferenceManager();
363
364        final PreferenceScreen xmlRoot = mPreferenceManager.inflateFromResource(mStyledContext,
365                preferencesResId, null);
366
367        final Preference root;
368        if (key != null) {
369            root = xmlRoot.findPreference(key);
370            if (!(root instanceof PreferenceScreen)) {
371                throw new IllegalArgumentException("Preference object with key " + key
372                        + " is not a PreferenceScreen");
373            }
374        } else {
375            root = xmlRoot;
376        }
377
378        setPreferenceScreen((PreferenceScreen) root);
379    }
380
381    /**
382     * {@inheritDoc}
383     */
384    public boolean onPreferenceTreeClick(Preference preference) {
385        if (preference.getFragment() != null &&
386                getActivity() instanceof OnPreferenceStartFragmentCallback) {
387            return ((OnPreferenceStartFragmentCallback)getActivity()).onPreferenceStartFragment(
388                    this, preference);
389        }
390        return false;
391    }
392
393    /**
394     * Called by
395     * {@link android.support.v7.preference.PreferenceScreen#onClick()} in order to navigate to a
396     * new screen of preferences. Calls
397     * {@link PreferenceFragmentCompat.OnPreferenceStartScreenCallback#onPreferenceStartScreen}
398     * if the containing activity implements
399     * {@link PreferenceFragmentCompat.OnPreferenceStartScreenCallback}.
400     * @param preferenceScreen The {@link android.support.v7.preference.PreferenceScreen} to
401     *                         navigate to.
402     */
403    @Override
404    public void onNavigateToScreen(PreferenceScreen preferenceScreen) {
405        if (getActivity() instanceof OnPreferenceStartScreenCallback) {
406            ((OnPreferenceStartScreenCallback)getActivity()).onPreferenceStartScreen(this,
407                    preferenceScreen);
408        }
409    }
410
411    /**
412     * Finds a {@link Preference} based on its key.
413     *
414     * @param key The key of the preference to retrieve.
415     * @return The {@link Preference} with the key, or null.
416     * @see android.support.v7.preference.PreferenceGroup#findPreference(CharSequence)
417     */
418    public Preference findPreference(CharSequence key) {
419        if (mPreferenceManager == null) {
420            return null;
421        }
422        return mPreferenceManager.findPreference(key);
423    }
424
425    private void requirePreferenceManager() {
426        if (mPreferenceManager == null) {
427            throw new RuntimeException("This should be called after super.onCreate.");
428        }
429    }
430
431    private void postBindPreferences() {
432        if (mHandler.hasMessages(MSG_BIND_PREFERENCES)) return;
433        mHandler.obtainMessage(MSG_BIND_PREFERENCES).sendToTarget();
434    }
435
436    private void bindPreferences() {
437        final PreferenceScreen preferenceScreen = getPreferenceScreen();
438        if (preferenceScreen != null) {
439            getListView().setAdapter(preferenceScreen.getAdapter());
440        }
441        onBindPreferences();
442    }
443
444    /** @hide */
445    protected void onBindPreferences() {
446    }
447
448    /** @hide */
449    protected void onUnbindPreferences() {
450    }
451
452    public final RecyclerView getListView() {
453        return mList;
454    }
455
456    /**
457     * Creates the {@link android.support.v7.widget.RecyclerView} used to display the preferences.
458     * Subclasses may override this to return a customized
459     * {@link android.support.v7.widget.RecyclerView}.
460     * @param inflater The LayoutInflater object that can be used to inflate the
461     *                 {@link android.support.v7.widget.RecyclerView}.
462     * @param parent The parent {@link android.view.View} that the RecyclerView will be attached to.
463     *               This method should not add the view itself, but this can be used to generate
464     *               the LayoutParams of the view.
465     * @param savedInstanceState If non-null, this view is being re-constructed from a previous
466     *                           saved state as given here
467     * @return A new RecyclerView object to be placed into the view hierarchy
468     */
469    public RecyclerView onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent,
470            Bundle savedInstanceState) {
471        RecyclerView recyclerView = (RecyclerView) inflater
472                .inflate(R.layout.preference_recyclerview, parent, false);
473
474        recyclerView.setLayoutManager(onCreateLayoutManager());
475
476        return recyclerView;
477    }
478
479    /**
480     * Called from {@link #onCreateRecyclerView} to create the
481     * {@link android.support.v7.widget.RecyclerView.LayoutManager} for the created
482     * {@link android.support.v7.widget.RecyclerView}.
483     * @return A new {@link android.support.v7.widget.RecyclerView.LayoutManager} instance.
484     */
485    public RecyclerView.LayoutManager onCreateLayoutManager() {
486        return new LinearLayoutManager(getActivity());
487    }
488
489    /**
490     * Called when a preference in the tree requests to display a dialog. Subclasses should
491     * override this method to display custom dialogs or to handle dialogs for custom preference
492     * classes.
493     *
494     * @param preference The Preference object requesting the dialog.
495     */
496    @Override
497    public void onDisplayPreferenceDialog(Preference preference) {
498        // check if dialog is already showing
499        if (getFragmentManager().findFragmentByTag(DIALOG_FRAGMENT_TAG) != null) {
500            return;
501        }
502
503        final DialogFragment f;
504        if (preference instanceof EditTextPreference) {
505            f = EditTextPreferenceDialogFragmentCompat.newInstance(preference.getKey());
506        } else if (preference instanceof ListPreference) {
507            f = ListPreferenceDialogFragmentCompat.newInstance(preference.getKey());
508        } else {
509            throw new IllegalArgumentException("Tried to display dialog for unknown " +
510                    "preference type. Did you forget to override onDisplayPreferenceDialog()?");
511        }
512        f.setTargetFragment(this, 0);
513        f.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
514    }
515
516}
517