I'm trying to theme my app in Android. However, each widget is an excrutiating pain in itself: I have to search for theming that particular widget and then create a style that hopefully derives from same style that widget uses.
Of course, answers about theming a particular widget don't always contain info about base style, just the particular colors.
So, instead of accepting fish to eat, can you teach me to fish instead?
How do I interpret those ObtainStyledAttributes() calls in widget constructors and extract styles from that? How do I recurse that?
In particular, can you walk me through AlertDialog button color? What style defines lollipop flat button + teal text color? How do I get to that style if I start from AlertDialog source and ObtainStyledAttributes call?
I find that styling is about sherlocking your way through the framework. The what (almost always) comes from the widget's implementation. The where, I find is all over the place. I will try my best to explain the process through your particular use-case - AlertDialog's button(s).
Starting off:
You already have this figured out: we start with the widget's source code. We are specifically trying to find - where AlertDialog buttons get their text-color. So, we start with looking at where these buttons come from. Are they being explicitly created at runtime? Or are they defined in an xml layout, which is being inflated?
In source code, we find that mAlert handles the button options among other things:
public void setButton(int whichButton, CharSequence text, Message msg) {
mAlert.setButton(whichButton, text, null, msg);
}
mAlert is an instance of AlertController. In its constructor, we find that the attribute alertDialogStyle defines the xml layout:
TypedArray a = context.obtainStyledAttributes(null,
com.android.internal.R.styleable.AlertDialog,
com.android.internal.R.attr.alertDialogStyle, 0);
mAlertDialogLayout =
a.getResourceId(
com.android.internal.R.styleable.AlertDialog_layout,
com.android.internal.R.layout.alert_dialog);
So, the layout we should look at is alert_dialog.xml - [sdk_folder]/platforms/android-21/data/res/layout/alert_dialog.xml:
The layout xml is quite long. This is the relevant part:
<LinearLayout>
....
....
<LinearLayout android:id="#+id/buttonPanel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="54dip"
android:orientation="vertical" >
<LinearLayout
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="4dip"
android:paddingStart="2dip"
android:paddingEnd="2dip"
android:measureWithLargestChild="true">
<LinearLayout android:id="#+id/leftSpacer"
android:layout_weight="0.25"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone" />
<Button android:id="#+id/button1"
android:layout_width="0dip"
android:layout_gravity="start"
android:layout_weight="1"
style="?android:attr/buttonBarButtonStyle"
android:maxLines="2"
android:layout_height="wrap_content" />
<Button android:id="#+id/button3"
android:layout_width="0dip"
android:layout_gravity="center_horizontal"
android:layout_weight="1"
style="?android:attr/buttonBarButtonStyle"
android:maxLines="2"
android:layout_height="wrap_content" />
<Button android:id="#+id/button2"
android:layout_width="0dip"
android:layout_gravity="end"
android:layout_weight="1"
style="?android:attr/buttonBarButtonStyle"
android:maxLines="2"
android:layout_height="wrap_content" />
<LinearLayout android:id="#+id/rightSpacer"
android:layout_width="0dip"
android:layout_weight="0.25"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone" />
</LinearLayout>
We now know that the buttons get the style held by attribute buttonBarButtonStyle.
Head over to [sdk_folder]/platforms/android-21/data/res/values/themes.material.xml and search for buttonBarButtonStyle:
<!-- Defined under `<style name="Theme.Material">` -->
<item name="buttonBarButtonStyle">#style/Widget.Material.Button.ButtonBar.AlertDialog</item>
<!-- Defined under `<style name="Theme.Material.Light">` -->
<item name="buttonBarButtonStyle">#style/Widget.Material.Light.Button.ButtonBar.AlertDialog</item>
Depending on what your activity's parent theme is, buttonBarButtonStyle will refer to one of these two styles. For now, let's assume your activity's theme extends Theme.Material. We'll look at #style/Widget.Material.Button.ButtonBar.AlertDialog:
Open [sdk_folder]/platforms/android-21/data/res/values/styles_material.xml and search for Widget.Material.Button.ButtonBar.AlertDialog:
<!-- Alert dialog button bar button -->
<style name="Widget.Material.Button.ButtonBar.AlertDialog" parent="Widget.Material.Button.Borderless.Colored">
<item name="minWidth">64dp</item>
<item name="maxLines">2</item>
<item name="minHeight">#dimen/alert_dialog_button_bar_height</item>
</style>
Okay. But these values don't help us in determining the button's text color. We should look at the parent style next - Widget.Material.Button.Borderless.Colored:
<!-- Colored borderless ink button -->
<style name="Widget.Material.Button.Borderless.Colored">
<item name="textColor">?attr/colorAccent</item>
<item name="stateListAnimator">#anim/disabled_anim_material</item>
</style>
At last, we find the textColor - and its supplied by attr/colorAccent initialized in Theme.Material:
<item name="colorAccent">#color/accent_material_dark</item>
For Theme.Material.Light, colorAccent is defined as:
<item name="colorAccent">#color/accent_material_light</item>
Browse to [sdk_folder]/platforms/android-21/data/res/values/colors_material.xml and locate these colors:
<color name="accent_material_dark">#color/material_deep_teal_200</color>
<color name="accent_material_light">#color/material_deep_teal_500</color>
<color name="material_deep_teal_200">#ff80cbc4</color>
<color name="material_deep_teal_500">#ff009688</color>
Screenshot of an AlertDialog and the corresponding text-color:
Shortcut:
Sometimes, its easier to read the color value (as in the picture above) and search for it using AndroidXRef. This approach would not have been useful in your case since #80cbc4 would have only pointed out that its the accent color. You would still have to locate Widget.Material.Button.Borderless.Colored and tie it with attribute buttonBarButtonStyle.
Changing button's text-color:
Ideally, we should create a style that extends Widget.Material.Button.ButtonBar.AlertDialog, override android:textColor inside it, and assign it to attribute buttonBarButtonStyle. But, this won't work - your project won't compile. This is because Widget.Material.Button.ButtonBar.AlertDialog is a non-public style and hence cannot be extended. You can confirm this by checking Link.
We'll do the next best thing - extend the parent style of Widget.Material.Button.ButtonBar.AlertDialog - Widget.Material.Button.Borderless.Colored which is public.
<style name="CusButtonBarButtonStyle"
parent="#android:style/Widget.Material.Button.Borderless.Colored">
<!-- Yellow -->
<item name="android:textColor">#ffffff00</item>
<!-- From Widget.Material.Button.ButtonBar.AlertDialog -->
<item name="android:minWidth">64dp</item>
<item name="android:maxLines">2</item>
<item name="android:minHeight">#dimen/alert_dialog_button_bar_height</item>
</style>
Note that we add 3 more items after overriding android:textColor. These are from non-public style Widget.Material.Button.ButtonBar.AlertDialog. Since we cannot extend it directly, we must include the items it defines. Note: the dimen value(s) will have to be looked up and transferred to appropriate res/values(-xxxxx)/dimens.xml files(s) in your project.
The style CusButtonBarButtonStyle will be assigned to attribute buttonBarButtonStyle. But the question is, how will an AlertDialog know of this? From the source code:
protected AlertDialog(Context context) {
this(context, resolveDialogTheme(context, 0), true);
}
Passing 0 as the second argument for resolveDialogTheme(Context, int) will end up in the else clause:
static int resolveDialogTheme(Context context, int resid) {
if (resid == THEME_TRADITIONAL) {
....
} else {
TypedValue outValue = new TypedValue();
context.getTheme().resolveAttribute(
com.android.internal.R.attr.alertDialogTheme,
outValue, true);
return outValue.resourceId;
}
}
We now know that the theme is held by alertDialogTheme attribute. Next, we look at what alertDialogTheme points to. The value of this attribute will depend on your activity's parent theme. Browse to your sdk folder and find the values/themes_material.xml inside android-21. Search for alertDialogTheme. Results:
<!-- Defined under `<style name="Theme.Material">` -->
<item name="alertDialogTheme">#style/Theme.Material.Dialog.Alert</item>
<!-- Defined under `<style name="Theme.Material.Light">` -->
<item name="alertDialogTheme">#style/Theme.Material.Light.Dialog.Alert</item>
<!-- Defined under `<style name="Theme.Material.Settings">` -->
<item name="alertDialogTheme">#style/Theme.Material.Settings.Dialog.Alert</item>
So, based on what your activity's base theme is, alertDialogTheme will hold one of these 3 values. To let AlertDialog know of CusButtonBarButtonStyle, we need to override attribute alertDialogTheme in our app's theme. Say, we're using Theme.Material as the base theme.
<style name="AppTheme" parent="android:Theme.Material">
<item name="android:alertDialogTheme">#style/CusAlertDialogTheme</item>
</style>
From above, we know that alertDialogTheme points to Theme.Material.Dialog.Alert when your app's base theme is Theme.Material. So, CusAlertDialogTheme should have Theme.Material.Dialog.Alert as its parent:
<style name="CusAlertDialogTheme"
parent="android:Theme.Material.Dialog.Alert">
<item name="android:buttonBarButtonStyle">#style/CusButtonBarButtonStyle</item>
</style>
Result:
So, instead of accepting fish to eat, can you teach me to fish
instead?
In the very least, I hope to have explained where the fish are.
P.S. I realize I have posted a mammoth.
Besides #Vikram excellent answer, it's worth noting that Android Studio can simplify your work enormously. You just need to hover your mouse over the Theme, it will show something like following.
actionBarStyle = #style/Widget.AppCompat.Light.ActionBar.Solid
=> #style/Widget.AppCompat.Light.ActionBar.Solid
You can also use mouse click to navigate between styles, like what you do with normal java code.
And you can find support library's res in
<sdk root>/extras/android/m2repository/com/android/support/<support library name>/<version number>/<support library>.aar/res
But the *.aar/res/values/values.xml contains all values, and it's not easy to read. You can get the original support library code and resources in
https://android.googlesource.com/platform/frameworks/support/+/master
There is a button named tgz to download current snapshot.
I'm trying to build an Android UI via layouts. I start with the following:
<TextView
android:id="#+id/..."
android:layout_marginTop="8dip"
android:text="..."
style="?android:attr/listSeparatorTextViewStyle"/>
And that looks good (all caps, smaller font, dividing bar underneath it). Now I want to extend the style, so I change it to the following:
<TextView
android:id="#+id/..."
android:layout_marginTop="8dip"
android:text="..."
style="#style/section_title"/>
With a style of:
<style name="section_title" parent="#android:attr/listSeparatorTextViewStyle">
<item name="android:layout_width">fill_parent</item>
<item name="android:layout_height">wrap_content</item>
</style>
And that doesn't work (font is correct, but the divider line is gone).
How come... that?
When you're using:
style="?android:attr/listSeparatorTextViewStyle"
you're using the style pointed by this attribute(listSeparatorTextViewStyle). If you look in the platform themes.xml you'll see that the style that is actually used for this attribute is Widget.TextView.ListSeparator.White. So this is the style you should extend in your custom style.
Unfortunately that style is private and you can't extend it, or you shouldn't extend it(for reference, see this bug report from google). Your best option would be to copy that entire style, Widget.TextView.ListSeparator.White(Widget.TextView.ListSeparator isn't public as well so would have to also copy that), in your custom style and use that instead of extending the style from the android platform(see this response from the link above).
I'm trying to build an Android UI via layouts. I start with the following:
<TextView
android:id="#+id/..."
android:layout_marginTop="8dip"
android:text="..."
style="?android:attr/listSeparatorTextViewStyle"/>
And that looks good (all caps, smaller font, dividing bar underneath it). Now I want to extend the style, so I change it to the following:
<TextView
android:id="#+id/..."
android:layout_marginTop="8dip"
android:text="..."
style="#style/section_title"/>
With a style of:
<style name="section_title" parent="#android:attr/listSeparatorTextViewStyle">
<item name="android:layout_width">fill_parent</item>
<item name="android:layout_height">wrap_content</item>
</style>
And that doesn't work (font is correct, but the divider line is gone).
How come... that?
When you're using:
style="?android:attr/listSeparatorTextViewStyle"
you're using the style pointed by this attribute(listSeparatorTextViewStyle). If you look in the platform themes.xml you'll see that the style that is actually used for this attribute is Widget.TextView.ListSeparator.White. So this is the style you should extend in your custom style.
Unfortunately that style is private and you can't extend it, or you shouldn't extend it(for reference, see this bug report from google). Your best option would be to copy that entire style, Widget.TextView.ListSeparator.White(Widget.TextView.ListSeparator isn't public as well so would have to also copy that), in your custom style and use that instead of extending the style from the android platform(see this response from the link above).
I want to extend Android's small button style. I can do it inline:
<Button android:id="#+id/myButton"
style="?android:attr/buttonStyleSmall"
android:layout_alignParentRight="true"
android:layout_gravity="right"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:textSize="10dp"
android:text="Click Here"/>
But in the interest of reusability, I want to transfer these styles to a custom style. How can I add buttonStyleSmall (or Widget.Button.Small?) as a parent to a style? Something like this in my custom style XML:
<style name="RightLink" parent="?android:attr/buttonStyleSmall">
<item name="android:layout_alignParentRight">true</item>
<item name="android:layout_gravity">right</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_width">wrap_content</item>
<item name="android:paddingLeft">2dp</item>
<item name="android:paddingRight">2dp</item>
<item name="android:textSize">10dp</item>
</style>
with the button declaration using that style:
<Button android:id="#+id/myButton"
style="#style/RightLink"
android:text="Click Here"/>
EDIT
Using the correct syntax as described by Lukas below (use #android:attr/buttonStyleSmall as the parent), I'm still seeing a difference between the two:
Button with buttonStyleSmall as style and inline styles added:
Custom style with buttonStyleSmall as the parent:
What am I missing?
So you try to inherit style informations from a standard Android style. For this, you'll need to use the parent-attribute as you already did.
The only exception when inheriting a standard style is, that you have to use an # not a ?:
<style name="RightLink" parent="#android:attr/buttonStyleSmall">
A little more about how to do that for platform styles (because that differs from styles you created yourself) can be found here.
After playing around with it i found that the way you inhirate is correct, but the recourse is wrong:
<style name="RightLink" parent="#android:style/Widget.Button.Small">
This works.
The difference seams to be that the integer-constant which is used to add this by using the style-attribute and in code is declared in Androids R-class in the attr-subclass.
The XML-Style definitions (from which you can actually inherit) are stored in the style-subclass of the R-class. So the above line should solve your problem.
I'm making out a few menus for my Android App and throughout there is a standard textview that is repeated 5 times, only changing the android:text tag each time, everything else is the same.
There are a good number of properties on this and it feels very inefficient to be copy/pasting all these for each of the textviews.
Is there a way I define the common properties just once and add them to each TextView element?
Yes you can define a style. Create a file in your values res folder names styles.xml and add something like this:
<resources>
<style name="my_header_text">
<item name="android:textStyle">bold</item>
<item name="android:textSize">18sp</item>
<item name="android:textColor">#android:color/white</item>
</style>
</resources>
This defines the style. In your layout you might have a field like this:
<TextView
android:id="#+id/my_title"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
style="#style/my_header_text"
android:layout_centerVertical="true"
android:layout_marginLeft="5dip"/>
Notice the style statement refers to the style defined above. Styles can contain almost any property. Read up on them here.