How could I use the same set of preference screens for all Android versions from 2.X to 4.X?

后端 未结 3 1967
我在风中等你
我在风中等你 2020-12-14 21:21

NOTICE: Please save yourself some time and refer to the accepted answer, no need to read all the quesiton.
You may read the rest of the question and the

相关标签:
3条回答
  • 2020-12-14 21:33

    If you are on the latest ADT plugin, there is an option to easily create a preference Activity that supports most older Android versions as well as all the new ones.

    Right click on your project -> Other -> Android Activity

    Then choose SettingsActivity enter image description here

    The Activity created will take take care of working with both high and low API versions since it uses if statements to choose the appropriate method of displaying the preferences.


    EDIT
    A good point was brought up: Phone-Sized devices, regardless of API version use the old PreferenceActivity methods.

    The quickest way to get API 11+ devices to use Fragments is to remove !isXLargeTablet(context); from isSimplePreferences()

    private static boolean isSimplePreferences(Context context) {
        return ALWAYS_SIMPLE_PREFS
                || Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB;
    }
    

    However, now the user has more navigation to do.
    Headers as root

    This is because onBuildHeaders() is called.

    To get rid of this, we will need to make our own PreferenceFragment that adds each xml resource.

    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
        public static class AllPreferencesFragment extends PreferenceFragment{
            @Override
            public void onCreate (Bundle savedInstanceState){
                super.onCreate(savedInstanceState);
                addPreferencesFromResource(R.xml.pref_general);
    
                // Add 'notifications' preferences, and a corresponding header.
                PreferenceCategory fakeHeader = new PreferenceCategory(getActivity());
                fakeHeader.setTitle(R.string.pref_header_notifications);
                getPreferenceScreen().addPreference(fakeHeader);
                addPreferencesFromResource(R.xml.pref_notification);
    
                // Add 'data and sync' preferences, and a corresponding header.
                fakeHeader = new PreferenceCategory(getActivity());
                fakeHeader.setTitle(R.string.pref_header_data_sync);
                getPreferenceScreen().addPreference(fakeHeader);
                addPreferencesFromResource(R.xml.pref_data_sync);
    
                // Bind the summaries of EditText/List/Dialog/Ringtone preferences to
                // their values. When their values change, their summaries are updated
                // to reflect the new value, per the Android Design guidelines.
                bindPreferenceSummaryToValue(findPreference("example_text"));
                bindPreferenceSummaryToValue(findPreference("example_list"));
                bindPreferenceSummaryToValue(findPreference("notifications_new_message_ringtone"));
                bindPreferenceSummaryToValue(findPreference("sync_frequency"));
            }
        }
    

    If you can determine the screen size from outside the Activity that launches the settings, you can specify a fragment for it to launch via EXTRA_SHOW_FRAGMENT

    i.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT, "com.example.test.SettingsActivity$AllPreferencesFragment");
    

    Or you can have the SettingsActivity determine whether or not to show this Fragment (assuming you're happy with the isXLargeTablet() method.

    Change onBuildHeaders() to:

    @Override
    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    public void onBuildHeaders(List<Header> target) {
        if (!isSimplePreferences(this) && isXLargeTablet(this)) {
            loadHeadersFromResource(R.xml.pref_headers, target);
        }
    }
    

    Add this method:

    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    private void setupNewApiPhoneSizePreferences() {
        if (!isXLargeTablet(this) && Build.VERSION.SDK_INT > Build.VERSION_CODES.HONEYCOMB){
                getFragmentManager().beginTransaction().replace(android.R.id.content, new AllPreferencesFragment()).commit();
        }
    }
    

    And in onPostCreate() add the method call.

    setupNewApiPhoneSizePreferences();
    

    This should now use non-deprecated calls from API 11 onwards.

    0 讨论(0)
  • 2020-12-14 21:39

    Well, working with the autogenerated SettingsActivity got pretty old pretty quickly. One has to scroll up and down past boilerplate code - moreover it's full of yellow warnings and I hate yellow (deprecated warnings can't be avoided altogether though - see What to use instead of "addPreferencesFromResource" in a PreferenceActivity?, where also the matter of how to make cross API PreferenceActivity is touched also - and Was PreferenceFragment intentionally excluded from the compatibility package? for a discussion). And also you may easily get an NPE - did you know that onPostCreate() is actually onPostStart() - so findPreference() returns null in onStart().

    Now there are solutions involving reflection but reflection is to be avoided (like hell it is) - and since we are not interested in pre 2 versions of android reflection can be avoided (see Is checking SDK_INT enough or is lazy loading needed for using newer android APIs ? Why?). Also there are solutions involving choosing a class at runtime - but having 2 classes sucks and is not OOP anyways (for those and other solutions see the answer to related question: PreferenceActivity Android 4.0 and earlier).

    So I came up with an abstract base class, which is the correct Java and OO way of doing things (except if you need Eclair and below where you do need reflection and/or lazy loading of classes to avoid VerifyErrors), where I moved the autogenerated boilerplate code:

    import android.annotation.TargetApi;
    import android.content.Context;
    import android.content.res.Configuration;
    import android.os.Build;
    import android.os.Bundle;
    import android.preference.PreferenceActivity;
    import android.preference.PreferenceFragment;
    
    import java.util.List;
    
    /**
     * A {@link PreferenceActivity} that presents a set of application settings. On
     * handset devices, settings are presented as a single list. On tablets,
     * settings are split by category, with category headers shown to the left of
     * the list of settings.
     * <p>
     * See <a href="http://developer.android.com/design/patterns/settings.html">
     * Android Design: Settings</a> for design guidelines and the <a
     * href="http://developer.android.com/guide/topics/ui/settings.html">Settings
     * API Guide</a> for more information on developing a Settings UI.
     *
     * Defines two abstract methods that need be implemented by implementators.
     */
    public abstract class BaseSettings extends PreferenceActivity {
    
        /**
         * Determines whether to always show the simplified settings UI, where
         * settings are presented in a single list. When false, settings are shown
         * as a master/detail two-pane view on tablets. When true, a single pane is
         * shown on tablets.
         */
        private static final boolean ALWAYS_SIMPLE_PREFS = false;
    
        /**
         * Helper method to determine if the device has an extra-large screen. For
         * example, 10" tablets are extra-large.
         */
        @TargetApi(Build.VERSION_CODES.GINGERBREAD)
        private static boolean isXLargeTablet(Context context) {
            return (context.getResources().getConfiguration().screenLayout &
                    Configuration.SCREENLAYOUT_SIZE_MASK)
                    >= Configuration.SCREENLAYOUT_SIZE_XLARGE;
        }
    
        /** {@inheritDoc} */
        @Override
        public final boolean onIsMultiPane() { // never used by us
            return isXLargeTablet(this) && !isSimplePreferences(this);
        }
    
        /**
         * Determines whether the simplified settings UI should be shown. This is
         * true if this is forced via {@link #ALWAYS_SIMPLE_PREFS}, or the device
         * doesn't have newer APIs like {@link PreferenceFragment}, or the device
         * doesn't have an extra-large screen. In these cases, a single-pane
         * "simplified" settings UI should be shown.
         */
        private static final boolean isSimplePreferences(Context context) {
            return ALWAYS_SIMPLE_PREFS
                || Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB
                || !isXLargeTablet(context);
        }
    
        @Override
        protected final void onCreate(Bundle savedInstanceState) {
            // disallow onCreate(), see comment in onPostCreate()
            super.onCreate(savedInstanceState);
        }
    
        @Override
        protected final void onStart() {
            // disallow onStart(), see comment in onPostCreate()
            super.onStart();
        }
    
        @Override
        protected void onPostCreate(Bundle savedInstanceState) {
            // onPostCreate() probably is needed because onBuildHeaders() is called
            // after onCreate() ? This piece of err code should be called
            // onPostStart() btw - so yeah
            super.onPostCreate(savedInstanceState);
            setupSimplePreferencesScreen();
            // findPreference will return null if setupSimplePreferencesScreen
            // hasn't run, so I disallow onCreate() and onStart()
        }
    
        /**
         * Shows the simplified settings UI if the device configuration if the
         * device configuration dictates that a simplified, single-pane UI should be
         * shown.
         */
        private void setupSimplePreferencesScreen() {
            if (!isSimplePreferences(this)) {
                return;
            }
            buildSimplePreferences();
        }
    
        /** {@inheritDoc} */
        /*
         * Subclasses of PreferenceActivity should implement onBuildHeaders(List) to
         * populate the header list with the desired items. Doing this implicitly
         * switches the class into its new "headers + fragments" mode rather than
         * the old style of just showing a single preferences list (from
         * http://developer
         * .android.com/reference/android/preference/PreferenceActivity.html) -> IE
         * this is called automatically - reads the R.xml.pref_headers and creates
         * the 2 panes view - it was driving me mad - @inheritDoc my - It does not
         * crash in Froyo cause isSimplePreferences is always true for
         * Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB - @Override has
         * nothing to do with runtime and of course on Froyo this is never called by
         * the system
         */
        @Override
        @TargetApi(Build.VERSION_CODES.HONEYCOMB)
        public final void onBuildHeaders(List<Header> target) {
            if (!isSimplePreferences(this)) {
                loadHeadersFromResource(getHeadersXmlID(), target);
            }
        }
    
        // =========================================================================
        // Abstract API
        // =========================================================================
        /**
         * Must return an id for the headers xml file. There you define the headers
         * and the corresponding PreferenceFragment for each header which you must
         * of course implement. This is used in the super implementation of
         * {@link #onBuildHeaders(List)}
         *
         * @return an id from the R file for the xml containing the headers
         */
        abstract int getHeadersXmlID();
    
        /**
         * Builds a pre Honeycomb preference screen. An implementation would use the
         * (deprecated)
    *{@link android.preference.PreferenceActivity#addPreferencesFromResource(int)}
         */
        abstract void buildSimplePreferences();
    }
    

    And a sample implementation:

    public final class SettingsActivity extends BaseSettings implements
            OnSharedPreferenceChangeListener {
    
        private static final int PREF_HEADERS_XML = R.xml.pref_headers;
        private static CharSequence master_enable;
        private OnPreferenceChangeListener listener;
        private static Preference master_pref;
        private static final String TAG = SettingsActivity.class.getSimpleName();
        private SharedPreferences sp;
        /** Used as canvas for the simple preferences screen */
        private static final int EMPTY_PREF_RESOURCE = R.xml.pref_empty;
        private static int PREF_RESOURCE_SETTINGS = R.xml.pref_data_sync;
    
        // abstract overrides   
        @Override
        int getHeadersXmlID() {
            return PREF_HEADERS_XML;
        }
    
        @Override
        void buildSimplePreferences() {
            // In the simplified UI, fragments are not used at all and we instead
            // use the older PreferenceActivity APIs.
            // THIS is a blank preferences layout - which I need so
            // getPreferenceScreen() does not return null - so I can add a header -
            // alternatively you can very well comment everything out apart from
            // addPreferencesFromResource(R.xml.pref_data_sync);
            addPreferencesFromResource(EMPTY_PREF_RESOURCE);
            // Add 'data and sync' preferences, and a corresponding header.
            PreferenceCategory fakeHeader = new PreferenceCategory(this);
            fakeHeader.setTitle(R.string.pref_header_data_sync);
            getPreferenceScreen().addPreference(fakeHeader);
            addPreferencesFromResource(PREF_RESOURCE_SETTINGS);
        }
    
        // here is the work done
        @Override
        protected void onPostCreate(Bundle savedInstanceState) {
            super.onPostCreate(savedInstanceState);
            master_enable = getResources().getText(
                R.string.enable_monitoring_master_pref_key);
            listener = new ToggleMonitoringListener();
            // DefaultSharedPreferences - register listener lest Monitor aborts
            sp = PreferenceManager.getDefaultSharedPreferences(this);
            sp.registerOnSharedPreferenceChangeListener(this);
            master_pref = findPreference(master_enable.toString());
        }
    
        @Override
        protected void onResume() {
            super.onResume();
            master_pref.setOnPreferenceChangeListener(listener); // no way to
            // unregister, see: https://stackoverflow.com/a/20493608/281545 This
            // listener reacts to *manual* updates - so no need to be active
            // outside onResume()/onPause()
        }
    
        @Override
        protected void onDestroy() {
            // may not be called (as onDestroy() is killable), but no leak,
            // see: https://stackoverflow.com/a/20493608/281545
            sp.unregisterOnSharedPreferenceChangeListener(this);
            super.onDestroy();
        }
    
        /**
         * Toggles monitoring and sets the preference summary.Triggered on *manual*
         * update of the *single* preference it is registered with, but before this
         * preference is updated and saved.
         */
        private static class ToggleMonitoringListener implements
                OnPreferenceChangeListener {
    
            ToggleMonitoringListener() {}
    
            @Override
            public boolean
                    onPreferenceChange(Preference preference, Object newValue) {
                if (newValue instanceof Boolean) {
                    final boolean enable = (Boolean) newValue;
                    Monitor.enableMonitoring(preference.getContext(), enable);
                    final CheckBoxPreference p = (CheckBoxPreference) preference;
                    preference.setSummary((enable) ? p.getSummaryOn() : p
                        .getSummaryOff());
                    return true;
                }
                return false;
            }
        }
    
        /**
         * This fragment is used when the activity is showing a two-pane
         * settings UI.
         */
        @TargetApi(Build.VERSION_CODES.HONEYCOMB)
        public final static class DataSyncPreferenceFragment extends
                PreferenceFragment {
    
            @Override
            public void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                Log.w(TAG, "onCreate");
                addPreferencesFromResource(PREF_RESOURCE_SETTINGS);
                master_pref = findPreference(master_enable.toString());
            }
        }
    
        @Override
        public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
                String key) {
            if (master_enable == null || master_pref == null) return;
            if (master_enable.toString().equals(key)) {
                refreshMasterPreference();
            }
        }
    
        /**
         * @param key
         */
        private void refreshMasterPreference() {
            final Boolean isMonitoringEnabled = AccessPreferences.get(this,
                master_enable.toString(), false);
            Log.w(TAG, "Stored value: " + isMonitoringEnabled);
            final CheckBoxPreference p = (CheckBoxPreference) master_pref;
            final boolean needsRefresh = p.isChecked() != isMonitoringEnabled;
            if (needsRefresh) {
                p.setChecked(isMonitoringEnabled);
                p.setSummary((isMonitoringEnabled) ? p.getSummaryOn() : p
                    .getSummaryOff());
            }
        }
    }
    

    So the main idea is you provide an xml for preferences with headers:

        public final void onBuildHeaders(List<Header> target) {
            if (!isSimplePreferences(this)) {
                loadHeadersFromResource(getHeadersXmlID(), target);
            }
        }
    

    where:

        @Override
        int getHeadersXmlID() {
            return PREF_HEADERS_XML;
        }
    

    and PREF_HEADERS_XML:

    <preference-headers xmlns:android="http://schemas.android.com/apk/res/android" >
        <!-- These settings headers are only used on tablets. -->
        <header
            android:fragment=".activities.SettingsActivity$DataSyncPreferenceFragment"
            android:title="@string/pref_header_data_sync" />
    </preference-headers>
    

    and setting up the simple preferences in buildSimplePreferences()

    I am interested into making this into a more general API - probably including the sBindPreferenceSummaryToValueListener - so ideas welcome.

    Ah, yes, the sBindPreferenceSummaryToValueListener fluff:

    // FLUFF AHEAD:
    // the fluff that follows is for binding preference summary to value -
    // essentially wrappers around OnPreferenceChangeListener - just so
    // you get an idea of the mess this autogenerated piece of, code, was
    // formatter:off
    /**
     * A preference value change listener that updates the preference's summary
     * to reflect its new value.
     */
    /* private static Preference.OnPreferenceChangeListener
            sBindPreferenceSummaryToValueListener =
            new Preference.OnPreferenceChangeListener() {
    
            @Override
            public boolean onPreferenceChange(Preference preference,
                                Object value) {
                String stringValue = value.toString();
                if (preference instanceof ListPreference) {
                    // For list preferences, look up the correct display value
                    // in the preference's 'entries' list.
                    ListPreference listPreference = (ListPreference) preference;
                    int index = listPreference.findIndexOfValue(stringValue);
                    // Set the summary to reflect the new value.
                    preference.setSummary(index >= 0
                            ? listPreference.getEntries()[index] : null);
                } else if (preference instanceof RingtonePreference) {
                    // For ringtone preferences, look up the correct display
                    // value using RingtoneManager.
                    if (TextUtils.isEmpty(stringValue)) {
                        // Empty values correspond to 'silent' (no ringtone).
                        // preference.setSummary(R.string.pref_ringtone_silent);
                    } else {
                        Ringtone ringtone = RingtoneManager.getRingtone(
                            preference.getContext(), Uri.parse(stringValue));
                        if (ringtone == null) {
                            // Clear the summary if there was a lookup error.
                            preference.setSummary(null);
                        } else {
                            // Set the summary to reflect the new ringtone
                            // display name.
                            String name = ringtone
                                .getTitle(preference.getContext());
                            preference.setSummary(name);
                        }
                    }
                } else if (preference instanceof CheckBoxPreference) {
                    boolean b = (Boolean) value;
                    Log.w(TAG, "::::value " + b);
                    final CheckBoxPreference p =(CheckBoxPreference)preference;
                    preference.setSummary((b) ? p.getSummaryOn() : p
                        .getSummaryOff());
                    Log.w(TAG, p.getKey() + " :: " + p.isChecked());
                } else {
                    // For all other preferences, set the summary to the value's
                    // simple string representation.
                    preference.setSummary(stringValue);
                }
                return true;
            }
        }; */
    
    /**
     * Binds a preference's summary to its value. More specifically, when the
     * preference's value is changed, its summary (line of text below the
     * preference title) is updated to reflect the value. The summary is also
     * immediately updated upon calling this method. The exact display format is
     * dependent on the type of preference.
     *
     * @see #sBindPreferenceSummaryToValueListener
     */
    /* private static void bindPreferenceSummaryToValue(Preference preference) {
        // Set the listener to watch for value changes.
        preference
          .setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener);
        // Trigger the listener immediately with the preference's
        // current value.
        sBindPreferenceSummaryToValueListener.onPreferenceChange(
            preference,
            PreferenceManager.getDefaultSharedPreferences(
                preference.getContext()).getString(preference.getKey(), ""));
    } */
    
    0 讨论(0)
  • 2020-12-14 21:48

    You can use this class to display a preference screen in all Android versions from 2.X to 4.X, by feeding it with a preference screen resource.

    You may use it directly by renaming it if you like, but I'd suggest you to add it to your project as is, and inherit from it, which is much cleaner if you need to work with several parent preference screens.

    If you'd like to use it directly, just replace prefs value with your preference screen resource ID.

    If you'd like to inherit from it, you should do it like this:

    import android.os.Bundle;
    
    public class MyPreferencesActivity extends CompatiblePreferenceActivity
    {   
        @Override
        protected void onCreate(final Bundle savedInstanceState)
        {
            setPrefs(R.xml.mypreferencesactivity);
            super.onCreate(savedInstanceState);
        }   
    }
    

    ALWAYS call setPrefs(int) before calling super.onCreate(Bundle)

    If, for some reason, you'd just like to take advantage of the glitch-fix and create preferences on your own, you may either just copy the glitch-fix code into your own preference activity, or inherit from the class and catch the PrefsNotSet exception as follows:

    import android.os.Bundle;
    
    public class MyPreferencesActivity extends CompatiblePreferenceActivity
    {   
        @Override
        protected void onCreate(final Bundle savedInstanceState)
        {
            try{
                super.onCreate(savedInstanceState);
            }catch(PrefsNotSetException e){};
        }   
    }
    

    And finally, the class:

    import android.annotation.TargetApi;
    import android.os.Bundle;
    import android.preference.Preference;
    import android.preference.PreferenceActivity;
    import android.preference.PreferenceFragment;
    import android.preference.PreferenceScreen;
    
    public class CompatiblePreferenceActivity extends PreferenceActivity
    {
        private int prefs=0;
    
        //Get/Set
        public void setPrefs(int prefs)
        {
            this.prefs=prefs;
        }
    
        //Exception
        protected static class PrefsNotSetException extends RuntimeException
        {
            private static final long serialVersionUID = 1L;
            PrefsNotSetException()
            {
                super("\"prefs\" should be set to a valid preference resource ID.");
            }
        }
    
        //Creation
        @Override
        protected void onCreate(final Bundle savedInstanceState)
        {
            super.onCreate(savedInstanceState);
            if (prefs==0)
                throw new PrefsNotSetException();
            else
                try {
                    getClass().getMethod("getFragmentManager");
                    AddResourceApi11AndGreater();
                    }
                catch (NoSuchMethodException e) { //Api < 11
                        AddResourceApiLessThan11();
                    }
        }
    
        @SuppressWarnings("deprecation")
        protected void AddResourceApiLessThan11()
        {
            addPreferencesFromResource(prefs);
        }
    
        @TargetApi(11)
        protected void AddResourceApi11AndGreater()
        {
            PF.prefs=prefs;
            getFragmentManager().beginTransaction().replace(
                android.R.id.content, new PF()).commit();
        }
    
        @TargetApi(11)
        public static class PF extends PreferenceFragment
        {
            private static int prefs;
            @Override
            public void onCreate(final Bundle savedInstanceState)
            {
                super.onCreate(savedInstanceState);
                addPreferencesFromResource(prefs);
            }
        }
    
        //Sub-screen background glitch fix
        @SuppressWarnings("deprecation")
        @Override
        public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen,
            Preference preference)
        {
            super.onPreferenceTreeClick(preferenceScreen, preference);
            if (preference!=null)
                if (preference instanceof PreferenceScreen)
                    if (((PreferenceScreen)preference).getDialog()!=null)
                        ((PreferenceScreen)preference).getDialog().
                            getWindow().getDecorView().
                            setBackgroundDrawable(this.getWindow().
                                getDecorView().getBackground().getConstantState().
                                newDrawable());
            return false;
        }
    }
    
    0 讨论(0)
提交回复
热议问题