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