Android 自定义 View 之 draw 原理分析

Android 自定义 View 系列文章至今已经分析了自定义流程的 measure,layout 以及对 TouchEvent 的处理。那么接下来当然应该讲到对draw原理分析了,draw这一步骤是整个自定义过程中极为重要的一步,而今天这一篇文章就是对 draw 原理进行分析。经过 measure 测量和 layou t定位后,自定义 View 便进入了 draw 绘制阶段。


android.view.View.draw()

既然是对 draw 原理分析,那么自然首先看一下 View 的 draw() 方法源码了

在看源码之前我们先看 Google 官方文档对 android.view.View.draw() 方法的描述,这有助于我们对源码的理解:

Manually render this view (and all of its children) to the given Canvas. The view must have already done a full layout before this function is called. When implementing a view, implement onDraw(android.graphics.Canvas) instead of overriding this method. If you do need to override this method, call the superclass version.

上面的话大约有以下几点需要注意:

  1. 在给定的画布(canvas)上手动渲染这个视图(以及它所有的子视图(子 view))

  2. 在调用此函数之前,视图必须已经完成了完整的布局

  3. 当实现一个视图时,实现o nDraw(android.graphics.Canvas)而不是覆盖(重写)这个方法

  4. 如果你确实需要重写这个方法,调用超类的版本

我们可以看到绘制(draw)的操作是在canvas(画布)上进行的,所以这里我们再来了解一下Canvas类吧!

Google官方文档对 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).

以上信息主要有以下几点:

  1. Canvas 类保存对 draw() 方法的调用

  2. 如果要进行绘制(draw),需要四个基本组成部分

  3. 储存绘制的内容,用于保存像素的位图(Bitmap )、一个 Canvas 来承载绘制调用(写入位图,调用各种方法,如 canvas.drawLine()、canvas。drawPaint())、绘图图元(根据自己需求绘制,例如圆(Rect),路径(Path),文本,Bitmap)、画笔(Paint,描述绘图的颜色和风格)

我们可以看到 Canvas 类在 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() 方法的具体实现逻辑了,主要有一下几步:

  1. 绘制背景

  2. 如果需要,保存画布的图层以准备褪色

  3. 绘制视图的内容

  4. 绘制子 View

  5. 如有必要,绘制褪色边缘并恢复图层

  6. 绘制装饰(例如滚动条)

接下来对以上几步进行具体分析:

第一步:

在 canvas 画布上进行背景绘制

第二步:

保存当前画布的堆栈状态并在该画布上创建 Layer 用于绘制 View 在滑动时的边框渐变效果

第三步:

绘制 View 的内容

这一步是 draw 过程的核心所在,这一步会调用 onDraw() 方法绘制 View 的内容。

在之前对 onLayout() 方法进行源码分析的时候发现 onLayout() 方法是一个空方法,而是由 ViewGroup 的子类去实现具体实现逻辑。onDraw() 方法与之相似,因为每个 View 实现的内容都不一样,所以需要由具体的子 View 去实现不同的需求

第四步:

用 dispatchDraw() 绘制 View 的子 View

第五步:

绘制当前视图在滑动时的边框渐变效果,通常情况下我们是不需要处理这一步的

第六步:

绘制 View 的滚动条


onDraw()

看完 draw() 源码后我们知道此方法的核心是在第三步调用了 onDraw() 方法,所以接下来我们就从此方法继续深入分析吧!

protected void onDraw(Canvas canvas) {
}

可以看到传入的是一个 canvas 对象,在前面我们已经知道了进行绘制需要的四个基本组成部分,这里我们再来看一下如何构建一个 canvas,先看 Canvas 类的两个构造方法:

Canvas()

Construct an empty raster canvas.

/**
 * 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对象来保存像素,如果画的东西没有地方可以保存,又还有什么意义呢?既然不推荐这么做,那就我们接着看有参的构造方法。

Canvas(Bitmap bitmap)

Construct a canvas with the specified bitmap to draw into.

/**
 * 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;
}

这一个方法就传入了一个 Bitmap 作为参数来保持绘制图像的信息。

好了,既然已经知道如何创建 canvas,那么接下来我们就尝试着使用 canvas 来进行绘制吧!

protected void drawOnBitmap(){
    Bitmap bitmap = Bitmap.createBitmap(800, 400, Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(bitmap);
    canvas.drawColor(Color.Blue);
    Paint paint = new Paint();
    paint.setColor(Color.RED);
    paint.setTextSize(60);
    canvas.drawText("This is my view", 150, 200, paint);
    mImageView.setImageBitmap(bitmap);
}

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

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

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

既然已经提到了 onDraw( ) 我们就利用 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(0,0,20,40,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(10);
        mPaint.setColor(Color.BLUE);
        mPaint.setAntiAlias(true);
        canvas.drawCircle(670, 300, 70, mPaint);
        mPaint.reset();

        mPaint.setColor(Color.GREEN);
        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();
    }

以上演示了常用图形的绘制方法,更多绘制方法请参考 Google 开发文档 API,除了绘制图形之外,还可以调用API方法设置动画如平移、旋转、缩放、剪裁等!下面演示此类常用方法:

  • canvas.translate()

  • canvas.rotate()

  • canvas.clipRect()

  • canvas.save()和canvas.restore()

  • PorterDuffXfermode

  • Bitmap和Matrix

  • Shader

  • PathEffect

下面分别介绍它们的使用:

canvas.translate()

使用 canvas.translate() 可以实现平移操作

@Override 
protected void onDraw(Canvas canvas){
    super.onDraw(canvas);

    canvas.drawColor(GREEN);
    Paint mPaint = new Paint();
    mPaint.setTextSize(70);
    mPaint.setColor(Color.RED);
    canvas.drawText("Test translate", 20, 80, mPaint);
    canvas.translate(100, 300);
    mPaint.setColor(Color.BLACK);
    canvas.drawText("Test translate", 20, 80, mPaint);
}

使用 translate() 使其在 X 方向平移了 100,在 Y 方向上平移了 300。

所以,执行平移后文字的位置 = 执行平移前的位置 + 平移的单位。也就是说 canvas.translate() 相当于移动了坐标原点,移动了坐标系。

canvas.rotate()

使用 canvas.rotate() 可以实现旋转操作

@Override
protected void onDraw(Canvas canvas){
    super.onDraw(canvas);

    canvas.drawColor(Color.GREEN);
    Paint mPaint = new Paint();
    mPaint.setTextSize(70);
    mPaint.setColor(Color.RED); 
    canvas.drawText("Test ratate", 20, 80, mPaint);
    canvas.rotate(15);
    mPaint.setColor(Color.BLACK);
    canvas.drawText("Test ratate", 20, 80, mPaint);
}

canvas.clipRect()

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

@Override
protected void onDraw(Canvas canvas){
    super.onDraw(cancas);

    canvas.drawColor(Color.GREEN);
    Paint mPaint = new Paint();
    mPaint.setTextSize(70);
    mPaint.setColor(Color.RED);
    canvas.drawText("Test clip", 20, 80, mPaint);
    Rect rect = new Rect(20, 200, 900, 1000);
    canvas.clipRect(rect);
    canvas.drawColor(Color.BLUE);
    mPaint.setColor(Color.BLACK);
    canvas.drawText("Test clip", 10, 310, mPaint);
}

调用 canvas.clipRect( ) 后,如果继续进行绘制那么所绘制的图只会在所剪裁的范围内体现。

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

canvas.save() 和 canvas.restore()

canvas.save() 表示对画布的锁定操作,执行方法后绘制的图形将被锁定,之后的绘制不会影响之前绘制好的图形。

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

当执行 canvas.save( ) 时会生成一个新的图层(Layer),并且这个图层是透明的。此时,所有 draw 的方法都是在这个图层上进行,所以不会对之前画好的图形造成任何影响。在进行一些绘制操作后再使用 canvas.restore() 将这个新的图层与底下原本的画好的图像相结合形成一个新的图像。

@Override
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("Before clip", 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("After clip", 10, 310, paint);
    canvas.restore();
    paint.setColor(Color.RED);
    canvas.drawText("Test", 20, 170, paint);
}

以上代码主要有以下几步:

  1. 执行 canvas.save( ) 锁定 canvas

  2. 在新的 Layer 上裁剪和绘图

  3. 执行 canvas.restore( ) 将 Layer 合并到原图

  4. 继续在原图上绘制

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

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

PorterDuffXfermode

在项目开发中,我们经常会遇到这样的需求,为图片设置圆角显示

   /**
     * @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

  2. 绘制圆角矩形

  3. 为 Paint 设置 PorterDuffXfermode

  4. 绘制原图

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

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

Android 自定义 View 之 draw 原理分析_第1张图片

Bitmap和Matrix

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

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

private void drawBitmapWithMatrix(Canvas canvas){
    Paint paint = new Paint();
    paint.setAntiAlias(true);
    Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.scale);
    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);
}

这里主要涉及到 Matrix,我们可以通过这个矩阵来实现对图片的一些操作。利用 Matrix 实现了图片的平移是和使用 cancas.translate() 方法一样将坐标系进行了平移吗?当然不是的,Matrix 所操作的是原图的每个像素点,它和坐标系是没有关系的。比如 Scale 是对每个像素点都进行了缩放,例如:

matrix.postScale(0.5f, 0.5f);

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

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

  • set表示清空队列

  • pre表示在队头插入一个方法

  • post表示在队尾插入一个方法

    队列中只保留该方法,其余方法都清除

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

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

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

为了便于理解,下面通过几个例子来解释:

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

调用了两个不同的 set 方法,因为队列中只保留该方法,其余方法都清除。所以只执行 setTranslate(80, 80);

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

执行了一次 set 方法后,pre 方法插入到set方法队首,所以先执行 matrix.preRotate(45); 再执行 matrixm.setTranslate(80, 80);

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

执行了一次 set 方法后,pre 方法插入到 set 方法队尾,所以先执行 matrixm.setTranslate(80, 80); 再执行 matrix.postRotate(45);

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

这里使用了两个 set 系列方法,两个 post 系列方法,pre 系列方法插入到队列的队首,post 系列方法插入到队尾,所以执行顺序为 matrix.preTranslate(50f, 20f); —>matrix.preScale(2f,2f);—>matrix.postScale(0.2f, 0.5f);—>matrix.postTranslate(20f, 20f);

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

Android 自定义 View 之 draw 原理分析_第2张图片

Shader

Shader 可以帮助我们现图像的渐变效果

看一下 Google 官方文档对 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.

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

以上代码主要可以分成以下几步分:

  • 生成 BitmapShader

  • 为 Pain 设置 Shader

  • 画出圆形图片

TileMode 有三种模式:

  • REPEAT :重复

  • MIRROR :镜像

  • CLAMP:拉伸

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

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

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

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

第三个参数:

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

Android 自定义 View 之 draw 原理分析_第3张图片

注意

这里调用的资源图片的大小会影响到整体图片效果,各位可以自行调整图片像素进行尝试,这里举个例子,如使用 Shader 实现圆形图片时,像素应为 200*200,才能够实现占满整个图像,而不会呈现重复,镜像,和拉伸的情况。

你可能感兴趣的:(Android,自定义View,移动开发)