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