本文介绍了如何自定义View中的前两个步骤:创建一个View类和自定义绘制。
Android系统给我们提供很多View用来和用户交互和显示不同类型的数据。但有时系统所提供的View并不能满足我们的需求,这时候就需要自定义View了。
自定义View分为下面四个步骤:
- 创建一个View类
- 自定义绘制
- 实现交互
- 优化View
创建一个View类
一个标准的自定义View应该:
- 遵循Android标准
- 提供自定义属性以便在XML布局中使用
- 发送accessibility事件
- 兼容多种Android平台
继承View
Android中所有的View类都继承View。自定义View可以直接继承View类,也可以继承View已存在的子类,例如:Button。
为了允许Android Studio和自定义View交互,我们必须提供一个含有 Context 和 AttributeSet 参数的构造函数。这个构造方法允许布局编辑器创建和编辑View的实例。
1 | class PieChart extends View { |
定义自定义属性
给自定义View添加属性,我们要做的是:
- 使用
资源元素为View添加自定义属性 - 在XML布局中为属性指定值
- 在运行的时候接收属性
- 应用接收的属性值到View上
我们一般把自定义属性放到res/values/attrs.xml文件中,这有一个例子:1
2
3
4
5
6
7
8
9<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>
这里定义了两个属性showText和labelPosition,其中labelPosition为枚举型,在布局文件中可以这样使用:1
2
3
4
5
6
7<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto>
<com.example.customviews.charting.PieChart
app:showText="true"
app:labelPosition="left" />
</LinearLayout>
命名空间也可以使用包名的形式,但那样不方便使用,所以我们这里使用res-auto的方式。
应用自定义属性
在构造函数中获取自定义属性的值:
1 | public PieChart(Context context, AttributeSet attrs) { |
注意:TypedArray对象是一个共享资源,在使用后必须回收。
添加属性和事件
除了提供在XML布局中修改自定义属性的值,我们还需要在代码中提供属性的getter和setter方法:1
2
3
4
5
6
7
8
9public boolean isShowText() {
return mShowText;
}
public void setShowText(boolean showText) {
mShowText = showText;
invalidate();
requestLayout();
}
注意setShowText调用invalidate()和requestLayout()。用于确保View重新绘制和布局。
自定义View还需要支持和重要事件通讯的事件监听。例如,PieChart暴露了一个名为OnCurrentItemChanged的自定义事件,用于通知监听用户选择了新的饼片。
设计无障碍
你的自定义View应该考虑到更多的人。这包括有阅读或使用触摸屏障碍的人群。
为了支持这些人,你应该:
- 使用android:contentDescription属性标记输入域
- 当合适的时候调用sendAccessibilityEvent()发送accessibility事件
- 支持其它控制器,例如D-pad 和 trackball
关于更多无障碍的信息,请查看让应用无障碍。
自定义绘制
重写onDraw()
首先我们需要重写onDraw()方法,方法中有一个Canvas参数用于绘制它自己。Canvas类中定义了一些绘制 text, lines, bitmaps,和其它图形的方法。你可以使用这些方法在onDraw()方法中创建自定义的UI。
创建绘制对象
android.graphics框架将绘制分为两个区域:
- 画什么,通过Canvas控制
- 怎样画,通过Paint控制
例如,Canvas有一个画线的方法,Paint则定义了线的颜色。Canvas有一个画矩形的方法,Paint定义了是否用颜色填充矩形或不填充。简而言之,Canvas定义了你可以在屏幕上绘制的图形,Paint定义了每个图形的颜色,样式,字体等等。
因此,在绘制之前,我们需要创建一个或多个Paint对象,在PieChart示例中,在init方法进行了处理,从构造函数进行了调用:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18private 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));
...
提前创建对象是很重要的优化。View会非常频繁地重新绘制,并且很多绘制对象需要昂贵的初始化。在onDraw()方法中创建绘制对象会显著降低性能并且会让你的UI出现卡顿。
处理布局事件
为了适当地处理自定义View,你需要知道它的大小。复杂的自定义View经常需要根据它们在屏幕上的大小和图形区域执行多布局计算。你不应该假设View在屏幕上的大小。甚至只有一个应用使用你的View,应用需要处理不同的屏幕大小,多个屏幕密度,在横屏和竖屏模式的不同方面比率。
尽管View有很多方法处理测量,它们的大多数不需要重写。如果自定义View不需要特殊控制它的大小,你只需要重写一个方法:onSizeChanged()。
当View第一次分配了一个大小会调用onSizeChanged(),之后因为任何原因导致了View大小的变化会再次调用。在onSizeChanged()方法中,计算位置、大小、和其它和View大小相关的所有值,而不是每次绘制的时候重新计算它们。在PieChart示例中, onSizeChanged()是用来计算饼状图的矩形边界和文本标签的相对位置和其它可见元素。
当View分配了一个大小,布局管理器会假设大小包含所有View的padding。当你计算View的大小时必须处理View的padding。下面是PieChart.onSizeChanged()中的一段代码说明了怎样处理:1
2
3
4
5
6
7
8
9
10
11
12// 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);
如果你需要更好地控制你的视图的布局参数,实现onMeasure()方法。方法的参数是View.MeasureSpec值,会告诉你父容器想要的你的View的大小或者最大的大小。为了优化,这些值打包存储在一个32位的整型变量中,你可以使用View.MeasureSpec的静态方法获取这些值。
一个MeasureSpec值由模式和大小组成,高2位为模式,低30位为大小。这里有三种可能的模式:
- UNSPECIFIED 未指定,一般用于系统内部
- EXACTLY 精确值,对应于match_parent或指定大小值
- AT_MOST 最小值,对应于wrap_content
使用MeasureSpec类获取测量模式和大小:1
2
3
4int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
这有一个onMeasure()的实现的例子。在这个实现中,PieChart尝试让它的区域变得足够大,让饼图和它的标签一样大:1
2
3
4
5
6
7
8
9
10
11
12
13@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。正如之前提到的,这是View的职责。
- 帮助方法resolveSizeAndState()用于创建最终的宽度和高度值。这个方法会返回一个合适的View.MeasureSpec值通过比较View期望大小和 onMeasure()方法的参数值。
- onMeasure()方法没有返回值。通过调用setMeasuredDimension()设置最终的宽高值。这个方法是必须要调用,如果你忘了,View类会抛出运行时异常。
绘制
完成了对象的创建和测量的代码,可以实现onDraw()。每个View的onDraw()实现都不一样,但这有一个大多数View常见的操作:
- 使用drawText()绘制文本。调用setTypeface()指定字体,调用setColor()设置文本颜色。
- 使用drawRect(), drawOval(), 和 drawArc()绘制原始图形。调用setStyle()设置图形是否被填充,轮廓等。
- 使用Path类绘制更复杂的图形。通过添加线和曲线到Path对象定义一个图形,然后使用drawPath()绘制图形。就像原始图形一样,调用setStyle()设置Path是否被填充,轮廓等。
- 通过创建LinearGradient对象定义渐变填充。调用setShader()设置渐变填充。
- 使用drawBitmap()绘制bitmaps。
例如,这是PieChart绘制的代码。它使用了文本,线,和图形。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Draw the shadow
canvas.drawOval(
mShadowBounds,
mShadowPaint
);
// Draw the label text
canvas.drawText(mData.get(mCurrentItem).mLabel, mTextX, mTextY, mTextPaint);
// Draw the pie slices
for (int i = 0; i < mData.size(); ++i) {
Item it = mData.get(i);
mPiePaint.setShader(it.mShader);
canvas.drawArc(mBounds,
360 - it.mEndAngle,
it.mEndAngle - it.mStartAngle,
true, mPiePaint);
}
// Draw the pointer
canvas.drawLine(mTextX, mPointerY, mPointerX, mPointerY, mTextPaint);
canvas.drawCircle(mPointerX, mPointerY, mPointerSize, mTextPaint);
}
查看PieChart示例完整源码:https://github.com/yuweiguocn/AndroidDemo