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