本文翻译自android开发指导文档Custom Drawing
自定义绘图
自定义view最重要的部分是它的外观。自定义绘图可以根据你的外观需要或简单或复杂。这篇文档包含了一些重要操作。
覆写onDraw()
画自定义view最重要的一步是覆写onDraw()方法。onDraw()的参数是一个Canvas对象,view可以用它来画自己的内容。Canvas类定义了画文字、线、bitmap还有很多其他图形的方法。你可以在onDraw()中用这些方法来创建你的view的用户界面。
在你调用绘画方法之前,你有必要创建一个Paint对象。接下来详细讨论Paint。
创建绘画对象
android.graphics框架将绘画分为两个方面:
- 画什么,由Canvas控制
- 怎么画,由Paint控制
例如,Canvas提供了一个画线的方法,然而Paint提供了定义线颜色的方法。Canvas有一个画矩形的方法,而Paint定义了是否用一种颜色来填充矩形或者不填充。简单讲,Canvas定义了你可以在屏幕上绘画的形状,Paint定义了颜色,样式,字体。所以,在你画任何东西之前,你需要创建一个或多个Piant对象。PieChar例子中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));
...
提前创建对象是一个重要的优化。View会被频繁地重画,很多绘画对象需要昂贵代价的初始化。在onDraw()方法中创建绘画对象会严重影响性能并且会使你的UI变得卡钝。
处理布局事件
为了正常绘制你的自定义view,你需要知道它的大小。复杂的自定义view经常需要根据view在屏幕上的位置、大小执行来进行多重的布局计算。你不应该对view在屏幕上的大小做出假设。即使只有一个app使用你的view,应用程序需要处理不同的屏幕尺寸,多样的屏幕密度,还有各种横屏、竖屏模式的比例。
尽管视图有多种方法来处理测量,但大部分不需要覆写。如果你的view不需要指定控制它的大小,你仅仅需要覆写一个方法:onSizeChanged()。onSizeChanged()在view第一次被分配大小时调用,并且在view大小改变时再次调用。在onSizeChanged()中计算位置、尺寸还有任何其它和view大小有关的值,而不是每次在你画的时候重新计算。
当给你的view分配大小时,布局管理器假设view的大小包括所有的padding。当计算view的大小时,你必须处理padding值。下面有一个代码片段告诉你怎么做:
// 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希望面积足够大以至于pie能够像标签一样大:
@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的期望大小和spec返回一个合适View.MeasureSpec值传进onMeasure()。
- onMeasure()没有返回值。相反,这个方法通过调用setMeasuredDimension()来传达它的值。该方法的调用时强制的。如果你忽略了,view类会抛出异常。
绘制!
一旦你完成了对象创建和测量代码定义,你就可以实现onDraw()方法了。每一个view实现onDraw()方法不相同,但大部分view共享一些相同的操作:- 用drawText()绘制文字。通过调用setTypeface()指定字体,setColor()指定文字颜色。
- 用drawRect(),drawOval(),drawArc()绘制简单图形。通过调用setStyle()改变形状是否填充,是否有轮廓,或者两个都有。
- 用Path类绘制更复杂的图形。通过添加直线和曲线Path对象定义形状,然后用drawPath()绘制。像简单图像一样,paths也可以指定填充或者轮廓。
- 通过创建LinearGradient定义渐变填充。调用setShader()用你的LinearGradient填充图形。
- 用drawBitmap()绘制bitmap。
例如:
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);
}