I want to implement custom style for view. Let's take an example of button. Some common values will be shared across the app for that I am using style like below:
<style name="Widget.Demo.Button.Primary" parent="#style/Widget.MaterialComponents.Button">
<item name="fontFamily">#font/roboto</item>
<item name="android:fontFamily">#font/roboto</item>
<item name="android:minHeight">64dp</item>
<item name="android:theme">#style/ThemeOverlay.Demo.GrayPrimary</item>
</style>
<style name="ThemeOverlay.Demo.GrayPrimary" parent="">
<item name="colorPrimary">#color/gray</item>
</style>
Now I want to add some of the custom attribute in the button like below (This is just an example not the real attribute):
<com.android.CustomButton
android:id="#+id/btn_first"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/first"
app:abc="primary"
style="#style/Widget.Demo.Button.Primary"/>
attrs.xml:
<declare-styleable name="CustomButtonAttr" >
<attr name="abc" format="enum" >
<enum name="primary" value="1"/>
<enum name="secondary" value="2"/>
</declare-styleable>
I don't want to define style and custom attribute in xml everytime. Is there any way to get custom attribute directly in my customview and Widget.Demo.Button.Primary set by default in my below class?
class CustomButton : MaterialButton {
constructor(context: Context) : super(context)
constructor(context: Context, attributeSet: AttributeSet) : super(
ThemeEnforcement.createThemedContext(context, attributeSet, 0, 0),
attributeSet,
0
)
}
I don't want to define style in xml for every button or view which I create. So is there a way to define in CustomButton class along with custom attributes ? If yes could you please give me some references.
Thanks in advance.
Yep, you can use the default style attribute in the constructor.
Define a new attribute:
<attr name="customButtonStyle" format="reference" />
Then use the appropriate view constructor:
private val defStyleAttr = R.attr.customButtonStyle
class CustomButton(context: Context, attrs: AttributeSet) : MaterialButton(
ThemeEnforcement.createThemedContext(context, attributeSet, defStyleAttr, 0),
attributeSet,
defStyleAttr
) {
init {
val typedArray = context.obtainStyledAttributes(
attrs,
R.styleable.CustomButton,
defStyleAttr,
0
)
...
}
In your theme, point customButtonStyle to the style resource that you want used by default:
<style name="Theme.Demo" parent="Base.Theme.Demo">
<item name="customButtonStyle">#style/Widget.Demo.Button.Primary</item>
</style>
Note that android:theme should be changed to materialThemeOverlay in that style resource, as it won't be applied when read from a default style. As you're already wrapping the context with the ThemeEnforcement function (newer versions of Material Design Components change this to MaterialThemeOverlay), this custom view supports materialThemeOverlay 👍
You can add your custom attributes to the style too:
<style name="Widget.Demo.Button.Primary" parent="#style/Widget.MaterialComponents.Button">
<item name="abc">primary</item>
<item name="fontFamily">#font/roboto</item>
<item name="android:fontFamily">#font/roboto</item>
<item name="android:minHeight">64dp</item>
<item name="materialThemeOverlay">#style/ThemeOverlay.Demo.GrayPrimary</item>
</style>
Reference (blog + video)
Related
I'm trying to create a custom view extending from MaterialButton and apply style in code so I don't need to do it in xml.
class CustomRedButton #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : MaterialButton(ContextThemeWrapper(context, R.style.ButtonRedStyle), attrs, defStyleAttr)
Style is:
<style name="ButtonRedStyle"
parent="Widget.MaterialComponents.Button.TextButton">
<item name="backgroundTint">#color/red</item>
<item name="rippleColor">#color/grey</item>
<item name="strokeWidth">1dp</item>
<item name="strokeColor">#color/black</item>
</style>
Everything works fine but backgroundTint property. For some reason background color is not changing, and it has Theme's primary color. However, if I try to apply the style to a MaterialButton in xml it does change the color.
Any idea why that can be happening or how I can achieve it?
Using
MaterialButton(ContextThemeWrapper(context, R.style.ButtonRedStyle), attrs, defStyleAttr)
you are applying a themeoverlay to default style, you are not applying a different style.
It means:
<style name="ButtonRedTheme" parent="...">
<item name="colorPrimary">#color/...</item>
<item name="colorOnPrimary">#color/...</item>
<item name="colorSecondary">#color/...</item>
</style>
If you want to apply a different style you have to:
Define a custom attribute in attrs.xml
<attr name="myButtonStyle" format="reference"/>
Assing a style to this attribute in your app theme:
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight">
<item name="myButtonStyle">#style/CustomButtonStyle</item>
</style>
Define the custom style:
<style name="CustomButtonStyle" parent="Widget.MaterialComponents.Button.*">
<item name="backgroundTint">#color/...</item>
<item name="rippleColor">#color/grey</item>
<item name="strokeWidth">1dp</item>
<item name="strokeColor">#color/black</item>
</style>
Finally use:
val customButton = MaterialButton(context, null, R.attr.myButtonStyle)
I'm also facing the same issue. The only workaround I've found so far is to set the tint programmatically like:
button.setBackgroundTintList(ColorStateList.valueOf(Color.RED));
For a TextButton there shouldn't be a background (just the text has a color). For a colored button, you should use the default Filled Button style which is Widget.MaterialComponents.Button.
And when applied as a theme, the button uses different attributes. It's described in section Themed Attribute Mapping here: https://material.io/develop/android/components/material-button/
Filled button
+------------------------+-----------------------------------------+
| Component Attribute | Default Theme Attribute Value |
+------------------------+-----------------------------------------+
| android:textAppearance | textAppearanceButton |
| android:textColor | colorOnPrimary |
| iconTint | colorOnPrimary |
| rippleColor | colorOnPrimary at 32% opacity (pressed) |
| iconTint | colorOnPrimary |
| backgroundTint | colorPrimary |
| ... | ... |
+------------------------+-----------------------------------------+
In your case, the theme should look something like:
<style name="ButtonRedTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
<item name="colorPrimary">#color/red</item>
<item name="colorOnPrimary">#color/white</item>
<item name="colorOnSurface">#color/black</item>
</style>
You can also change all buttons to a specific style with
<item name="materialButtonStyle">#style/ButtonRedTheme</item>
in your app theme.
If you want to change your style for CustomView, you've to pass it to constructor by passing it into third param defStyleAttr like this:
class CustomRedButton #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.style.ButtonRedStyle // Just default style like this
) : MaterialButton(context, attrs, defStyleAttr)
and you can initialize it like this programmatically,
CustomRedButton(this, null, R.style.ButtonRedStyle) // Initialization, ('this' is context)
For more details refer here
I had the same issue for a simple use case, i need to update the button backgroundTint and isEnabled state
what i did:
i created a custom class that extends from MaterialButton
class MyCustomMaterialButton #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : MaterialButton(context, attrs)
then i added an extension to this class to update button styling attributes:
fun MyCustomMaterialButton.updateEnabledState(enabled: Boolean){
apply {
if(enabled){
isEnabled = true
setBackgroundColor(ContextCompat.getColor(context, R.color.primary))
}
else{
isEnabled = false
setBackgroundColor(ContextCompat.getColor(context, R.color.primary_warm_grey_five))
}
}
}
this is how it looks like in Xml:
<com.karny.branding.KarnyMaterialButton
android:id="#+id/smsAuthButton"
style="#style/PrimaryButtonDisabled"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="94dp"
android:layout_marginBottom="24dp"
android:text="#string/sms_auth_check"
app:layout_constraintEnd_toStartOf="#+id/left_middle_guide_line"
app:layout_constraintStart_toEndOf="#+id/right_middle_guide_line"
app:layout_constraintTop_toBottomOf="#+id/smsAuthNumberContainer" />
Let's say there's a custom control and related style in an Android library project. The application that uses this library wants to override certain attributes of that control, while inheriting the others. In my current approach, I have the following code:
In library/styles.xml:
<style name="CreditCardInputField">
<item name="android:layout_margin">10dp</item>
<item name="android:background">#drawable/border</item>
<item name="android:textStyle">bold|italic</item>
</style>
In app/styles.xml:
<style name="CreditCardInputField">
<item name="android:layout_margin">50dp</item>
</style>
The result I have is that the style from the app completely overrides the style from the library. I.e. I lose the background and textStyle properties, while correctly overriding the layout_margin. This is not what I want, I want to keep the background and textStyle as they're defined in library. Is it possible, and, if yes, how?
EDIT: To clarify, I don't want to use the style directly in the app, only the custom control from the library that uses the style. Therefore creating a new style in the app (with a parent from the library) does effectively nothing.
Use a different name to your style in app/styles.xml and make the other style as it's parent.
<style name="newCreditCardInputField" parent="CreditCardInputField">
<item name="android:layout_margin">50dp</item>
</style>
This will override your layout_margin while restoring background and textStyle.
I've found a way to achieve what I want through the custom attributes. Not as convenient as with a style, but more flexible. In short, declare custom attributes in the library, read them in the control's code, provide in the app. Here's the almost complete code, maybe this will help someone:
In lib/values/attrs.xml (custom attributes are declared here):
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="test_view">
<attr name="field_margins" format="dimension">50dp</attr>
<attr name="field_background" format="reference">#drawable/border</attr>
<attr name="name_field_hint" format="reference"/>
<attr name="number_field_hint" format="reference"/>
</declare-styleable>
</resources>
In lib/layout/credit_card_view.xml (this is the custom control's layout):
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android" >
<EditText
style="#style/CreditCardInputField"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<EditText
style="#style/CreditCardInputField"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</merge>
In lib/java/TestView.java (the custom control itself):
public class TestView extends LinearLayout {
public TestView(Context context) {
super(context);
}
public TestView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.test_view, 0, 0);
int margins = (int)a.getDimension(R.styleable.test_view_field_margins, 0f);
int background = a.getResourceId(R.styleable.test_view_field_background, R.drawable.border);
int nameFieldHint = a.getResourceId(R.styleable.test_view_name_field_hint, R.string.name_field_hint_lib);
int numberFieldHint = a.getResourceId(R.styleable.test_view_number_field_hint, R.string.number_field_hint_lib);
a.recycle();
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.credit_card_view, this, true);
setOrientation(LinearLayout.VERTICAL);
setGravity(Gravity.CENTER_VERTICAL);
TextView title = (TextView) getChildAt(0);
title.setHint(nameFieldHint);
title.setBackgroundResource(background);
LinearLayout.LayoutParams p = new LinearLayout.LayoutParams(context, attrs);
p.setMargins(margins, margins, margins, margins);
title.setLayoutParams(p);
TextView number = (TextView) getChildAt(1);
number.setHint(numberFieldHint);
number.setBackgroundResource(background);
number.setLayoutParams(p);
}
}
And finally in app/layout/main_activity.xml, custom control's usage and configuration:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:custom="http://schemas.android.com/apk/res-auto"
...>
<com.example.testlibrary.TestView
custom:field_margins="20dp"
custom:field_background="#drawable/field_background"
custom:name_field_hint="#string/name_field_hint"
custom:number_field_hint="#string/number_field_hint"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
You should use parent attribute e.g.:
<style name="CreditCardInputField" parent="parentStyle">
<item name="android:layout_margin">10dp</item>
<item name="android:background">#drawable/border</item>
<item name="android:textStyle">bold|italic</item>
</style>
I am trying to create a custom compound view (subclassed out of relativelayout) on android and am trying to specify default styles. I can accomplish this, but I want to know what the defStyleAttr parameter is used for, and how to use it.
I have run into this method context.obtainStyledAttributes (AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes).
I understand that:
AttributeSet set I will get from the view's constructor if
inflating from XML,
int[] attrs will be the key values for any
custom view fields I may have created,
int defStyleRes will be a
default style that I can provide, which is defined as a style
resource.
However, I can't figure out how to use int defStyleAttr parameter. I've read the documentation and have provided an attr resource like this:
<declare-styleable name="BMTheme">
<attr name="BMButtonStyle" format="reference" />
</declare-styleable>
and then in my themes resource file I have this:
<style name="Theme.Custom" parent="#android:style/Theme">
<item name="BMButtonStyle">#style/BMButton</item>
</style>
which in turn references this style:
<style name="BMButton">
<item name="text">some string</item>
<item name="android:paddingLeft">10dp</item>
<item name="android:paddingRight">10dp</item>
</style>
In code, when I pass in 0 for defStyleAttr and the style resource id for defStyleRes, I successfully get the default values that I defined in my BMButton style:
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.RightArrowButton, 0, R.style.BMButton);
However when I pass in the attr resource id for BMButtonStyle(which in turn references the BMButton style) for defStyleAttr, and 0 for defStyleRes then I do not get the values I specified in the style.
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.RightArrowButton, R.attr.BMButtonStyle, 0);
Please let me know if I am using the defStyleAttr parameter the wrong way, and if anyone knows, why is there a need for both a defStyleAttr and a defStyleRes parameter when it seems like just a defStyleRes will suffice?
I figured out where I was going wrong after reading this post:
Creating default style with custom attributes
Basically, in order to use the defStyleAttr parameter, my project needed to set my custom theme defined theme in the manifest.
<application
android:allowBackup="true"
android:icon="#drawable/ic_launcher"
android:label="#string/app_name"
android:theme="#style/Theme.Custom" >
I've written a custom widget for a control that we use widely throughout our application. The widget class derives from ImageButton and extends it in a couple of simple ways. I've defined a style which I can apply to the widget as it's used, but I'd prefer to set this up through a theme. In R.styleable I see widget style attributes like imageButtonStyle and textViewStyle. Is there any way to create something like that for the custom widget I wrote?
Yes, there's one way:
Suppose you have a declaration of attributes for your widget (in attrs.xml):
<declare-styleable name="CustomImageButton">
<attr name="customAttr" format="string"/>
</declare-styleable>
Declare an attribute you will use for a style reference (in attrs.xml):
<declare-styleable name="CustomTheme">
<attr name="customImageButtonStyle" format="reference"/>
</declare-styleable>
Declare a set of default attribute values for the widget (in styles.xml):
<style name="Widget.ImageButton.Custom" parent="android:style/Widget.ImageButton">
<item name="customAttr">some value</item>
</style>
Declare a custom theme (in themes.xml):
<style name="Theme.Custom" parent="#android:style/Theme">
<item name="customImageButtonStyle">#style/Widget.ImageButton.Custom</item>
</style>
Use this attribute as the third argument in your widget's constructor (in CustomImageButton.java):
public class CustomImageButton extends ImageButton {
private String customAttr;
public CustomImageButton( Context context ) {
this( context, null );
}
public CustomImageButton( Context context, AttributeSet attrs ) {
this( context, attrs, R.attr.customImageButtonStyle );
}
public CustomImageButton( Context context, AttributeSet attrs,
int defStyle ) {
super( context, attrs, defStyle );
final TypedArray array = context.obtainStyledAttributes( attrs,
R.styleable.CustomImageButton, defStyle,
R.style.Widget_ImageButton_Custom ); // see below
this.customAttr =
array.getString( R.styleable.CustomImageButton_customAttr, "" );
array.recycle();
}
}
Now you have to apply Theme.Custom to all activities that use CustomImageButton (in AndroidManifest.xml):
<activity android:name=".MyActivity" android:theme="#style/Theme.Custom"/>
That's all. Now CustomImageButton tries to load default attribute values from customImageButtonStyle attribute of current theme. If no such attribute is found in the theme or attribute's value is #null then the final argument to obtainStyledAttributes will be used: Widget.ImageButton.Custom in this case.
You can change names of all instances and all files (except AndroidManifest.xml) but it would be better to use Android naming convention.
Another aspect in addition to michael's excellent answer is overriding custom attributes in themes.
Suppose you have a number of custom views that all refer to the custom attribute "custom_background".
<declare-styleable name="MyCustomStylables">
<attr name="custom_background" format="color"/>
</declare-styleable>
In a theme you define what the value is
<style name="MyColorfulTheme" parent="AppTheme">
<item name="custom_background">#ff0000</item>
</style>
or
<style name="MyBoringTheme" parent="AppTheme">
<item name="custom_background">#ffffff</item>
</style>
You can refer to the attribute in a style
<style name="MyDefaultLabelStyle" parent="AppTheme">
<item name="android:background">?background_label</item>
</style>
Notice the question mark, as also used for reference android attribute as in
?android:attr/colorBackground
As most of you have noticed, you can -and probably should- use #color references instead of hard coded colors.
So why not just do
<item name="android:background">#color/my_background_color</item>
You can not change the definition of "my_background_color" at runtime, whereas you can easily switch themes.
I have 2 styles defined inside styles.xml. I want to apply it to a textview. How to implement that using style = "#style/"
You can't. You will have to create a style which combines the two styles. (Or create just one style that inherits from one of your styles, and add the extra data of the second style).
You can make a style that inherit the other style
For example:
<style name="Side_Menu_Button" parent="android:attr/buttonStyleSmall">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">match_parent</item>
</style>
Where the side_menu_button inherit from all the attribute of buttonStyleSmall
As a workaround that can work in some situations, you can wrap your target view with LinearLayout and assign one style to the layout another to the view:
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
style="#style/padding">
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Bold text with padding"
style="#style/text_bold" />
</LinearLayout>
This is a hack that I got to work:
<style name="TextAppearance.Title.App" parent="TextAppearance.AppCompat.Subhead">
<item name="android:textColor">#color/primary_text_default_material_light</item>
</style>
<style name="Custom.TV" parent="TextView.App">
<item name="android:textAppearance">#style/TextAppearance.Other.App</item>
</style>
For the particular case of a Button and other Views which support the textAttribute attribute, you can divide the two styles into a Button specific style which would be assigned to attribute:style and a Text specific style which would be assigned to attribute:textAppearance. Note though, that attributes defined in attribute:style will override values defined in attribute:textAppearance.
I know I'm 10 years late but I came across this problem myself and found a solution for it albeit it's quite a workaround.
To get started you need to declare styleable attributes to assign to your view later on
<declare-styleable name="TextView">
<attr name="style1" format="reference" />
<attr name="style2" format="reference" />
<attr name="style3" format="reference" />
<attr name="style4" format="reference" />
<attr name="style5" format="reference" />
</declare-styleable>
You can just add these style attributes to your view within the layout like
<TextView
android:id="#+id/button_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/nice_cta"
app:style1="#style/Background_Blue"
app:style2="#style/CallToAction.Primary"
app:style3="#style/Button_Layout" />
To make it work you need to implement a custom ViewInflater that you assign to your application's theme under viewInflaterClass. Inside this ViewInflater you collect the styleable attributes and merge them into a theme as follows:
class MultiStyleViewInflater : MaterialComponentsViewInflater() {
// override the creators of any view you want to have multiple styles
override fun createTextView(context: Context, attrs: AttributeSet?): AppCompatTextView {
// create context if needed and set the attributes as usual
return super.createTextView(createContextIfMultiStyle(context, attrs), attrs)
}
// override fun anyOtherView as needed ...
private fun createContextIfMultiStyle(context: Context, attrs: AttributeSet?): Context {
// get our handy custom attributes
val styleAttributes = context.obtainStyledAttributes(attrs, R.styleable.TextView)
// collect the styles added to the view
val styles = extractStyles(styleAttributes)
// create the custom ContextThemeWrapper only if the view has a custom multi style attribute
val createdContext = if (styles.any { it != 0 }) {
// create a theme, add styles and create the wrapper using the theme
val theme = context.resources.newTheme()
theme.applyValidStyles(styles)
ContextThemeWrapper(context, theme)
} else {
// or just return the original context
context
}
// don't forget to call this!
styleAttributes.recycle()
return createdContext
}
private fun extractStyles(styleAttributes: TypedArray) = listOf(
// the zero values help us determine if we have a custom style added at all
styleAttributes.getResourceId(R.styleable.TextView_style1, 0),
styleAttributes.getResourceId(R.styleable.TextView_style2, 0),
styleAttributes.getResourceId(R.styleable.TextView_style3, 0),
styleAttributes.getResourceId(R.styleable.TextView_style4, 0),
styleAttributes.getResourceId(R.styleable.TextView_style5, 0)
)
private fun Resources.Theme.applyValidStyles(styles: List<Int>) {
// adding styles that actually exist. note we force update duplicate attributes
styles.filterNot { it == 0 }.forEach { this.applyStyle(it, true) }
}
}
To make this your app theme's ViewInflater add this line to it:
<item name="viewInflaterClass">com.agostonr.multistyleapp.utils.MultiStyleViewInflater</item>
After this if you build your application the styles should show up in the editor as well as on the running app on your device.
For a more detailed explanation see the article I wrote about it on Medium.