/* * Copyright 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package androidx.preference; import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX; import android.content.Context; import android.content.pm.PackageManager; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.util.Log; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; import androidx.annotation.XmlRes; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; /** * A PreferenceFragmentCompat is the entry point to using the Preference library. This * {@link Fragment} displays a hierarchy of {@link Preference} objects to the user. It also * handles persisting values to the device. To retrieve an instance of * {@link android.content.SharedPreferences} that the preference hierarchy in this fragment will * use by default, call * {@link PreferenceManager#getDefaultSharedPreferences(android.content.Context)} with a context * in the same package as this fragment. * *
You can define a preference hierarchy as an XML resource, or you can build a hierarchy in * code. In both cases you need to use a {@link PreferenceScreen} as the root component in your * hierarchy. * *
To inflate from XML, use the {@link #setPreferencesFromResource(int, String)}. An example * example XML resource is shown further down. * *
To build a hierarchy from code, use * {@link PreferenceManager#createPreferenceScreen(Context)} to create the root * {@link PreferenceScreen}. Once you have added other {@link Preference}s to this root scree * with {@link PreferenceScreen#addPreference(Preference)}, you then need to set the screen as * the root screen in your hierarchy with {@link #setPreferenceScreen(PreferenceScreen)}. * *
As a convenience, this fragment implements a click listener for any preference in the * current hierarchy, see {@link #onPreferenceTreeClick(Preference)}. * *
For more information about * building a settings screen using the AndroidX Preference library, see * Settings.
The following sample code shows a simple settings screen using an XML resource. The XML * resource is as follows:
* * {@sample frameworks/support/samples/SupportPreferenceDemos/src/main/res/xml/preferences.xml preferences} * *The fragment that loads the XML resource is as follows:
* * {@sample frameworks/support/samples/SupportPreferenceDemos/src/main/java/com/example/androidx/preference/Preferences.java preferences} * * @see Preference * @see PreferenceScreen */ public abstract class PreferenceFragmentCompat extends Fragment implements PreferenceManager.OnPreferenceTreeClickListener, PreferenceManager.OnDisplayPreferenceDialogListener, PreferenceManager.OnNavigateToScreenListener, DialogPreference.TargetFragment { private static final String TAG = "PreferenceFragment"; /** * Fragment argument used to specify the tag of the desired root {@link PreferenceScreen} * object. */ public static final String ARG_PREFERENCE_ROOT = "androidx.preference.PreferenceFragmentCompat.PREFERENCE_ROOT"; private static final String PREFERENCES_TAG = "android:preferences"; private static final String DIALOG_FRAGMENT_TAG = "androidx.preference.PreferenceFragment.DIALOG"; private static final int MSG_BIND_PREFERENCES = 1; private final DividerDecoration mDividerDecoration = new DividerDecoration(); private PreferenceManager mPreferenceManager; @SuppressWarnings("WeakerAccess") /* synthetic access */ RecyclerView mList; private boolean mHavePrefs; private boolean mInitDone; private int mLayoutResId = R.layout.preference_list_fragment; private Runnable mSelectPreferenceRunnable; private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_BIND_PREFERENCES: bindPreferences(); break; } } }; final private Runnable mRequestFocus = new Runnable() { @Override public void run() { mList.focusableViewAvailable(mList); } }; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); final TypedValue tv = new TypedValue(); getActivity().getTheme().resolveAttribute(R.attr.preferenceTheme, tv, true); int theme = tv.resourceId; if (theme == 0) { // Fallback to default theme. theme = R.style.PreferenceThemeOverlay; } getActivity().getTheme().applyStyle(theme, false); mPreferenceManager = new PreferenceManager(getContext()); mPreferenceManager.setOnNavigateToScreenListener(this); final Bundle args = getArguments(); final String rootKey; if (args != null) { rootKey = getArguments().getString(ARG_PREFERENCE_ROOT); } else { rootKey = null; } onCreatePreferences(savedInstanceState, rootKey); } /** * Called during {@link #onCreate(Bundle)} to supply the preferences for this fragment. * Subclasses are expected to call {@link #setPreferenceScreen(PreferenceScreen)} either * directly or via helper methods such as {@link #addPreferencesFromResource(int)}. * * @param savedInstanceState If the fragment is being re-created from a previous saved state, * this is the state. * @param rootKey If non-null, this preference fragment should be rooted at the * {@link PreferenceScreen} with this key. */ public abstract void onCreatePreferences(Bundle savedInstanceState, String rootKey); @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { TypedArray a = getContext().obtainStyledAttributes(null, R.styleable.PreferenceFragmentCompat, R.attr.preferenceFragmentCompatStyle, 0); mLayoutResId = a.getResourceId(R.styleable.PreferenceFragmentCompat_android_layout, mLayoutResId); final Drawable divider = a.getDrawable( R.styleable.PreferenceFragmentCompat_android_divider); final int dividerHeight = a.getDimensionPixelSize( R.styleable.PreferenceFragmentCompat_android_dividerHeight, -1); final boolean allowDividerAfterLastItem = a.getBoolean( R.styleable.PreferenceFragmentCompat_allowDividerAfterLastItem, true); a.recycle(); final LayoutInflater themedInflater = inflater.cloneInContext(getContext()); final View view = themedInflater.inflate(mLayoutResId, container, false); final View rawListContainer = view.findViewById(AndroidResources.ANDROID_R_LIST_CONTAINER); if (!(rawListContainer instanceof ViewGroup)) { throw new IllegalStateException("Content has view with id attribute " + "'android.R.id.list_container' that is not a ViewGroup class"); } final ViewGroup listContainer = (ViewGroup) rawListContainer; final RecyclerView listView = onCreateRecyclerView(themedInflater, listContainer, savedInstanceState); if (listView == null) { throw new RuntimeException("Could not create RecyclerView"); } mList = listView; listView.addItemDecoration(mDividerDecoration); setDivider(divider); if (dividerHeight != -1) { setDividerHeight(dividerHeight); } mDividerDecoration.setAllowDividerAfterLastItem(allowDividerAfterLastItem); // If mList isn't present in the view hierarchy, add it. mList is automatically inflated // on an Auto device so don't need to add it. if (mList.getParent() == null) { listContainer.addView(mList); } mHandler.post(mRequestFocus); return view; } /** * Sets the {@link Drawable} that will be drawn between each item in the list. * *Note: If the drawable does not have an intrinsic height, you should also
* call {@link #setDividerHeight(int)}.
*
* @param divider The drawable to use
* {@link android.R.attr#divider}
*/
public void setDivider(Drawable divider) {
mDividerDecoration.setDivider(divider);
}
/**
* Sets the height of the divider that will be drawn between each item in the list. Calling
* this will override the intrinsic height as set by {@link #setDivider(Drawable)}.
*
* @param height The new height of the divider in pixels
* {@link android.R.attr#dividerHeight}
*/
public void setDividerHeight(int height) {
mDividerDecoration.setDividerHeight(height);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (savedInstanceState != null) {
Bundle container = savedInstanceState.getBundle(PREFERENCES_TAG);
if (container != null) {
final PreferenceScreen preferenceScreen = getPreferenceScreen();
if (preferenceScreen != null) {
preferenceScreen.restoreHierarchyState(container);
}
}
}
if (mHavePrefs) {
bindPreferences();
if (mSelectPreferenceRunnable != null) {
mSelectPreferenceRunnable.run();
mSelectPreferenceRunnable = null;
}
}
mInitDone = true;
}
@Override
public void onStart() {
super.onStart();
mPreferenceManager.setOnPreferenceTreeClickListener(this);
mPreferenceManager.setOnDisplayPreferenceDialogListener(this);
}
@Override
public void onStop() {
super.onStop();
mPreferenceManager.setOnPreferenceTreeClickListener(null);
mPreferenceManager.setOnDisplayPreferenceDialogListener(null);
}
@Override
public void onDestroyView() {
mHandler.removeCallbacks(mRequestFocus);
mHandler.removeMessages(MSG_BIND_PREFERENCES);
if (mHavePrefs) {
unbindPreferences();
}
mList = null;
super.onDestroyView();
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
final PreferenceScreen preferenceScreen = getPreferenceScreen();
if (preferenceScreen != null) {
Bundle container = new Bundle();
preferenceScreen.saveHierarchyState(container);
outState.putBundle(PREFERENCES_TAG, container);
}
}
/**
* Returns the {@link PreferenceManager} used by this fragment.
*
* @return The {@link PreferenceManager} used by this fragment
*/
public PreferenceManager getPreferenceManager() {
return mPreferenceManager;
}
/**
* Gets the root of the preference hierarchy that this fragment is showing.
*
* @return The {@link PreferenceScreen} that is the root of the preference hierarchy
*/
public PreferenceScreen getPreferenceScreen() {
return mPreferenceManager.getPreferenceScreen();
}
/**
* Sets the root of the preference hierarchy that this fragment is showing.
*
* @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy
*/
public void setPreferenceScreen(PreferenceScreen preferenceScreen) {
if (mPreferenceManager.setPreferences(preferenceScreen) && preferenceScreen != null) {
onUnbindPreferences();
mHavePrefs = true;
if (mInitDone) {
postBindPreferences();
}
}
}
/**
* Inflates the given XML resource and adds the preference hierarchy to the current
* preference hierarchy.
*
* @param preferencesResId The XML resource ID to inflate
*/
public void addPreferencesFromResource(@XmlRes int preferencesResId) {
requirePreferenceManager();
setPreferenceScreen(mPreferenceManager.inflateFromResource(getContext(),
preferencesResId, getPreferenceScreen()));
}
/**
* Inflates the given XML resource and replaces the current preference hierarchy (if any) with
* the preference hierarchy rooted at {@code key}.
*
* @param preferencesResId The XML resource ID to inflate
* @param key The preference key of the {@link PreferenceScreen} to use as the
* root of the preference hierarchy, or {@code null} to use the root
* {@link PreferenceScreen}.
*/
public void setPreferencesFromResource(@XmlRes int preferencesResId, @Nullable String key) {
requirePreferenceManager();
final PreferenceScreen xmlRoot = mPreferenceManager.inflateFromResource(getContext(),
preferencesResId, null);
final Preference root;
if (key != null) {
root = xmlRoot.findPreference(key);
if (!(root instanceof PreferenceScreen)) {
throw new IllegalArgumentException("Preference object with key " + key
+ " is not a PreferenceScreen");
}
} else {
root = xmlRoot;
}
setPreferenceScreen((PreferenceScreen) root);
}
/**
* {@inheritDoc}
*/
@Override
public boolean onPreferenceTreeClick(Preference preference) {
if (preference.getFragment() != null) {
boolean handled = false;
if (getCallbackFragment() instanceof OnPreferenceStartFragmentCallback) {
handled = ((OnPreferenceStartFragmentCallback) getCallbackFragment())
.onPreferenceStartFragment(this, preference);
}
if (!handled && getActivity() instanceof OnPreferenceStartFragmentCallback) {
handled = ((OnPreferenceStartFragmentCallback) getActivity())
.onPreferenceStartFragment(this, preference);
}
if (!handled) {
Log.w(TAG,
"onPreferenceStartFragment is not implemented in the parent activity - "
+ "attempting to use a fallback implementation. You should "
+ "implement this method so that you can configure the new "
+ "fragment that will be displayed, and set a transition between "
+ "the fragments.");
final FragmentManager fragmentManager = requireActivity()
.getSupportFragmentManager();
final Bundle args = preference.getExtras();
final Fragment fragment = fragmentManager.getFragmentFactory().instantiate(
requireActivity().getClassLoader(), preference.getFragment());
fragment.setArguments(args);
fragment.setTargetFragment(this, 0);
fragmentManager.beginTransaction()
// Attempt to replace this fragment in its root view - developers should
// implement onPreferenceStartFragment in their activity so that they can
// customize this behaviour and handle any transitions between fragments
.replace((((View) getView().getParent()).getId()), fragment)
.addToBackStack(null)
.commit();
}
return true;
}
return false;
}
/**
* Called by {@link PreferenceScreen#onClick()} in order to navigate to a new screen of
* preferences. Calls
* {@link PreferenceFragmentCompat.OnPreferenceStartScreenCallback#onPreferenceStartScreen}
* if the target fragment or containing activity implements
* {@link PreferenceFragmentCompat.OnPreferenceStartScreenCallback}.
*
* @param preferenceScreen The {@link PreferenceScreen} to navigate to
*/
@Override
public void onNavigateToScreen(PreferenceScreen preferenceScreen) {
boolean handled = false;
if (getCallbackFragment() instanceof OnPreferenceStartScreenCallback) {
handled = ((OnPreferenceStartScreenCallback) getCallbackFragment())
.onPreferenceStartScreen(this, preferenceScreen);
}
if (!handled && getActivity() instanceof OnPreferenceStartScreenCallback) {
((OnPreferenceStartScreenCallback) getActivity())
.onPreferenceStartScreen(this, preferenceScreen);
}
}
@Override
@SuppressWarnings("TypeParameterUnusedInFormals")
@Nullable
public