Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
226 views
in Technique[技术] by (71.8m points)

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

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 answer I provided for an alternative (although less sophisticated) method.
Also, you may want to take advantage of the fix for the background glitch in Android 2.X, by adding the related piece of code to your preference activity class.

Background
Being a newbie to Android coding, but somewhat experienced in other programming languages/frameworks, I was expecting my walk to Android application coding would be a rather pleasant one. It was so, until I stumbled upon this problem:

Eclipse wizard for Android projects suggested I could reach a 95% of devices if I set my minimum API to 8 (Android 2.2). I didn't need to do any fancy things with my app anyways, so I thought, "sure, why not?". Everything was okay, except occasionally I'd find several methods/classes that were deprecated in most recent API versions, and so I had to devise ways to keep using the old ways for old devices, and try to use as much as possible the new ways for newer Android versions. This is one such occasion.

After using the Eclipse wizard for creating a preference activity, I realized that the Eclipse precompiler/parser/checker(or whatever it's called) Lint, would complain about not being able to use the new ways of creating/managing preferences in older API versions. So I thought, "all right, screw the new ways. Let's do it old way and since new API versions are supposed to be backward-compatible, it should be okay", but it wasn't. Old way used methods/classes that are marked as deprecated; which, to me, means, even though they'd still work in current API, they'd stop working at some point in future releases.

So I started searching for the right way to do this, and finally hit this page: What to use instead of "addPreferencesFromResource" in a PreferenceActivity? where Garret Wilson, explains a way to use old preference screen resources in a way compatible with the new ways. It was great, and finally had the feeling I could move on with my app coding, except it wouldn't work when targeting older APIs, as it was using newer APIs code. So I had to devise a way to make it work for both old APIs and newer. After tinkering with it for a while I managed to find a way, by using precompiler(or whatever it's called) annotations and the great getClass().getMethod() along with exceptions.

Everything seemed to work flawlessly until I created a preference sub-screen. It was displaying correctly in newer Android versions, but when I tried in older ones, I could merely see a black screen. After much searching, I found this page which explains the issue: http://code.google.com/p/android/issues/detail?id=4611 This is apparently a known glitch that's been around several Android versions for a good while. I read the whole thread and found several proposed solutions to the problem, but I really didn't like entirely any of them. I, for one, prefer to avoid as much static stuff as I can, and do things programmatically. I prefer automation over repetitive work. Some solutions suggested to create sub-screens as parent screens, then adding them onto the manifest file, and calling them from the parent screen through an intent. I'd really hate having to keep track of those things: entries in manifest, separated screen resource file, intents... So that was a no-no for me. I kept looking and found a programmatic approach I liked much better... only to find that it didn't work. It consisted of iterating through the whole view tree of the preference screen and assigning a proper background to preference sub-screens, but it just didn't work because, as I later found out after much debugging, preference sub-screens views are not a child of preference screen views. I had to find a way to achieve this myself. I tried as many things as I could think of, researched and researched to no avail. I was at the verge of abandoning at several occasions, but after some two weeks of continued effort and much debugging I found a workaround, which I posted in comment #35.

Opinion
It really isn't the perfect solution/approach, and I'm aware of several of its drawbacks, but it's one that works, so I decided I would share it. Hopefully I'm not being too ridiculous in my enthusiasm to share what has taken me what I'd consider quite a lot of effort, as I'm aware it's not that great of an issue, that any experienced coder could solve. But hey, I think sharing knowledge makes me a bit better, no matter how much I brag, than an experienced coder who keeps everything to himself. Just sharing my opinion, because I can't believe nobody ever had this problem before, but I do believe many have had it and didn't bother to share their knowledge.

I present you in the answer with a proposed class to use over several versions of Android, and some suggestions on its usage. I'm open to discussion and contributions to make it a better class.

Known issues:

  • Parent screen Decor view background is cloned onto child screen Decor view background, which apparently isn't the normal behavior.
    Status: dismissed until somebody comes up with a good reason to fix this
  • Crashes upon screen rotation
    Status: Fixed.
    Probably related to resource visibility by newer API implementation (inner class PF)
    Apparently inherited classes from preferenceFragment need to have all their members static. I guess it makes sense if you're supposed to inherit every time you need to use a new fragment
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...