Reputation: 520
I've try to theme a custom (and derived) View with a default style. My solution is based on this answer but only works for api >= 21 and will crash on all earlier versions. I'm using AppCompat appcompat-v7 with the new AppCompatActivity base class.
My custom View:
public class TintableImageButton extends ImageButton {
private ColorStateList tint;
public TintableImageButton(Context context) {
this(context, null);
}
public TintableImageButton(Context context, AttributeSet attrs) {
this(context, attrs, R.attr.tintedImageButtonStyle);
}
public TintableImageButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public TintableImageButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs, defStyleAttr);
}
private void init(Context context, AttributeSet attrs, int defStyle) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TintableImageButton, defStyle, 0);
tint = a.getColorStateList(R.styleable.TintableImageButton_tint);
a.recycle();
}
....
}
the custom view without using tintedImageButtonStyle works (found too on stackoverflow).
attr.xml
<resources>
<declare-styleable name="TintableImageButton">
<attr name="tint" format="reference|color" />
</declare-styleable>
<declare-styleable name="CustomTheme">
<attr name="tintedImageButtonStyle" format="reference"/>
</declare-styleable>
</resources>
i've seen some examples declaring the theme-attribute reference outside an declare-styleable. But i don't understand the difference, especially the meaning of name="CustomTheme" in the referenced stackoverflow posting.
themes.xml
<resources>
<style name="Base.Theme.ElectroBoxApp" parent="Theme.AppCompat.Light.NoActionBar">
....
</style>
<style name="AppBaseTheme" parent="Base.Theme.ElectroBoxApp">
....
</style>
<style name="AppTheme" parent="AppBaseTheme">
<item name="tintedImageButtonStyle">@style/TintedImageButton</item>
</style>
</resources>
styles.xml
<resources>
<style name="TintedImageButton" parent="Base.Widget.AppCompat.Button">
<item name="android:minWidth">48dp</item>
<item name="android:paddingLeft">4dp</item>
<item name="android:paddingRight">4dp</item>
<item name="android:clickable">true</item>
<item name="android:background">@drawable/btn_default_background</item>
<item name="android:scaleType">center</item>
<item name="tint">@color/button_tint_csl</item>
</style>
</resources>
drawable/btn_default_background.xml
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?attr/selectableItemBackgroundBorderless"/>
<item android:top="4dp" android:left="4dp" android:bottom="4dp" android:right="4dp">
<shape android:shape="oval">
<size android:height="30dp" android:width="30dp"/>
<solid android:color="?attr/colorAccent"/>
</shape>
</item>
</layer-list>
AndroidManifest.xml
....
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="23"/>
<application android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:name=".AppController"
android:theme="@style/AppTheme">
....
</application>
sample layout snippet
<!-- line 31 is below -->
<de.NullZero.ManiDroid.presentation.views.TintableImageButton
android:id="@+id/btnPlaylist"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ic_library_music_white_24dp"
/>
On Lollipop it works as expected, but it would crash on inflating a layout with such a button on kitkat and below.
09-14 12:06:57.800 1939-1962/de.NullZero.ManiDroid E/AndroidRuntime﹕ FATAL EXCEPTION: main
Process: de.NullZero.ManiDroid, PID: 1939
java.lang.RuntimeException: Unable to start activity ComponentInfo{de.NullZero.ManiDroid/de.NullZero.ManiDroid.presentation.ManiDroidAppActivity}: android.view.InflateException: Binary XML file line #31: Error inflating class de.NullZero.ManiDroid.presentation.views.TintableImageButton
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2195)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2245)
at android.app.ActivityThread.access$800(ActivityThread.java:135)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1196)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:136)
at android.app.ActivityThread.main(ActivityThread.java:5017)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:515)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:779)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:595)
at dalvik.system.NativeStart.main(Native Method)
Caused by: android.view.InflateException: Binary XML file line #31: Error inflating class de.NullZero.ManiDroid.presentation.views.TintableImageButton
at android.view.LayoutInflater.createView(LayoutInflater.java:621)
at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:697)
at android.view.LayoutInflater.rInflate(LayoutInflater.java:756)
at android.view.LayoutInflater.rInflate(LayoutInflater.java:759)
at android.view.LayoutInflater.inflate(LayoutInflater.java:492)
at android.view.LayoutInflater.inflate(LayoutInflater.java:397)
at android.view.LayoutInflater.inflate(LayoutInflater.java:353)
at de.NullZero.ManiDroid.presentation.fragments.MiniPlayerFragment.onCreateView(MiniPlayerFragment.java:70)
at android.support.v4.app.Fragment.performCreateView(Fragment.java:1789)
at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:924)
at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1116)
at android.support.v4.app.FragmentManagerImpl.addFragment(FragmentManager.java:1218)
at android.support.v4.app.FragmentManagerImpl.onCreateView(FragmentManager.java:2170)
at android.support.v4.app.FragmentActivity.onCreateView(FragmentActivity.java:300)
at android.support.v7.app.AppCompatDelegateImplV7.callActivityOnCreateView(AppCompatDelegateImplV7.java:842)
at android.support.v7.app.AppCompatDelegateImplV11.callActivityOnCreateView(AppCompatDelegateImplV11.java:34)
at android.support.v7.app.AppCompatDelegateImplV7.onCreateView(AppCompatDelegateImplV7.java:830)
at android.support.v4.view.LayoutInflaterCompatHC$FactoryWrapperHC.onCreateView(LayoutInflaterCompatHC.java:44)
at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:685)
at android.view.LayoutInflater.rInflate(LayoutInflater.java:756)
at android.view.LayoutInflater.rInflate(LayoutInflater.java:759)
at android.view.LayoutInflater.inflate(LayoutInflater.java:492)
at android.view.LayoutInflater.inflate(LayoutInflater.java:397)
at android.view.LayoutInflater.inflate(LayoutInflater.java:353)
at android.support.v7.app.AppCompatDelegateImplV7.setContentView(AppCompatDelegateImplV7.java:249)
at android.support.v7.app.AppCompatActivity.setContentView(AppCompatActivity.java:106)
at de.NullZero.lib.navdrawer.AbstractNavDrawerActivity.onCreate(AbstractNavDrawerActivity.java:43)
at de.NullZero.ManiDroid.presentation.ManiDroidAppActivity.onCreate(ManiDroidAppActivity.java:75)
at android.app.Activity.performCreate(Activity.java:5231)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1087)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2159)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2245)
at android.app.ActivityThread.access$800(ActivityThread.java:135)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1196)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:136)
at android.app.ActivityThread.main(ActivityThread.java:5017)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:515)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:779)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:595)
at dalvik.system.NativeStart.main(Native Method)
Caused by: java.lang.reflect.InvocationTargetException
at java.lang.reflect.Constructor.constructNative(Native Method)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at android.view.LayoutInflater.createView(LayoutInflater.java:595)
Upvotes: 0
Views: 6559
Reputation: 520
okay, i've found a solution without loosing to use references in default-theme-styles:
i've changed my default-theme-style to:
<style name="TintedImageButton" parent="Base.Widget.AppCompat.Button.Borderless">
<item name="android:minWidth">48dp</item>
<item name="android:paddingLeft">4dp</item>
<item name="android:paddingRight">4dp</item>
<item name="android:clickable">true</item>
<item name="android:background">?attr/buttonBackground</item>
<item name="android:scaleType">center</item>
<item name="tint">?attr/buttonTint</item>
</style>
i've added both attributes to my attr.xml
<declare-styleable name="CustomTheme">
<attr name="tintedImageButtonStyle" format="reference"/>
<attr name="buttonTint" format="reference"/>
<attr name="buttonBackground" format="reference"/>
</declare-styleable>
and in my themes.xml i've selected the values for the attributes:
<style name="AppTheme" parent="AppBaseTheme">
<!-- All customizations that are NOT specific to a particular API-level can go here. -->
<item name="tintedImageButtonStyle">@style/TintedImageButton</item>
<item name="buttonTint">@color/button_tint_csl</item>
<item name="buttonBackground">?attr/selectableItemBackgroundBorderless</item>
</style>
the semantic is equal but now it works on api < 21 too without loosing control ;)
Upvotes: 2