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