PreferenceFragmentCompat.java revision 6904f67c96a28a0e5966b4fb6d37a0ad5f136858
1/* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17package android.support.v7.preference; 18 19import android.content.Context; 20import android.content.res.TypedArray; 21import android.os.Bundle; 22import android.os.Handler; 23import android.os.Message; 24import android.support.annotation.Nullable; 25import android.support.annotation.XmlRes; 26import android.support.v4.app.DialogFragment; 27import android.support.v4.app.Fragment; 28import android.support.v7.widget.LinearLayoutManager; 29import android.support.v7.widget.RecyclerView; 30import android.util.TypedValue; 31import android.view.ContextThemeWrapper; 32import android.view.LayoutInflater; 33import android.view.View; 34import android.view.ViewGroup; 35 36/** 37 * Shows a hierarchy of {@link Preference} objects as 38 * lists. These preferences will 39 * automatically save to {@link android.content.SharedPreferences} as the user interacts with 40 * them. To retrieve an instance of {@link android.content.SharedPreferences} that the 41 * preference hierarchy in this fragment will use, call 42 * {@link PreferenceManager#getDefaultSharedPreferences(android.content.Context)} 43 * with a context in the same package as this fragment. 44 * <p> 45 * Furthermore, the preferences shown will follow the visual style of system 46 * preferences. It is easy to create a hierarchy of preferences (that can be 47 * shown on multiple screens) via XML. For these reasons, it is recommended to 48 * use this fragment (as a superclass) to deal with preferences in applications. 49 * <p> 50 * A {@link PreferenceScreen} object should be at the top of the preference 51 * hierarchy. Furthermore, subsequent {@link PreferenceScreen} in the hierarchy 52 * denote a screen break--that is the preferences contained within subsequent 53 * {@link PreferenceScreen} should be shown on another screen. The preference 54 * framework handles showing these other screens from the preference hierarchy. 55 * <p> 56 * The preference hierarchy can be formed in multiple ways: 57 * <li> From an XML file specifying the hierarchy 58 * <li> From different {@link android.app.Activity Activities} that each specify its own 59 * preferences in an XML file via {@link android.app.Activity} meta-data 60 * <li> From an object hierarchy rooted with {@link PreferenceScreen} 61 * <p> 62 * To inflate from XML, use the {@link #addPreferencesFromResource(int)}. The 63 * root element should be a {@link PreferenceScreen}. Subsequent elements can point 64 * to actual {@link Preference} subclasses. As mentioned above, subsequent 65 * {@link PreferenceScreen} in the hierarchy will result in the screen break. 66 * <p> 67 * To specify an object hierarchy rooted with {@link PreferenceScreen}, use 68 * {@link #setPreferenceScreen(PreferenceScreen)}. 69 * <p> 70 * As a convenience, this fragment implements a click listener for any 71 * preference in the current hierarchy, see 72 * {@link #onPreferenceTreeClick(Preference)}. 73 * 74 * <div class="special reference"> 75 * <h3>Developer Guides</h3> 76 * <p>For information about using {@code PreferenceFragment}, 77 * read the <a href="{@docRoot}guide/topics/ui/settings.html">Settings</a> 78 * guide.</p> 79 * </div> 80 * 81 * <a name="SampleCode"></a> 82 * <h3>Sample Code</h3> 83 * 84 * <p>The following sample code shows a simple preference fragment that is 85 * populated from a resource. The resource it loads is:</p> 86 * 87 * {@sample development/samples/ApiDemos/res/xml/preferences.xml preferences} 88 * 89 * <p>The fragment implementation itself simply populates the preferences 90 * when created. Note that the preferences framework takes care of loading 91 * the current values out of the app preferences and writing them when changed:</p> 92 * 93 * {@sample development/samples/ApiDemos/src/com/example/android/apis/preference/FragmentPreferences.java 94 * fragment} 95 * 96 * @see Preference 97 * @see PreferenceScreen 98 */ 99public abstract class PreferenceFragmentCompat extends Fragment implements 100 PreferenceManager.OnPreferenceTreeClickListener, 101 PreferenceManager.OnDisplayPreferenceDialogListener, 102 PreferenceManager.OnNavigateToScreenListener, 103 DialogPreference.TargetFragment { 104 105 /** 106 * Fragment argument used to specify the tag of the desired root 107 * {@link android.support.v7.preference.PreferenceScreen} object. 108 */ 109 public static final String ARG_PREFERENCE_ROOT = 110 "android.support.v7.preference.PreferenceFragmentCompat.PREFERENCE_ROOT"; 111 112 private static final String PREFERENCES_TAG = "android:preferences"; 113 114 private static final String DIALOG_FRAGMENT_TAG = 115 "android.support.v7.preference.PreferenceFragment.DIALOG"; 116 117 private PreferenceManager mPreferenceManager; 118 private RecyclerView mList; 119 private boolean mHavePrefs; 120 private boolean mInitDone; 121 122 private Context mStyledContext; 123 124 private int mLayoutResId = R.layout.preference_list_fragment; 125 126 /** 127 * The starting request code given out to preference framework. 128 */ 129 private static final int FIRST_REQUEST_CODE = 100; 130 131 private static final int MSG_BIND_PREFERENCES = 1; 132 private Handler mHandler = new Handler() { 133 @Override 134 public void handleMessage(Message msg) { 135 switch (msg.what) { 136 137 case MSG_BIND_PREFERENCES: 138 bindPreferences(); 139 break; 140 } 141 } 142 }; 143 144 final private Runnable mRequestFocus = new Runnable() { 145 public void run() { 146 mList.focusableViewAvailable(mList); 147 } 148 }; 149 150 /** 151 * Interface that PreferenceFragment's containing activity should 152 * implement to be able to process preference items that wish to 153 * switch to a specified fragment. 154 */ 155 public interface OnPreferenceStartFragmentCallback { 156 /** 157 * Called when the user has clicked on a Preference that has 158 * a fragment class name associated with it. The implementation 159 * should instantiate and switch to an instance of the given 160 * fragment. 161 */ 162 boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref); 163 } 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 new screen of preferences. 169 */ 170 public interface OnPreferenceStartScreenCallback { 171 /** 172 * Called when the user has clicked on a PreferenceScreen item in order to navigate to a new 173 * screen of preferences. 174 * @param caller The fragment requesting navigation. 175 * @param pref The preference screen to navigate to. 176 */ 177 boolean onPreferenceStartScreen(PreferenceFragmentCompat caller, PreferenceScreen pref); 178 } 179 180 @Override 181 public void onCreate(Bundle savedInstanceState) { 182 super.onCreate(savedInstanceState); 183 final TypedValue tv = new TypedValue(); 184 getActivity().getTheme().resolveAttribute(R.attr.preferenceTheme, tv, true); 185 final int theme = tv.resourceId; 186 if (theme <= 0) { 187 throw new IllegalStateException("Must specify preferenceTheme in theme"); 188 } 189 mStyledContext = new ContextThemeWrapper(getActivity(), theme); 190 191 mPreferenceManager = new PreferenceManager(mStyledContext); 192 mPreferenceManager.setOnNavigateToScreenListener(this); 193 final Bundle args = getArguments(); 194 final String rootKey; 195 if (args != null) { 196 rootKey = getArguments().getString(ARG_PREFERENCE_ROOT); 197 } else { 198 rootKey = null; 199 } 200 onCreatePreferences(savedInstanceState, rootKey); 201 } 202 203 /** 204 * Called during {@link #onCreate(Bundle)} to supply the preferences for this fragment. 205 * Subclasses are expected to call {@link #setPreferenceScreen(PreferenceScreen)} either 206 * directly or via helper methods such as {@link #addPreferencesFromResource(int)}. 207 * 208 * @param savedInstanceState If the fragment is being re-created from 209 * a previous saved state, this is the state. 210 * @param rootKey If non-null, this preference fragment should be rooted at the 211 * {@link android.support.v7.preference.PreferenceScreen} with this key. 212 */ 213 public abstract void onCreatePreferences(Bundle savedInstanceState, String rootKey); 214 215 @Override 216 public View onCreateView(LayoutInflater inflater, ViewGroup container, 217 Bundle savedInstanceState) { 218 219 TypedArray a = mStyledContext.obtainStyledAttributes(null, 220 R.styleable.PreferenceFragmentCompat, 221 R.attr.preferenceFragmentStyle, 222 0); 223 224 mLayoutResId = a.getResourceId(R.styleable.PreferenceFragmentCompat_layout, 225 mLayoutResId); 226 227 a.recycle(); 228 229 final View view = inflater.inflate(mLayoutResId, container, false); 230 231 final View rawListContainer = view.findViewById(R.id.list_container); 232 if (!(rawListContainer instanceof ViewGroup)) { 233 throw new RuntimeException("Content has view with id attribute 'R.id.list_container' " 234 + "that is not a ViewGroup class"); 235 } 236 237 final ViewGroup listContainer = (ViewGroup) rawListContainer; 238 239 final RecyclerView listView = onCreateRecyclerView(inflater, listContainer, 240 savedInstanceState); 241 if (listView == null) { 242 throw new RuntimeException("Could not create RecyclerView"); 243 } 244 245 mList = listView; 246 listContainer.addView(mList); 247 mHandler.post(mRequestFocus); 248 return view; 249 } 250 251 @Override 252 public void onActivityCreated(Bundle savedInstanceState) { 253 super.onActivityCreated(savedInstanceState); 254 255 if (mHavePrefs) { 256 bindPreferences(); 257 } 258 259 mInitDone = true; 260 261 if (savedInstanceState != null) { 262 Bundle container = savedInstanceState.getBundle(PREFERENCES_TAG); 263 if (container != null) { 264 final PreferenceScreen preferenceScreen = getPreferenceScreen(); 265 if (preferenceScreen != null) { 266 preferenceScreen.restoreHierarchyState(container); 267 } 268 } 269 } 270 } 271 272 @Override 273 public void onStart() { 274 super.onStart(); 275 mPreferenceManager.setOnPreferenceTreeClickListener(this); 276 mPreferenceManager.setOnDisplayPreferenceDialogListener(this); 277 } 278 279 @Override 280 public void onStop() { 281 super.onStop(); 282 mPreferenceManager.setOnPreferenceTreeClickListener(null); 283 mPreferenceManager.setOnDisplayPreferenceDialogListener(null); 284 } 285 286 @Override 287 public void onDestroyView() { 288 mList = null; 289 mHandler.removeCallbacks(mRequestFocus); 290 mHandler.removeMessages(MSG_BIND_PREFERENCES); 291 super.onDestroyView(); 292 } 293 294 @Override 295 public void onSaveInstanceState(Bundle outState) { 296 super.onSaveInstanceState(outState); 297 298 final PreferenceScreen preferenceScreen = getPreferenceScreen(); 299 if (preferenceScreen != null) { 300 Bundle container = new Bundle(); 301 preferenceScreen.saveHierarchyState(container); 302 outState.putBundle(PREFERENCES_TAG, container); 303 } 304 } 305 306 /** 307 * Returns the {@link PreferenceManager} used by this fragment. 308 * @return The {@link PreferenceManager}. 309 */ 310 public PreferenceManager getPreferenceManager() { 311 return mPreferenceManager; 312 } 313 314 /** 315 * Sets the root of the preference hierarchy that this fragment is showing. 316 * 317 * @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy. 318 */ 319 public void setPreferenceScreen(PreferenceScreen preferenceScreen) { 320 if (mPreferenceManager.setPreferences(preferenceScreen) && preferenceScreen != null) { 321 onUnbindPreferences(); 322 mHavePrefs = true; 323 if (mInitDone) { 324 postBindPreferences(); 325 } 326 } 327 } 328 329 /** 330 * Gets the root of the preference hierarchy that this fragment is showing. 331 * 332 * @return The {@link PreferenceScreen} that is the root of the preference 333 * hierarchy. 334 */ 335 public PreferenceScreen getPreferenceScreen() { 336 return mPreferenceManager.getPreferenceScreen(); 337 } 338 339 /** 340 * Inflates the given XML resource and adds the preference hierarchy to the current 341 * preference hierarchy. 342 * 343 * @param preferencesResId The XML resource ID to inflate. 344 */ 345 public void addPreferencesFromResource(@XmlRes int preferencesResId) { 346 requirePreferenceManager(); 347 348 setPreferenceScreen(mPreferenceManager.inflateFromResource(mStyledContext, 349 preferencesResId, getPreferenceScreen())); 350 } 351 352 /** 353 * Inflates the given XML resource and replaces the current preference hierarchy (if any) with 354 * the preference hierarchy rooted at {@code key}. 355 * 356 * @param preferencesResId The XML resource ID to inflate. 357 * @param key The preference key of the {@link android.support.v7.preference.PreferenceScreen} 358 * to use as the root of the preference hierarchy, or null to use the root 359 * {@link android.support.v7.preference.PreferenceScreen}. 360 */ 361 public void setPreferencesFromResource(@XmlRes int preferencesResId, @Nullable String key) { 362 requirePreferenceManager(); 363 364 final PreferenceScreen xmlRoot = mPreferenceManager.inflateFromResource(mStyledContext, 365 preferencesResId, null); 366 367 final Preference root; 368 if (key != null) { 369 root = xmlRoot.findPreference(key); 370 if (!(root instanceof PreferenceScreen)) { 371 throw new IllegalArgumentException("Preference object with key " + key 372 + " is not a PreferenceScreen"); 373 } 374 } else { 375 root = xmlRoot; 376 } 377 378 setPreferenceScreen((PreferenceScreen) root); 379 } 380 381 /** 382 * {@inheritDoc} 383 */ 384 public boolean onPreferenceTreeClick(Preference preference) { 385 if (preference.getFragment() != null && 386 getActivity() instanceof OnPreferenceStartFragmentCallback) { 387 return ((OnPreferenceStartFragmentCallback)getActivity()).onPreferenceStartFragment( 388 this, preference); 389 } 390 return false; 391 } 392 393 /** 394 * Called by 395 * {@link android.support.v7.preference.PreferenceScreen#onClick()} in order to navigate to a 396 * new screen of preferences. Calls 397 * {@link PreferenceFragmentCompat.OnPreferenceStartScreenCallback#onPreferenceStartScreen} 398 * if the containing activity implements 399 * {@link PreferenceFragmentCompat.OnPreferenceStartScreenCallback}. 400 * @param preferenceScreen The {@link android.support.v7.preference.PreferenceScreen} to 401 * navigate to. 402 */ 403 @Override 404 public void onNavigateToScreen(PreferenceScreen preferenceScreen) { 405 if (getActivity() instanceof OnPreferenceStartScreenCallback) { 406 ((OnPreferenceStartScreenCallback)getActivity()).onPreferenceStartScreen(this, 407 preferenceScreen); 408 } 409 } 410 411 /** 412 * Finds a {@link Preference} based on its key. 413 * 414 * @param key The key of the preference to retrieve. 415 * @return The {@link Preference} with the key, or null. 416 * @see android.support.v7.preference.PreferenceGroup#findPreference(CharSequence) 417 */ 418 public Preference findPreference(CharSequence key) { 419 if (mPreferenceManager == null) { 420 return null; 421 } 422 return mPreferenceManager.findPreference(key); 423 } 424 425 private void requirePreferenceManager() { 426 if (mPreferenceManager == null) { 427 throw new RuntimeException("This should be called after super.onCreate."); 428 } 429 } 430 431 private void postBindPreferences() { 432 if (mHandler.hasMessages(MSG_BIND_PREFERENCES)) return; 433 mHandler.obtainMessage(MSG_BIND_PREFERENCES).sendToTarget(); 434 } 435 436 private void bindPreferences() { 437 final PreferenceScreen preferenceScreen = getPreferenceScreen(); 438 if (preferenceScreen != null) { 439 getListView().setAdapter(preferenceScreen.getAdapter()); 440 } 441 onBindPreferences(); 442 } 443 444 /** @hide */ 445 protected void onBindPreferences() { 446 } 447 448 /** @hide */ 449 protected void onUnbindPreferences() { 450 } 451 452 public final RecyclerView getListView() { 453 return mList; 454 } 455 456 /** 457 * Creates the {@link android.support.v7.widget.RecyclerView} used to display the preferences. 458 * Subclasses may override this to return a customized 459 * {@link android.support.v7.widget.RecyclerView}. 460 * @param inflater The LayoutInflater object that can be used to inflate the 461 * {@link android.support.v7.widget.RecyclerView}. 462 * @param parent The parent {@link android.view.View} that the RecyclerView will be attached to. 463 * This method should not add the view itself, but this can be used to generate 464 * the LayoutParams of the view. 465 * @param savedInstanceState If non-null, this view is being re-constructed from a previous 466 * saved state as given here 467 * @return A new RecyclerView object to be placed into the view hierarchy 468 */ 469 public RecyclerView onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent, 470 Bundle savedInstanceState) { 471 RecyclerView recyclerView = (RecyclerView) inflater 472 .inflate(R.layout.preference_recyclerview, parent, false); 473 474 recyclerView.setLayoutManager(onCreateLayoutManager()); 475 476 return recyclerView; 477 } 478 479 /** 480 * Called from {@link #onCreateRecyclerView} to create the 481 * {@link android.support.v7.widget.RecyclerView.LayoutManager} for the created 482 * {@link android.support.v7.widget.RecyclerView}. 483 * @return A new {@link android.support.v7.widget.RecyclerView.LayoutManager} instance. 484 */ 485 public RecyclerView.LayoutManager onCreateLayoutManager() { 486 return new LinearLayoutManager(getActivity()); 487 } 488 489 /** 490 * Called when a preference in the tree requests to display a dialog. Subclasses should 491 * override this method to display custom dialogs or to handle dialogs for custom preference 492 * classes. 493 * 494 * @param preference The Preference object requesting the dialog. 495 */ 496 @Override 497 public void onDisplayPreferenceDialog(Preference preference) { 498 // check if dialog is already showing 499 if (getFragmentManager().findFragmentByTag(DIALOG_FRAGMENT_TAG) != null) { 500 return; 501 } 502 503 final DialogFragment f; 504 if (preference instanceof EditTextPreference) { 505 f = EditTextPreferenceDialogFragmentCompat.newInstance(preference.getKey()); 506 } else if (preference instanceof ListPreference) { 507 f = ListPreferenceDialogFragmentCompat.newInstance(preference.getKey()); 508 } else { 509 throw new IllegalArgumentException("Tried to display dialog for unknown " + 510 "preference type. Did you forget to override onDisplayPreferenceDialog()?"); 511 } 512 f.setTargetFragment(this, 0); 513 f.show(getFragmentManager(), DIALOG_FRAGMENT_TAG); 514 } 515 516} 517