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>
Related
I need to add a variable number of Seekbars depending on configuration.
for(int i=0;i<length;i++){
seeks[i] = new SeekBar(getActivity());
seeks[i].setMax(4);
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
layoutParams.setMargins(30,30,30,30);
seeks[i].setLayoutParams(layoutParams);
ll.addView(seeks[i]);
seeks[i].setVisibility(View.VISIBLE);
Works perfectly, but I would like the Seekbar to be discrete. In XML you can just add:
<SeekBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="#style/Base.Widget.AppCompat.SeekBar.Discrete"/>
However, if I follow the Docs, you should be able to add a Theme in the Seekbar constructor like so:
seeks[i] = new SeekBar(getActivity(), null, R.style.myTheme2 );
where I have set the theme in the styles.xml, OR
seeks[i] = new SeekBar(getActivity(), null, R.style.Widget_AppCompat_SeekBar_Discrete);
However, as soon as I do either of those things, the Seekbar disappears from view completely.
Got it working, leaving up for others who have the same problem, as I couldn't find it anywhere:
First setup a layout for each individual SeekBar:
seekbar_base.xml
<?xml version="1.0" encoding="utf-8"?>
<SeekBar
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:progressBackgroundTint="#color/seekBarBackground"
android:progressTint="#color/seekBarProgress"
android:thumbTint="#color/seekBar"
android:paddingLeft="20sp"
android:paddingTop="3dp"
android:paddingRight="20dp"
android:paddingBottom="30dp"
android:theme="#style/Widget.AppCompat.SeekBar.Discrete" />
Then instantiate it like so:
SeekBar bar = (SeekBar)LayoutInflater.from(getActivity()).inflate(R.layout.seekbar_base, null);
I still have no idea why it didn't work the way the docs said it should, but oh well.
when you use the XML, it actually set some default attribute. it is located in core / res / res / values / styles.xml
<style name="Widget.SeekBar">
<item name="android:indeterminateOnly">false</item>
<item name="android:progressDrawable">#android:drawable/progress_horizontal</item>
<item name="android:indeterminateDrawable">#android:drawable/progress_horizontal</item>
<item name="android:minHeight">20dip</item>
<item name="android:maxHeight">20dip</item>
<item name="android:thumb">#android:drawable/seek_thumb</item>
<item name="android:thumbOffset">8dip</item>
<item name="android:focusable">true</item>
</style>
at the same time, the SeekBar(Context context) also use the defalut style. it relaize that by call the same method you called, but set a defalut style.
public SeekBar(Context context) {
this(context, null);
}
public SeekBar(Context context, AttributeSet attrs) {
this(context, attrs, com.android.internal.R.attr.seekBarStyle);
}
so change your theme extends it.the add your custom item.
seeks[i] = new SeekBar(getActivity(), null, R.style.Widget_AppCompat_SeekBar_Discrete);
here are the document descirbe for three params constructor:
Perform inflation from XML and apply a class-specific base style from a
theme attribute. This constructor of View allows subclasses to use their
own base style when they are inflating. For example, a Button class's
constructor would call this version of the super class constructor and
supply R.attr.buttonStyle for defStyleAttr; this
allows the theme's button style to modify all of the base view attributes
(in particular its background) as well as the Button class's attributes.
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
In Android M, I saw that getColor(int id) was replaced with ContextCompat.getColor(Context context, int id).
I'm new to styling with themes in Android, so I wasn't sure how to use this function correctly. Currently, the way I'm organizing my colors is by defining attrs like so:
values/attr.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="button_bg" format="reference|color"/>
</resources>
then I reference them like so:
values/themes.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="MyTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowAnimationStyle">#null</item>
</style>
<style name="MyTheme.White">
<item name="button_bg">#fff</item>
</style>
<style name="MyTheme.Black">
<item name="button_bg">#000</item>
</style>
</resources>
this works.
However, how do I now get button_bg in java?
I tried ContextCompat.getColor(context, R.attr.button_bg) but that gave me an error of Resource not found.
Am I approaching this incorrectly?
Have seen, the Question was longer than a year ago, so hope i can still help other people with this.
You can simply use the attribute id in your XML:
<View
android:layout_height="50dp"
android:layout_width="50dp"
android:background="?attr/button_bg">
..../>
In your Activity you could set your theme in on Create:
protected void onCreate(Bundle savedInstanceState) {
setTheme(R.style.your_theme);//your theme
super.onCreate(savedInstanceState);
setContentView(R.layout.your_layout);
....
}
The rest is made automatically by android.
To access the color depending on the theme via code you can use the following function:
#ColorInt
public int getColorByAttributeId(Context context, #AttrRes int attrIdForColor){
TypedValue typedValue = new TypedValue();
Resources.Theme theme = context.getTheme();
theme.resolveAttribute(attrIdForColor, typedValue, true);
return typedValue.data;
}
The method is simply "stupid", which means there no check that the requested attribute color is a real color resource.
Given I want some subclassing of a LinearLayout like this:
public class MyLayout extends LinearLayout {
public int someState=0;
public ListViewHeadView(Context context) {
super(context);
View.inflate(context, R.layout.some_layout, this);
}
// more code here
}
with some_layout like this:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
style="#style/textStyle"
android:id="#+id/someTextView"/>
<Button
style="#style/buttonStyle"
android:id="#+id/someButton"/>
</LinearLayout>
this works fine, but I have one LinearLayout to much which costs performance. The only way I see at the moment to remove the unneeded LinearLayout is to use a <merge> inside the xml instead of the LinearLayout. But then I would have to do a setOrientation in my code which I would love to see in an xml ( once - not at all usages ).
Can somebody point me to a pattern on how to do this nicely? If I do not have a state I can use the decorator-pattern and just decorate the LinearLayout with the functions I need - but when I need a state in this Layout I see no good way yet. Any hints and best practices welcome!
I'd define the android:orientation attribute in your project's attrs.xml for your custom ViewGroup and add some code to evaluate this in the ViewGroups constructor.
This is one of the uses of the <merge> element. Put it as the root in your XML file in place of the <LinearLayout>
The orientation can then be set whenever you use your custom view using <com.example.MyView ... android:orientation="....">
Or in code with setOrientation
If the orientation is always the same for this customs view, you can override set orientation and not call the super or something to that effect
Define a stylable attribute and provide the orientation through the style
attr.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MyLayout">
<attr name="myLayoutStyle" format="reference"/>
</declare-styleable>
</resources>
style.xml
<!-- Base application theme. -->
<style name="AppTheme" parent="android:Theme.Holo.Light.DarkActionBar">
<item name="android:windowBackground">#color/main_background</item>
<item name="myLayoutStyle">#style/MyLayoutStyle</item>
</style>
<style name="MyLayoutStyle" parent="android:Widget.Holo.Light">
<item name="android:orientation">vertical</item>
</style>
</resources>
Then in your constructor:
public class MyLayout extends LinearLayout {
public int someState=0;
public ListViewHeadView(Context context) {
super(context, null, R.attr.MyLayoutStyle)
View.inflate(context, R.layout.some_layout, this);
}
// more code here
}
In the constructor the reference is to the Stylable, not the style
Solution adapted from:
http://trickyandroid.com/protip-inflating-layout-for-your-custom-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.