Skip to content

Instantly share code, notes, and snippets.

@cbeyls
Last active April 6, 2023 09:07
Show Gist options
  • Save cbeyls/7475726 to your computer and use it in GitHub Desktop.
Save cbeyls/7475726 to your computer and use it in GitHub Desktop.
A PreferenceFragment for the Android support library. Based on the platform's code with some removed features and a basic ListView layout.It uses reflection but works with every device I've tested so far.
package android.support.v4.preference;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.preference.Preference;
import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ListView;
/**
* A PreferenceFragment for the support library. Based on the platform's code with some removed features and a basic ListView layout.
*
* @author Christophe Beyls
*/
public abstract class PreferenceFragment extends Fragment {
private static final int FIRST_REQUEST_CODE = 100;
static final int MSG_BIND_PREFERENCES = 1;
static final int MSG_REQUEST_FOCUS = 2;
private static final String PREFERENCES_TAG = "android:preferences";
private static final float HC_HORIZONTAL_PADDING = 16f;
@SuppressLint("HandlerLeak")
private final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_BIND_PREFERENCES:
bindPreferences();
break;
case MSG_REQUEST_FOCUS:
mList.focusableViewAvailable(mList);
break;
}
}
};
private boolean mHavePrefs;
private boolean mInitDone;
ListView mList;
private PreferenceManager mPreferenceManager;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
try {
Constructor<PreferenceManager> c = PreferenceManager.class.getDeclaredConstructor(Activity.class, int.class);
c.setAccessible(true);
mPreferenceManager = c.newInstance(this.getActivity(), FIRST_REQUEST_CODE);
} catch (Exception ignored) {
}
}
@Override
public View onCreateView(LayoutInflater layoutInflater, ViewGroup viewGroup, Bundle savedInstanceState) {
ListView listView = new ListView(getActivity());
listView.setId(android.R.id.list);
if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) && (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)) {
final int horizontalPadding = (int) (HC_HORIZONTAL_PADDING * getResources().getDisplayMetrics().density + 0.5f);
listView.setPadding(horizontalPadding, 0, horizontalPadding, 0);
listView.setClipToPadding(false);
listView.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY);
}
return listView;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (mHavePrefs) {
bindPreferences();
}
mInitDone = true;
if (savedInstanceState != null) {
Bundle container = savedInstanceState.getBundle(PREFERENCES_TAG);
if (container != null) {
final PreferenceScreen preferenceScreen = getPreferenceScreen();
if (preferenceScreen != null) {
preferenceScreen.restoreHierarchyState(container);
}
}
}
}
public void onStop() {
super.onStop();
try {
Method m = PreferenceManager.class.getDeclaredMethod("dispatchActivityStop");
m.setAccessible(true);
m.invoke(mPreferenceManager);
} catch (Exception ignored) {
}
}
public void onDestroyView() {
mList = null;
mHandler.removeCallbacksAndMessages(null);
super.onDestroyView();
}
public void onDestroy() {
super.onDestroy();
try {
Method m = PreferenceManager.class.getDeclaredMethod("dispatchActivityDestroy");
m.setAccessible(true);
m.invoke(mPreferenceManager);
} catch (Exception ignored) {
}
}
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
PreferenceScreen preferenceScreen = getPreferenceScreen();
if (preferenceScreen != null) {
Bundle container = new Bundle();
preferenceScreen.saveHierarchyState(container);
outState.putBundle(PREFERENCES_TAG, container);
}
}
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
try {
Method m = PreferenceManager.class.getDeclaredMethod("dispatchActivityResult", int.class, int.class, Intent.class);
m.setAccessible(true);
m.invoke(mPreferenceManager, requestCode, resultCode, data);
} catch (Exception ignored) {
}
}
public PreferenceManager getPreferenceManager() {
return mPreferenceManager;
}
public void setPreferenceScreen(PreferenceScreen screen) {
try {
Method m = PreferenceManager.class.getDeclaredMethod("setPreferences", PreferenceScreen.class);
m.setAccessible(true);
boolean result = (Boolean) m.invoke(mPreferenceManager, screen);
if (result && (screen != null)) {
mHavePrefs = true;
if (mInitDone) {
postBindPreferences();
}
}
} catch (Exception ignored) {
}
}
public PreferenceScreen getPreferenceScreen() {
try {
Method m = PreferenceManager.class.getDeclaredMethod("getPreferenceScreen");
m.setAccessible(true);
return (PreferenceScreen) m.invoke(mPreferenceManager);
} catch (Exception e) {
return null;
}
}
public void addPreferencesFromIntent(Intent intent) {
requirePreferenceManager();
try {
Method m = PreferenceManager.class.getDeclaredMethod("inflateFromIntent", Intent.class, PreferenceScreen.class);
m.setAccessible(true);
PreferenceScreen screen = (PreferenceScreen) m.invoke(mPreferenceManager, intent, getPreferenceScreen());
setPreferenceScreen(screen);
} catch (Exception ignored) {
}
}
public void addPreferencesFromResource(int resId) {
requirePreferenceManager();
try {
Method m = PreferenceManager.class.getDeclaredMethod("inflateFromResource", Context.class, int.class, PreferenceScreen.class);
m.setAccessible(true);
PreferenceScreen screen = (PreferenceScreen) m.invoke(mPreferenceManager, getActivity(), resId, getPreferenceScreen());
setPreferenceScreen(screen);
} catch (Exception ignored) {
}
}
public Preference findPreference(CharSequence key) {
if (mPreferenceManager == null) {
return null;
}
return mPreferenceManager.findPreference(key);
}
private void requirePreferenceManager() {
if (this.mPreferenceManager == null) {
throw new RuntimeException("This should be called after super.onCreate.");
}
}
private void postBindPreferences() {
if (!mHandler.hasMessages(MSG_BIND_PREFERENCES)) {
mHandler.sendEmptyMessage(MSG_BIND_PREFERENCES);
}
}
void bindPreferences() {
final PreferenceScreen preferenceScreen = getPreferenceScreen();
if (preferenceScreen != null) {
preferenceScreen.bind(getListView());
}
}
public ListView getListView() {
ensureList();
return mList;
}
private void ensureList() {
if (mList != null) {
return;
}
View root = getView();
if (root == null) {
throw new IllegalStateException("Content view not yet created");
}
View rawListView = root.findViewById(android.R.id.list);
if (rawListView == null) {
throw new RuntimeException("Your content must have a ListView whose id attribute is 'android.R.id.list'");
}
if (!(rawListView instanceof ListView)) {
throw new RuntimeException("Content has view with id attribute 'android.R.id.list' that is not a ListView class");
}
mList = (ListView) rawListView;
mHandler.sendEmptyMessage(MSG_REQUEST_FOCUS);
}
}
@keir-nellyer
Copy link

Thanks for sharing, using this in my app now! Works like a charm! Literally didn't have to make any changes apart from change the class my preference fragment extended and it worked

@cbeyls
Copy link
Author

cbeyls commented Apr 23, 2015

This class is now deprecated. Starting with AppCompat 22.1.0, it's recommended to extend AppCompatPreferenceActivity from the new samples instead, unless you want to customize the content layout more deeply.

@ferrannp
Copy link

Does this work with nested FragmentPreferences? I am trying the `AppCompatPreferenceActivity``(as you suggested) and as the last answer of this thread suggests too: http://stackoverflow.com/a/29820402/2277631 . However, I can't make that 100% works. On a nested preference fragment, the layout that I have in my PreferenceActivity (extending AppCompatPreferenceActivity) is not respected, and also the toolbar disappears on Nested preferences screens.

I am not sure if you have time but you might want to take a look: https://github.com/ferrannp/material-preferences

Thank you!

@cbeyls
Copy link
Author

cbeyls commented May 17, 2015

By default, nested preferences launch new built-in Activities that you can't control. It's at the OS level. So neither this class nor AppCompatPreferenceActivity allows you to have a modern ActionBar in the nested preferences since these 2 solutions reuse most of the framework's code.

The library you suggested uses a hack to support nested preferences without reimplementing the Preference classes themselves and it's probably your best option if you need nested preferences with Material style. Please note however that the Preference Dialogs will not be Material-styled and if you want that too you need to reimplement all Preference classes using a Dialog.

@jathanasiou
Copy link

@cbeyls what do you mean this is deprecated exactly? I had the impression that PreferenceFragment was the "modern" way of implementing a preference screen. What is best if one only wants to support API15+ but simply needs an appcompat version due to using support classes in main activity?

@pt321
Copy link

pt321 commented May 25, 2015

is there anyway to stop the fragment busting out of the frame given by the activity to load the fragnent in, it ends up taking the fulls screen.

@cbeyls
Copy link
Author

cbeyls commented Jun 24, 2015

@jathanasiou For API 11+, you should use the Framework's PreferenceFragment instead, in combination with the Framework's FragmentManager, inside a PreferenceActivity.
For API 7+, you should use PreferenceActivity with the old deprecated methods to populate the preferences.

I created this Fragment because there was no way to combine Preferences with an Action Bar on older devices, since you had to inherit from ActionBarActivity to get an ActionBar. Because AppCompat 22.1.0+ allows to have an ActionBar in any Activity including the PreferenceActivity, this class is deprecated.

@summers314
Copy link

Is there an AppCompat version of PreferenceFragment too? Im using the Material Actionbar, so if im using the standard PreferenceFragment the ActionBar isnt visible.

Edit: My bad. I solved this by adding the PreferenceFragment to an AppCompatActivity

@cbeyls
Copy link
Author

cbeyls commented Dec 13, 2016

Since AppCompat 23, you can now also use the Preferences Support Library. However, preferences tend to look bad with that library (even on modern devices) and a lot of hacks are required to make it work decently, not to mention all the added code to your apk file. For now I'm still using AppCompatPreferenceActivity since my preference screens are quite simple.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment