128

Action Bar compatibility has been added into support library, revision 18. It now has ActionBarActivity class for creating activities with Action Bar on older versions of Android.

Is there any way to add Action Bar from support library into PreferenceActivity?

Previously I used ActionBarSherlock and it has SherlockPreferenceActivity.

Roman
  • 5,358
  • 9
  • 34
  • 43

8 Answers8

127

EDIT: In appcompat-v7 22.1.0 Google added the AppCompatDelegate abstract class as a delegate you can use to extend AppCompat's support to any activity.

Use it like this:

...
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatDelegate;
import android.support.v7.widget.Toolbar;
...

public class SettingsActivity extends PreferenceActivity {

    private AppCompatDelegate mDelegate;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        getDelegate().installViewFactory();
        getDelegate().onCreate(savedInstanceState);
        super.onCreate(savedInstanceState);
    }

    @Override
    protected void onPostCreate(Bundle savedInstanceState) {
        super.onPostCreate(savedInstanceState);
        getDelegate().onPostCreate(savedInstanceState);
    }

    public ActionBar getSupportActionBar() {
        return getDelegate().getSupportActionBar();
    }

    public void setSupportActionBar(@Nullable Toolbar toolbar) {
        getDelegate().setSupportActionBar(toolbar);
    }

    @Override
    public MenuInflater getMenuInflater() {
        return getDelegate().getMenuInflater();
    }

    @Override
    public void setContentView(@LayoutRes int layoutResID) {
        getDelegate().setContentView(layoutResID);
    }

    @Override
    public void setContentView(View view) {
        getDelegate().setContentView(view);
    }

    @Override
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        getDelegate().setContentView(view, params);
    }

    @Override
    public void addContentView(View view, ViewGroup.LayoutParams params) {
        getDelegate().addContentView(view, params);
    }

    @Override
    protected void onPostResume() {
        super.onPostResume();
        getDelegate().onPostResume();
    }

    @Override
    protected void onTitleChanged(CharSequence title, int color) {
        super.onTitleChanged(title, color);
        getDelegate().setTitle(title);
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        getDelegate().onConfigurationChanged(newConfig);
    }

    @Override
    protected void onStop() {
        super.onStop();
        getDelegate().onStop();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        getDelegate().onDestroy();
    }

    public void invalidateOptionsMenu() {
        getDelegate().invalidateOptionsMenu();
    }

    private AppCompatDelegate getDelegate() {
        if (mDelegate == null) {
            mDelegate = AppCompatDelegate.create(this, null);
        }
        return mDelegate;
    }
}

No more hacking. Code taken from AppCompatPreferenceActivity.java.

Ľubomír Kučera
  • 1,306
  • 1
  • 9
  • 6
  • please put your essential code snippets in your answer. to avoid potential broken link issue. – Yohanes Khosiawan 许先汉 Nov 02 '14 at 23:44
  • thanks, this works for me - i'd change new LinearLayout in the first inflate call to: ``(ViewGroup) getWindow().getDecorView().getRootView()`` – ahmedre Nov 05 '14 at 07:47
  • Works for me too and helped me recover the settings title which got lost when upgrading to release 21. – 3c71 Nov 05 '14 at 10:37
  • This works for me to bring the ActionBar back into preference activity after upgrading to release 21. I use `mActionBar.setLogo(R.drawable.icon);` to show my app icon instead of the app name. – hfann Nov 27 '14 at 17:00
  • This doesn't work with nested elements. Nested preference screens have no Toolbar. – petrsyn Nov 28 '14 at 23:19
  • 2
    @petrsyn It actually works. Look [here](https://github.com/Frozen-Developers/android-cache-cleaner/blob/master/CacheCleaner/src/main/java/com/frozendevs/cache/cleaner/activity/SettingsActivity.java) and [here](https://github.com/Frozen-Developers/android-cache-cleaner/blob/master/CacheCleaner/src/main/res/xml/settings.xml) to find out how you should do it. – Ľubomír Kučera Nov 29 '14 at 13:02
  • @Ľubomír Kučera Thanks for answering. Your solution creates nested PreferenceScreen manually in the onCreate() method of the PreferenceActivity child class. My comment was about using nested under another in the prefs.xml. This stopped working in AppCompat v21. This used to work in older versions of AppCompat lib and also in the ActionBarSherlock lib. Anyway, thanks for responding. – petrsyn Dec 01 '14 at 00:40
  • If I use then my Settings text is black on dark gray. What is the simplest way to fix this? – swooby Feb 08 '15 at 18:50
  • 1
    @swooby You might suffer from [this](https://code.google.com/p/android/issues/detail?id=78819) bug. – Ľubomír Kučera Feb 09 '15 at 19:31
  • @petrsyn I have solved the nesting problem here http://stackoverflow.com/a/27455363/566127 – David Passmore Jun 10 '15 at 12:54
  • Any idea why ListPreferences radio button stop working after applying this approach? (android 4.0.3). – Kurovsky Oct 14 '15 at 15:05
  • When I commented out the //getDelegate().installViewFactory(); ListPreference started to work. Strange behaviour. – Kurovsky Oct 14 '15 at 15:40
  • @ĽubomírKučera This gives me almost completely 2.x look. Ok, checkboxes are material style but everything else is old. What is the point of this then? – f470071 Nov 04 '15 at 11:07
  • @petrsyn: I have problem like you. How can i fix it? I tried in the way of Ľubomír Kučerabut, but it did not work. – Sinh Phan Aug 30 '16 at 14:54
  • 1
    Okay so where do I put the toolbar in the xml? I'm getting a NullPointerException – Martin Erlic Nov 21 '16 at 18:37
78

There is currently no way to achieve with AppCompat. I've opened a bug internally.

Chris Banes
  • 31,763
  • 16
  • 59
  • 50
24

I have managed to create a workaround similar to what the Google Play Store uses. Link to Original Answer

Please find the GitHub Repo: Here


Very Similar to your own code but added xml to allow for set title:

Continuing to use PreferenceActivity:

settings_toolbar.xml :

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.Toolbar
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/toolbar"
    app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:minHeight="?attr/actionBarSize"
    app:navigationContentDescription="@string/abc_action_bar_up_description"
    android:background="?attr/colorPrimary"
    app:navigationIcon="?attr/homeAsUpIndicator"
    app:title="@string/action_settings"
    />

SettingsActivity.java :

public class SettingsActivity extends PreferenceActivity {

    @Override
    protected void onPostCreate(Bundle savedInstanceState) {
        super.onPostCreate(savedInstanceState);

        LinearLayout root = (LinearLayout)findViewById(android.R.id.list).getParent().getParent().getParent();
        Toolbar bar = (Toolbar) LayoutInflater.from(this).inflate(R.layout.settings_toolbar, root, false);
        root.addView(bar, 0); // insert at top
        bar.setNavigationOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                finish();
            }
        });
    }

}

Result :

example


UPDATE (Gingerbread Compatibility) :

As pointed out here, Gingerbread Devices are returning NullPointerException on this line:

LinearLayout root = (LinearLayout)findViewById(android.R.id.list).getParent().getParent().getParent();

FIX:

SettingsActivity.java :

public class SettingsActivity extends PreferenceActivity {

    @Override
    protected void onPostCreate(Bundle savedInstanceState) {
        super.onPostCreate(savedInstanceState);
        Toolbar bar;

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
            LinearLayout root = (LinearLayout) findViewById(android.R.id.list).getParent().getParent().getParent();
            bar = (Toolbar) LayoutInflater.from(this).inflate(R.layout.settings_toolbar, root, false);
            root.addView(bar, 0); // insert at top
        } else {
            ViewGroup root = (ViewGroup) findViewById(android.R.id.content);
            ListView content = (ListView) root.getChildAt(0);

            root.removeAllViews();

            bar = (Toolbar) LayoutInflater.from(this).inflate(R.layout.settings_toolbar, root, false);
            

            int height;
            TypedValue tv = new TypedValue();
            if (getTheme().resolveAttribute(R.attr.actionBarSize, tv, true)) {
                height = TypedValue.complexToDimensionPixelSize(tv.data, getResources().getDisplayMetrics());
            }else{
                height = bar.getHeight();
            }

            content.setPadding(0, height, 0, 0);

            root.addView(content);
            root.addView(bar);
        }

        bar.setNavigationOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                finish();
            }
        });
    }
}

Any issues with the above let me know!


UPDATE 2: TINTING WORKAROUND

As pointed out in many dev notes PreferenceActivity does not support tinting of elements, however by utilising a few internal classes you CAN achieve this. That is until these classes are removed. (Works using appCompat support-v7 v21.0.3).

Add the following imports:

import android.support.v7.internal.widget.TintCheckBox;
import android.support.v7.internal.widget.TintCheckedTextView;
import android.support.v7.internal.widget.TintEditText;
import android.support.v7.internal.widget.TintRadioButton;
import android.support.v7.internal.widget.TintSpinner;

Then override the onCreateView method:

@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
    // Allow super to try and create a view first
    final View result = super.onCreateView(name, context, attrs);
    if (result != null) {
        return result;
    }

    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
        // If we're running pre-L, we need to 'inject' our tint aware Views in place of the
        // standard framework versions
        switch (name) {
            case "EditText":
                return new TintEditText(this, attrs);
            case "Spinner":
                return new TintSpinner(this, attrs);
            case "CheckBox":
                return new TintCheckBox(this, attrs);
            case "RadioButton":
                return new TintRadioButton(this, attrs);
            case "CheckedTextView":
                return new TintCheckedTextView(this, attrs);
        }
    }

    return null;
}

Result:

example 2


AppCompat 22.1

AppCompat 22.1 introduced new tinted elements, meaning that there is no longer a need to utilise the internal classes to achieve the same effect as the last update. Instead follow this (still overriding onCreateView):

@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
    // Allow super to try and create a view first
    final View result = super.onCreateView(name, context, attrs);
    if (result != null) {
        return result;
    }

    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
        // If we're running pre-L, we need to 'inject' our tint aware Views in place of the
        // standard framework versions
        switch (name) {
            case "EditText":
                return new AppCompatEditText(this, attrs);
            case "Spinner":
                return new AppCompatSpinner(this, attrs);
            case "CheckBox":
                return new AppCompatCheckBox(this, attrs);
            case "RadioButton":
                return new AppCompatRadioButton(this, attrs);
            case "CheckedTextView":
                return new AppCompatCheckedTextView(this, attrs);
        }
    }

    return null;
}

NESTED PREFERENCE SCREENS

A lot of people are experiencing issues with including the Toolbar in nested <PreferenceScreen />s however, I have found a solution!! - After a lot of trial and error!

Add the following to your SettingsActivity:

@SuppressWarnings("deprecation")
@Override
public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) {
    super.onPreferenceTreeClick(preferenceScreen, preference);

    // If the user has clicked on a preference screen, set up the screen
    if (preference instanceof PreferenceScreen) {
        setUpNestedScreen((PreferenceScreen) preference);
    }

    return false;
}

public void setUpNestedScreen(PreferenceScreen preferenceScreen) {
    final Dialog dialog = preferenceScreen.getDialog();

    Toolbar bar;

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
        LinearLayout root = (LinearLayout) dialog.findViewById(android.R.id.list).getParent();
        bar = (Toolbar) LayoutInflater.from(this).inflate(R.layout.settings_toolbar, root, false);
        root.addView(bar, 0); // insert at top
    } else {
        ViewGroup root = (ViewGroup) dialog.findViewById(android.R.id.content);
        ListView content = (ListView) root.getChildAt(0);

        root.removeAllViews();

        bar = (Toolbar) LayoutInflater.from(this).inflate(R.layout.settings_toolbar, root, false);

        int height;
        TypedValue tv = new TypedValue();
        if (getTheme().resolveAttribute(R.attr.actionBarSize, tv, true)) {
            height = TypedValue.complexToDimensionPixelSize(tv.data, getResources().getDisplayMetrics());
        }else{
            height = bar.getHeight();
        }

        content.setPadding(0, height, 0, 0);

        root.addView(content);
        root.addView(bar);
    }

    bar.setTitle(preferenceScreen.getTitle());

    bar.setNavigationOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            dialog.dismiss();
        }
    });
}

The reason that PreferenceScreen's are such a pain is because they are based as a wrapper dialog, so we need to capture the dialog layout to add the toolbar to it.


Toolbar Shadow

By design importing the Toolbar does not allow for elevation and shadowing in pre-v21 devices, so if you would like to have elevation on your Toolbar you need to wrap it in a AppBarLayout:

`settings_toolbar.xml :

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

   <android.support.v7.widget.Toolbar
       .../>

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

Not forgetting to add the add the Design Support library as a dependency in build.gradle file:

compile 'com.android.support:support-v4:22.2.0'
compile 'com.android.support:appcompat-v7:22.2.0'
compile 'com.android.support:design:22.2.0'

Android 6.0

I have investigated the reported overlapping issue and I cannot reproduce the issue.

The full code in use as above produces the following:

enter image description here

If I am missing something please let me know via this repo and I will investigate.

Community
  • 1
  • 1
David Passmore
  • 6,089
  • 4
  • 46
  • 70
  • 1
    Great! Works like a charm :) – Tobi May 21 '15 at 20:59
  • Why does this not work for the nested PreferenceScreen, but only for the main PreferenceScreen? – Tobi May 22 '15 at 05:18
  • @Tobi I have updated my answer to solve the nested issue, and explained why. `:)` – David Passmore Jun 10 '15 at 12:52
  • Thanks! Your advice "AppCompat 22.1" did the trick for me :) Very useful! – Martin Pfeffer Jun 23 '15 at 02:35
  • @DavidPassmore why i cant see shadow on api 10 when i wrap toolbar with appbarlayout ? – Jemshit Jul 12 '15 at 11:53
  • @DavidPassmore why wrap it inside AppBarLayout for shadow if it works v21+ ? why not Just use "elevation"? – Jemshit Jul 12 '15 at 12:02
  • 1
    why is `PreferenceActivity` *such a pain in the ass to use* ??? it's supposed to save time. I might as well make a regular activity and manually lay out all the settings in a linear layout myself. Fuuuuck ! – Someone Somewhere Sep 03 '15 at 01:50
  • In Android API 24, in NESTED PREFERENCE SCREENS, in this line ( LinearLayout root = (LinearLayout) dialog.findViewById(android.R.id.list).getParent(); ) I'm getting this error: java.lang.ClassCastException: android.widget.FrameLayout cannot be cast to android.widget.LinearLayout. Is any solution for this??? – S.P. Jun 28 '16 at 09:58
  • Hi @D.O. can you raise this on the GitHub so I can track the errors :) – David Passmore Jun 28 '16 at 10:01
  • Hi @David, you mean in this github link, isn't it? https://github.com/davcpas1234/MaterialSettings – S.P. Jun 28 '16 at 10:05
  • Done! raise in GitHub – S.P. Jun 28 '16 at 10:11
12

Found a PreferenceFragment implementation based on support-v4 Fragment:

https://github.com/kolavar/android-support-v4-preferencefragment

Edit: I just tested it and its working great!

Ostkontentitan
  • 6,930
  • 5
  • 53
  • 71
  • @Konstantin, can you add some code example? I downloaded it, changed the import from android.preference.PreferenceFragment to android.support.v4.preference.PreferenceFragment, and I see it added some headers in the middle of the screen, but not the ActionBar on the top – Gavriel Aug 03 '14 at 10:54
  • It doesnt add the actionbar thats the activity's job. Unfortunally i have no samplecode at hand but it should work similar to: http://developer.android.com/reference/android/preference/PreferenceFragment.html – Ostkontentitan Aug 04 '14 at 06:53
3

Integrating PreferenceActivity with ABC is not possible, at least for me. I tried the two possibilities I could find but none worked:

Option 1:

ActionBarPreferenceActivity extends PreferenceActivity. When you do this you get restricted by ActionBarActivityDelegate.createDelegate(ActionBarActivity activity). Also you need to implement ActionBar.Callbacks which is not accessible

Option 2:

ActionBarPreferenceActivity extends ActionBarActivity. This approach requires rewriting a whole new PreferenceActivity, PreferenceManager and may be PreferenceFragment which means you need access to hidden classes like com.android.internal.util.XmlUtils The solution to this can only come from Google devs implementing an ActionBarWrapper that can be added to any activity.

If you really need a preference activity, my advice for now is ActionBarSherlock.

However, I managed to implement it here.

Sufian
  • 6,405
  • 16
  • 66
  • 120
Maxwell Weru
  • 453
  • 1
  • 3
  • 9
3

Problem Background:

The OP wants to know how can we put MenuItems in the ActionBar of PreferenceActivity for pre-Honeycomb because Android's support library has a bug which doesn't allow this to happen.

My Solution:

I've found a much cleaner way, than already proposed, to achieve the target (and found it in the Android Docs):

android:parentActivityName

The class name of the logical parent of the activity. The name here must match the class name given to the corresponding element's android:name attribute.

The system reads this attribute to determine which activity should be started when the use presses the Up button in the action bar. The system can also use this information to synthesize a back stack of activities with TaskStackBuilder.

To support API levels 4 - 16, you can also declare the parent activity with a element that specifies a value for "android.support.PARENT_ACTIVITY". For example:

<activity
    android:name="com.example.app.ChildActivity"
    android:label="@string/title_child_activity"
    android:parentActivityName="com.example.myfirstapp.MainActivity" >
    <!-- Parent activity meta-data to support API level 4+ -->
    <meta-data
        android:name="android.support.PARENT_ACTIVITY"
        android:value="com.example.app.MainActivity" />
</activity>

Now do what you would normally do in your onOptionsItemSelected(). Since it's a part of Android Docs, it has no side-affects.

Happy coding. :)

Update:

This solution no longer works if you're targeting Lollipop. If you're using AppCompat, this answer is what you should be looking for.

Community
  • 1
  • 1
Sufian
  • 6,405
  • 16
  • 66
  • 120
1

I was able to get android.app.Actionbar by using getActionBar(). It returned a null value at first... then I went to the manifest and changed the theme to:

android:theme="@style/Theme.AppCompat"

Then I was able to have the actionbar again. I'm assuming this will only work for certain build levels. So you might want to do a check for the build number or check if the value returned is null.

It'll be fine for me because the app I'm working on is for ICS/4.0+.

Sagar Hatekar
  • 8,700
  • 14
  • 56
  • 72
RCB
  • 560
  • 4
  • 9
  • Here's a simpler solution, one which doesn't uses any workarounds http://stackoverflow.com/a/25603448/1276636 – Sufian Sep 10 '14 at 10:05
0

Now the official answer for this problem has been released. It is the v7/v14 Preference Support library.

See How to use the v7/v14 Preference Support library? for the discussion how to use it.

Community
  • 1
  • 1
Randy Sugianto 'Yuku'
  • 71,383
  • 57
  • 178
  • 228