How to: Define theme (style) item for custom widget - android

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.

Related

Define style and custom attribute in View instead of xml

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)

Default values for attributes not available in theme

I have defined custom attribute to use in my views:
<attr name="customColor" format="color"/>
And specified its value in my theme:
<style name="AppTheme" parent="Theme.AppCompat.Light">
<!-- some other values -->
<item name="customColor">#6d4c41</item>
</style>
Now, I can use this attribute value in my view. Both in XML:
<TextView
android:textColor="?attr/customColor"
/>
and in code (Kotlin):
with(
context.theme.obtainStyledAttributes(
intArrayOf(R.attr.customColor)
)
) {
textColor = getColor(0, Color.parseColor("#ffffff"))
recycle()
}
(Java):
TypedArray a = context.getTheme().obtainStyledAttributes(
new int[]{R.attr.customColor}
);
textColor = a.getColor(0, Color.parseColor("#ffffff"));
a.recycle();
In case I forget to define the value in my theme, the code above will work fine, since I specified the default value. However, the XML will throw an exception when I try to inflate it.
Is there a way to specify default attribute value for XML?

Android how to use app attribute for custom class options [duplicate]

I know it is possible to create custom UI element (by way of View or specific UI element extension). But is it possible to define new properties or attributes to newly created UI elements (I mean not inherited, but brand new to define some specific behavior I am not able to handle with default propertis or attributes)
e.g. element my custom element:
<com.tryout.myCustomElement
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Element..."
android:myCustomValue=<someValue>
/>
So is it possible to define MyCustomValue?
Thx
Yes. Short guide:
Create an attribute XML
Create a new XML file inside /res/values/attrs.xml, with the attribute and its type
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<declare-styleable name="MyCustomElement">
<attr name="distanceExample" format="dimension"/>
</declare-styleable>
</resources>
Basically you have to set up one <declare-styleable /> for your view that contains all your custom attributes (here just one). I never found a full list of possible types, so you need to look at the source for one I guess. Types that I know are reference (to another resource), color, boolean, dimension, float, integer and string. They are pretty self-explanatory
Use the attributes in your layout
That works the same way you did above, with one exception. Your custom attribute needs its own XML namespace.
<com.example.yourpackage.MyCustomElement
xmlns:customNS="http://schemas.android.com/apk/res/com.example.yourpackage"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Element..."
customNS:distanceExample="12dp"
/>
Pretty straight forward.
Make use of the values you get passed
Modify the constructor of your custom view to parse the values.
public MyCustomElement(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MyCustomElement, 0, 0);
try {
distanceExample = ta.getDimension(R.styleable.MyCustomElement_distanceExample, 100.0f);
} finally {
ta.recycle();
}
// ...
}
distanceExample is a private member variable in this example. TypedArray has lots of other things to parse other types of values.
And that's it. Use the parsed value in your View to modify it, e.g. use it in onDraw() to change the look accordingly.
In your res/values folder create attr.xml. There you can define your attribues:
<declare-styleable name="">
<attr name="myCustomValue" format="integer/boolean/whatever" />
</declare-styleable>
When you then want to use it in your layout file you have to add
xmlns:customname="http://schemas.android.com/apk/res/your.package.name"
and then you can use the value with customname:myCustomValue=""
Yes , you can.Just use <resource> tag.
like this:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="CodeFont" parent="#android:style/TextAppearance.Medium">
<item name="android:layout_width">fill_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:textColor">#00FF00</item>
<item name="android:typeface">monospace</item>
</style>
</resources>
link from official website

How is the defStyleAttrs parameter used in obtainStyledAttributes

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" >

multiple style values inside a view

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.

Categories

Resources