0

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)
Community
  • 1
  • 1
Markus Schulz
  • 500
  • 5
  • 12
  • uses lollipop specific style attributes in a folder under values-21/themes.xml , this way it wont crash on pre-lollipop devices – rahul.ramanujam Sep 14 '15 at 17:26
  • Do you have references in your drawables/color state lists to color from style? – dtx12 Sep 14 '15 at 19:14
  • it's not the CSL, it's the background definition in my default TintedImageButton style. If i comment it out, it starts on kitkat. But i don't know why? i will add my @drawable/btn_default_background to my post. – Markus Schulz Sep 15 '15 at 15:12
  • @dtx12 kann you explain what is wrong with using references in default styles, like the ref to ?attr/colorAccent and ?attr/selectableItemBackgroundBorderless in my example? – Markus Schulz Sep 17 '15 at 15:02
  • It's not supported before lollipop, you can't have reference to color from attributes in your drawable on pre-L. – dtx12 Sep 17 '15 at 15:07
  • hmm okay, you mean i can't do *any* attr references in theme-styles? But if i use style="@drawable/btn_default_background" (the above definition with two attr-references) at the Widget it works fine in kitkat. And how it works in the all of the appcompat theme definitions? Can you point me to the android docs about this new lollipop feature? – Markus Schulz Sep 17 '15 at 20:15
  • Written article on [styling custom views in Android](http://onetouchcode.com/2016/11/25/styling-custom-views-android/). – Shailendra Nov 27 '16 at 05:41

1 Answers1

2

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 ;)

Markus Schulz
  • 500
  • 5
  • 12