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

通过之前的详细分析,我们知道:在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;

9       if (!dirtyOpaque) {
10          drawBackground(canvas);
11      }
12
13      final int viewFlags = mViewFlags;
14      boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
15      boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
16      if (!verticalEdges && !horizontalEdges) {
17
18          if (!dirtyOpaque) onDraw(canvas);
19
20          dispatchDraw(canvas);
21
22          if (mOverlay != null && !mOverlay.isEmpty()) {
23              mOverlay.getOverlayView().dispatchDraw(canvas);
24          }
25           // Step 6, draw decorations (foreground, scrollbars)
26          onDrawForeground(canvas);
27
28          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;

42      int paddingLeft = mPaddingLeft;
43
44      final boolean offsetRequired = isPaddingOffsetRequired();
45      if (offsetRequired) {
46          paddingLeft += getLeftPaddingOffset();
47      }
48
49      int left = mScrollX + paddingLeft;
50      int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
51      int top = mScrollY + getFadeTop(offsetRequired);
52      int bottom = top + getFadeHeight(offsetRequired);
53
54      if (offsetRequired) {
55          right += getRightPaddingOffset();
56          bottom += getBottomPaddingOffset();
57      }

        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) {
100             canvas.saveLayer(left, top, left + length, bottom, null, flags);
101         }
102
103         if (drawRight) {
104             canvas.saveLayer(right - length, top, right, bottom, null, flags);
105         }
106     } else {
107         scrollabilityCache.setFadeColor(solidColor);
108     }

        if (!dirtyOpaque) onDraw(canvas);

        dispatchDraw(canvas);

114     final Paint p = scrollabilityCache.paint;
115     final Matrix matrix = scrollabilityCache.matrix;
116     final Shader fade = scrollabilityCache.shader;
117
118     if (drawTop) {
119         matrix.setScale(1, fadeHeight * topFadeStrength);
120         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);
        }
152
153     canvas.restoreToCount(saveCount);
154
155     if (mOverlay != null && !mOverlay.isEmpty()) {
156         mOverlay.getOverlayView().dispatchDraw(canvas);
157     }
158
159     onDrawForeground(canvas);
160 }

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

Draw the background
If necessary, save the canvas’ layers to prepare for fading
Draw view’s content
Draw children
If necessary, draw the fading edges and restore layers
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的滚动条,请参见代码第26行
其实,不单单是常见的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):

  • 用什么工具画? 这个小问题很简单,我们需要用一支画笔(Paint)来绘图。 当然,我们可以选择不同颜色的笔,不同大小的笔。
  • 把图画在哪里呢? 我们把图画在了Bitmap上,它保存了所绘图像的各个像素(pixel)。 也就是说Bitmap承载和呈现了画的各种图形。
  • 画的内容? 根据自己的需求画圆,画直线,画路径。
  • 怎么画? 调用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 Bitmap drawBitmap() {
        String str = "Hello World!";
        int height = 500;
        int width = 200;

        Bitmap bitmap = Bitmap.createBitmap(height, width, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        canvas.drawColor(Color.GRAY);
        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

        paint.setColor(Color.BLUE);
        paint.setTextSize(60);
        Rect bounds = new Rect();
        paint.getTextBounds(str, 0, str.length(), bounds);

        /**
         * x,默认是这个字符串的左边在屏幕的位置
         * y是指定这个字符baseline在屏幕上的位置,如果设置y的值为0,则绘制的内容不会显示,
         * 这里baseline是bounds.height(),即是字符内容的高度,设置x为0,y为bounds.height(),内容才会从左上角定点显示,下面代码设置的是居中显示
         * */
        canvas.drawText("Hello World", bitmap.getWidth()/2-bounds.width()/2,bitmap.getHeight()/2-bounds.height()/2+bounds.height(), paint);
        return bitmap;
    }

瞅瞅效果:
效果图

在此处为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();
}
Demo效果图

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


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

canvas.translate
canvas.rotate
canvas.clipRect
canvas.save和canvas.restore
PorterDuffXfermode
Bitmap和Matrix
Shader
PathEffect
嗯哼,它们已经洗干净,挨个躺这了;我们就依次瞅瞅。

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

1 protected void onDraw(Canvas canvas) {
2     super.onDraw(canvas);
3     canvas.drawColor(Color.GREEN);
4     Paint paint=new Paint();
5     paint.setTextSize(70);
6     paint.setColor(Color.BLUE);
7     canvas.drawText("蓝色字体为Translate前所画", 20, 80, paint);
8     canvas.translate(100,300);
9     paint.setColor(Color.BLACK);
10     canvas.drawText("黑色字体为Translate后所画", 20, 80, paint);
11}

这段代码的主要操作:

  1. 画一句话,请参见代码第7行
  2. 使用translate在X方向平移了100个单位在Y方向平移了300个单位,请参见代码第8行
  3. 再画一句话,请参见代码第10行
    运行一下,看看它的效果:
    这里写图片描述

    看到了吧,在执行了平移之后所画的文字的位置=平移前坐标+平移的单位。 比如,平移后所画文字的实际位置为:120(20+100)和380(80+300)。 这就是说,canvas.translate相当于移动了坐标的原点,移动了坐标系。 这么说可能还是不够直观,那就上图:
    这里写图片描述
    喏,看到了吧:黑色的是原来的坐标系,蓝色的是移动后的坐标系。这就好理解了。
    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);
}

这里写图片描述
前面说了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);
    }

效果如下图所示:
这里写图片描述

当我们调用了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),然后在上面画了一朵花,最后我把这个纸盖在了画板上,呈现给世人的效果就是:一个美丽的姑娘手拿一朵鲜花。 还是继续看个例子:

1 protected void onDraw(Canvas canvas) {
2    super.onDraw(canvas);
3    canvas.drawColor(Color.GREEN);
4    Paint paint=new Paint();
5    paint.setTextSize(60);
6    paint.setColor(Color.BLUE);
7    canvas.drawText("绿色部分为Canvas剪裁前的区域", 20, 80, paint);
8    canvas.save();
9    Rect rect=new Rect(20,200,900,1000);
10   canvas.clipRect(rect);
11   canvas.drawColor(Color.YELLOW);
12   paint.setColor(Color.BLACK);
13   canvas.drawText("黄色部分为Canvas剪裁后的区域", 10, 310, paint);
14   canvas.restore();
15   paint.setColor(Color.RED);
16   canvas.drawText("XXOO", 20, 170, paint);
17 }

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

  1. 执行canvas.save( )锁定canvas,请参见代码第8行
  2. 在新的Layer上裁剪和绘图,请参见代码第9-13行
  3. 执行canvas.restore( )将Layer合并到原图,请参见代码第14行
  4. 继续在原图上绘制,请参见代码第15-16行

    来瞅瞅效果。
    效果图

在使用canvas.save和canvas.restore时需注意一个问题: save( )和restore( )最好配对使用,若restore( )的调用次数比save( )多可能会造成异常

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

Demo效果图
看见了吧,图片的几个角不是直角而是具有一定弧度的圆角。 这个是咋做的呢?我们来瞅瞅其中一种实现方式

/**
     * @param bitmap 原图
     * @param pixels 角度
     * @return 带圆角的图
     */
    public Bitmap getRoundCornerBitmap(Bitmap bitmap, float pixels) {
7         int width=bitmap.getWidth();
8         int height=bitmap.getHeight();
9         Bitmap roundCornerBitmap = Bitmap.createBitmap(width,height,Bitmap.Config.ARGB_8888);
10        Canvas canvas = new Canvas(roundCornerBitmap);
11        Paint paint = new Paint();
12        paint.setColor(Color.BLACK);
13        paint.setAntiAlias(true);
14        Rect rect = new Rect(0, 0, width, height);
15        RectF rectF = new RectF(rect);
16        canvas.drawRoundRect(rectF, pixels, pixels, paint);
17        PorterDuffXfermode xfermode=new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
18        paint.setXfermode(xfermode);
19        canvas.drawBitmap(bitmap, rect, rect, paint);
20        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描述的规则做一个介绍:
    这里写图片描述

    PorterDuff.Mode.CLEAR 绘制不会提交到画布上
    PorterDuff.Mode.SRC 只显示绘制源图像
    PorterDuff.Mode.DST 只显示目标图像,即已在画布上的初始图像
    PorterDuff.Mode.SRC_OVER 正常绘制显示,即后绘制的叠加在原来绘制的图上
    PorterDuff.Mode.DST_OVER 上下两层都显示但是下层(DST)居上显示
    PorterDuff.Mode.SRC_IN 取两层绘制的交集且只显示上层(SRC)
    PorterDuff.Mode.DST_IN 取两层绘制的交集且只显示下层(DST)
    PorterDuff.Mode.SRC_OUT 取两层绘制的不相交的部分且只显示上层(SRC)
    PorterDuff.Mode.DST_OUT 取两层绘制的不相交的部分且只显示下层(DST)
    PorterDuff.Mode.SRC_ATOP 两层相交,取下层(DST)的非相交部分和上层(SRC)的相交部分
    PorterDuff.Mode.DST_ATOP 两层相交,取上层(SRC)的非相交部分和下层(DST)的相交部分
    PorterDuff.Mode.XOR 挖去两图层相交的部分
    PorterDuff.Mode.DARKEN 显示两图层全部区域且加深交集部分的颜色
    PorterDuff.Mode.LIGHTEN 显示两图层全部区域且点亮交集部分的颜色
    PorterDuff.Mode.MULTIPLY 显示两图层相交部分且加深该部分的颜色
    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行
这里写图片描述

嗯哼,看到效果了吧。 在这里主要涉及到了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为示例实现圆形图片。

效果如下图所示:
这里写图片描述
嗯哼,看到这样的代码心情还是挺舒畅的,十来行就搞定了一个小功能。 好吧,借着这股小舒畅,我们来瞅瞅代码 1 生成BitmapShader,请参见代码第7行 2 为Paint设置Shader,请参见代码第8行 3 画出圆形图片,请参见代码第10行

在这段代码中,可能稍感陌生的就是BitmapShader构造方法。
BitmapShader(Bitmap bitmap, TileMode tileX, TileMode tileY)

第一个参数: bitmap表示在渲染的对象
第二个参数: tileX 表示在位图上X方向渲染器平铺模式(TileMode) TileMode一共有三种:

  1. 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);
}

效果如下图所示:
Demo效果图

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

  1. 设置Path为CornerPathEffect效果,请参见代码第16行 在构建CornerPathEffect时传入了radius,它表示圆角的度数
  2. 设置Path为DashPathEffect效果,请参见代码第19行 在构建DashPathEffect时传入的参数要稍微复杂些。

DashPathEffect构造方法的第一个参数: 数组float[ ] { }中第一个数表示每条实线的长度,第二个数表示每条虚线的长度。 DashPathEffect构造方法的第二个参数: phase表示偏移量,动态改变该值会使路径产生动画效果


Demo

自定义View系列教程01--常用工具介绍
自定义View系列教程02--onMeasure源码详尽分析
自定义View系列教程03--onLayout源码详尽分析
自定义View系列教程04--Draw源码分析及其实践
自定义View系列教程05–示例分析
自定义View系列教程06–详解View的Touch事件处理
自定义View系列教程07–详解ViewGroup分发Touch事件
自定义View系列教程08–滑动冲突的产生及其处理

参考文章
Android LayoutInflater原理分析,带你一步步深入了解View(一)
Android视图绘制流程完全解析,带你一步步深入了解View(二)
Android视图状态及重绘流程分析,带你一步步深入了解View(三)
Android自定义View的实现方法,带你一步步深入了解View(四)

原文地址:http://blog.csdn.net/lfdfhl/article/details/51435968

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容