Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android - Headers categories in PreferenceActivity with PreferenceFragment

I would like to display a preference screen like the one in the Android settings app : using headers, PreferenceActivity, PreferenceFragment and headers categories.

I wan't this result on a tablet :

enter image description here

And this one on a smartphone :

enter image description here

It works if I just use the basic headers, but if I try to add categories, it works on the smartphone, and crash on the tablet, where I get the exception "java.lang.NullPointerException: name == null" :

FATAL EXCEPTION: main
java.lang.RuntimeException: Unable to start activity ComponentInfo{fr.ifremer.testandroid/fr.ifremer.testandroid.models.preferences.MainPreferenceActivity}: java.lang.NullPointerException: name == null
    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2110)
    at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2135)
    at android.app.ActivityThread.access$700(ActivityThread.java:140)
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1237)
    at android.os.Handler.dispatchMessage(Handler.java:99)
    at android.os.Looper.loop(Looper.java:137)
    at android.app.ActivityThread.main(ActivityThread.java:4921)
    at java.lang.reflect.Method.invokeNative(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:511)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1038)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:805)
    at dalvik.system.NativeStart.main(Native Method)
Caused by: java.lang.NullPointerException: name == null
    at java.lang.VMClassLoader.findLoadedClass(Native Method)
    at java.lang.ClassLoader.findLoadedClass(ClassLoader.java:354)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:491)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:461)
    at android.app.Fragment.instantiate(Fragment.java:574)
    at android.preference.PreferenceActivity.switchToHeaderInner(PreferenceActivity.java:1222)
    at android.preference.PreferenceActivity.switchToHeader(PreferenceActivity.java:1255)
    at android.preference.PreferenceActivity.onCreate(PreferenceActivity.java:630)
    at fr.ifremer.testandroid.models.preferences.MainPreferenceActivity.onCreate(MainPreferenceActivity.java:19)
    at android.app.Activity.performCreate(Activity.java:5206)
    at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1094)
    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2074)
    ... 11 more

Bellow are the pieces of code involved. I got them mostly from the Android settings app source.

Any idea ?

Thanks in advance


MainPreferenceActivity :

public class MainPreferenceActivity extends PreferenceActivity {

    private static List<Header> _headers;

    @Override
    public void onBuildHeaders(List<Header> headers) {

        _headers = headers;
        loadHeadersFromResource(R.xml.preference_headers, headers);
    }

    @Override
    public void setListAdapter(ListAdapter adapter) {

        if (adapter == null) {
            super.setListAdapter(null);
        } else {
            super.setListAdapter(new HeaderAdapter(this, _headers));
        }
    }
}

PreferencesFragment :

public class PreferencesFragment extends PreferenceFragment {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        String settings = getArguments().getString("settings");

        if (settings.equals("DIVE")) {

            addPreferencesFromResource(R.xml.preference_dive_tile);
        }
        else if (settings.equals("MAP")) {

            addPreferencesFromResource(R.xml.preference_map_tile);
        }
    }
}

preference_headers.xml :

<?xml version="1.0" encoding="utf-8"?>
<preference-headers xmlns:android="http://schemas.android.com/apk/res/android" >

    <header
        android:id="@+id/header_section_1"
        android:title="Section 1" />

    <header
        android:fragment="fr.ifremer.testandroid.models.preferences.PreferencesFragment"
        android:summary="DIVE summary"
        android:title="DIVE title" >
        <extra
            android:name="settings"
            android:value="DIVE" />
    </header>
    <header
        android:fragment="fr.ifremer.testandroid.models.preferences.PreferencesFragment"
        android:summary="MAP summary"
        android:title="MAP title" >
        <extra
            android:name="settings"
            android:value="MAP" />
    </header>

</preference-headers>

Last but not least, HeaderAdapter :

public class HeaderAdapter extends ArrayAdapter<Header> {

    static final int HEADER_TYPE_CATEGORY = 0;
    static final int HEADER_TYPE_NORMAL = 1;
    private static final int HEADER_TYPE_COUNT = HEADER_TYPE_NORMAL + 1;

    private LayoutInflater mInflater;

    private static class HeaderViewHolder {
        ImageView icon;
        TextView title;
        TextView summary;
    }

    public HeaderAdapter(Context context, List<Header> objects) {

        super(context, 0, objects);

        mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }

    static int getHeaderType(Header header) {

        if (header.fragment == null && header.intent == null) return HEADER_TYPE_CATEGORY;
        else return HEADER_TYPE_NORMAL;
    }

    @Override
    public int getItemViewType(int position) {
        Header header = getItem(position);
        return getHeaderType(header);
    }

    @Override
    public boolean areAllItemsEnabled() { return false; /* because of categories */ }

    @Override
    public boolean isEnabled(int position) { return getItemViewType(position) != HEADER_TYPE_CATEGORY; }

    @Override
    public int getViewTypeCount() { return HEADER_TYPE_COUNT; }

    @Override
    public boolean hasStableIds() { return true; }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        HeaderViewHolder holder;
        Header header = getItem(position);
        int headerType = getHeaderType(header);
        View view = null;

        if (convertView == null) {

            holder = new HeaderViewHolder();

            switch (headerType) {

                case HEADER_TYPE_CATEGORY:

                    view = new TextView(getContext(), null, android.R.attr.listSeparatorTextViewStyle);
                    holder.title = (TextView) view;
                    break;

                case HEADER_TYPE_NORMAL:

                    view = mInflater.inflate(R.layout.preference_header_item, parent, false);
                    holder.icon = (ImageView) view.findViewById(R.id.icon);
                    holder.title = (TextView) view.findViewById(R.id.title);
                    holder.summary = (TextView) view.findViewById(R.id.summary);
                    break;
            }

            view.setTag(holder);
        }
        else {

            view = convertView;
            holder = (HeaderViewHolder) view.getTag();
        }

        // All view fields must be updated every time, because the view may be recycled
        switch (headerType) {

            case HEADER_TYPE_CATEGORY :

                holder.title.setText(header.getTitle(getContext().getResources()));
                break;

            case HEADER_TYPE_NORMAL :

                holder.icon.setImageResource(header.iconRes);

                holder.title.setText(header.getTitle(getContext().getResources()));
                CharSequence summary = header.getSummary(getContext().getResources());

                if (!TextUtils.isEmpty(summary)) {

                    holder.summary.setVisibility(View.VISIBLE);
                    holder.summary.setText(summary);
                }
                else {
                    holder.summary.setVisibility(View.GONE);
                }
                break;
        }

        return view;
    }
}
like image 507
Tim Autin Avatar asked Mar 21 '13 15:03

Tim Autin


1 Answers

As bestofbest1 said, the problem was that Android tried to show the first element in the preferences_headers.xml, which did not contain a fragment.

To fix it, I added in MainPreferenceActivity's onCreate the line below (BEFORE super.onCreate) to select a default fragment when using a tablet :

if(onIsMultiPane()) getIntent().putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT, PreferencesFragment.class.getName());

I also set a default fragment in PreferencesFragment :

String settings = "DIVE";
if(getArguments() != null) settings = getArguments().getString("settings");

Then a last problem, PreferenceActivity.EXTRA_SHOW_FRAGMENT does not select the header in the left side. To fix it in MainPreferencesActivity save a reference to your headers (in onBuildHeaders), and add :

@Override
protected void onResume() {

    // Call super :
    super.onResume();

    // Select the displayed fragment in the headers (when using a tablet) :
    // This should be done by Android, it is a bug fix
    if(_headers != null) {

        final String displayedFragment = getIntent().getStringExtra(EXTRA_SHOW_FRAGMENT);
        if (displayedFragment != null) {
            for (final Header header : _headers) {
                if (displayedFragment.equals(header.fragment)) {
                    switchToHeader(header);
                    break;
                }
            }
        }
    }
}
like image 119
Tim Autin Avatar answered Sep 27 '22 23:09

Tim Autin