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).
<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>
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.
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