在 Android 中,一个设计精良的自定义 View 就像其他设计精良的类一样,它封装了一些特殊的功能并且有一个方便使用的界面。真正设计好的自定义 View ,可以更有效地利用 CPU 和内存的资源。一个自定义 View 需要符合以下几点要求:
符合 Android 设计标准
为 Android XML layouts 界面提供自定义的 styleable 属性。
能发送可访问的事件
兼容更多的 Android 平台版本
Android 平台所有的 view classes都是继承于 View,我们的自定义 view 可以直接继承于 View ,也可以直接继承有更多功能的 View 的子类。比如 Button 、 TextView。
为了 Android Studio 的布局编辑器能创建并编辑你的自定义 View,你至少要提供一个参数为 Context 和 AttrubuteSet 的构造器。
class CoolView extends View {
public CoolView(Context context, AttributeSet attrs) {
super(context, attrs);
}
}
自定义 view 的第一个步骤,为其定义自定义属性,并且能够使用元素属性来控制它的外观和行为。一般来说有以下 4 步:
通过
资源标签来定义你的自定义的属性
在 XMl 布局中为你的自定义属性指定一个值
运行时能获取到你指定的属性值
根据获取到的属性值对 view 做出相应的修改
为了在使用自定义 view 的时候能够使用我们自己定义的属性标签,就像 Android 内置的属性一样。你要做的,是为你的自定义 view 设置自定义属性,使得在使用的时候看起来像内置的一样。
在你的项目中添加包含
的资源文件,一般放在 res/values/attrs.xml
文件中,以下是官方的例子:
<resources>
<declare-styleable name="CoolView">
<attr name="showText" format="boolean" />
<attr name="labelPosition" format="enum">
<enum name="left" value="0"/>
<enum name="right" value="1"/>
attr>
declare-styleable>
resources>
上面这段代码定义了一个名为 CoolView
自定义样式,showText
和 labelPosition
是其中的两个属性值。对于自定义属性样式的命名,官方建议保持与类名相同。
自定义属性的format,可以有以下多种:
现在你可以在布局文件中使用像是内置属性一样的自定义的属性,唯一有一点不同的地方,就是 XML 的命名空间。官方建议使用 xmlns:custom="http://schemas.android.com/apk/res/[your package name]"
来定义你自定义的命名空间。
看下 XML 文件
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<com.smartni.userinterface.sample.main.CoolView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
nj:labelPosition="left"
nj:showText="true"/>
android.support.design.widget.CoordinatorLayout>
但是在实践的时候,发现提示 In Gradle Projects,always use xmlns:app="http://schemas.android.com/apk/res-auto" 为自定义属性的命名空间
。也就是说在 Gradle 文件中使用 res-auto 代表统一的自定义的命名空间,这样也比较方便,不管自定义 view 内部是否有内部类,都可以统一使用这个命名空间。
我们定义在自定义属性,然后在布局文件中使用并写上了指定的值,下一步是如何获取到这些值。
为了解决这个问题,先直接推荐使用:
Resources.Theme.obtainStyledAttributes()
它能自动把引用资源解析出来,并且能应用主题样式。看一下代码:
public class CoolView extends View {
boolean mShowText;
Integer mLabelPosition;
public CoolView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.getTheme().obtainStyledAttributes(
attrs,
R.styleable.CoolView,
0,0);
try{
mShowText = a.getBoolean(R.styleable.CoolView_showText,false);
mLabelPosition = a.getInteger(R.styleable.CoolView_labelPosition,0);
}finally {
a.recycle();
}
}
}
注意 R.styleable.CoolView_showText
的形式。还有 TypedArrayy 使用完要回收。
记得在开头定义的那个 CoolView 中的构造函数中有个 AttributeSet
参数,它就是 XML 布局文件里这个 view 的一系列属性的集合。它的确可以获取到自定义属性的值,来看看代码:
简单修改后的XML
<com.smartni.userinterface.sample.main.CoolView
android:id="@+id/cv"
android:layout_width="100dp"
android:layout_height="100dp"
app:labelPosition="right"
app:text="@string/hello"
app:showText="true"/>
这里把新增了一个 text 属性(format = “reference” ),值写成了资源Id。
然后在 CoolView.java 中
private void initAttrs(Context context, AttributeSet attrs) {
int count = attrs.getAttributeCount();
for (int i = 0; i < count; i++) {
String attrName = attrs.getAttributeName(i);
String attrVal = attrs.getAttributeValue(i);
Log.e(TAG, "attrName = " + attrName + " , attrVal = " + attrVal);
}
}
得到结果
CoolView: attrName = id , attrVal = @2131558529
CoolView: attrName = layout_width , attrVal = 100.0dip
CoolView: attrName = layout_height , attrVal = 100.0dip
CoolView: attrName = labelPosition , attrVal = 1
CoolView: attrName = text , attrVal = @2131361809
CoolView: attrName = showText , attrVal = true
可以看到 text 的值是 @xxxx ,这明显不符合要求。说明 AttributeSet 不会去检索引用资源。其实还有主题也不会解析。
那么如何使用 AttributeSet 得到引用类型的属性呢?。
int resId = attrs.getAttributeResourceValue(1, -1);
String text = getResources().getDimension(widthDimensionId));
一句话,不推荐直接使用它。
保持 XML 保持不变,我们使用 TypedArray ,来看看代码:
private void initAttrs(Context context, AttributeSet attrs) {
TypedArray a = context.getTheme().obtainStyledAttributes(
attrs,
R.styleable.CoolView,
0, 0);
try {
mShowText = a.getBoolean(R.styleable.CoolView_showText, false);
mLabelPosition = a.getInteger(R.styleable.CoolView_labelPosition, 0);
mText = a.getString(R.styleable.CoolView_text);
} finally {
a.recycle();
}
Log.d(TAG, "mShowText:" + mShowText);
Log.d(TAG, "mLabelPosition:" + mLabelPosition);
Log.d(TAG, "mText:" + mText);
}
结果是:
CoolView: mShowText:true
CoolView: mLabelPosition:1
CoolView: mText:你好
TypedArray 的意义就是帮助我们自动处理引用类型的资源和主题(style)资源,来简化我们的工作。
众所周知,系统提供的一个默认的属性: android:text
,鸿洋大神提出的一个问题
如果系统中已经有了语义比较明确的属性,我可以直接使用嘛?
答案是可以的,只要修改 attrs 文件
<declare-styleable name="test">
<attr name="android:text" />
<attr name="testAttr" format="integer" />
declare-styleable>
唯一差别就是,它没有 format 属性!!!
然后在类中这么获取:a.getString(R.styleable.test_android_text)
;布局文件中直接 android:text="@string/hello_world"
即可。
还有一个问题
styleable 的含义是什么?可以不写嘛?我自定义属性,我声明属性就好了,为什么一定要写个styleable呢?
确实是可以不用写的,先看下现在 attrs.xml 文件里的内容
<resources>
<declare-styleable name="CoolView">
<attr name="showText" format="boolean"/>
<attr name="labelPosition" format="enum">
<enum name="left" value="0"/>
<enum name="right" value="1"/>
attr>
declare-styleable>
resources>
其实,在 R.java 文件中 android 已经帮我生成了相应的代码
public static final int[] CoolView = {
0x7f0100e6, 0x7f0100e7
};
public static final int CoolView_showText = 0;
public static final int CoolView_labelPosition = 1;
现在修改 attrs.xml
<resources>
<attr name="showText" format="boolean"/>
<attr name="labelPosition" format="enum">
<enum name="left" value="0"/>
<enum name="right" value="1"/>
attr>
<attr name="onItemClick" format="string"/>
resources>
编译一下,发现 R.java 中少了 CoolView 那个 int 数组常量。我想,原来在 obtainStyledAttributes()
方法中第二个参数传递的是 R.styleable.CoolView ,其实这个参数在 R 文件中就是这个 int 常量数组。
那我是不是可以模拟一下这个数组呢?
public class CoolView extends android.support.v7.widget.AppCompatTextView {
private static final int[] mAttr= {android.R.attr.showText,R.attr.labelPosition};
private static final int mShowText = 0;
private static final int mLabelPosition = 1;
private void initAttrs(Context context, AttributeSet attrs) {
TypedArray a = context.getTheme().obtainStyledAttributes(
attrs,
mAttr,
0, 0);
try {
boolean showText = a.getBoolean(mShowText, false);
int labelPosition = a.getInteger(mLabelPosition, -1);
Log.d(TAG, "showText:" + showText);
Log.d(TAG, "labelPosition:" + labelPosition);
} finally {
a.recycle();
}
}
}
结果一样:
CoolView: showText:false
CoolView: labelPosition:1
开始,不用解释了吧。这个数组中的元素就像上面 R 文件中的一样,定义的是我们要获取的 attr 的 id 。然后根据元素在数组中的位置,定义一些整形的常量代表下表。
这样,就和没有写 declare-styleable 一样了。
但是呢,明明有更方便的 styleable ,我们为什么不用呢?Google 推荐 declare-styleable 的 name 属性一般与自定义 view 的名字一样。
之前的代码只能在 XML 中提前定义,在实际中我们肯定需要在运行时动态改变它的属性值。看下代码:
public boolean isShowText() {
return mShowText;
}
public void setShowText(boolean showText) {
mShowText = showText;
invalidate();
requestLayout();
}
invalidate():在任何可能改变 view 内容的操作后都要调用。
requestLayout(): 在任何可能改变 view 大小或者形状的操作后调用。
当然也可以添加事件,先在 attrs 中定义属性。
"onItemClick" format="string"/>
然后在布局文件中
<com.smartni.userinterface.sample.main.CoolView
android:id="@+id/cv"
android:layout_width="100dp"
android:layout_height="100dp"
android:gravity="center"
android:textSize="20sp"
android:background="@android:color/holo_purple"
android:text="你好"
app:labelPosition="right"
app:layout_anchor="@id/rv"
app:layout_anchorGravity="bottom|center"
android:onClick="onDo"
app:onItemClick="onItemClick"
app:showText="true"/>
这里我定义了默认的点击事件和自定义点击事件的属性。
参照 Android 官方那样,在布局文件中定义好 onClick 的名字,然后在 Activity 中定义带 View 参数的同名函数。
我也模仿着实现了一下:
public class CoolView extends android.support.v7.widget.AppCompatTextView {
...
public void onItemClick() throws NoSuchMethodException {
Class clz = mContext.getClass();
try {
Method method = clz.getMethod(mOnItemClick, View.class);
if(method != null){
method.invoke(mContext,this);
}
} catch (Exception e) {
throw new NoSuchMethodException("make sure define your onItemClick :" +
mOnItemClick +" method in your Activity.");
}
}
...
}
通过反射调用 Activity 中,同名的方法。然后:
...
private void bindListener() {
this.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
try {
onItemClick();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
});
}
...
然后通过调用系统的 onClickListener 调用我们自己的事件。bindListener 可以在构造函数中调用。
在 Activity 中就可以定义 XML 中设置的名字的方法,要有一个 View 参数。
...
public void onDo(View view){
Log.d("CoolView", "hello");
Snackbar.make(mCoordinatorLayout,"我是默认的点击事件",Snackbar.LENGTH_SHORT).show();
}
//这个方法被调用
public void onItemClick(View view){
Log.d(TAG, "hello , my custom View!");
Snackbar.make(mCoordinatorLayout,"我是自定义的点击事件",Snackbar.LENGTH_SHORT).show();
}
...
当我点击这个 view 的时候结果是
输出了 hello , my custom View!
并没有输出 hello
。
这里默认的点击事件被我们自定义的 View 覆盖了。至于为什么,就必须要谈到 Android 的事件分发机制了。这两天我就去好好研究一下,到时候写一个总结。
总结一下:
我们编写自定义 view 的 4 个一般步骤。
attrs.xml里面的 declare-styleable 以及 item,android 会根据其在 R.java 中生成一些常量方便我们使用(aapt干的),本质上,我们可以不声明declare-styleable仅仅声明所需的属性即可。
我们在 View 的构造方法中,可以通过AttributeSet去获得自定义属性的值,但是比较麻烦,而TypedArray可以很方便的便于我们去获取。
我们在自定义View的时候,可以使用系统已经定义的属性。
TypedArray 的意义就是帮助我们自动处理引用类型的资源和主题(style)资源,来简化我们的工作。
参考自:
官方 training
鸿洋大神的 深入理解Android中的自定义属性