[go: nahoru, domu]

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