Android学习总结 :自定义 View(一)
在 Android 中,一个设计精良的自定义 View 就像其他设计精良的类一样,它封装了一些特殊的功能并且有一个方便使用的界面。真正设计好的自定义 View ,可以更有效地利用 CPU 和内存的资源。一个自定义 View 需要符合以下几点要求:
符合 Android 设计标准
为 Android XML layouts 界面提供自定义的 styleable 属性。
能发送可访问的事件
兼容更多的 Android 平台版本
一、创建自定义 view 类
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 做出相应的修改
1. 定义自定义属性
为了在使用自定义 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,可以有以下多种:
- reference
- string
- color
- dimension
- boolean
- integer
- float
- fraction
- enum
- flag
2. 在布局文件中使用自定义属性
现在你可以在布局文件中使用像是内置属性一样的自定义的属性,唯一有一点不同的地方,就是 XML 的命名空间。官方建议使用 xmlns:custom="http://schemas.android.com/apk/res/[your package name]"
来定义你自定义的命名空间。
看下 XML 文件
<?xml version="1.0" encoding="utf-8"?><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 内部是否有内部类,都可以统一使用这个命名空间。
3. 在运行时获取指定的属性值
我们定义在自定义属性,然后在布局文件中使用并写上了指定的值,下一步是如何获取到这些值。
为了解决这个问题,先直接推荐使用:
Resources.Theme.obtainStyledAttributes()
4. 做你想要做的
它能自动把引用资源解析出来,并且能应用主题样式。看一下代码:
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 使用完要回收。
AttributeSet 和 TypedArray
- AttributeSet
记得在开头定义的那个 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 = @2131558529CoolView: attrName = layout_width , attrVal = 100.0dipCoolView: attrName = layout_height , attrVal = 100.0dipCoolView: attrName = labelPosition , attrVal = 1CoolView: attrName = text , attrVal = @2131361809CoolView: attrName = showText , attrVal = true
可以看到 text 的值是 @xxxx ,这明显不符合要求。说明 AttributeSet 不会去检索引用资源。其实还有主题也不会解析。
那么如何使用 AttributeSet 得到引用类型的属性呢?。
int resId = attrs.getAttributeResourceValue(1, -1);String text = getResources().getDimension(widthDimensionId));
一句话,不推荐直接使用它。
- TypedArray
保持 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:trueCoolView: mLabelPosition:1CoolView: mText:你好
TypedArray 的意义就是帮助我们自动处理引用类型的资源和主题(style)资源,来简化我们的工作。
declare-styleable
众所周知,系统提供的一个默认的属性: 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:falseCoolView: 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中的自定义属性
更多相关文章
- Android中的一个简单的List应用
- android 滚动条颜色设置(android Progressbar color)
- Android(安卓)总结:ContentProvider 的使用
- android framework层 学习笔记(一)
- Android修改主题,去掉ActionBar、TitleBar
- Hack4-自定义PreferenceActivity界面
- Android(安卓)Studio编译jar架包必看
- Android笔记 - Android启动之Android(安卓)Framework启动
- android定制化软件修改或添加按键驱动的核心操作步骤讲解