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