Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Inheriting AppCompat 22.1.1 Dialog colorAccent from app theme doesn't work

I'm trying to set up AppCompat's Dialogs so that the buttons use the same color as the application's accent color, without repeating the color itself. This was working perfectly with AppCompat v22 (only for Lollipop, obviously) by using this styles file in values-v21:

<style name="AppTheme" parent="@style/Theme.AppCompat">
    <item name="colorAccent">#FF9800</item>
    <item name="android:alertDialogTheme">@style/AlertDialogTheme</item>
</style>

<style name="AlertDialogTheme" parent="android:Theme.Material.Dialog.Alert">
    <item name="android:colorAccent">?attr/colorAccent</item>
</style>

When AppCompat v22.1 was released I tried to set this up for all Android versions, so I moved these styles to the base values folder, and basically replaced all the android: and v21-specific attributes by their AppCompat counterparts.

<style name="AppTheme" parent="Theme.AppCompat">
    <item name="colorAccent">#FF9800</item>
    <item name="alertDialogTheme">@style/AlertDialogTheme</item>
</style>

<style name="AlertDialogTheme" parent="Theme.AppCompat.Dialog.Alert">
    <item name="colorAccent">?attr/colorAccent</item>
</style>

However, it doesn't work -- the app crashes when it attempts to show an alert dialog. There are some warnings in logcat which I strongly suspect are related to the problem:

05-08 16:55:44.863  W/ResourceType﹕ Too many attribute references, stopped at: 0x7f01009e

And the exception is:

05-08 16:55:44.900  21301-21301/com.example.test.testaccentcolor E/AndroidRuntime﹕ FATAL EXCEPTION: main
    Process: com.example.test.testaccentcolor, PID: 21301
    android.view.InflateException: Binary XML file line #124: Error inflating class Button
            at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:763)
            at android.view.LayoutInflater.rInflate(LayoutInflater.java:806)
            at android.view.LayoutInflater.rInflate(LayoutInflater.java:809)
            at android.view.LayoutInflater.inflate(LayoutInflater.java:504)
            at android.view.LayoutInflater.inflate(LayoutInflater.java:414)
            at android.view.LayoutInflater.inflate(LayoutInflater.java:365)
            at android.support.v7.app.AppCompatDelegateImplV7.setContentView(AppCompatDelegateImplV7.java:249)
            at android.support.v7.app.AppCompatDialog.setContentView(AppCompatDialog.java:75)
            at android.support.v7.app.AlertController.installContent(AlertController.java:216)
            at android.support.v7.app.AlertDialog.onCreate(AlertDialog.java:240)
            at android.app.Dialog.dispatchOnCreate(Dialog.java:373)
            at android.app.Dialog.show(Dialog.java:274)
            at android.support.v7.app.AlertDialog$Builder.show(AlertDialog.java:902)
            at com.example.test.testaccentcolor.MainActivity.onOptionsItemSelected(MainActivity.java:37)
            at android.app.Activity.onMenuItemSelected(Activity.java:2885)
            at android.support.v4.app.FragmentActivity.onMenuItemSelected(FragmentActivity.java:353)
            at android.support.v7.app.AppCompatActivity.onMenuItemSelected(AppCompatActivity.java:144)
            at android.support.v7.internal.view.WindowCallbackWrapper.onMenuItemSelected(WindowCallbackWrapper.java:99)
            at android.support.v7.app.AppCompatDelegateImplV7.onMenuItemSelected(AppCompatDelegateImplV7.java:538)
            at android.support.v7.internal.view.menu.MenuBuilder.dispatchMenuItemSelected(MenuBuilder.java:802)
            at android.support.v7.internal.view.menu.MenuItemImpl.invoke(MenuItemImpl.java:153)
            at android.support.v7.internal.view.menu.MenuBuilder.performItemAction(MenuBuilder.java:949)
            at android.support.v7.internal.view.menu.MenuBuilder.performItemAction(MenuBuilder.java:939)
            at android.support.v7.internal.view.menu.MenuPopupHelper.onItemClick(MenuPopupHelper.java:187)
            at android.widget.AdapterView.performItemClick(AdapterView.java:305)
            at android.widget.AbsListView.performItemClick(AbsListView.java:1146)
            at android.widget.AbsListView$PerformClick.run(AbsListView.java:3053)
            at android.widget.AbsListView$3.run(AbsListView.java:3860)
            at android.os.Handler.handleCallback(Handler.java:739)
            at android.os.Handler.dispatchMessage(Handler.java:95)
            at android.os.Looper.loop(Looper.java:135)
            at android.app.ActivityThread.main(ActivityThread.java:5254)
            at java.lang.reflect.Method.invoke(Native Method)
            at java.lang.reflect.Method.invoke(Method.java:372)
            at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903)
            at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:698)
     Caused by: java.lang.RuntimeException: Failed to resolve attribute at index 5
            at android.content.res.TypedArray.getColorStateList(TypedArray.java:425)
            at android.widget.TextView.<init>(TextView.java:991)
            at android.widget.Button.<init>(Button.java:111)
            at android.widget.Button.<init>(Button.java:107)
            at android.support.v7.widget.AppCompatButton.<init>(AppCompatButton.java:60)
            at android.support.v7.widget.AppCompatButton.<init>(AppCompatButton.java:56)
            at android.support.v7.internal.app.AppCompatViewInflater.createView(AppCompatViewInflater.java:97)
            at android.support.v7.app.AppCompatDelegateImplV7.createView(AppCompatDelegateImplV7.java:782)
            at android.support.v7.app.AppCompatDelegateImplV7.onCreateView(AppCompatDelegateImplV7.java:810)
            at android.support.v4.view.LayoutInflaterCompatHC$FactoryWrapperHC.onCreateView(LayoutInflaterCompatHC.java:44)
            at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:725)
            at android.view.LayoutInflater.rInflate(LayoutInflater.java:806)
            at android.view.LayoutInflater.rInflate(LayoutInflater.java:809)
            at android.view.LayoutInflater.inflate(LayoutInflater.java:504)
            at android.view.LayoutInflater.inflate(LayoutInflater.java:414)
            at android.view.LayoutInflater.inflate(LayoutInflater.java:365)
            at android.support.v7.app.AppCompatDelegateImplV7.setContentView(AppCompatDelegateImplV7.java:249)
            at android.support.v7.app.AppCompatDialog.setContentView(AppCompatDialog.java:75)
            at android.support.v7.app.AlertController.installContent(AlertController.java:216)
            at android.support.v7.app.AlertDialog.onCreate(AlertDialog.java:240)
            at android.app.Dialog.dispatchOnCreate(Dialog.java:373)
            at android.app.Dialog.show(Dialog.java:274)
            at android.support.v7.app.AlertDialog$Builder.show(AlertDialog.java:902)
            at com.example.test.testaccentcolor.MainActivity.onOptionsItemSelected(MainActivity.java:37)
            at android.app.Activity.onMenuItemSelected(Activity.java:2885)
            at android.support.v4.app.FragmentActivity.onMenuItemSelected(FragmentActivity.java:353)
            at android.support.v7.app.AppCompatActivity.onMenuItemSelected(AppCompatActivity.java:144)
            at android.support.v7.internal.view.WindowCallbackWrapper.onMenuItemSelected(WindowCallbackWrapper.java:99)
            at android.support.v7.app.AppCompatDelegateImplV7.onMenuItemSelected(AppCompatDelegateImplV7.java:538)
            at android.support.v7.internal.view.menu.MenuBuilder.dispatchMenuItemSelected(MenuBuilder.java:802)
            at android.support.v7.internal.view.menu.MenuItemImpl.invoke(MenuItemImpl.java:153)
            at android.support.v7.internal.view.menu.MenuBuilder.performItemAction(MenuBuilder.java:949)
            at android.support.v7.internal.view.menu.MenuBuilder.performItemAction(MenuBuilder.java:939)
            at android.support.v7.internal.view.menu.MenuPopupHelper.onItemClick(MenuPopupHelper.java:187)
            at android.widget.AdapterView.performItemClick(AdapterView.java:305)
            at android.widget.AbsListView.performItemClick(AbsListView.java:1146)
            at android.widget.AbsListView$PerformClick.run(AbsListView.java:3053)
            at android.widget.AbsListView$3.run(AbsListView.java:3860)
            at android.os.Handler.handleCallback(Handler.java:739)
            at android.os.Handler.dispatchMessage(Handler.java:95)
            at android.os.Looper.loop(Looper.java:135)
            at android.app.ActivityThread.main(ActivityThread.java:5254)
            at java.lang.reflect.Method.invoke(Native Method)
            at java.lang.reflect.Method.invoke(Method.java:372)
            at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903)
            at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:698)

Of course, duplicating the color value (or creating a color resource and referencing it) works... but due to the project structure I need it this way.

To sum up: I assume the reference to ?attr/colorAccent is somehow creating a cycle... but I cannot figure out why or how to fix it.

Any ideas?


Update: Why do I need/want to set accentColor only in the theme?

We have a library that is common to many applications, that defines "base" themes from which the actual application theme must be derived. So the above example, which is simplified, is actually more akin to something like this:

<!-- Styles that are inherited by application-generated styles. -->
<style name="Theme.Library.Dark" parent="@style/Theme.AppCompat">
    <item name="alertDialogTheme">@style/Theme.Library.Dark.AlertDialog</item>
    <!-- more properties -->
</style>

<style name="Theme.Library.Dark.AlertDialog" parent="@style/Theme.AppCompat.Dialog.Alert">
    <item name="colorAccent">?attr/colorAccent</item>
    <!-- more properties -->
</style>

(plus the corresponding variants for Light and Light with Dark Action Bar).

After this, the application project should only pick a base theme from the list provided, and then have something like:

<!-- Application theme -->
<style name="ApplicationTheme" parent="Theme.Library.Dark">
    <-- Ohter material colors -->
    <item name="colorAccent">#FF9800</item>
</style>

At least this is the ideal situation, which worked before (as @Vikram points out in his answer, apparently only because there are two different colorAccent attributes).

The advantage of this scheme is that I am not forced to provide default values for colorAccent in the library themes, as those are inherited from the AppCompat base ones.

Also, should I need to create color resources, I would need two of them (for light and dark variants) since the default colors are not the same.

So, ideally, I'm looking for a similar solution (provided one exists).

like image 359
matiash Avatar asked May 08 '15 20:05

matiash


2 Answers

<item name="colorAccent">?attr/colorAccent</item>

You already know that this doesn't work. Here's why:

ssize_t ResTable::Theme::getAttribute(uint32_t resID, Res_value* outValue,
    uint32_t* outTypeSpecFlags) const 
{
    int cnt = 20;

    if (outTypeSpecFlags != NULL) *outTypeSpecFlags = 0;

    do {
        ....
        ....        
        if (type == Res_value::TYPE_ATTRIBUTE) {
            if (cnt > 0) {
                cnt--;
                resID = te.value.data;
                continue;
            }
            ALOGW("Too many attribute references, stopped at: 0x%08x\n", resID);
            return BAD_INDEX;
        } 
        ....
        ....
    } while (true);

    return BAD_INDEX;
}

The snippet is self-explanatory. You can have:

<item name="colorZ" >?attr/colorY</item> 

and then:

<item name="colorY" >?attr/colorX</item> 

.... ....

But chaining attributes like this cannot be more that 20 levels deep. Android gives up because of Too many attribute references....

In your case, you are setting an attribute's value by trying to read its present value. Android goes looking, but finds another reference instead - endless pursuit.

Why did it work before?

<style name="AppTheme" parent="@style/Theme.AppCompat">
    <item name="colorAccent">#FF9800</item>
    <item name="android:alertDialogTheme">@style/AlertDialogTheme</item>
</style>

<style name="AlertDialogTheme" parent="android:Theme.Material.Dialog.Alert">
    <item name="android:colorAccent">?attr/colorAccent</item>
</style>

In AppTheme, you are setting support library's colorAccent attribute (no android: namespace). Under AlertDialogTheme, you set android:colorAccent to support library's colorAccent. This is why an endless loop does not take shape.

?attr/colorAccent is resolved within the Context you pass when creating the AlertDialog. For this reason, it is resolved against <item name="colorAccent">#FF9800</item>.

One possible workaround would be to define a custom attr in res/values/attrs.xml:

<attr name="alertDialogColorAccent" format="reference|color"/>

Now, we can initialize this attribute under AppTheme:

<style name="AppTheme" parent="@style/Theme.AppCompat">
    <item name="colorAccent">#FF9800</item>
    <item name="alertDialogColorAccent" >?attr/colorAccent</item>
    <item name="android:alertDialogTheme">@style/AlertDialogTheme</item>
</style>

and use it in AlertDialogTheme:

<style name="AlertDialogTheme" parent="android:Theme.Material.Dialog.Alert">
    <item name="colorAccent">?attr/alertDialogColorAccent</item>
</style>

BUT, this will still not work. Its still creates a cycle - colorAccent is alertDialogColorAccent & alertDialogColorAccent is colorAccent. Somewhere along the chain, an actual color value needs to be set:

<style name="AppTheme" parent="@style/Theme.AppCompat">
    <item name="colorAccent">#FF9800</item>
    <item name="alertDialogColorAccent" >#FF9800</item>
    <item name="android:alertDialogTheme">@style/AlertDialogTheme</item>
</style>

Now, you can initialize colorAccent with alertDialogColorAccent since it refers to an actual color value:

<style name="AlertDialogTheme" parent="android:Theme.Material.Dialog.Alert">
    <item name="colorAccent">?attr/alertDialogColorAccent</item>
</style>
like image 83
Vikram Avatar answered Nov 15 '22 13:11

Vikram


Based on @Vikram's excellent answer, I ended up with this (I'm putting it up for future reference, in case someone is interested in the actual solution).

For the base styles file, in values (note: different XML files for attributes, colors, &c are preferable -- they are all together here for compactness):

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="BaseAppTheme" parent="Theme.AppCompat.Light.DarkActionBar" />

    <attr name="dialogColorAccent" />
    <color name="myAccentColor">#FF9800</color>

    <style name="AppTheme" parent="BaseAppTheme">
        <item name="dialogTheme">@style/DialogTheme</item>
        <item name="alertDialogTheme">@style/AlertDialogTheme</item>
        <item name="colorAccent">@color/myAccentColor</item>
        <item name="dialogColorAccent">@color/myAccentColor</item>
    </style>

    <style name="DialogTheme" parent="Theme.AppCompat.Light.Dialog">
        <item name="colorAccent">?attr/dialogColorAccent</item>
    </style>

    <style name="AlertDialogTheme" parent="Theme.AppCompat.Light.Dialog.Alert">
        <item name="colorAccent">?attr/dialogColorAccent</item>
    </style>
</resources>

The new dialogColorAccent attribute is necessary so that it can be referenced from the dialog styles. Unfortunately it cannot reference colorAccent itself (and neither is the reverse possible) so both must have the same value (in this case, a reference to a color resource).

This makes sure that AlertDialogs built with android.support.v7.app.AlertDialog.Builder use the accent color for the positive/negative buttons for all Android versions.

However, an additional file is necessary for some dialogs (such as DatePickerDialog, TimePickerDialog and ProgressDialog) that have material versions in Lollipop but no AppCompat equivalent so far. Hence, the following style file goes in values-v21:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="BaseAppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <item name="android:dialogTheme">@style/DialogTheme</item>
    </style>
</resources>

This will make these new dialogs have the accent color too (but only in Lollipop, obviously).


The solution might seem a little convoluted (referencing @myAccentColor from the AlertDialogTheme style is way simpler :)) but it was necessary because multiple themes are defined in a library, and the idea was that these themes can be inherited and used with minimal changes.

like image 43
matiash Avatar answered Nov 15 '22 15:11

matiash