PreferenceFragmentCompat custom layout
Asked Answered
Z

3

21

I need a custom layout for my PreferenceFragmentCompat. In the docs for PreferenceFragmentCompat it seems that you can possibly inflate and return a view in onCreateView().

However a NPE results:-

Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.support.v7.widget.RecyclerView.setAdapter(android.support.v7.widget.RecyclerView$Adapter)' on a null object reference
                                                                       at android.support.v7.preference.PreferenceFragmentCompat.bindPreferences(PreferenceFragmentCompat.java:511)
                                                                       at android.support.v7.preference.PreferenceFragmentCompat.onActivityCreated(PreferenceFragmentCompat.java:316)
                                                                       at com.cls.example.MyPrefFrag.onActivityCreated(MyPrefFrag.java:42) 

After I checked the source of PreferenceFragmentCompat:onCreateView I found the following piece of code :-

 RecyclerView listView = this.onCreateRecyclerView(themedInflater, listContainer, savedInstanceState);
 if(listView == null) {
    throw new RuntimeException("Could not create RecyclerView");
 } else {
    this.mList = listView;   //problem
    ...
    return view;
 }

So if you override onCreateView() and return a custom layout the onCreateRecyclerView() is not called plus the RecyclerView private field mList will not be set. So the NPE on setAdapter() results.

Should I assume that having a custom layout is not feasible for PreferenceFragmentCompat ?

Zosema answered 13/3, 2016 at 5:25 Comment(7)
PreferenceFragment(Compat) was designed to provide a standard UI for settings. May I ask, why do you want to use a custom layout? Anyways, you can use a custom layout with custom preferences handling if you really need that.Cesaria
I got a custom view at the beginning of the custom layout which changes with each preference change. With native PreferenceFragment it is easy. You need to just embed a listview with id=android:id/list in your custom layout. PreferencefragmentCompat does not seem to have a similar functionality.Zosema
You can do the same with overriding onCreateRecyclerView(...) which should return the RecyclerView from your layout.Cesaria
That does not serve my requirement. The recycler view is used for the preferences. I need a custom layout.Zosema
Then you have to implement it your own way. The PreferenceFragmentCompat is for the standard layout, it does nothing special behind the scenes so it shouldn't be a problem implementing your custom settings UI with custom business logic using the classic SharedPreferences.Cesaria
Yes. But IMO its hardly a trivial task.Zosema
TBH I don't see the difference between PreferenceFragment with a ListView and PreferenceFragmentCompat with a RecyclerView. Both fill the given layouts with the preference layouts loaded according to the XML supplied as your preference list. If you just need custom list items, you can customize the individual layouts by overriding the style elements inside the PreferenceThemeOverlay (use CTRL + click in your IDE or check out the sources on the Internet to explore possible values).Cesaria
H
26

You can specify a custom layout in your theme.

For example:

styles.xml

<style name="YourTheme" parent="Theme.AppCompat">
    <!-- ... -->
    <item name="preferenceTheme">@style/YourTheme.PreferenceThemeOverlay</item>
</style>

<style name="YourTheme.PreferenceThemeOverlay" parent="@style/PreferenceThemeOverlay">
    <item name="android:layout">@layout/fragment_your_preferences</item>
</style>

fragment_your_preferences.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <android.support.v7.widget.Toolbar
            android:id="@+id/custom_toolbar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

    </android.support.design.widget.AppBarLayout>

    <!-- Required ViewGroup for PreferenceFragmentCompat -->
    <FrameLayout
        android:id="@android:id/list_container"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

    </FrameLayout>

</LinearLayout>

And then in onViewCreated() of your fragment class you can start using the views:

@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState)
{
    super.onViewCreated(view, savedInstanceState);

    Toolbar toolbar = (Toolbar)view.findViewById(R.id.custom_toolbar);

    if (toolbar != null)
    {
        getMainActivity().setSupportActionBar(toolbar);
    }
}
Hard answered 29/3, 2016 at 13:53 Comment(7)
I did try the custom layout with just a textview and list_container viewgroup in the theme. It did not work. The preferences did not get populated. Additionally on scrolling multiple textviews got scrolled. To further troubleshoot I tried making the custom layout exactly same as the library layout for preferencefragmentcompat and tried. Again the preferences did not get populated. Did this code work for you ?Zosema
Did you add the preferences in onCreate()? @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.preferences); } And are you using PreferenceFragmentCompat?Hard
No I added it in onCreatePreferences(). I dont know why I did that. I will add it in onCreate() and revert. I had given up and gone back to native fragments.Zosema
You're welcome! If in the future you run into a similar problem, you can open the class that gives you the problem like PreferenceFragmentCompat.class in Android Studio. When you open the class file, it should show you (decompiled) readable Java code. If you don't see Java code, install the Java Bytecode Decompiler plugin in the Android Studio settings. After that you can start digging around where the issue is: in this case, you can see that PreferenceFragmentCompat.onCreateView has a stylable layout and looks for R.id.listContainer.Hard
I tried same as you told me. but it didn't worked for me.Somnambulate
Why is the framelayout required for PreferenceFragmentCompat? Documentation ref? I am unable to display such PreferenceFragmentCompat, hence the question. Also valid for AndroidX? ThanksCubature
If you have multiple settings screens, you are basically doomed. Love the cr*ppy and useless solutions from GoogleGavel
V
3

I had the same problem, however my requirements were a bit different, I just needed to show a layout above the settings list, so I did this

<NestedScrollView>
  <ConstrainLayout>
    <CustomView>
    <SettingsFragmentHolder/>
  </ConstraintLayout>
</NestedScrollView>

Then later in code I just do this

FragmentManager.beginTransaction().replace(holder, PreferencesFragment()).commit()

One thing to note is that, preferences fragment has its own scrollview or a list, that will cause the NestedScrollView to scroll to the beginning of PreferencesFragment layout, to fix that add this to the parent layout of the PreferencesFragment, in this case it is ConstraintLayout

android:descendantFocusability="beforeDescendants"
android:focusableInTouchMode="true"

That will stop the default scrolling behavior

Vieva answered 22/6, 2018 at 13:27 Comment(1)
For me android:descendantFocusability="blocksDescendants" did the trick.Classical
S
0

You can. In my example I specified custom layout:

preferences_fragment.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <TextView
        android:text="Custom Layout"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"/>

    <FrameLayout
        android:id="@+id/settings_container_view"
        android:layout_width="wrap_content"
        android:layout_height="match_parent" />

    <TextView
        android:text="End of Layout"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"/>

</LinearLayout>

I created class it extends PreferenceFragmentCompat. You must override methods:

onCreatePreferences

@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
    setPreferencesFromResource(R.xml.app_main_preferences, rootKey);
}

This method sets your preferences. In my Example I use app_main_preferences.xml

app_main_preferences.xml

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

    <Preference
        app:key="logout"
        app:title="Logout"
        app:summary="Logout From The Application Account"/>

</PreferenceScreen>

onCreateView

@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View layout = inflater.inflate(R.layout.preferences_fragment, container, false);
        ViewGroup settingsContainerView = layout.findViewById(R.id.settings_container_view);

        Toolbar toolbar = layout.findViewById(R.id.toolbar);

        MainActivity mainActivity = (MainActivity) requireActivity();
        mainActivity.setSupportActionBar(toolbar);

        View settingsView = super.onCreateView(inflater, settingsContainerView, savedInstanceState);
        settingsContainerView.addView(settingsView);
        return layout;
}

Congratulations!

enter image description here

How it works:

In our custom layout we create FrameLayout it's our container for settings, Toolbar and TextView widgets.

Then we create class it extends PreferenceFragmentCompat class. We override methods. In onCreatePreferences method we set our setting's layout using setPreferencesFromResource.

And our "Main" method is onCreateView method.

View layout = inflater.inflate(R.layout.preferences_fragment, container, false);

We inflate our custom layout using our inflater and it inflate method, we pass our custom layout and other parameters such container and savedInstanceBundle.

ViewGroup settingsContainerView = layout.findViewById(R.id.settings_container_view);

Toolbar toolbar = layout.findViewById(R.id.toolbar);

MainActivity mainActivity = (MainActivity) requireActivity();

We get our FrameLayout (it's container for our settings), toolbar and MainActivity of our application.

mainActivity.setSupportActionBar(toolbar);

We set our toolbar using setSupportActionBar method.

View settingsView = super.onCreateView(inflater, settingsContainerView, savedInstanceState);
settingsContainerView.addView(settingsView);
return layout;

Finally we call super.onCreateView method which populate settings and returns populated view. We must add our populated view as child to our settingsContainerView using addView method and return layout variable.

Si answered 18/4, 2023 at 15:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.