Theme/style 一个自定义视图

Theme/style a custom View

我尝试使用默认样式为自定义(和派生)视图设置主题。 我的解决方案基于此 answer 但仅适用于 api >= 21 并且会在所有早期版本上崩溃。 我将 AppCompat appcompat-v7 与新的 AppCompatActivity 基础 class.

一起使用

我的自定义视图:

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();
    }
    ....
}

不使用 tintedImageButtonStyle 的自定义视图有效(在 Whosebug 上也找到了)。

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>

我见过一些在 declare-styleable 之外声明主题属性引用的示例。但我不明白其中的区别,尤其是引用的 Whosebug 帖子中 name="CustomTheme" 的含义。

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>

布局片段示例

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

在 Lollipop 上它按预期工作,但它会在 kitkat 及以下版本的带有此类按钮的布局中膨胀时崩溃。

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.access0(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.access0(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)

好吧,我找到了一个解决方案,同时又不会在默认主题样式中使用引用:

我已将我的默认主题样式更改为:

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

我已将这两个属性添加到我的 attr.xml

<declare-styleable name="CustomTheme">
    <attr name="tintedImageButtonStyle" format="reference"/>
    <attr name="buttonTint" format="reference"/>
    <attr name="buttonBackground" format="reference"/>
</declare-styleable>

在我的 themes.xml 中,我选择了属性值:

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

语义相同,但现在它也适用于 api < 21 而不会失去控制 ;)