自定义View系列教程04--Draw源码分析及其实践


深入探讨Android异步精髓Handler


站在源码的肩膀上全解Scroller工作机制


Android多分辨率适配框架(1)— 核心基础

Android多分辨率适配框架(2)— 原理剖析

Android多分辨率适配框架(3)— 使用指南


自定义View系列教程00–推翻自己和过往,重学自定义View

自定义View系列教程01–常用工具介绍

自定义View系列教程02–onMeasure源码详尽分析

自定义View系列教程03–onLayout源码详尽分析

自定义View系列教程04–Draw源码分析及其实践

自定义View系列教程05–示例分析

自定义View系列教程06–详解View的Touch事件处理

自定义View系列教程07–详解ViewGroup分发Touch事件

自定义View系列教程08–滑动冲突的产生及其处理


PS:如果觉得文章太长,那就直接看视频


通过之前的详细分析,我们知道:在measure中测量了View的大小,在layout阶段确定了View的位置。

完成这两步之后就进入到了我们相对熟悉的draw阶段,在该阶段真正地开始对视图进行绘制。

按照之前的惯例,我们来瞅瞅View中draw( )的源码

 public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN; int saveCount; if (!dirtyOpaque) {
drawBackground(canvas);
} final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) { if (!dirtyOpaque) onDraw(canvas); dispatchDraw(canvas); if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
} onDrawForeground(canvas); return;
} boolean drawTop = false;
boolean drawBottom = false;
boolean drawLeft = false;
boolean drawRight = false; float topFadeStrength = 0.0f;
float bottomFadeStrength = 0.0f;
float leftFadeStrength = 0.0f;
float rightFadeStrength = 0.0f; int paddingLeft = mPaddingLeft; final boolean offsetRequired = isPaddingOffsetRequired();
if (offsetRequired) {
paddingLeft += getLeftPaddingOffset();
} int left = mScrollX + paddingLeft;
int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
int top = mScrollY + getFadeTop(offsetRequired);
int bottom = top + getFadeHeight(offsetRequired); if (offsetRequired) {
right += getRightPaddingOffset();
bottom += getBottomPaddingOffset();
} final ScrollabilityCache scrollabilityCache = mScrollCache;
final float fadeHeight = scrollabilityCache.fadingEdgeLength;
int length = (int) fadeHeight; if (verticalEdges && (top + length > bottom - length)) {
length = (bottom - top) / 2;
} if (horizontalEdges && (left + length > right - length)) {
length = (right - left) / 2;
} if (verticalEdges) {
topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength()));
drawTop = topFadeStrength * fadeHeight > 1.0f;
bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));
drawBottom = bottomFadeStrength * fadeHeight > 1.0f;
} if (horizontalEdges) {
leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength()));
drawLeft = leftFadeStrength * fadeHeight > 1.0f;
rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength()));
drawRight = rightFadeStrength * fadeHeight > 1.0f;
} saveCount = canvas.getSaveCount(); int solidColor = getSolidColor();
if (solidColor == 0) {
final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG; if (drawTop) {
canvas.saveLayer(left, top, right, top + length, null, flags);
} if (drawBottom) {
canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
} if (drawLeft) {
canvas.saveLayer(left, top, left + length, bottom, null, flags);
} if (drawRight) {
canvas.saveLayer(right - length, top, right, bottom, null, flags);
}
} else {
scrollabilityCache.setFadeColor(solidColor);
} if (!dirtyOpaque) onDraw(canvas); dispatchDraw(canvas); final Paint p = scrollabilityCache.paint;
final Matrix matrix = scrollabilityCache.matrix;
final Shader fade = scrollabilityCache.shader; if (drawTop) {
matrix.setScale(1, fadeHeight * topFadeStrength);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, top, right, top + length, p);
} if (drawBottom) {
matrix.setScale(1, fadeHeight * bottomFadeStrength);
matrix.postRotate(180);
matrix.postTranslate(left, bottom);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, bottom - length, right, bottom, p);
} if (drawLeft) {
matrix.setScale(1, fadeHeight * leftFadeStrength);
matrix.postRotate(-90);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, top, left + length, bottom, p);
} if (drawRight) {
matrix.setScale(1, fadeHeight * rightFadeStrength);
matrix.postRotate(90);
matrix.postTranslate(right, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(right - length, top, right, bottom, p);
} canvas.restoreToCount(saveCount); if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
} onDrawForeground(canvas);
}

以上为draw()的具体实现,在Andorid官方文档中将该过程概况成了六步:

  1. Draw the background
  2. If necessary, save the canvas’ layers to prepare for fading
  3. Draw view’s content
  4. Draw children
  5. If necessary, draw the fading edges and restore layers
  6. Draw decorations (scrollbars for instance)

在此就按照该顺序对照源码看看每一步都做了哪些操作。

第一步:

绘制背景,请参见代码第9-11行。

第二步:

保存当前画布的堆栈状态并在该画布上创建Layer用于绘制View在滑动时的边框渐变效果,请参见代码第42-108行

通常情况下我们是不需要处理这一步的,正如上面的描述

If necessary, save the canvas’ layers to prepare for fading

第三步:

绘制View的内容,请参见代码第18行

这一步是整个draw阶段的核心,在此会调用onDraw()方法绘制View的内容。

之前我们在分析layout的时候发现onLayout()方法是一个抽象方法,具体的逻辑由ViewGroup的子类去实现。与之类似,在此onDraw()是一个空方法;因为每个View所要绘制的内容不同,所以需要由具体的子View去实现各自不同的需求。

第四步:

调用dispatchDraw()绘制View的子View,请参见代码第20行

第五步:

绘制当前视图在滑动时的边框渐变效果,请参见代码第114-157行

通常情况下我们是不需要处理这一步的,正如上面的描述

If necessary, draw the fading edges and restore layers

第六步:

绘制View的滚动条,请参见代码第29行

其实,不单单是常见的ScrollView和ListView等滑动控件任何一个View(比如:TextView,Button)都是有滚动条的,只是一般情况下我们都没有将它显示出来而已。


好了,看完draw()的源码,我们就要把注意力集中在第三步onDraw()了。

protected void onDraw(Canvas canvas) {

}

此处,该方法只有个输入参数canvas,我们就先来瞅瞅什么是canvas。

The Canvas class holds the “draw” calls. To draw something, you need 4 basic components: A Bitmap to hold the pixels, a Canvas to host the draw calls (writing into the bitmap), a drawing primitive (e.g. Rect,Path, text, Bitmap), and a paint (to describe the colors and styles for the drawing).

这段Android官方关于canvas的介绍告诉开发者:

在绘图时需要明确四个核心的东西(basic components):

  1. 用什么工具画?

    这个小问题很简单,我们需要用一支画笔(Paint)来绘图。

    当然,我们可以选择不同颜色的笔,不同大小的笔。
  2. 把图画在哪里呢?

    我们把图画在了Bitmap上,它保存了所绘图像的各个像素(pixel)。

    也就是说Bitmap承载和呈现了画的各种图形。
  3. 画的内容?

    根据自己的需求画圆,画直线,画路径。
  4. 怎么画?

    调用canvas执行绘图操作。

    比如,canvas.drawCircle(),canvas.drawLine(),canvas.drawPath()将我们需要的图像画出来。

知道了绘图过程中必不可少的四样东西,我们就要看看该怎么样构建一个canvas了。

在此依次分析canvas的两个构造方法Canvas( )和Canvas(Bitmap bitmap)

/**
* Construct an empty raster canvas. Use setBitmap() to specify a bitmap to
* draw into. The initial target density is {@link Bitmap#DENSITY_NONE};
* this will typically be replaced when a target bitmap is set for the
* canvas.
*/
public Canvas() {
if (!isHardwareAccelerated()) {
mNativeCanvasWrapper = initRaster(null);
mFinalizer = new CanvasFinalizer(mNativeCanvasWrapper);
} else {
mFinalizer = null;
}
}

请注意该构造的第一句注释。官方不推荐通过该无参的构造方法生成一个canvas。如果要这么做那就需要调用setBitmap( )为其设置一个Bitmap。为什么Canvas非要一个Bitmap对象呢?原因很简单:Canvas需要一个Bitmap对象来保存像素,如果画的东西没有地方可以保存,又还有什么意义呢?既然不推荐这么做,那就接着有参的构造方法。

/**
* Construct a canvas with the specified bitmap to draw into. The bitmap
* must be mutable.
*
* The initial target density of the canvas is the same as the given
* bitmap's density.
*
* @param bitmap Specifies a mutable bitmap for the canvas to draw into.
*/
public Canvas(Bitmap bitmap) {
if (!bitmap.isMutable()) {
throw new IllegalStateException("Immutable bitmap passed to Canvas constructor");
}
throwIfCannotDraw(bitmap);
mNativeCanvasWrapper = initRaster(bitmap);
mFinalizer = new CanvasFinalizer(mNativeCanvasWrapper);
mBitmap = bitmap;
mDensity = bitmap.mDensity;
}

通过该构造方法为Canvas设置了一个Bitmap来保存所绘图像的像素信息。

好了,知道了怎么构建一个canvas就来看看怎么利用它进行绘图。

下面是一个很简单的例子:

private void drawOnBitmap(){
Bitmap bitmap=Bitmap.createBitmap(800, 400, Bitmap.Config.ARGB_8888);
Canvas canvas=new Canvas(bitmap);
canvas.drawColor(Color.GREEN);
Paint paint=new Paint();
paint.setColor(Color.RED);
paint.setTextSize(60);
canvas.drawText("hello , everyone", 150, 200, paint);
mImageView.setImageBitmap(bitmap);
}

瞅瞅效果:

自定义View系列教程04--Draw源码分析及其实践

在此处为canvas设置一个Bitmap,然后利用canvas画了一小段文字,最后使用ImageView显示了Bitmap。

好了,看到这有人就有疑问了:

我们平常用得最多的View的onDraw()方法,为什么没有Bitmap也可以画出各种图形呢?

请注意onDraw( )的输入参数是一个canvas,它与我们自己创建的canvas不同。这个系统传递给我们的canvas来自于ViewRootImpl的Surface,在绘图时系统将会SkBitmap设置到SkCanvas中并返回与之对应Canvas。所以,在onDraw()中也是有一个Bitmap的,只是这个Bitmap是由系统创建的罢了。

好吧,既然已经提到了onDraw( )我们就在它里面画一些常见的图形。

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//-------->绘制白色矩形
mPaint.setColor(Color.WHITE);
canvas.drawRect(0, 0, 800, 800, mPaint);
mPaint.reset(); //-------->绘制直线
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(10);
canvas.drawLine(450, 30, 570, 170, mPaint);
mPaint.reset(); //-------->绘制带边框的矩形
mPaint.setStrokeWidth(10);
mPaint.setARGB(150, 90, 255, 0);
mPaint.setStyle(Paint.Style.STROKE);
RectF rectF1=new RectF(30, 60, 350, 350);
canvas.drawRect(rectF1, mPaint);
mPaint.reset(); //-------->绘制实心圆
mPaint.setStrokeWidth(14);
mPaint.setColor(Color.GREEN);
mPaint.setAntiAlias(true);
canvas.drawCircle(670, 300, 70, mPaint);
mPaint.reset(); //-------->绘制椭圆
mPaint.setColor(Color.YELLOW);
RectF rectF2=new RectF(200, 430, 600, 600);
canvas.drawOval(rectF2, mPaint);
mPaint.reset(); //-------->绘制文字
mPaint.setColor(Color.BLACK);
mPaint.setTextSize(60);
mPaint.setUnderlineText(true);
canvas.drawText("Hello Android", 150, 720, mPaint);
mPaint.reset();
}

自定义View系列教程04--Draw源码分析及其实践

在此只列举了几种最常用图形的绘制,其余的API就不再举例了,在需要的时候去查看相应文档就行。


除了调用canvas画各种图形,我们有时候还有对canvas做一些操作,比如旋转,剪裁,平移等等;有时候为了达到理想的效果,我们可能还需要一些特效。在此,对相关内容做一些介绍。

  • canvas.translate
  • canvas.rotate
  • canvas.clipRect
  • canvas.save和canvas.restore
  • PorterDuffXfermode
  • Bitmap和Matrix
  • Shader
  • PathEffect

嗯哼,它们已经洗干净,挨个躺这了;我们就依次瞅瞅。

canvas.translate

从字面意思也可以知道它的作用是位移,那么这个位移到底是怎么实现的的呢?我们看段代码:

 protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.GREEN);
Paint paint=new Paint();
paint.setTextSize(70);
paint.setColor(Color.BLUE);
canvas.drawText("蓝色字体为Translate前所画", 20, 80, paint);
canvas.translate(100,300);
paint.setColor(Color.BLACK);
canvas.drawText("黑色字体为Translate后所画", 20, 80, paint);
}

这段代码的主要操作:

1 画一句话,请参见代码第7行

2 使用translate在X方向平移了100个单位在Y方向平移了300个单位,请参见代码第8行

3 再画一句话,请参见代码第10行

运行一下,看看它的效果:

自定义View系列教程04--Draw源码分析及其实践

看到了吧,在执行了平移之后所画的文字的位置=平移前坐标+平移的单位。

比如,平移后所画文字的实际位置为:120(20+100)和380(80+300)。

这就是说,canvas.translate相当于移动了坐标的原点,移动了坐标系。

这么说可能还是不够直观,那就上图:

自定义View系列教程04--Draw源码分析及其实践

喏,看到了吧:黑色的是原来的坐标系,蓝色的是移动后的坐标系。这就好理解了。

canvas.rotate

与translate类似,可以用rotate实现旋转。

protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.GREEN);
Paint paint=new Paint();
paint.setTextSize(70);
paint.setColor(Color.BLUE);
canvas.drawText("绿色字体为Rotate前所绘", 20, 80, paint);
canvas.rotate(15);
paint.setColor(Color.BLACK);
canvas.drawText("黑色字体为Rotate后所绘", 20, 80, paint);
}

自定义View系列教程04--Draw源码分析及其实践

前面说了canvas.translate相当于把坐标系平移了。与此同理,canvas.rotate相当于把坐标系旋转了一定角度。

canvas.clipRect

看完了canvas.translate和canv.rotate,接下来看看canvas.clipRect的使用。

canvas.clipRect表示剪裁操作,执行该操作后的绘制将显示在剪裁区域。

protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.GREEN);
Paint paint=new Paint();
paint.setTextSize(60);
paint.setColor(Color.BLUE);
canvas.drawText("绿色部分为Canvas剪裁前的区域", 20, 80, paint);
Rect rect=new Rect(20,200,900,1000);
canvas.clipRect(rect);
canvas.drawColor(Color.YELLOW);
paint.setColor(Color.BLACK);
canvas.drawText("黄色部分为Canvas剪裁后的区域", 10, 310, paint);
}

效果如下图所示:

自定义View系列教程04--Draw源码分析及其实践

当我们调用了canvas.clipRect( )后,如果再继续画图那么所绘的图只会在所剪裁的范围内体现。

当然除了按照矩形剪裁以外,还可以有别的剪裁方式,比如:canvas.clipPath( )和canvas.clipRegion( )。

canvas.save和canvas.restore

刚才在说canvas.clipRect( )时,有人可能有这样的疑问:在调用canvas.clipRect( )后,如果还需要在剪裁范围外绘图该怎么办?是不是系统有一个canvas.restoreClipRect( )方法呢?去看看官方的API就有点小失望了,我们期待的东西是不存在的;不过可以换种方式来实现这个需求,这就是即将要介绍的canvas.save和canvas.restore。看到这个玩意,可能绝大部分人就想起来了Activity中的onSaveInstanceState和onRestoreInstanceState这两者用来保存和还原Activity的某些状态和数据。canvas也可以这样么?

canvas.save

先来看这个玩意,它表示画布的锁定。如果我们把一个妹子锁在屋子里,那么外界的刮风下雨就影响不到她了;同理,如果对一个canvas执行了save操作就表示将已经所绘的图形锁定,之后的绘图就不会影响到原来画好的图形。

既然不会影响到原本已经画好的图形,那之后的操作又发生在哪里呢?

当执行canvas.save( )时会生成一个新的图层(Layer),并且这个图层是透明的。此时,所有draw的方法都是在这个图层上进行,所以不会对之前画好的图形造成任何影响。在进行一些绘制操作后再使用canvas.restore()将这个新的图层与底下原本的画好的图像相结合形成一个新的图像。打个比方:原本在画板上画了一个姑娘,我又找了一张和画板一样大小的透明的纸(Layer),然后在上面画了一朵花,最后我把这个纸盖在了画板上,呈现给世人的效果就是:一个美丽的姑娘手拿一朵鲜花。

还是继续看个例子:

protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.GREEN);
Paint paint=new Paint();
paint.setTextSize(60);
paint.setColor(Color.BLUE);
canvas.drawText("绿色部分为Canvas剪裁前的区域", 20, 80, paint);
canvas.save();
Rect rect=new Rect(20,200,900,1000);
canvas.clipRect(rect);
canvas.drawColor(Color.YELLOW);
paint.setColor(Color.BLACK);
canvas.drawText("黄色部分为Canvas剪裁后的区域", 10, 310, paint);
canvas.restore();
paint.setColor(Color.RED);
canvas.drawText("XXOO", 20, 170, paint);
}

这个例子由刚才讲canvas.clipRect( )稍加修改而来

1 执行canvas.save( )锁定canvas,请参见代码第8行

2 在新的Layer上裁剪和绘图,请参见代码第9-13行

3 执行canvas.restore( )将Layer合并到原图,请参见代码第14行

4 继续在原图上绘制,请参见代码第15-16行

来瞅瞅效果。

自定义View系列教程04--Draw源码分析及其实践

在使用canvas.save和canvas.restore时需注意一个问题:

save( )和restore( )最好配对使用,若restore( )的调用次数比save( )多可能会造成异常

PorterDuffXfermode

在项目开发中,我们常用到这样的功能:显示圆角图片。

效果如下:

自定义View系列教程04--Draw源码分析及其实践

看见了吧,图片的几个角不是直角而是具有一定弧度的圆角。

这个是咋做的呢?我们来瞅瞅其中一种实现方式

   /**
* @param bitmap 原图
* @param pixels 角度
* @return 带圆角的图
*/
public Bitmap getRoundCornerBitmap(Bitmap bitmap, float pixels) {
int width=bitmap.getWidth();
int height=bitmap.getHeight();
Bitmap roundCornerBitmap = Bitmap.createBitmap(width,height,Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(roundCornerBitmap);
Paint paint = new Paint();
paint.setColor(Color.BLACK);
paint.setAntiAlias(true);
Rect rect = new Rect(0, 0, width, height);
RectF rectF = new RectF(rect);
canvas.drawRoundRect(rectF, pixels, pixels, paint);
PorterDuffXfermode xfermode=new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
paint.setXfermode(xfermode);
canvas.drawBitmap(bitmap, rect, rect, paint);
return roundCornerBitmap;
}

主要操作如下:

1 生成canvas,请参见代码第7-10行

注意给canvas设置的Bitmap的大小是和原图的大小一致的

2 绘制圆角矩形,请参见代码第11-16行

3 为Paint设置PorterDuffXfermode,请参见代码第17-18行

4 绘制原图,请参见代码第19行

纵观代码,发现一个陌生的东西PorterDuffXfermode而且陌生到了我们看到它的名字却不容易猜测其用途的地步;这在Android的源码中还是很少有的。

我以前郁闷了很久,不知道它为什么叫这个名字,直到后来看到《Android多媒体开发高级编程》才略知其原委。

Thomas Porter和Tom Duff于1984年在ACM SIGGRAPH计算机图形学刊物上发表了《Compositing digital images》。在这篇文章中详细介绍了一系列不同的规则用于彼此重叠地绘制图像;这些规则中定义了哪些图像的哪些部分将出现在输出结果中。

这就是PorterDuffXfermode的名字由来及其核心作用。

现将PorterDuffXfermode描述的规则做一个介绍:

自定义View系列教程04--Draw源码分析及其实践

  1. PorterDuff.Mode.CLEAR

    绘制不会提交到画布上
  2. PorterDuff.Mode.SRC

    只显示绘制源图像
  3. PorterDuff.Mode.DST

    只显示目标图像,即已在画布上的初始图像
  4. PorterDuff.Mode.SRC_OVER

    正常绘制显示,即后绘制的叠加在原来绘制的图上
  5. PorterDuff.Mode.DST_OVER

    上下两层都显示但是下层(DST)居上显示
  6. PorterDuff.Mode.SRC_IN

    取两层绘制的交集且只显示上层(SRC)
  7. PorterDuff.Mode.DST_IN

    取两层绘制的交集且只显示下层(DST)
  8. PorterDuff.Mode.SRC_OUT

    取两层绘制的不相交的部分且只显示上层(SRC)
  9. PorterDuff.Mode.DST_OUT

    取两层绘制的不相交的部分且只显示下层(DST)
  10. PorterDuff.Mode.SRC_ATOP

    两层相交,取下层(DST)的非相交部分和上层(SRC)的相交部分
  11. PorterDuff.Mode.DST_ATOP

    两层相交,取上层(SRC)的非相交部分和下层(DST)的相交部分
  12. PorterDuff.Mode.XOR

    挖去两图层相交的部分
  13. PorterDuff.Mode.DARKEN

    显示两图层全部区域且加深交集部分的颜色
  14. PorterDuff.Mode.LIGHTEN

    显示两图层全部区域且点亮交集部分的颜色
  15. PorterDuff.Mode.MULTIPLY

    显示两图层相交部分且加深该部分的颜色
  16. PorterDuff.Mode.SCREEN

    显示两图层全部区域且将该部分颜色变为透明色

了解了这些规则,再回头看我们刚才例子中的代码,就好理解多了。

我们先画了一个圆角矩形,然后设置了PorterDuff.Mode为SRC_IN,最后绘制了原图。 所以,它会取圆角矩形和原图相交的部分但只显示原图部分;这样就形成了圆角的Bitmap。

Bitmap和Matrix

除了刚才提到的给图片设置圆角之外,在开发中还常有其他涉及到图片的操作,比如图片的旋转,缩放,平移等等,这些操作可以结合Matrix来实现。

在此举个例子,看看利用matrix实现图片的平移和缩放。

private void drawBitmapWithMatrix(Canvas canvas){
Paint paint = new Paint();
paint.setAntiAlias(true);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.mm);
int width=bitmap.getWidth();
int height=bitmap.getHeight();
Matrix matrix = new Matrix();
canvas.drawBitmap(bitmap, matrix, paint);
matrix.setTranslate(width/2, height);
canvas.drawBitmap(bitmap, matrix, paint);
matrix.postScale(0.5f, 0.5f);
//matrix.preScale(2f, 2f);
canvas.drawBitmap(bitmap, matrix, paint);
}

梳理一下这段代码的主要操作:

1 画出原图,请参见代码第2-8行

2 平移原图,请参见代码第9-10行

3 缩放原图,请参见代码第11-13行

自定义View系列教程04--Draw源码分析及其实践

嗯哼,看到效果了吧。

在这里主要涉及到了Matrix。我们可以通过这个矩阵来实现对图片的一些操作。有人可能会问:利用Matrix实现了图片的平移(Translate)是将坐标系进行了平移么?不是的。Matrix所操作的是原图的每个像素点,它和坐标系是没有关系的。比如Scale是对每个像素点都进行了缩放,例如:

matrix.postScale(0.5f, 0.5f);

将原图的每个像素点的X的坐标都缩放成了原本的0.5

将原图的每个像素点的Y坐标也都缩放成了原本的0.5

同样的道理在调用matrix.setTranslate( )时是对于原图中的每个像素都执行了位移操作。

在使用Matrix时经常用到一系列的set,pre,post方法。它们有什么区别呢?它们的调用顺序又会对实际效果有什么影响呢?

在此对该问题做一个总结:

在调用set,pre,post时可视为将这些方法插入到一个队列。

  • pre表示在队头插入一个方法
  • post表示在队尾插入一个方法
  • set表示清空队列

    队列中只保留该set方法,其余的方法都会清除。

当执行了一次set后pre总是插入到set之前的队列的最前面;post总是插入到set之后的队列的最后面。

也可以这么简单的理解:

set在队列的中间位置,per执行队头插入,post执行队尾插入。

当绘制图像时系统会按照队列中从头至尾的顺序依次调用这些方法。

请看下面的几个小示例:

Matrix m = new Matrix();
m.setRotate(45);
m.setTranslate(80, 80);

只有m.setTranslate(80, 80)有效,因为m.setRotate(45)被清除.

Matrix m = new Matrix();
m.setTranslate(80, 80);
m.postRotate(45);

先执行m.setTranslate(80, 80)后执行m.postRotate(45)

Matrix m = new Matrix();
m.setTranslate(80, 80);
m.preRotate(45);

先执行m.preRotate(45)后执行m.setTranslate(80, 80)

Matrix m = new Matrix();
m.preScale(2f,2f);
m.preTranslate(50f, 20f);
m.postScale(0.2f, 0.5f);
m.postTranslate(20f, 20f);

执行顺序:

m.preTranslate(50f, 20f)–>m.preScale(2f,2f)–>m.postScale(0.2f, 0.5f)–>m.postTranslate(20f, 20f)

Matrix m = new Matrix();
m.postTranslate(20, 20);
m.preScale(0.2f, 0.5f);
m.setScale(0.8f, 0.8f);
m.postScale(3f, 3f);
m.preTranslate(0.5f, 0.5f);

执行顺序:

m.preTranslate(0.5f, 0.5f)–>m.setScale(0.8f, 0.8f)–>m.postScale(3f, 3f)

Shader

有时候我们需要实现图像的渐变效果,这时候Shader就派上用场啦。

先来瞅瞅啥是Shader:

Shader is the based class for objects that return horizontal spans of colors during drawing. A subclass of Shader is installed in a Paint calling paint.setShader(shader). After that any object (other than a bitmap) that is drawn with that paint will get its color(s) from the shader.

Android提供的Shader类主要用于渲染图像以及几何图形。

Shader的主要子类如下:

  • BitmapShader———图像渲染
  • LinearGradient——–线性渲染
  • RadialGradient——–环形渲染
  • SweepGradient——–扫描渲染
  • ComposeShader——组合渲染

在开发中调用paint.setShader(Shader shader)就可以实现渲染效果,在此以常用的BitmapShader为示例实现圆形图片。

protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint();
paint.setAntiAlias(true);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.mm);
int radius = bitmap.getWidth()/2;
BitmapShader bitmapShader = new BitmapShader(bitmap,Shader.TileMode.REPEAT,Shader.TileMode.REPEAT);
paint.setShader(bitmapShader);
canvas.translate(250,430);
canvas.drawCircle(radius, radius, radius, paint);
}

效果如下图所示:

自定义View系列教程04--Draw源码分析及其实践

嗯哼,看到这样的代码心情还是挺舒畅的,十来行就搞定了一个小功能。

好吧,借着这股小舒畅,我们来瞅瞅代码

1 生成BitmapShader,请参见代码第7行

2 为Paint设置Shader,请参见代码第8行

3 画出圆形图片,请参见代码第10行

在这段代码中,可能稍感陌生的就是BitmapShader构造方法。

BitmapShader(Bitmap bitmap, TileMode tileX, TileMode tileY)

第一个参数:

bitmap表示在渲染的对象

第二个参数:

tileX 表示在位图上X方向渲染器平铺模式(TileMode)

TileMode一共有三种:

  • REPEAT :重复
  • MIRROR :镜像
  • CLAMP:拉伸

这三种效果类似于给电脑屏幕设置屏保时,若图片太小可选择重复,拉伸,镜像。

若选择REPEAT(重复 ):横向或纵向不断重复显示bitmap

若选择MIRROR(镜像):横向或纵向不断翻转重复

若选择CLAMP(拉伸) :横向或纵向拉伸图片在该方向的最后一个像素。这点和设置电脑屏保有些不同

第三个参数:

tileY表示在位图上Y方向渲染器平铺模式(TileMode)。与tileX同理,不再赘述。

PathEffect

我们可以通过canvas.drawPath( )绘制一些简单的路径。但是假若需要给路径设置一些效果或者样式,这时候就要用到PathEffect了。

PathEffect有如下几个子类:

  • CornerPathEffect

    用平滑的方式衔接Path的各部分
  • DashPathEffect

    将Path的线段虚线化
  • PathDashPathEffect

    与DashPathEffect效果类似但需要自定义路径虚线的样式
  • DiscretePathEffect

    离散路径效果
  • ComposePathEffect

    两种样式的组合。先使用第一种效果然后在此基础上应用第二种效果
  • SumPathEffect

    两种样式的叠加。先将两种路径效果叠加起来再作用于Path

在此以CornerPathEffect和DashPathEffect为示例:

protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.translate(0,300);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.STROKE);
paint.setColor(Color.GREEN);
paint.setStrokeWidth(8);
Path path = new Path();
path.moveTo(15, 60);
for (int i = 0; i <= 35; i++) {
path.lineTo(i * 30, (float) (Math.random() * 150));
}
canvas.drawPath(path, paint);
canvas.translate(0, 400);
paint.setPathEffect(new CornerPathEffect(60));
canvas.drawPath(path, paint);
canvas.translate(0, 400);
paint.setPathEffect(new DashPathEffect(new float[] {15, 8}, 1));
canvas.drawPath(path, paint);
}

效果如下图所示:

自定义View系列教程04--Draw源码分析及其实践

分析一下这段代码中的主要操作:

1 设置Path为CornerPathEffect效果,请参见代码第16行

在构建CornerPathEffect时传入了radius,它表示圆角的度数

2 设置Path为DashPathEffect效果,请参见代码第19行

在构建DashPathEffect时传入的参数要稍微复杂些。

DashPathEffect构造方法的第一个参数:

数组float[ ] { }中第一个数表示每条实线的长度,第二个数表示每条虚线的长度。

DashPathEffect构造方法的第二个参数:

phase表示偏移量,动态改变该值会使路径产生动画效果


至此关于自定义View的第三个阶段draw就写这么多吧。其实,在该阶段涉及到的东西非常多,我们这提到的仅仅是九牛一毛,也只是一些常用的基础性的东西。我也期望自己以后有时间和精力和大家一道深入地学习draw部分的相关知识和开发技能。

PS:如果觉得文章太长,那就直接看视频

源码下载


who is the next one? ——> demo

上一篇:[Winform]线程间操作无效,从不是创建控件的线程访问它的几个解决方案,async和await?


下一篇:C/C++ makefile自动生成工具(comake2,autotools,linux),希望能为开源做点微薄的贡献!