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 :
And this one on a smartphone :
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;
}
}
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;
}
}
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With