How do I create custom preferences using android.support.v7.preference library?
Asked Answered
P

4

24

I want to support at least api 10, I want to be able to style my preferences nicely, I want to be able to have headers (or to show PreferenceScreens). It seems that PreferenceActivity, not fully supported by AppCompat's coloring, will not fit. So I'm trying to use AppCompatActivity and PreferenceFragmentCompat.

public class Prefs extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (savedInstanceState == null)
            getSupportFragmentManager().beginTransaction()
                    .replace(android.R.id.content, new PreferencesFragment())
                    .commit();
    }

    public static class PreferencesFragment extends PreferenceFragmentCompat {
        @Override public void onCreate(final Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            addPreferencesFromResource(R.xml.preferences);
        }

        @Override
        public void onDisplayPreferenceDialog(Preference preference) {
            // the following call results in a dialogue being shown
            super.onDisplayPreferenceDialog(preference);
        }

        @Override public void onNavigateToScreen(PreferenceScreen preferenceScreen) {
            // I can probably use this to go to to a nested preference screen
            // I'm not sure...
        }
    }
}

Now, I want to create a custom preference that will provide the choice of a font. With PreferenceActivity, I could simply do

import android.preference.DialogPreference;

public class FontPreference extends DialogPreference {

    public FontPreference(Context context, AttributeSet attrs) {super(context, attrs);}

    @Override protected void onPrepareDialogBuilder(Builder builder) {
        super.onPrepareDialogBuilder(builder);
        // do something with builder and make a nice cute dialogue, for example, like this
        builder.setSingleChoiceItems(new FontAdapter(), 0, null);
    }
}

and use xml such as this to display it

<my.app.FontPreference android:title="Choose font" android:summary="Unnecessary summary" />

But now, there is no onPrepareDialogBuilder in android.support.v7.preference.DialogPreference. Instead, it's been moved to PreferenceDialogFragmentCompat. I found little information on how to use that thing, and I'm not sure how to go from xml to displaying it. v14 preference fragment has the following code:

public void onDisplayPreferenceDialog(Preference preference) {
    ...

    final DialogFragment f;
    if (preference instanceof EditTextPreference)
        f = EditTextPreferenceDialogFragment.newInstance(preference.getKey());
    ...
    f.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
}

I tried subclassing android.support.v7.preference.DialogPreference and having onDisplayPreferenceDialog use a similar piece of code to instantiate a dummy FontPreferenceFragment but it fails with the following exception.

java.lang.IllegalStateException: Target fragment must implement TargetFragment interface

At this point I'm already too deep into the mess and don't want to dig further. Google knows nothing about this exception. Anyways, this method seems to be overly complicated. So, what's the best way to create custom preferences using android.support.v7.preference library?

Paulson answered 17/9, 2015 at 1:50 Comment(0)
O
40

Important note: Currently (v23.0.1 of the v7 library) there are still a lot of theme-issues with the 'PreferenceThemeOverlay'(see this issue). On Lollipop for example, you end up with Holo-styled category headers.

After some frustrating hours, I finally succeeded to create a custom v7 Preference. Creating your own Preference appears to be harder than you might think is needed. So make sure to take some time.

At first you might be wondering why you will find both a DialogPreference and a PreferenceDialogFragmentCompat for each preference type. As it turns out, the first one is the actual preference, the second is the DialogFragment where the preference would be displayed in. Sadly, you are required to subclass both of them.

Don't worry, you won't need to change any piece of code. You only need to relocate some methods:

  • All preference-editing methods (like setTitle() or persist*()) can be found in the DialogPreference class.
  • All dialog (-editing) methods (onBindDialogView(View) & onDialogClosed(boolean)) have been moved to PreferenceDialogFragmentCompat.

You might want your existing class to extend the first one, that way you don't have to change to much I think. Autocomplete should help you find missing methods.

When you have completed the above steps, it is time to bind these two classes together. In your xml file, you will refer to the preference-part. However, Android doesn't know yet which Fragment it must inflate when your custom preference needs to be. Therefore, you need to override onDisplayPreferenceDialog(Preference):

@Override
public void onDisplayPreferenceDialog(Preference preference) {
    DialogFragment fragment;
    if (preference instanceof LocationChooserDialog) {
        fragment = LocationChooserFragmentCompat.newInstance(preference);
        fragment.setTargetFragment(this, 0);
        fragment.show(getFragmentManager(),
                "android.support.v7.preference.PreferenceFragment.DIALOG");
    } else super.onDisplayPreferenceDialog(preference);
}

and also your DialogFragment needs to handle the 'key':

public static YourPreferenceDialogFragmentCompat newInstance(Preference preference) {
    YourPreferenceDialogFragmentCompat fragment = new YourPreferenceDialogFragmentCompat();
    Bundle bundle = new Bundle(1);
    bundle.putString("key", preference.getKey());
    fragment.setArguments(bundle);
    return fragment;
}

That should do the trick. If you encounter problems, try taking a look at existing subclasses and see how Android solved it (in Android Studio: type a class' name and press Ctrl+b to see the decompiled class). Hope it helps.

Octal answered 27/9, 2015 at 20:41 Comment(6)
Almost a perfect solution! It took me a while to realize that onDisplayPreferenceDialog needed to be overriden in the preference fragment (which inherits PreferenceFragmentCompat). Thanks a lot!Trinitroglycerin
Just a note for anyone reading this: if you get a ClassCastException in PreferenceDialogFragmentCompat.java:57/58, make sure your bundle.putString("key", preference.getKey()); line says "key" as above. Otherwise, getArguments().getString(ARG_KEY) will fail with the ClassCastException.Chippy
Could you please post a full example if you have one? I succeeded to implement the new support Preferences, but the DialogPreferences are a nighmare. Google does not provide any documentation about it.Untune
This is a sensible answer. However, when I try to implement this, I get ClassCastException: java.lang.ClassCastException: android.support.v7.preference.PreferenceScreen cannot be cast to android.support.v7.preference.DialogPreference The exception is thrown in super.onCreate call of PreferenceDialogFragmentCompatCooky
@Chippy thanks, did you also try to change "key" to your own preference name? xDHotpress
what is the alternative to this now that setTargetFragment is deprecated?Flaxen
C
3

There is a good tutorial and Github project that explains in detail how to make a custom preference class that extends the Support Preference library:

The key points are:

  • You will need a custom DialogPreference or ListPreference, which controls how the preference row looks and functions. (It can also contain a reference to a layout that should display in the launched dialog). Add this DialogPreference to your XML preference file.

  • You will need a custom PreferenceDialogFragmentCompat, which controls the launching of the Dialog when the preference row is clicked. You can configure the Dialog's view in onBindDialogView().

  • In your preference screen which extends PreferenceFragmentCompat, override onDisplayPreferenceDialog() to launch your custom PreferenceDialogFragmentCompat.

  • You must only extend the support classes, not the platform classes. For example, extend androidx.preference.EditTextPreference instead of android.preference.EditTextPreference

Chilblain answered 3/4, 2020 at 17:41 Comment(0)
B
0

The exception is caused when your FontPreferenceFragment does not implement DialogPreference.TargetFragment. You'll need to make sure your fragment implements that interface.

Bombardon answered 17/9, 2015 at 2:8 Comment(1)
It does implement DialogPreference.TargetFragment. But even if implementing that solved anything, it'd still be a workaround. The right way must be way more simple.Paulson
I
0

Notice about deprecated fragment.setTargetFragment(this, 0); Sadly, for now, we must keep this deprecated method until PreferenceDialogFragmentCompat.java source deprecates getTargetFragment from onCreate(). recent google source commit:

Also, you can check it in latest source: PreferenceDialogFragmentCompat.java

Inlier answered 17/9, 2022 at 13:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.