声明,翻译自谷歌官方文档:http://developer.android.com/training/custom-views/create-view.html
一、创建View类
一个好的自定义view应该遵循以下规则:
- 符合android标准
- 提供自定义的 styleable attributes 以便在XML layouts中试用。
- 设计辅助功能。(这个是用来支持无障碍阅读的)
- 兼容android的多平台。
android 的 framework 提供了一系列的 类和 XML 的标签来帮助创建 符合上述规则的view。本课程将讨论怎么使用 Android的framework 创建 view的核心功能。
1.继承View类
Android framework中的所有view都继承自View类,所以 你自定义的view也应该继承自 View类,或者也可以继承自 framework中的其它已经存在的view 类(例如:Button类)。
为了允许 Android Developer Tools 关联到你的 view(在XML 布局中预览你的view),你必须声明一个包含
Context
和AttributeSet 参数的构造方法。 这个构造函数允许 布局编辑器 来创建和编辑 你的view。
class PieChart extends View { public PieChart(Context context, AttributeSet attrs) { super(context, attrs); } }
2.声明自定义属性(Attributes)
要在XML布局中添加你的view,并通过XML 来控制view的外观和行为,你必须实现以下功能:
- 在
res/values/attrs.xml
资源文件中添加 <declare-styleable> 来声明自定属性。 - 在XML布局中指定 属性的值。
- 在运行时解析 指定的属性值。
- 根据解析的数据值来初始化view。
下面讲解如何自定义属性:
需要在
res/values/attrs.xml
资源文件中声明<declare-styleable> 标签,直接看例子:
<resources> <declare-styleable name="PieChart"> <attr name="showText" format="boolean" /> <attr name="labelPosition" format="enum"> <enum name="left" value="0"/> <enum name="right" value="1"/> </attr> </declare-styleable> </resources>
name属性必须指定,在运行时需要根据这个名称来取得 自定义属性列表中的所有值。具体attr 属性的定义规则,这里不做讲解,可以自行百度。需要注意的一点是自定义属性的 name 值应该与你的自定view的 类名相同,这样方便IDE来自动补全代码。
定义好属性后,可以在XML布局中调用,调用之前需要声明一个自定义命名空间,这个是用来区分 属性归属的,下面看例子:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:custom="http://schemas.android.com/apk/res/com.example.customviews"> <com.example.customviews.charting.PieChart custom:showText="true" custom:labelPosition="left" /> </LinearLayout>
每个布局中都会有
xmlns:android="http://schemas.android.com/apk/res/android"这个命名空间,这个是声明 android framework中的view需要的属性的命名空间,而自定义的属性命名空间需要 http://schemas.android.com/apk/res/[your package name] 这样申请。申请完后就可以通过 命名空间 名称 来调用了 。
3.应用在XML布局中设置的自定义属性
当view 从XML 布局中创建时,所有在XML标签中声明的属性都会通过AttributeSet 传递给view的构造方法。这样就能够直接从传递过来的AttributeSet 中读取到自定义属性的值了,这样做有以下几个缺点;
- 在AttributeSet 的属性值不会自动解析。
- 主题Styles 不会自动申请。
而是会传递
AttributeSet
给 obtainStyledAttributes() 。这个方法会返回一个用来提取已经被样式化的属性值集合 TypedArray
。
Android的资源编辑器在你调用obtainStyledAttributes() 时会做很多工作,会把所有的
<declare-styleable>
值封装到 TypedArray 中,而你需要从TypedArray 中提取需要的属性值,看例子:
public PieChart(Context context, AttributeSet attrs) { super(context, attrs); TypedArray a = context.getTheme().obtainStyledAttributes( attrs, R.styleable.PieChart, 0, 0); try { mShowText = a.getBoolean(R.styleable.PieChart_showText, false); mTextPos = a.getInteger(R.styleable.PieChart_labelPosition, 0); } finally { a.recycle(); } }注意:
TypedArray
是系统共享资源,所以必须在用完后 回收掉!
4.动态添加属性和事件
在XML中添加属性是非常强大的方法来控制view的显示和行为,但是只能在初始化时控制view。如果需要动态的控制view的属性就需要公开每个 属性的 getter和 setter 方法,看例子:
public boolean isShowText() { return mShowText; } public void setShowText(boolean showText) { mShowText = showText; invalidate(); requestLayout(); }注意: setShowText 调用了
invalidate()
和
requestLayout() 方法。这两个方法的调用是非常重要的能够确保view的可靠性。
invalidate() 让系统重新绘制view 、
requestLayout() 通知系统重新布局view(例如view的尺寸,形状改变等)。
自定义view 应该支持监听view的重要事件。例如
PieChart
声明一个自定义的事件回调OnCurrentItemChanged
来通知监听者 用户旋转了饼图。
5.设计辅助功能
自定义view应该支持更广泛的使用者,包括聋哑人和盲人等,为了支持辅助功能,你应该:
- 使用Android的标签输入字段android:contentDescription 。
- 在适当的时候通过调用
sendAccessibilityEvent()
发送辅助事件。 - 支持设备控制器,例如 轨迹球等。
更多关于辅助功能请访问 Making Applications Accessible 。
二、Custom Drawing
自定义view的最重要的就是其外观。自定义绘制 能够非常容易或者复杂的实现你的需求。本课程介绍一些最常见的操作。
1.重写 onDraw()方法
自定义view的最重要的步骤是重写
onDraw()
方法。该方法的参数是Canvas 类,让view可以来绘制自己。Canvas 类定义了绘制文本、线条、位图和许多其它基本图形的方法。你可以在onDraw()
方法中创建自定义的 UI。
在可以调用 绘制方法的前,需要创建
Paint
类。
2.Create Drawing Objects (创建绘图对象)
android.graphics framework 把绘制分成两个部分:
例如:Canvas 提供方法来绘制线条,而Paint 提供方法来确定线条的颜色。Canvas 提供方法来绘制矩形,而Paint 提供方法来确定怎样填充矩形。总的来说,Canvas 提供方法绘制形状,而Paint 提供方法来确定形状的颜色、样式、字体等。
所以在你绘制任何东西之前,你需要创建一个或者多个Paint 对象。
PieChart
例子在init 方法(在构造函数中调用)中声明:
private void init() { mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mTextPaint.setColor(mTextColor); if (mTextHeight == 0) { mTextHeight = mTextPaint.getTextSize(); } else { mTextPaint.setTextSize(mTextHeight); } mPiePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPiePaint.setStyle(Paint.Style.FILL); mPiePaint.setTextSize(mTextHeight); mShadowPaint = new Paint(0); mShadowPaint.setColor(0xff101010); mShadowPaint.setMaskFilter(new BlurMaskFilter(8, BlurMaskFilter.Blur.NORMAL)); ...提前创建对象是一个重要的优化。Views 会非常频繁的重绘,并且许多对象都需要昂贵的初始化。在 onDraw() 方法中创建绘制对象会显著的降低性能,造成UI卡顿。
3.Handle Layout Events
为了正确的绘制出自定义的view,需要确定view的尺寸。复杂的自定义view需要根据多个布局在屏幕上的大小和形状来计算。哪怕只有一个app使用你的view,你都不能假设自定义view在屏幕上的尺寸。你应该处理不同屏幕尺寸、多屏幕密度以及各种宽高纵横比和屏幕方向。
虽然View 有很多方法来获取尺寸,但是大多数的这些方法不需要重写。如果自定义view不需要特别控制它的尺寸,只需要重写一个方法:onSizeChanged() 。
onSizeChanged() 方法会在view第一次分配尺寸大小 或者 由于其它原因view的尺寸改变了 的时候调用。在onSizeChanged() 方法中计算view的位置、尺寸和其它与view尺寸有关的值,而不是每次绘制view的时候计算。 在
PieChart
例子中,在onSizeChanged() 方法中计算了 饼图的边框、文本标签 和 其它视觉元素的 相对位置。
当view被分配给一个尺寸,布局管理器会设定这个大小包括视图的所有padding。当你计算view的尺寸时,你必须自己取得padding值。下面是PieChart.onSizeChanged() :
// Account for padding float xpad = (float)(getPaddingLeft() + getPaddingRight()); float ypad = (float)(getPaddingTop() + getPaddingBottom()); // Account for the label if (mShowText) xpad += mTextWidth; float ww = (float)w - xpad; float hh = (float)h - ypad; // Figure out how big we can make the pie. float diameter = Math.min(ww, hh);如果需要更好的控制view的布局参数,重写 onMeasure() 方法。这个方法 的参数是 View.MeasureSpec 它会描述view的 父容器分配给view的尺寸,和分配的尺寸是硬性的最大值还是只是一个建议。为了优化,这些值都会压缩成整数,可以使用 View.MeasureSpec 的静态方法来解压出存储的每个整数信息。
下面是一个重写
onMeasure()
方法的例子。在实现过程中,PieChart
视图使它的区域足够大来使饼图能够放的下它的标签。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // Try for a width based on our minimum int minw = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth(); int w = resolveSizeAndState(minw, widthMeasureSpec, 1); // Whatever the width ends up being, ask for a height that would let the pie // get as big as it can int minh = MeasureSpec.getSize(w) - (int)mTextWidth + getPaddingBottom() + getPaddingTop(); int h = resolveSizeAndState(MeasureSpec.getSize(w) - (int)mTextWidth, heightMeasureSpec, 0); setMeasuredDimension(w, h); }在代码中有三个地方需要注意:
- 考虑到view的 padding值。正如前面提到的。
- 辅助方法
resolveSizeAndState()
用来创建view最终的宽度和高度。这个辅助方法通过比较view期望的大小和onMeasure() 传递过来的规范值 返回一个适当的View.MeasureSpec 值。 - onMeasure() 方法没有返回值。该方法通过调用setMeasuredDimension() 来传递结果。在onMeasure() 方法中必须调用setMeasuredDimension() ,否则会抛出 runtime exception。
4.Draw !