Android中自定义视图的绘制方法
自定义视图最重要的部分是它的外观.你可以根据应用的需求简单或复杂的实现它. 这个教程包含了最常见的操作.
重写onDraw()
绘制自定义视图里最重要的一步是重写onDraw())方法. onDraw())的参数是视图可以用来绘制自己的Canvas对象. Canvas定义用来绘制文本、线条、位图和其他图像单元. 你可以在onDraw())里使用这些方法创建你的自定义用户界面(UI).
不过, 在你调用任何绘画的方法之前, 你必须创建Paint对象. 下一章节将会探讨Paint的更多细节.
创建绘画对象
android.graphics框架把绘图分成了两部分:
例如, 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 18 | 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));
... |
提前创建对象是一个很重要的优化. 视图频繁的被重画, 并且许多绘图对象初始化需要消耗大量的资源. 在onDraw())方法里创建绘图对象会严重降低性能, 并可以让你的UI显得有些迟钝.
处理布局事件
为了正确的绘制你的自定义视图, 你需要知道它的大小. 复杂的自定义视图经常需要根据它的大小和在屏幕上的图形区域执行多次布局计算. 你永远不应该假设视图在屏上的大小. 即使只有一个应用使用你的视图, 应用也需要处理不同的屏幕尺寸, 多种屏幕分辨率, 以及在横屏和竖屏模式下的各种高宽比.
虽然View有很多处理尺寸大小的方法, 但是大部分的需要重写. 如果你的视图不需要特别控制它的大小, 你只需要重写方法: onSizeChanged() .
onSizeChanged()在你的视图第一次分配大小的时候调用, 如果你的视图因为任何原因改变了大小也会再次调用. 在该方法里计算位置、大小和其他一些与视图大小相关的值, 而不是你每次绘制的时候重新计算. 在PieChart(饼图)例子里, PieChar视图在onSizeChanged()里计算饼图的图形边界、文本标签的相对位置和其他视觉元素.
当你的视图分配了一个大小, 布局管理器会假设这个大小包含了所有视图的padding值. 你必须在计算你视图的大小的时候处理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); |
如果你需要出色的控制你视图的布局参数, 实现int) onMeasure()方法. 这个方法的参数是View.MeasureSpec值, 这个会告诉你你的视图的父元素想让你的视图有多大, 并且告诉你这个大小是否是最大值或只是一个建议. 作为优化, 这些值保存为整数的封装类型, 你可以用View.MeasureSpec里的静态方法解析每个整数里面的信息.
下面是实现int) 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);
} |
在这段代码中有三个重点需要注意:
- 计算需要考虑视图的padding. 如上所述, 这个是视图的职责.
- 方法resolveSizeAndState()用来创建最终的宽和高. 这个方法通过比较视图的期望大小返回一个合适的View.MeasureSpec值传入int) onMeasure()
- onMeasure()方法没有返回值. 相反, 这个方法通过调用int) setMeasureDismension()方法传递结果. 调用这个方法是强制的. 如果你省略这个, View类会抛出runtime exception
绘图
一旦你有了创建的对象和定义了测绘布局的代码, 你可以实现方法onDraw()) . 每个视图实现不同的onDraw()) , 但是这里有些大多数视图常用的操作:
使用drawText()画文本, setTypeface())指定字体, setColor())指定文本颜色
画基本的形状用drawRect()) 、drawOval()) 、drawArc()) . 不论改变图形的填充样式还是边框样式还是都修改, 都是调用setStyle())
绘制复杂的形状用Path类. 通过给Path对象增加线条和曲线定义形状, 然后使用drawPath())绘制形状. 就像基本的形状一样, Path可以设置填充样式、边框样式、或者都设置, 都依靠setStyle())
定义渐变的填充样式通过创建LinearGradient对象. 在要填充的形状上通过调用setShader())使用LinearGradient对象
绘制位图使用drawBitmap()) .
例如, 这是是画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 26 | protected 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);
} |