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