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