PreferenceFragmentCompat.java revision dc8e099ed130434c0238f558277c5bcb6a533121
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.graphics.Canvas;
22import android.graphics.Rect;
23import android.graphics.drawable.Drawable;
24import android.os.Bundle;
25import android.os.Handler;
26import android.os.Message;
27import android.support.annotation.Nullable;
28import android.support.annotation.XmlRes;
29import android.support.v4.app.DialogFragment;
30import android.support.v4.app.Fragment;
31import android.support.v4.view.ViewCompat;
32import android.support.v7.widget.LinearLayoutManager;
33import android.support.v7.widget.RecyclerView;
34import android.util.TypedValue;
35import android.view.ContextThemeWrapper;
36import android.view.LayoutInflater;
37import android.view.View;
38import android.view.ViewGroup;
39
40/**
41 * Shows a hierarchy of {@link Preference} objects as
42 * lists. These preferences will
43 * automatically save to {@link android.content.SharedPreferences} as the user interacts with
44 * them. To retrieve an instance of {@link android.content.SharedPreferences} that the
45 * preference hierarchy in this fragment will use, call
46 * {@link PreferenceManager#getDefaultSharedPreferences(android.content.Context)}
47 * with a context in the same package as this fragment.
48 * <p>
49 * Furthermore, the preferences shown will follow the visual style of system
50 * preferences. It is easy to create a hierarchy of preferences (that can be
51 * shown on multiple screens) via XML. For these reasons, it is recommended to
52 * use this fragment (as a superclass) to deal with preferences in applications.
53 * <p>
54 * A {@link PreferenceScreen} object should be at the top of the preference
55 * hierarchy. Furthermore, subsequent {@link PreferenceScreen} in the hierarchy
56 * denote a screen break--that is the preferences contained within subsequent
57 * {@link PreferenceScreen} should be shown on another screen. The preference
58 * framework handles this by calling {@link #onNavigateToScreen(PreferenceScreen)}.
59 * <p>
60 * The preference hierarchy can be formed in multiple ways:
61 * <li> From an XML file specifying the hierarchy
62 * <li> From different {@link android.app.Activity Activities} that each specify its own
63 * preferences in an XML file via {@link android.app.Activity} meta-data
64 * <li> From an object hierarchy rooted with {@link PreferenceScreen}
65 * <p>
66 * To inflate from XML, use the {@link #addPreferencesFromResource(int)}. The
67 * root element should be a {@link PreferenceScreen}. Subsequent elements can point
68 * to actual {@link Preference} subclasses. As mentioned above, subsequent
69 * {@link PreferenceScreen} in the hierarchy will result in the screen break.
70 * <p>
71 * To specify an object hierarchy rooted with {@link PreferenceScreen}, use
72 * {@link #setPreferenceScreen(PreferenceScreen)}.
73 * <p>
74 * As a convenience, this fragment implements a click listener for any
75 * preference in the current hierarchy, see
76 * {@link #onPreferenceTreeClick(Preference)}.
77 *
78 * <div class="special reference">
79 * <h3>Developer Guides</h3>
80 * <p>For information about using {@code PreferenceFragment},
81 * read the <a href="{@docRoot}guide/topics/ui/settings.html">Settings</a>
82 * guide.</p>
83 * </div>
84 *
85 * <a name="SampleCode"></a>
86 * <h3>Sample Code</h3>
87 *
88 * <p>The following sample code shows a simple preference fragment that is
89 * populated from a resource.  The resource it loads is:</p>
90 *
91 * {@sample development/samples/ApiDemos/res/xml/preferences.xml preferences}
92 *
93 * <p>The fragment implementation itself simply populates the preferences
94 * when created.  Note that the preferences framework takes care of loading
95 * the current values out of the app preferences and writing them when changed:</p>
96 *
97 * {@sample development/samples/ApiDemos/src/com/example/android/apis/preference/FragmentPreferences.java
98 *      fragment}
99 *
100 * @see Preference
101 * @see PreferenceScreen
102 */
103public abstract class PreferenceFragmentCompat extends Fragment implements
104        PreferenceManager.OnPreferenceTreeClickListener,
105        PreferenceManager.OnDisplayPreferenceDialogListener,
106        PreferenceManager.OnNavigateToScreenListener,
107        DialogPreference.TargetFragment {
108
109    /**
110     * Fragment argument used to specify the tag of the desired root
111     * {@link android.support.v7.preference.PreferenceScreen} object.
112     */
113    public static final String ARG_PREFERENCE_ROOT =
114            "android.support.v7.preference.PreferenceFragmentCompat.PREFERENCE_ROOT";
115
116    private static final String PREFERENCES_TAG = "android:preferences";
117
118    private static final String DIALOG_FRAGMENT_TAG =
119            "android.support.v7.preference.PreferenceFragment.DIALOG";
120
121    private PreferenceManager mPreferenceManager;
122    private RecyclerView mList;
123    private boolean mHavePrefs;
124    private boolean mInitDone;
125
126    private Context mStyledContext;
127
128    private int mLayoutResId = R.layout.preference_list_fragment;
129
130    private final DividerDecoration mDividerDecoration = new DividerDecoration();
131
132    private static final int MSG_BIND_PREFERENCES = 1;
133    private Handler mHandler = new Handler() {
134        @Override
135        public void handleMessage(Message msg) {
136            switch (msg.what) {
137
138                case MSG_BIND_PREFERENCES:
139                    bindPreferences();
140                    break;
141            }
142        }
143    };
144
145    final private Runnable mRequestFocus = new Runnable() {
146        @Override
147        public void run() {
148            mList.focusableViewAvailable(mList);
149        }
150    };
151
152    private Runnable mSelectPreferenceRunnable;
153
154    /**
155     * Interface that PreferenceFragment's containing activity should
156     * implement to be able to process preference items that wish to
157     * switch to a specified fragment.
158     */
159    public interface OnPreferenceStartFragmentCallback {
160        /**
161         * Called when the user has clicked on a Preference that has
162         * a fragment class name associated with it.  The implementation
163         * should instantiate and switch to an instance of the given
164         * fragment.
165         * @param caller The fragment requesting navigation.
166         * @param pref The preference requesting the fragment.
167         * @return true if the fragment creation has been handled
168         */
169        boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref);
170    }
171
172    /**
173     * Interface that PreferenceFragment's containing activity should
174     * implement to be able to process preference items that wish to
175     * switch to a new screen of preferences.
176     */
177    public interface OnPreferenceStartScreenCallback {
178        /**
179         * Called when the user has clicked on a PreferenceScreen item in order to navigate to a new
180         * screen of preferences.
181         * @param caller The fragment requesting navigation.
182         * @param pref The preference screen to navigate to.
183         * @return true if the screen navigation has been handled
184         */
185        boolean onPreferenceStartScreen(PreferenceFragmentCompat caller, PreferenceScreen pref);
186    }
187
188    public interface OnPreferenceDisplayDialogCallback {
189
190        /**
191         *
192         * @param caller The fragment containing the preference requesting the dialog.
193         * @param pref The preference requesting the dialog.
194         * @return true if the dialog creation has been handled.
195         */
196        boolean onPreferenceDisplayDialog(PreferenceFragmentCompat caller, Preference pref);
197    }
198
199    @Override
200    public void onCreate(Bundle savedInstanceState) {
201        super.onCreate(savedInstanceState);
202        final TypedValue tv = new TypedValue();
203        getActivity().getTheme().resolveAttribute(R.attr.preferenceTheme, tv, true);
204        final int theme = tv.resourceId;
205        if (theme <= 0) {
206            throw new IllegalStateException("Must specify preferenceTheme in theme");
207        }
208        mStyledContext = new ContextThemeWrapper(getActivity(), theme);
209
210        mPreferenceManager = new PreferenceManager(mStyledContext);
211        mPreferenceManager.setOnNavigateToScreenListener(this);
212        final Bundle args = getArguments();
213        final String rootKey;
214        if (args != null) {
215            rootKey = getArguments().getString(ARG_PREFERENCE_ROOT);
216        } else {
217            rootKey = null;
218        }
219        onCreatePreferences(savedInstanceState, rootKey);
220    }
221
222    /**
223     * Called during {@link #onCreate(Bundle)} to supply the preferences for this fragment.
224     * Subclasses are expected to call {@link #setPreferenceScreen(PreferenceScreen)} either
225     * directly or via helper methods such as {@link #addPreferencesFromResource(int)}.
226     *
227     * @param savedInstanceState If the fragment is being re-created from
228     *                           a previous saved state, this is the state.
229     * @param rootKey If non-null, this preference fragment should be rooted at the
230     *                {@link android.support.v7.preference.PreferenceScreen} with this key.
231     */
232    public abstract void onCreatePreferences(Bundle savedInstanceState, String rootKey);
233
234    @Override
235    public View onCreateView(LayoutInflater inflater, ViewGroup container,
236            Bundle savedInstanceState) {
237
238        TypedArray a = mStyledContext.obtainStyledAttributes(null,
239                R.styleable.PreferenceFragmentCompat,
240                R.attr.preferenceFragmentCompatStyle,
241                0);
242
243        mLayoutResId = a.getResourceId(R.styleable.PreferenceFragmentCompat_android_layout,
244                mLayoutResId);
245
246        final Drawable divider = a.getDrawable(
247                R.styleable.PreferenceFragmentCompat_android_divider);
248        final int dividerHeight = a.getDimensionPixelSize(
249                R.styleable.PreferenceFragmentCompat_android_dividerHeight, -1);
250
251        a.recycle();
252
253        // Need to theme the inflater to pick up the preferenceFragmentListStyle
254        final TypedValue tv = new TypedValue();
255        getActivity().getTheme().resolveAttribute(R.attr.preferenceTheme, tv, true);
256        final int theme = tv.resourceId;
257
258        final Context themedContext = new ContextThemeWrapper(inflater.getContext(), theme);
259        final LayoutInflater themedInflater = inflater.cloneInContext(themedContext);
260
261        final View view = themedInflater.inflate(mLayoutResId, container, false);
262
263        final View rawListContainer = view.findViewById(AndroidResources.ANDROID_R_LIST_CONTAINER);
264        if (!(rawListContainer instanceof ViewGroup)) {
265            throw new RuntimeException("Content has view with id attribute "
266                    + "'android.R.id.list_container' that is not a ViewGroup class");
267        }
268
269        final ViewGroup listContainer = (ViewGroup) rawListContainer;
270
271        final RecyclerView listView = onCreateRecyclerView(themedInflater, listContainer,
272                savedInstanceState);
273        if (listView == null) {
274            throw new RuntimeException("Could not create RecyclerView");
275        }
276
277        mList = listView;
278
279        listView.addItemDecoration(mDividerDecoration);
280        setDivider(divider);
281        if (dividerHeight != -1) {
282            setDividerHeight(dividerHeight);
283        }
284
285        listContainer.addView(mList);
286        mHandler.post(mRequestFocus);
287
288        return view;
289    }
290
291    /**
292     * Sets the drawable that will be drawn between each item in the list.
293     * <p>
294     * <strong>Note:</strong> If the drawable does not have an intrinsic
295     * height, you should also call {@link #setDividerHeight(int)}.
296     *
297     * @param divider the drawable to use
298     * @attr ref R.styleable#PreferenceFragmentCompat_android_divider
299     */
300    public void setDivider(Drawable divider) {
301        mDividerDecoration.setDivider(divider);
302    }
303
304    /**
305     * Sets the height of the divider that will be drawn between each item in the list. Calling
306     * this will override the intrinsic height as set by {@link #setDivider(Drawable)}
307     *
308     * @param height The new height of the divider in pixels.
309     * @attr ref R.styleable#PreferenceFragmentCompat_android_dividerHeight
310     */
311    public void setDividerHeight(int height) {
312        mDividerDecoration.setDividerHeight(height);
313    }
314
315    @Override
316    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
317        super.onViewCreated(view, savedInstanceState);
318
319        if (mHavePrefs) {
320            bindPreferences();
321            if (mSelectPreferenceRunnable != null) {
322                mSelectPreferenceRunnable.run();
323                mSelectPreferenceRunnable = null;
324            }
325        }
326
327        mInitDone = true;
328    }
329
330    @Override
331    public void onActivityCreated(Bundle savedInstanceState) {
332        super.onActivityCreated(savedInstanceState);
333
334        if (savedInstanceState != null) {
335            Bundle container = savedInstanceState.getBundle(PREFERENCES_TAG);
336            if (container != null) {
337                final PreferenceScreen preferenceScreen = getPreferenceScreen();
338                if (preferenceScreen != null) {
339                    preferenceScreen.restoreHierarchyState(container);
340                }
341            }
342        }
343    }
344
345    @Override
346    public void onStart() {
347        super.onStart();
348        mPreferenceManager.setOnPreferenceTreeClickListener(this);
349        mPreferenceManager.setOnDisplayPreferenceDialogListener(this);
350    }
351
352    @Override
353    public void onStop() {
354        super.onStop();
355        mPreferenceManager.setOnPreferenceTreeClickListener(null);
356        mPreferenceManager.setOnDisplayPreferenceDialogListener(null);
357    }
358
359    @Override
360    public void onDestroyView() {
361        mHandler.removeCallbacks(mRequestFocus);
362        mHandler.removeMessages(MSG_BIND_PREFERENCES);
363        if (mHavePrefs) {
364            unbindPreferences();
365        }
366        mList = null;
367        super.onDestroyView();
368    }
369
370    @Override
371    public void onSaveInstanceState(Bundle outState) {
372        super.onSaveInstanceState(outState);
373
374        final PreferenceScreen preferenceScreen = getPreferenceScreen();
375        if (preferenceScreen != null) {
376            Bundle container = new Bundle();
377            preferenceScreen.saveHierarchyState(container);
378            outState.putBundle(PREFERENCES_TAG, container);
379        }
380    }
381
382    /**
383     * Returns the {@link PreferenceManager} used by this fragment.
384     * @return The {@link PreferenceManager}.
385     */
386    public PreferenceManager getPreferenceManager() {
387        return mPreferenceManager;
388    }
389
390    /**
391     * Sets the root of the preference hierarchy that this fragment is showing.
392     *
393     * @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy.
394     */
395    public void setPreferenceScreen(PreferenceScreen preferenceScreen) {
396        if (mPreferenceManager.setPreferences(preferenceScreen) && preferenceScreen != null) {
397            onUnbindPreferences();
398            mHavePrefs = true;
399            if (mInitDone) {
400                postBindPreferences();
401            }
402        }
403    }
404
405    /**
406     * Gets the root of the preference hierarchy that this fragment is showing.
407     *
408     * @return The {@link PreferenceScreen} that is the root of the preference
409     *         hierarchy.
410     */
411    public PreferenceScreen getPreferenceScreen() {
412        return mPreferenceManager.getPreferenceScreen();
413    }
414
415    /**
416     * Inflates the given XML resource and adds the preference hierarchy to the current
417     * preference hierarchy.
418     *
419     * @param preferencesResId The XML resource ID to inflate.
420     */
421    public void addPreferencesFromResource(@XmlRes int preferencesResId) {
422        requirePreferenceManager();
423
424        setPreferenceScreen(mPreferenceManager.inflateFromResource(mStyledContext,
425                preferencesResId, getPreferenceScreen()));
426    }
427
428    /**
429     * Inflates the given XML resource and replaces the current preference hierarchy (if any) with
430     * the preference hierarchy rooted at {@code key}.
431     *
432     * @param preferencesResId The XML resource ID to inflate.
433     * @param key The preference key of the {@link android.support.v7.preference.PreferenceScreen}
434     *            to use as the root of the preference hierarchy, or null to use the root
435     *            {@link android.support.v7.preference.PreferenceScreen}.
436     */
437    public void setPreferencesFromResource(@XmlRes int preferencesResId, @Nullable String key) {
438        requirePreferenceManager();
439
440        final PreferenceScreen xmlRoot = mPreferenceManager.inflateFromResource(mStyledContext,
441                preferencesResId, null);
442
443        final Preference root;
444        if (key != null) {
445            root = xmlRoot.findPreference(key);
446            if (!(root instanceof PreferenceScreen)) {
447                throw new IllegalArgumentException("Preference object with key " + key
448                        + " is not a PreferenceScreen");
449            }
450        } else {
451            root = xmlRoot;
452        }
453
454        setPreferenceScreen((PreferenceScreen) root);
455    }
456
457    /**
458     * {@inheritDoc}
459     */
460    @Override
461    public boolean onPreferenceTreeClick(Preference preference) {
462        if (preference.getFragment() != null) {
463            boolean handled = false;
464            if (getCallbackFragment() instanceof OnPreferenceStartFragmentCallback) {
465                handled = ((OnPreferenceStartFragmentCallback) getCallbackFragment())
466                        .onPreferenceStartFragment(this, preference);
467            }
468            if (!handled && getActivity() instanceof OnPreferenceStartFragmentCallback){
469                handled = ((OnPreferenceStartFragmentCallback) getActivity())
470                        .onPreferenceStartFragment(this, preference);
471            }
472            return handled;
473        }
474        return false;
475    }
476
477    /**
478     * Called by
479     * {@link android.support.v7.preference.PreferenceScreen#onClick()} in order to navigate to a
480     * new screen of preferences. Calls
481     * {@link PreferenceFragmentCompat.OnPreferenceStartScreenCallback#onPreferenceStartScreen}
482     * if the target fragment or containing activity implements
483     * {@link PreferenceFragmentCompat.OnPreferenceStartScreenCallback}.
484     * @param preferenceScreen The {@link android.support.v7.preference.PreferenceScreen} to
485     *                         navigate to.
486     */
487    @Override
488    public void onNavigateToScreen(PreferenceScreen preferenceScreen) {
489        boolean handled = false;
490        if (getCallbackFragment() instanceof OnPreferenceStartScreenCallback) {
491            handled = ((OnPreferenceStartScreenCallback) getCallbackFragment())
492                    .onPreferenceStartScreen(this, preferenceScreen);
493        }
494        if (!handled && getActivity() instanceof OnPreferenceStartScreenCallback) {
495            ((OnPreferenceStartScreenCallback) getActivity())
496                    .onPreferenceStartScreen(this, preferenceScreen);
497        }
498    }
499
500    /**
501     * Finds a {@link Preference} based on its key.
502     *
503     * @param key The key of the preference to retrieve.
504     * @return The {@link Preference} with the key, or null.
505     * @see android.support.v7.preference.PreferenceGroup#findPreference(CharSequence)
506     */
507    @Override
508    public Preference findPreference(CharSequence key) {
509        if (mPreferenceManager == null) {
510            return null;
511        }
512        return mPreferenceManager.findPreference(key);
513    }
514
515    private void requirePreferenceManager() {
516        if (mPreferenceManager == null) {
517            throw new RuntimeException("This should be called after super.onCreate.");
518        }
519    }
520
521    private void postBindPreferences() {
522        if (mHandler.hasMessages(MSG_BIND_PREFERENCES)) return;
523        mHandler.obtainMessage(MSG_BIND_PREFERENCES).sendToTarget();
524    }
525
526    private void bindPreferences() {
527        final PreferenceScreen preferenceScreen = getPreferenceScreen();
528        if (preferenceScreen != null) {
529            getListView().setAdapter(onCreateAdapter(preferenceScreen));
530            preferenceScreen.onAttached();
531        }
532        onBindPreferences();
533    }
534
535    private void unbindPreferences() {
536        final PreferenceScreen preferenceScreen = getPreferenceScreen();
537        if (preferenceScreen != null) {
538            preferenceScreen.onDetached();
539        }
540        onUnbindPreferences();
541    }
542
543    /** @hide */
544    protected void onBindPreferences() {
545    }
546
547    /** @hide */
548    protected void onUnbindPreferences() {
549    }
550
551    public final RecyclerView getListView() {
552        return mList;
553    }
554
555    /**
556     * Creates the {@link android.support.v7.widget.RecyclerView} used to display the preferences.
557     * Subclasses may override this to return a customized
558     * {@link android.support.v7.widget.RecyclerView}.
559     * @param inflater The LayoutInflater object that can be used to inflate the
560     *                 {@link android.support.v7.widget.RecyclerView}.
561     * @param parent The parent {@link android.view.View} that the RecyclerView will be attached to.
562     *               This method should not add the view itself, but this can be used to generate
563     *               the LayoutParams of the view.
564     * @param savedInstanceState If non-null, this view is being re-constructed from a previous
565     *                           saved state as given here
566     * @return A new RecyclerView object to be placed into the view hierarchy
567     */
568    public RecyclerView onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent,
569            Bundle savedInstanceState) {
570        RecyclerView recyclerView = (RecyclerView) inflater
571                .inflate(R.layout.preference_recyclerview, parent, false);
572
573        recyclerView.setLayoutManager(onCreateLayoutManager());
574        recyclerView.setAccessibilityDelegateCompat(
575                new PreferenceRecyclerViewAccessibilityDelegate(recyclerView));
576
577        return recyclerView;
578    }
579
580    /**
581     * Called from {@link #onCreateRecyclerView} to create the
582     * {@link android.support.v7.widget.RecyclerView.LayoutManager} for the created
583     * {@link android.support.v7.widget.RecyclerView}.
584     * @return A new {@link android.support.v7.widget.RecyclerView.LayoutManager} instance.
585     */
586    public RecyclerView.LayoutManager onCreateLayoutManager() {
587        return new LinearLayoutManager(getActivity());
588    }
589
590    /**
591     * Creates the root adapter.
592     *
593     * @param preferenceScreen Preference screen object to create the adapter for.
594     * @return An adapter that contains the preferences contained in this {@link PreferenceScreen}.
595     */
596    protected RecyclerView.Adapter onCreateAdapter(PreferenceScreen preferenceScreen) {
597        return new PreferenceGroupAdapter(preferenceScreen);
598    }
599
600    /**
601     * Called when a preference in the tree requests to display a dialog. Subclasses should
602     * override this method to display custom dialogs or to handle dialogs for custom preference
603     * classes.
604     *
605     * @param preference The Preference object requesting the dialog.
606     */
607    @Override
608    public void onDisplayPreferenceDialog(Preference preference) {
609
610        boolean handled = false;
611        if (getCallbackFragment() instanceof OnPreferenceDisplayDialogCallback) {
612            handled = ((OnPreferenceDisplayDialogCallback) getCallbackFragment())
613                    .onPreferenceDisplayDialog(this, preference);
614        }
615        if (!handled && getActivity() instanceof OnPreferenceDisplayDialogCallback) {
616            handled = ((OnPreferenceDisplayDialogCallback) getActivity())
617                    .onPreferenceDisplayDialog(this, preference);
618        }
619
620        if (handled) {
621            return;
622        }
623
624        // check if dialog is already showing
625        if (getFragmentManager().findFragmentByTag(DIALOG_FRAGMENT_TAG) != null) {
626            return;
627        }
628
629        final DialogFragment f;
630        if (preference instanceof EditTextPreference) {
631            f = EditTextPreferenceDialogFragmentCompat.newInstance(preference.getKey());
632        } else if (preference instanceof ListPreference) {
633            f = ListPreferenceDialogFragmentCompat.newInstance(preference.getKey());
634        } else {
635            throw new IllegalArgumentException("Tried to display dialog for unknown " +
636                    "preference type. Did you forget to override onDisplayPreferenceDialog()?");
637        }
638        f.setTargetFragment(this, 0);
639        f.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
640    }
641
642    /**
643     * Basically a wrapper for getParentFragment which is v17+. Used by the leanback preference lib.
644     * @return Fragment to possibly use as a callback
645     * @hide
646     */
647    public Fragment getCallbackFragment() {
648        return null;
649    }
650
651    public void scrollToPreference(final String key) {
652        scrollToPreferenceInternal(null, key);
653    }
654
655    public void scrollToPreference(final Preference preference) {
656        scrollToPreferenceInternal(preference, null);
657    }
658
659    private void scrollToPreferenceInternal(final Preference preference, final String key) {
660        final Runnable r = new Runnable() {
661            @Override
662            public void run() {
663                final RecyclerView.Adapter adapter = mList.getAdapter();
664                if (!(adapter instanceof
665                        PreferenceGroup.PreferencePositionCallback)) {
666                    if (adapter != null) {
667                        throw new IllegalStateException("Adapter must implement "
668                                + "PreferencePositionCallback");
669                    } else {
670                        // Adapter was set to null, so don't scroll I guess?
671                        return;
672                    }
673                }
674                final int position;
675                if (preference != null) {
676                    position = ((PreferenceGroup.PreferencePositionCallback) adapter)
677                            .getPreferenceAdapterPosition(preference);
678                } else {
679                    position = ((PreferenceGroup.PreferencePositionCallback) adapter)
680                            .getPreferenceAdapterPosition(key);
681                }
682                if (position != RecyclerView.NO_POSITION) {
683                    mList.scrollToPosition(position);
684                } else {
685                    // Item not found, wait for an update and try again
686                    adapter.registerAdapterDataObserver(
687                            new ScrollToPreferenceObserver(adapter, mList, preference, key));
688                }
689            }
690        };
691        if (mList == null) {
692            mSelectPreferenceRunnable = r;
693        } else {
694            r.run();
695        }
696    }
697
698    private static class ScrollToPreferenceObserver extends RecyclerView.AdapterDataObserver {
699        private final RecyclerView.Adapter mAdapter;
700        private final RecyclerView mList;
701        private final Preference mPreference;
702        private final String mKey;
703
704        public ScrollToPreferenceObserver(RecyclerView.Adapter adapter, RecyclerView list,
705                Preference preference, String key) {
706            mAdapter = adapter;
707            mList = list;
708            mPreference = preference;
709            mKey = key;
710        }
711
712        private void scrollToPreference() {
713            mAdapter.unregisterAdapterDataObserver(this);
714            final int position;
715            if (mPreference != null) {
716                position = ((PreferenceGroup.PreferencePositionCallback) mAdapter)
717                        .getPreferenceAdapterPosition(mPreference);
718            } else {
719                position = ((PreferenceGroup.PreferencePositionCallback) mAdapter)
720                        .getPreferenceAdapterPosition(mKey);
721            }
722            if (position != RecyclerView.NO_POSITION) {
723                mList.scrollToPosition(position);
724            }
725        }
726
727        @Override
728        public void onChanged() {
729            scrollToPreference();
730        }
731
732        @Override
733        public void onItemRangeChanged(int positionStart, int itemCount) {
734            scrollToPreference();
735        }
736
737        @Override
738        public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
739            scrollToPreference();
740        }
741
742        @Override
743        public void onItemRangeInserted(int positionStart, int itemCount) {
744            scrollToPreference();
745        }
746
747        @Override
748        public void onItemRangeRemoved(int positionStart, int itemCount) {
749            scrollToPreference();
750        }
751
752        @Override
753        public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
754            scrollToPreference();
755        }
756    }
757
758    private class DividerDecoration extends RecyclerView.ItemDecoration {
759
760        private Drawable mDivider;
761        private int mDividerHeight;
762
763        @Override
764        public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
765            if (mDivider == null) {
766                return;
767            }
768            final int childCount = parent.getChildCount();
769            final int width = parent.getWidth();
770            for (int childViewIndex = 0; childViewIndex < childCount; childViewIndex++) {
771                final View view = parent.getChildAt(childViewIndex);
772                if (shouldDrawDividerBelow(view, parent)) {
773                    int top = (int) ViewCompat.getY(view) + view.getHeight();
774                    mDivider.setBounds(0, top, width, top + mDividerHeight);
775                    mDivider.draw(c);
776                }
777            }
778        }
779
780        @Override
781        public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
782                RecyclerView.State state) {
783            if (shouldDrawDividerBelow(view, parent)) {
784                outRect.bottom = mDividerHeight;
785            }
786        }
787
788        private boolean shouldDrawDividerBelow(View view, RecyclerView parent) {
789            final RecyclerView.ViewHolder holder = parent.getChildViewHolder(view);
790            final boolean dividerAllowedBelow = holder instanceof PreferenceViewHolder
791                    && ((PreferenceViewHolder) holder).isDividerAllowedBelow();
792            if (!dividerAllowedBelow) {
793                return false;
794            }
795            boolean nextAllowed = true;
796            int index = parent.indexOfChild(view);
797            if (index < parent.getChildCount() - 1) {
798                final View nextView = parent.getChildAt(index + 1);
799                final RecyclerView.ViewHolder nextHolder = parent.getChildViewHolder(nextView);
800                nextAllowed = nextHolder instanceof PreferenceViewHolder
801                        && ((PreferenceViewHolder) nextHolder).isDividerAllowedAbove();
802            }
803            return nextAllowed;
804        }
805
806        public void setDivider(Drawable divider) {
807            if (divider != null) {
808                mDividerHeight = divider.getIntrinsicHeight();
809            } else {
810                mDividerHeight = 0;
811            }
812            mDivider = divider;
813            mList.invalidateItemDecorations();
814        }
815
816        public void setDividerHeight(int dividerHeight) {
817            mDividerHeight = dividerHeight;
818            mList.invalidateItemDecorations();
819        }
820    }
821}
822