View 绘制体系知识梳理(8) - obtainStyledAttributes 详解

一、基本概念

1.1 资源

Android使用xml文件来描述各种资源,包括字符串、颜色、主题、布局等等。资源分为两个部分,及 属性

1.1.1 属性

App开发的过程中,如果需要为自定义View声明一个新的属性,那么我们会在res/values/attr.xml文件中进行定义。

<resources>
    <declare-styleable name="AttrTextView">
        <attr name="attrTvName" format="string"/>
        <attr name="attrTvColor" format="color"/>
    </declare-styleable>
</resources>

其中declare-styleable相当于一个属性的集合,而attr则是其内部的属性,在R.java文件中declare-styleable对应一个int[]数组。

declare-styleable 对应的 int[] 数组

需要注意的是:

  • 如果attr后面仅有一个name:,那么这就是 引用,其声明在别的styleable中。
  • 如果attr后面不仅有name,还有format,那就是 声明,不能在别的styleable中再次声明。

1.1.2 值

常见的值存放在以下几个位置:

  • 字符串、颜色、样式,主题等,在res/values/xxx.xml下,文件名可以为strings/colors/styles/theme等。
  • drawable,在res/drawable/xxx.xml
  • layout,在res/layout/xxx.xml

对于值的类型分为两种,一种是基本类型,例如integerstringboolean等;另一种是引用类型,例如reference

1.2 资源解析

资源解析涉及到两个类,AttributeSetTypedArray

1.2.1 AttributeSet

AttributeSetxml文件解析时会返回的对象,它包含了 解析元素的所有属性及属性值AttributeSet提供了一组接口可以根据attr.xml中已有的名称获取相应的值。

  • 操作特定属性
  • 操作通用属性
  • 获取特定类型的值

1.2.2 TypedArray

TypedArray是对AttributeSet数据类的某种抽象,对于下面在控件的xml自定义的属性而言:

browser:image_border_width="@dimen/light_app_icon_border_width"

如果采用AttributeSet的方法,那么仅仅可以获取@dimen/light_app_icon_border_width

如果想要获取light_app_icon_border_width对应的值,那么可以通过contextobtainStyledAttributesAttributeSet作为参数构造TypedArray对象,从而直接获取TypeArray的值。

TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RoundedCornerImageView, defStyle, 0);

二、实例讲解

下面我们来看以下,如何在控件中使用自定义的属性,实现的核心就是obtainStyledAttributes函数。

2.1 直接在 View 的 xml 中指定具体的属性

这是最直观也最常见的方式,我们需要做的有以下三步:

  • res/values/attr.xml中定义属性名称,为了方便管理,我们一般都会将一个控件中的所有自定义属性通过<declare-styleable>标签包裹起来,里面的每一个<attr>代表一个自定义属性。
  • layout布局中,指定对应的自定义属性的属性值。
  • View的构造方法中,通过obtainStyledAttributes得到TypedArray对象,obtainStyledAttributes的第一个参数传入构造函数中AttributeSet对象,它包含了该控件在xml中声明的属性及其值,第二个参数就是我们在第一步中定义的styleable。得到该TypedArray对象后,再通过getXXX获得对应的属性值,从而实现通过xml自定义控件属性的效果。

2.1.1 代码

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="AttrTextView">
        <attr name="attrTvName" format="string"/>
        <attr name="attrTvColor" format="color"/>
    </declare-styleable>
</resources>
public class AttrTextView extends TextView {

    public AttrTextView(Context context) {
        super(context);
    }

    public AttrTextView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initAttr(context, attrs);
    }

    public AttrTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initAttr(context, attrs);
    }

    private void initAttr(Context context, @Nullable AttributeSet attrs) {
        //attrs 类型为 AttributeSet,其包含了在 xml 中指定的属性和值。
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.AttrTextView, 0, 0);
        if (typedArray != null) {
            String text = typedArray.getString(R.styleable.AttrTextView_attrTvName);
            int color = typedArray.getColor(R.styleable.AttrTextView_attrTvColor, 0);
            setText(text);
            setTextColor(color);
            typedArray.recycle();
        }
    }
    
}
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.demo.lizejun.attrdemo.AttrTextView
        android:id="@+id/tv_content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:attrTvColor="@color/attrColorXmlDirect"
        app:attrTvName="直接在 View 的 xml 中指定具体的属性"/> <!-- 直接通过属性来指定 -->

</FrameLayout>

2.1.2 运行结果

2.2 在 View 的 xml 中声明 style,通过 style 间接指定属性

2.1中直接指定的方式,优点是 简单且直观,但是也有它的局限性,就是每次使用该控件的时候都需要对属性进行定义,假如我们有 多个相同的控件要使用相同的样式 时,就可以采用指定相同@style的方式进行复用。

对于自定义的控件的Java代码,和2.1的方式是相同的,区别在于在xml中不指定具体的属性值,而通过style="@style/xxx"的方式,在style中再指定属性。

2.2.1 代码

public class AttrTextView extends TextView {

    public AttrTextView(Context context) {
        super(context);
    }

    public AttrTextView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initAttr(context, attrs);
    }

    public AttrTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initAttr(context, attrs);
    }

    private void initAttr(Context context, @Nullable AttributeSet attrs) {
        //attrs 类型为 AttributeSet,其包含了在 xml 中指定的属性和值。
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.AttrTextView, 0, 0);
        if (typedArray != null) {
            String text = typedArray.getString(R.styleable.AttrTextView_attrTvName);
            int color = typedArray.getColor(R.styleable.AttrTextView_attrTvColor, 0);
            setText(text);
            setTextColor(color);
            typedArray.recycle();
        }
    }
    
}
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.demo.lizejun.attrdemo.AttrTextView
        android:id="@+id/tv_content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:attrTvColor="@color/attrColorXmlDirect"
        style="@style/AttrUseXmlStyle"/>

</FrameLayout>
<resources>

    <style name="AttrUseXmlStyle">
        <item name="attrTvName">"在 View 的 xml 中声明 style,通过 style 间接指定属性"</item>
    </style>

</resources>

2.2.2 运行结果

2.3 在主题中指定某个 attr 的 style,并将该 attr 作为第三个参数传给 obtainStyledAttributes

除了在xml中通过@style的方式进行复用,还可以采用定义主题的方式,这样我们就可以不必在每个xml中都指定@style,系统会自动去主题当中寻找对应的属性值,实现这种方案需要做以下三步:

  • res/values/attr.xml中声明一个新的AttrThemeStyle属性,其类型为reference
  • res/values/style.xml中定义一个新的style,命名为AttrUseThemeStyle,它包含了自定义控件的属性,将该AttrUseThemeStyle在应用的主题AppTheme中,指定给第一步定义的AttrThemeStyle属性。
  • 在自定义控件中,将R.attr.AttrThemeStyle作为obtainStyledAttributes方法的第三个参数。

2.3.1 代码

public class AttrTextView extends TextView {

    public AttrTextView(Context context) {
        super(context);
    }

    public AttrTextView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initAttr(context, attrs);
    }

    public AttrTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initAttr(context, attrs);
    }

    private void initAttr(Context context, @Nullable AttributeSet attrs) {
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.AttrTextView, R.attr.AttrThemeStyle, 0);
        if (typedArray != null) {
            String text = typedArray.getString(R.styleable.AttrTextView_attrTvName);
            int color = typedArray.getColor(R.styleable.AttrTextView_attrTvColor, 0);
            setText(text);
            setTextColor(color);
            typedArray.recycle();
        }
    }

}
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <attr name="AttrThemeStyle" format="reference"/>
    <declare-styleable name="AttrTextView">
        <attr name="attrTvName" format="string"/>
        <attr name="attrTvColor" format="color"/>
    </declare-styleable>
</resources>
<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
        <item name="AttrThemeStyle">@style/AttrUseThemeStyle</item>
    </style>

    <style name="AttrUseThemeStyle">
        <item name="attrTvName">"在主题中指定某个 attr 的 style,并将该 attr 作为第三个参数传给 obtainStyledAttributes"</item>
    </style>

</resources>

2.3.2 运行结果

2.4 通过 obtainStyledAttributes 的第四个参数直接传入一个 style,该 style 中的 item 指定字符串

最后一种方法最简单(给所有用到这个控件的地方都设置一个默认值),做法就是定义一个新的style,在该style中指定控件的自定义属性,作为obtainStyledAttributes的第四个参数。

2.4.1 代码

public class AttrTextView extends TextView {

    public AttrTextView(Context context) {
        super(context);
    }

    public AttrTextView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initAttr(context, attrs);
    }

    public AttrTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initAttr(context, attrs);
    }

    private void initAttr(Context context, @Nullable AttributeSet attrs) {
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.AttrTextView, 0, R.style.AttrUseDirectStyle);
        if (typedArray != null) {
            String text = typedArray.getString(R.styleable.AttrTextView_attrTvName);
            int color = typedArray.getColor(R.styleable.AttrTextView_attrTvColor, 0);
            setText(text);
            setTextColor(color);
            typedArray.recycle();
        }
    }

}
<resources>

    <style name="AttrUseDirectStyle">
        <item name="attrTvName">"通过 obtainStyledAttributes 的第四个参数直接传入一个 style,该 style 中的 item 指定字符串"</item>
    </style>

</resources>

2.4.2 运行结果

2.5 小结

对于以上四种处理方式,如果 重复指定了同一个属性的不同值,那么将会按照优先级的顺序进行匹配,优先级按照2.1-2.4的顺序依次递减,最终以高优先级指定的值为准。

也就是说,如果通过2.1的方式直接指定了attrTvName,那么即使通过2.2的方式在style中指定attrTvName的属性,也会以2.1为准,使用下来我们可以发现,它的粒度其实是依次增大的:

  • 单一xml的属性
  • 多个xml复用@style
  • 应用或者Activity的主题复用@style
  • 全局默认@style

三、夜间模式

下面,通过一个项目中的例子来介绍一下obtainStyledAttributes的应用,其最终效果就是通过Attr的方式来实现不重启的夜间模式切换,项目的地址为 AttrDemo

该方案的原理就是给控件声明一个属性dayNightAttr,该属性指向了一个style,该style中又包含两个新的attr,分别指向白天和夜间模式的style,在切换的时候,通过遍历对应模式的style中的属性,将其属性值应用到对应的控件当中。

3.1 使用方法

首先来看一下使用的方法,首先在控件中对dayNightAttr进行指定:

xml 中的指定

接下来为NightModeTextViewStyle定义两个style,分别指向白天和夜间模式:

style 的定义

NightModeTextView实现了INightMode接口,当需要切换的时候,调用该接口传入当前的主题即可

切换方式

3.2 实现原理

首先看一下支持夜间模式的控件的实现:

public class NightModeTextView extends AppCompatTextView implements INightMode {

    private HashMap<String, Integer> mTheme = new HashMap<>();
    private String mCurTheme = null;

    public NightModeTextView(Context context) {
        super(context);
    }

    public NightModeTextView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initView(context, attrs, R.attr.dayNightAttr);
    }

    public NightModeTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView(context, attrs, defStyleAttr);
    }

    private void initView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        int[] dayNightResId = DayNightHelper.getDayNightStyleId(context, attrs, defStyleAttr);
        if (dayNightResId[0] != 0) {
            mTheme.put(DayNightHelper.THEME_DAY, dayNightResId[0]);
        }
        if (dayNightResId[1] != 0) {
            mTheme.put(DayNightHelper.THEME_NIGHT, dayNightResId[1]);
        }
        applyTheme(DayNightHelper.getCurTheme(context));
    }

    @Override
    public void applyTheme(String theme) {
        if (TextUtils.equals(mCurTheme, theme)) {
            return;
        }
        Integer styleId = mTheme.get(theme);
        if (styleId != null) {
            DayNightHelper.applyTextView(this, styleId);
        }
    }
}

第一个关键的函数为DayNightHelper.getDayNightStyleId,它会解析我们在3.1中定义的dayNightAttr的属性值,得到白天和夜间模式主题的ID,这里用到的方式就是我们前面看到的obtainStyledAttributes方法。

第二个关键的函数是applyTextView,当得到了对应styleID后,我们会去遍历它下面的所有attr,然后与反射获得的ID对比,找到对应的属性,然后调用View对应的方法去设置。

public class DayNightHelper {

    public static final String SP_NAME = "sp";
    public static final String DAY_NIGHT_THEME_KEY = "theme_day_night";
    public static final String THEME_DAY = "theme_day";
    public static final String THEME_NIGHT = "theme_night";

    public static int[] getDayNightStyleId(Context context, @Nullable AttributeSet attrs, int defStyle) {
        int dayStyleId = 0;
        int nightStyleId = 0;
        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.ThemeCommonStyleable, defStyle, 0);
        if (a != null) {
            int dayNightStyleId = a.getResourceId(R.styleable.ThemeCommonStyleable_dayNightAttr, 0);
            if (dayNightStyleId != 0) {
                TypedArray dayNightArray = context.getTheme().obtainStyledAttributes(dayNightStyleId, R.styleable.DayNightStyleable);
                if (dayNightArray != null) {
                    dayStyleId = dayNightArray.getResourceId(R.styleable.DayNightStyleable_themeDayAttr, 0);
                    nightStyleId = dayNightArray.getResourceId(R.styleable.DayNightStyleable_themeNightAttr, 0);
                    dayNightArray.recycle();
                }
            }
            a.recycle();
        }
        return new int[]{ dayStyleId, nightStyleId };
    }

    public static void applyTextView(TextView textView, int styleId) {
        TypedArray a = textView.getContext().getTheme().obtainStyledAttributes(styleId,
                ReflectHelper.Styleable.sTextView);
        if (a != null) {
            int indexCount = a.getIndexCount();
            for (int i = 0; i < indexCount; i++) {
                int attr = a.getIndex(i);
                if (attr == ReflectHelper.Styleable.sTextViewSize) {
                    int textSize = a.getDimensionPixelSize(attr, 0);
                    if (textSize != 0) {
                        textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
                    }
                } else if (attr == ReflectHelper.Styleable.sTextViewColor) {
                    ColorStateList textColor = a.getColorStateList(attr);
                    textView.setTextColor(textColor);
                } else if (attr == ReflectHelper.Styleable.sText) {
                    String text = a.getString(attr);
                    textView.setText(text);
                }
            }
            a.recycle();
        }
    }

    public static String getCurTheme(Context context) {
        SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
        return sp.getString(DAY_NIGHT_THEME_KEY, THEME_DAY);
    }

    public static void switchTheme(Context context) {
        SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
        String curTheme = sp.getString(DAY_NIGHT_THEME_KEY, THEME_DAY);
        String nextTheme;
        if (TextUtils.equals(curTheme, THEME_DAY)) {
            nextTheme = THEME_NIGHT;
        } else {
            nextTheme = THEME_DAY;
        }
        sp.edit().putString(DAY_NIGHT_THEME_KEY, nextTheme).apply();
    }
}

最后看一下反射获取ID值的代码:

public class ReflectHelper {

    private static final String TAG = ReflectHelper.class.getSimpleName();

    public static class Styleable {

        private static final Class<?> CLASS_STYLEABLE = getStyleableClass();

        public static int[] sTextView;
        public static int sTextViewSize;
        public static int sTextViewColor;
        public static int sText;

        static {

            try {
                sTextView = (int[]) CLASS_STYLEABLE.getField("TextView").get(null);
            } catch (Exception e) {
                Log.w(TAG, "", e);
            }

            try {
                sTextViewSize = CLASS_STYLEABLE.getField("TextView_textSize").getInt(null);
            } catch (Exception e) {
                Log.w(TAG, "", e);
            }

            try {
                sTextViewColor = CLASS_STYLEABLE.getField("TextView_textColor").getInt(null);
            } catch (Exception e) {
                Log.w(TAG, "", e);
            }

            try {
                sText = CLASS_STYLEABLE.getField("TextView_text").getInt(null);
            } catch (Exception e) {
                Log.w(TAG, "", e);
            }
        }

        private static Class<?> getStyleableClass() {
            try {
                Class<?> clz = Class.forName("com.android.internal.R$styleable");
                return clz;
            } catch (Exception e) {
                Log.w(TAG, "", e);
            }
            return null;
        }
    }
}

四、参考文献

Android 中 View 自定义 XML 属性详解以及 R.attr 与 R.styleable 的区别

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,511评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,495评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,595评论 0 225
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,558评论 0 190
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,715评论 3 270
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,672评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,112评论 2 291
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,837评论 0 181
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,417评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,928评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,316评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,773评论 2 234
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,253评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,827评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,440评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,523评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,583评论 2 249