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