Shader是什么,Canvas可以绘制图形(圆形、弧形、矩形等),Shader是为这些图形着色的,改变这些图形外观的,例如在一个圆形上将图片贴在圆形上,就可以实现圆形头像控件,在这里BitmapShader改变了圆形这个图形的外观,将图片内容附着到了图形上面。Shader不只有BitmapShader,它总共包括如下Shader:BitmapShaderLinearGradientSweepGradientRadialGradientComposeShader。接下来我们会详细讲解这几个Shader的用法。

一、基础知识

1. BitmapShader

我们前面已经提过,它是用图片来改变图形外观的Shader,这里就以制作圆形头像为例。代码很简单,如下所示:

package com.cb.paint_gradient;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ComposeShader;
import android.graphics.LinearGradient;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.RadialGradient;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.SweepGradient;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.OvalShape;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;

/**
 * Created by xw.gao
 */

public class MyShaderView extends View {
    private Paint mPaint;
    private Bitmap mBitMap = null;

    private int mWidth;
    private int mHeight;
    //gxw-private int[] mColors = {Color.RED,Color.GREEN,Color.BLUE,Color.YELLOW};

    public MyShaderView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mBitMap = ((BitmapDrawable)getResources().getDrawable(R.drawable.head)).getBitmap();
        mPaint = new Paint();
        mWidth = mBitMap.getWidth();
        mHeight = mBitMap.getHeight();
    }

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

        /**
         * TileMode.CLAMP 拉伸最后一个像素去铺满剩下的地方
         * TileMode.MIRROR 通过镜像翻转铺满剩下的地方。
         * TileMode.REPEAT 重复图片平铺整个画面(电脑设置壁纸)
         */
       BitmapShader bitMapShader = new BitmapShader(mBitMap, Shader.TileMode.MIRROR,
                Shader.TileMode.MIRROR);
        mPaint.setShader(bitMapShader);
        mPaint.setAntiAlias(true);
        canvas.drawCircle(500,900, 500 ,mPaint);
    }
}

主要看以下2处代码:

(1)    BitmapShader bitMapShader = new BitmapShader(mBitMap, Shader.TileMode.MIRROR,Shader.TileMode.MIRROR);

              mPaint.setShader(bitMapShader);

这行代码生成了一个BitmapShader,指明了图片,以及图片将要附着在图形上的方式是:Shader.TileMode.MIRROR(镜像模式)。如果图形的面积大于图片的面积,则图片会重复生成,并且相邻图片成镜像。显示效果见下面(2)中的图片。

(2)    canvas.drawCircle(500,900, 500 ,mPaint); 这里画了一个圆形,前2个参数是圆心,第3个参数是半径,最后一个参数paint已经设置了我们之前的bitMapShader。那最终的显示效果将是将图片mBitMap粘贴在圆形上。在这里我们圆形的大小远大于图片的大小,且 覆盖模式是  Shader.TileMode.MIRROR。所以显示效果就如下图所示:

android渲染管线hwui 安卓图形渲染_android

把上述canvas.drawCircle这行代码改为如下代码,可以将图形设置为矩形,然后把图片贴在矩形里。

canvas.drawRect(new Rect(0,0 , 1000, 1600),mPaint);

那么这时的效果图如下:

android渲染管线hwui 安卓图形渲染_android_02

再把上述canvas.drawRect代码换成如下代码,则是绘制一个椭圆,然后图片贴在椭圆里。

canvas.drawOval(new RectF(0 , 0, mWidth, mHeight),mPaint);

这里的mWidth和mHeight是图片的宽和高。也就是说这个椭圆的外切矩形的宽高就是图片的宽高。这时图片贴上去,一张图片就可以铺满这个椭圆。效果如下图(由于我们这张图片的宽和高比较相近,所以呈现出来的就是一个近乎圆形的头像):

android渲染管线hwui 安卓图形渲染_android_03

至此,BitmapShader就讲完了,一句话,它是用图片改变图形外观的渲染器。

2. LinearGradient线性渲染

LinearGradient线性渲染就是颜色对图形外观的改变,主要是在着色上面。之所以为线性,指的是在1条直线上的颜色渐变。比如从一个矩形图形的左上角到右下角,颜色可以从绿色渐变为蓝色。我们实现这么一个例子:让一个矩形的颜色从左上角到右下角渐变。代码如下:

package com.cb.paint_gradient;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ComposeShader;
import android.graphics.LinearGradient;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.RadialGradient;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.SweepGradient;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.OvalShape;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;

/**
 * Created by xw.gao
 */

public class MyShaderView extends View {
    private Paint mPaint;
    private Bitmap mBitMap = null;

    private int mWidth;
    private int mHeight;
    private int[] mColors = {Color.RED,Color.GREEN,Color.BLUE,Color.YELLOW};

    public MyShaderView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mBitMap = ((BitmapDrawable)getResources().getDrawable(R.drawable.head)).getBitmap();
        mPaint = new Paint();
        mWidth = mBitMap.getWidth();
        mHeight = mBitMap.getHeight();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(Color.WHITE);
/*gxw-s for BitmapShader
         * TileMode.CLAMP 拉伸最后一个像素去铺满剩下的地方
         * TileMode.MIRROR 通过镜像翻转铺满剩下的地方。
         * TileMode.REPEAT 重复图片平铺整个画面(电脑设置壁纸)
       BitmapShader bitMapShader = new BitmapShader(mBitMap, Shader.TileMode.MIRROR,Shader.TileMode.MIRROR);
        mPaint.setShader(bitMapShader);
        mPaint.setAntiAlias(true);
        //canvas.drawCircle(500,900, 500 ,mPaint);图形为圆形
        //canvas.drawRect(new Rect(0,0 , 1000, 1600),mPaint);图形为矩形
        //canvas.drawOval(new RectF(0 , 0, mWidth, mHeight),mPaint);图形为椭圆
 gxw-e for BitmapShader */



      /*  线性渐变
         * x0, y0, 起始点
         *  x1, y1, 结束点
         * int[]  mColors, 中间依次要出现的几个颜色
         * float[] positions,数组大小跟colors数组一样大,中间依次摆放的几个颜色分别放置在那个位置上(参考比例从左往右)
         *    tile
         */
		LinearGradient linearGradient = new LinearGradient( 0, 0,800, 800, mColors, null, Shader.TileMode.CLAMP);
        // linearGradient = new LinearGradient(0, 0, 400, 400, mColors, null, Shader.TileMode.REPEAT);
		mPaint.setShader(linearGradient);
		canvas.drawRect(0, 0, 800, 800, mPaint);
    }
}

还是看2处代码,

(1)        LinearGradient linearGradient = new LinearGradient( 0, 0,800, 800, mColors, null, Shader.TileMode.CLAMP);
                  mPaint.setShader(linearGradient);

在这里生成了一个线性渲染器LinearGradient,并将它设置给paint. 

参数解释

参数1:渐变线起点的x坐标

参数2:渐变线起点的y坐标

参数3:渐变线终点的x坐标

参数4:渐变线终点的y坐标

参数5:渐变的颜色,是一个数组,可以有多种颜色间的渐变。如,

private int[] mColors = {Color.RED,Color.GREEN,Color.BLUE,Color.YELLOW};从红色先渐变为绿色;再由绿色渐变为蓝色;最后再由蓝色渐变为黄色。

参数6:float [ ]position数组,与参数5中color数组的元素个数是相同的,为null时,各渐变过程在对角线(渐变线)上的跨度是均匀的。如红到绿占1/3,绿到蓝再占1/3,蓝色到黄色最后再占约1/3。总之是平分的。效果图1如下:

android渲染管线hwui 安卓图形渲染_android渲染管线hwui_04

                    图1

 

position参数不为null时,例如float[]position = {0,0.3f,0.8f,1}; 

红色渐变到绿色时,这2种颜色的跨度为对角线的30%。然后再另起一个绿色,渐变为蓝色,这2种颜色的跨度为对角线的30%到80%这一部分,也就是总跨度站了对角线的一半。最后蓝色渐变为黄色,这一段渐变效果在对角线上的跨度占了20%

注意这里position数组的元素个数应该和color数组的元素个数相同。具体效果图如下图2所示。

android渲染管线hwui 安卓图形渲染_数组_05


                    图2

参数7: Shader.TileMode,平铺模式,和BitmapShader一样,也分为3种模式且概念一样:

 Shader.TileMode.CLAMP

 Shader.TileMode.REPEAT

 Shader.TileMode.MIRROR

(2)canvas.drawRect(0, 0, 800, 800, mPaint); 

绘制一个矩形,对角线长度刚好和LinearGradient渐变的范围一样(对角线一样)。这时“线性渐变渲染 将 矩形图形加以渲染,效果如上图1(position为null)和图2所示(position不为null的情况)。

3. SweepGradient扫描式渐变渲染器

何为扫描式渐变渲染器,直观的给大家展示一下,如下图所示:

android渲染管线hwui 安卓图形渲染_android_06

                图3 扫描式渐变渲染器

实现这种各个颜色围绕圆心渐变的效果,代码如下:

package com.cb.paint_gradient;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ComposeShader;
import android.graphics.LinearGradient;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.RadialGradient;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.SweepGradient;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.OvalShape;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;

/**
 * Created by xw.gao
 */

public class MyShaderView extends View {
    private Paint mPaint;
    private Bitmap mBitMap = null;

    private int mWidth;
    private int mHeight;
    private int[] mColors = {Color.RED,Color.GREEN,Color.BLUE,Color.YELLOW};

    public MyShaderView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mBitMap = ((BitmapDrawable)getResources().getDrawable(R.drawable.head)).getBitmap();
        mPaint = new Paint();
        mWidth = mBitMap.getWidth();
        mHeight = mBitMap.getHeight();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(Color.WHITE);
        SweepGradient mSweepGradient = new SweepGradient(300, 300, mColors, null);
	mPaint.setShader(mSweepGradient);
	canvas.drawCircle(300, 300, 300, mPaint);
    
    }
}

还是看2处代码:

(1)SweepGradient mSweepGradient = new SweepGradient(300,300,color,null);

         mPaint.setShader(mSweepGradient);

     这里生成了一个“扫描式渐变渲染器”,参数解释如下:

     参数1和参数2是扫描圆心,

     参数3 int[]color数组和之前的一样,可以有多种颜色之间的渐变来平分这个圆。

     参数4:float[]position , 这个和之前一样是一个float[]position数组,为null则平分这个圆就如上图3所示,红-绿 占1/3; 绿-蓝 占1/3;  蓝-黄 占1/3.   如果不平分,假如设置float[]position = {0,0.3f,0.8f,1};,则效果会如下图4所示:红色-绿色这一对跨度为30%,绿色-蓝色这一对跨度为50%,最后蓝色-黄色这一对跨度为20%。

android渲染管线hwui 安卓图形渲染_数组_07

                                图4

 

注意:这次没有Shader.TileMode这个填充模式参数了。因为既然是圆周扫描,肯定要和图形(圆形)的大小一样。

(2)canvas.drawCircle(300, 300, 300, mPaint);,

  绘制一个图形:“圆”;然后用带有SweepGradient渲染器的paint去渲染这个图形。这个图形的圆心(第1、2参数)和SweepGradient的圆心一样。第3个参数是圆的半径,决定了这个四彩缤纷的圆的大小,第4个参数是画笔。

至此SweepGradient (扫描式渐变渲染器)就介绍到这里。

4. RadialGradient环形(从中心向外辐射)渐变渲染

何为环形渐变,就是颜色从圆心向外不断扩散,最终成为多个颜色渐变的圆环,如下图效果:

android渲染管线hwui 安卓图形渲染_数组_08

              图5

实现上述环形渐变RadialGradien的核心代码还是2处:

(1)RadialGradient mRadialGradient = new RadialGradient(300, 300, 100, mColors, null, Shader.TileMode.REPEAT); mPaint.setShader(mRadialGradient);

生成了一个环形渲染器,

参数1:圆心X坐标

参数2:圆心Y坐标

参数4:颜色数组:int[] mColors = {Color.RED,Color.GREEN,Color.BLUE,Color.YELLOW}

参数3:圆的半径100,就是经过Color.RED,Color.GREEN,Color.BLUE,Color.YELLOW这4种颜色的圆环总宽度。

参数5:position数组,和之前的一样。

参数6:平铺模式,选择repeat, 颜色会循环,从红-绿-蓝-黄,然后接着红-绿-蓝-黄...以此类推,直到贴满整个圆形

(2)canvas.drawCircle(300, 300, 300, mPaint);

这里绘制一个圆形,圆形的圆心和RadialGradient的圆心一样,圆形的半径是300,由于RadialGradient环形渲染的从是100,这说明将有3组【Color.RED,Color.GREEN,Color.BLUE,Color.YELLOW】渐变,贴满这个圆形,效果如上图5所示。我们如果把圆形的半径改为100,同环形渐变RadialGradient的半径一样,那么只需1组【Color.RED,Color.GREEN,Color.BLUE,Color.YELLOW】就可以把圆形贴满,效果如下图6:

 

android渲染管线hwui 安卓图形渲染_android_09

                  图6

 

(3)改变平铺模式看一下效果:

---平铺模式改为:Shader.TileMode.CLAMP

RadialGradient mRadialGradient = new RadialGradient(300, 300, 100, mColors, null, Shader.TileMode.CLAMP);
mPaint.setShader(mRadialGradient);
canvas.drawCircle(300, 300, 300, mPaint);

运行效果如下:

android渲染管线hwui 安卓图形渲染_android渲染管线hwui_10

我们发现当半径为100的RadialGradient不能贴满半径为300的圆形时,它会把RadialGradient的最后一个像素(黄色)拉伸至半径300. 模式Shader.TileMode.CLAMP就是这样,只拉伸最后一个像素,这点也适用于所有渲染器.,不只是RadialGradient。

 

---平铺模式改为:Shader.TileMode.MIRROR镜像模式

RadialGradient mRadialGradient = new RadialGradient(300, 300, 100, mColors, null, Shader.TileMode.MIRROR);
mPaint.setShader(mRadialGradient);
canvas.drawCircle(300, 300, 300, mPaint);

效果如下:

android渲染管线hwui 安卓图形渲染_渲染器_11

我们发现 【Color.RED,Color.GREEN,Color.BLUE,Color.YELLOW】总共3组,相邻组互为镜像,从圆心向外扩散,3组顺序依次为:

【Color.RED,Color.GREEN,Color.BLUE,Color.YELLOW】,【Color.YELLOW,Color.BLUE,Color.GREEN,Color.RED】,

【Color.RED,Color.GREEN,Color.BLUE,Color.YELLOW】。

 

5. 组合渲染ComposeShader

ComposeShader就是把前面介绍的各渲染器组合起来,作用于同一图形,例如下面是把图片渲染器bitmapShader和线性渐变渲染器linearGradient组合起来,作用在同一矩形里,最终的效果是,“心”图片贴在矩形图形里,同时被linearGradient渐变渲染器着色为了绿色。

“心”图片为:

android渲染管线hwui 安卓图形渲染_android_12

最终组合bitmapShader和bitmapShader,使矩形变为了如下图所示:

android渲染管线hwui 安卓图形渲染_渲染器_13

上述使用组合渲染ComposeShader的代码如下:

//创建BitmapShader,用以绘制心
mBitMap = ((BitmapDrawable)getResources().getDrawable(R.drawable.heart)).getBitmap();
BitmapShader bitmapShader = new BitmapShader(mBitMap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
//创建LinearGradient,用以产生从左上角到右下角的颜色渐变效果
LinearGradient linearGradient = new LinearGradient(0, 0, mWidth, mHeight, Color.GREEN, Color.BLUE, Shader.TileMode.CLAMP);
//bitmapShader对应目标像素,linearGradient对应源像素,像素颜色混合采用MULTIPLY模式
ComposeShader composeShader = new ComposeShader(bitmapShader, linearGradient, PorterDuff.Mode.MULTIPLY);
//将组合的composeShader作为画笔paint绘图所使用的shader
mPaint.setShader(composeShader);
//用composeShader绘制矩形区域
canvas.drawRect(0, 0, mWidth, mHeight, mPaint);

我们主要看一处核心代码,因为其它前面已经介绍过了。

ComposeShader composeShader = new ComposeShader(bitmapShader, linearGradient, PorterDuff.Mode.MULTIPLY);

第一个参数是 图片渲染器;第二个参数是线性渲染器,将要组合的就是这2个渲染器,共同作用于图形上。

第三个参数PorterDuff.Mode.MULTIPL:取两图层交集部分叠加后颜色。关于这个PorterDuffMode我们会在以后研究。

OK,至此大家对渲染器已经有所了解,下一篇我们再详细介绍渲染器的使用实例。

源码下载地址:

二、实例开发

1. 雷达扫描

效果如下:

android渲染管线hwui 安卓图形渲染_android_14

1.1 MainActivity.java代码:

package com.xwgao.radarview;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;



public class MainActivity extends AppCompatActivity {

    private RadarView mRadarView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mRadarView = (RadarView) findViewById(R.id.radarview);
    }

    public void start(View view){
        mRadarView.startScan();
    }

    public void stop(View view){
        mRadarView.stopScan();
    }
}

其中R.id.radarview就是我们将要自定义的雷达扫描控件。start(View view)和stop(View view)是开始雷达扫描和停止扫描两个按钮。具体布局如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

    <com.xwgao.radarview.RadarView
        android:id="@+id/radarview"
        android:layout_width="300dp"
        android:layout_height="300dp"
        android:layout_centerInParent="true"
        app:backgroundColor="#000000"
        app:circleNum="4"
        app:endColor="#aaff0000"
        app:lineColor="#00ff00"
        app:startColor="#aa0000ff"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentBottom="true"
        android:onClick="start"
        android:text="开始" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_alignParentBottom="true"
        android:onClick="stop"
        android:text="停止" />

</RelativeLayout>

接下来我们就看一下自定义雷达控件:RadarView如何实现。

1.2 RadarView自定义控件

要实现雷达RadarView自定义控件,我们都需要做什么工作? (1)雷达扫描渐变,使用SweepGradient来实现。 (2)旋转扫描:不断(用handler实现一个循环的定时器,每隔20ms)让Matrix矩阵来改变 “扫描式渐变”的角度。接下来我们贴出代码来分析。RadarView的代码如下:

package com.xwgao.radarview;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Shader;
import android.graphics.SweepGradient;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

public class RadarView extends View {
    private final String TAG = "RadarView";

    private static final int MSG_WHAT = 1;

    private static final int DELAY_TIME = 20;

    //设置默认宽高,雷达一般都是圆形,所以我们下面取宽高会去Math.min(宽,高)
    private final int DEFAULT_WIDTH = 200;

    private final int DEFAULT_HEIGHT = 200;
    //雷达的半径
    private int mRadarRadius;
    //雷达画笔
    private Paint mRadarPaint;
    //雷达底色画笔
    private Paint mRadarBg;
    //雷达圆圈的个数,默认4个
    private int mCircleNum = 4;
    //雷达线条的颜色,默认为白色
    private int mCircleColor = Color.WHITE;
    //雷达圆圈背景色
    private int mRadarBgColor = Color.BLACK;
    //paintShader
    private Shader mRadarShader;

    //雷达扫描时候的起始和终止颜色
    private int mStartColor = 0x0000ff00;

    private int mEndColor = 0xaa00ff00;


    private Matrix mMatrix;

    //旋转的角度
    private int mRotate = 0;

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);

            mRotate += 3;
            postInvalidate();

            mMatrix.reset();
            mMatrix.preRotate(mRotate, 0, 0);
            mHandler.sendEmptyMessageDelayed(MSG_WHAT, DELAY_TIME);
        }
    };

    public RadarView(Context context) {
        this(context, null);
    }

    public RadarView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public RadarView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);


        mRadarBg = new Paint(Paint.ANTI_ALIAS_FLAG);     //设置抗锯齿
        mRadarBg.setColor(mRadarBgColor);                  //画笔颜色
        mRadarBg.setStyle(Paint.Style.FILL);           //画实心圆

        mRadarPaint = new Paint(Paint.ANTI_ALIAS_FLAG);     //设置抗锯齿
        mRadarPaint.setColor(mCircleColor);                  //画笔颜色
        mRadarPaint.setStyle(Paint.Style.STROKE);           //设置空心的画笔,只画圆边
        mRadarPaint.setStrokeWidth(2);                      //画笔宽度

        mRadarShader = new SweepGradient(0, 0, mStartColor, mEndColor);

        mMatrix = new Matrix();
    }


    //初始化,拓展可设置参数供布局使用
    private void init(Context context, AttributeSet attrs) {
        if (attrs != null) {
            TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.RadarView);
            mStartColor = ta.getColor(R.styleable.RadarView_startColor, mStartColor);
            mEndColor = ta.getColor(R.styleable.RadarView_endColor, mEndColor);
            mRadarBgColor = ta.getColor(R.styleable.RadarView_backgroundColor, mRadarBgColor);
            mCircleColor = ta.getColor(R.styleable.RadarView_lineColor, mCircleColor);
            mCircleNum = ta.getInteger(R.styleable.RadarView_circleNum, mCircleNum);
            ta.recycle();
        }
    }


    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mRadarRadius = Math.min(w / 2, h / 2);

        //Log.d(TAG, "onSizeChanged");
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int width = measureSize(1, DEFAULT_WIDTH, widthMeasureSpec);
        int height = measureSize(0, DEFAULT_HEIGHT, heightMeasureSpec);
        Log.i(TAG,"width="+width+",height="+height);

        //取最大的 宽|高
        int measureSize = Math.max(width, height);
        setMeasuredDimension(measureSize, measureSize);
    }



    /**
     * 测绘measure
     *
     * @param specType    1为宽, 其他为高
     * @param contentSize 默认值
     */
    private int measureSize(int specType, int contentSize, int measureSpec) {
        int result = 0;
        //获取测量的模式和Size
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        if (specMode == MeasureSpec.EXACTLY) {
            result = Math.max(contentSize, specSize);
        } else if (specMode == MeasureSpec.AT_MOST){
            result = contentSize;

            if (specType == 1) {
                // 根据传人方式计算宽
                result += (getPaddingLeft() + getPaddingRight());
            } else {
                // 根据传人方式计算高
                result += (getPaddingTop() + getPaddingBottom());
            }
        }

        return result;

    }


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

        Log.d(TAG, "onDraw   " + mRotate);

        mRadarBg.setShader(null);

        //将画板移动到屏幕的中心点
        canvas.translate(mRadarRadius, mRadarRadius);
        //绘制底色,让雷达的线看起来更清晰
        canvas.drawCircle(0, 0, mRadarRadius, mRadarBg);
        //画圆圈
        for (int i = 1; i <= mCircleNum; i++) {
            canvas.drawCircle(0, 0, (float) (i * 1.0 / mCircleNum * mRadarRadius), mRadarPaint);
        }
        //绘制雷达基线 x轴
        canvas.drawLine(-mRadarRadius, 0, mRadarRadius, 0, mRadarPaint);
        //绘制雷达基线 y轴
        canvas.drawLine(0, mRadarRadius, 0, -mRadarRadius, mRadarPaint);

//        canvas.rotate(mRotate,0,0);
        //设置颜色渐变从透明到不透明
        mRadarBg.setShader(mRadarShader);
        canvas.concat(mMatrix);
        canvas.drawCircle(0, 0, mRadarRadius, mRadarBg);
    }


    public void startScan() {
        mHandler.removeMessages(MSG_WHAT);
        mHandler.sendEmptyMessage(MSG_WHAT);
    }

    public void stopScan() {
        mHandler.removeMessages(MSG_WHAT);
    }
}

 

我们主要看一下onDraw函数,

(1)canvas.translate(mRadarRadius, mRadarRadius);这个将画布水平和垂直方向各移动 “半径长度”mRadarRadius。

这样的话canvas的原点(0,0)就是雷达圆的圆心了。在绘制雷达圆圈的时候以(0,0)为圆心绘制出的圆自然也不会超出canvas屏幕。

(2)canvas.drawCircle(0, 0, mRadarRadius, mRadarBg);绘制雷达圆圈的背景,默认是黑色,将来在这个黑色背景上绘制“扫描式渐变”,

白色圆圈,雷达“十字”线等。

(3)//绘制白色圆圈
for (int i = 1; i <= mCircleNum; i++) {
    canvas.drawCircle(0, 0, (float) (i * 1.0 / mCircleNum * mRadarRadius), mRadarPaint);
}

(4)  绘制雷达 水平+垂直方向的那个“十字线”

//绘制雷达基线 x轴
canvas.drawLine(-mRadarRadius, 0, mRadarRadius, 0, mRadarPaint);
//绘制雷达基线 y轴
canvas.drawLine(0, mRadarRadius, 0, -mRadarRadius, mRadarPaint);

(5)绘制“扫描式渐变”

mRadarBg.setShader(mRadarShader);
canvas.concat(mMatrix);
canvas.drawCircle(0, 0, mRadarRadius, mRadarBg);

其中mRadarShader就是“扫描式渲染器",它的颜色渐变是从红色渐变到蓝色:

mRadarShader = new SweepGradient(0, 0, mStartColor, mEndColor);

mMatrix是用来改变”扫描“角度的。canvas.concat(mMatrix);通过这行代码,让mMatrix和canvas画布关联起来,我们定于了一个handler不断的发送msg,来让mMatrix不断改变角度,从而旋转了画布canvas,形成”扫描效果“。定于的hander处理消息的代码如下:

private Handler mHandler = new Handler() {
         @Override
         public void handleMessage(Message msg) {
             super.handleMessage(msg);            mRotate += 3;
             postInvalidate();            mMatrix.reset();
             mMatrix.preRotate(mRotate, 0, 0);
             mHandler.sendEmptyMessageDelayed(MSG_WHAT, DELAY_TIME);
         }
     };

在这里,我们每次改变角度mRotate后,让mMatrix.reset(),让画布先恢复回去,然后再重新源码canvas到最新的角度:            mMatrix.preRotate(mRotate, 0, 0);。当然从恢复到再次重新旋转之间的空隙时间,用户眼睛是感觉不到的,并且是每隔20ms改变一次角度(+3),所以最终的效果还是连续的。如果不恢复回去的话,你可以试试看,它会以当前角度位置为起点,跳跃式的增量 value (mRotate += 3)度,会是跳跃的感觉。

Ok,至此雷达扫描效果就先介绍到这里。

第(二)节,雷达扫描源码地址:

第(一)节源码下载地址:

2. 辐射渐变:RadiaGradient

现在我们用RadiaGradient实现以下效果的按钮,当按下按钮时,会在按钮上绘制一个辐射渐变圆,类似于第(一)节中的“4. RadialGradient环形(从中心向外辐射)渐变渲染”。

android渲染管线hwui 安卓图形渲染_渲染器_15

 

2.1 原理:

当触摸按钮时,会执行onTouch事件,在这里面根据触摸的x,y值,来改变“辐射渐变圆”的圆心位置。当执行UP抬起手指事件时,执行一个动画,让圆的半径从DEFAULT_RADIUS逐渐扩展到整个按钮的宽度,类似于android Material Design里的button水波纹效果。

2.2代码如下:

package com.xiaowei.paint_radialgradient;
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RadialGradient;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.animation.AccelerateInterpolator;
import android.widget.Button;

public class RippleView extends Button {
    // 点击位置
    private int mX, mY;

    private ObjectAnimator mAnimator;
    // 默认半径
    private int DEFAULT_RADIUS = 100;

    private int mCurRadius = 0;

    private RadialGradient mRadialGradient;

    private Paint mPaint;

    public RippleView(Context context) {
        super(context);
        init();
    }

    public RippleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        // 禁用硬件加速
        setLayerType(LAYER_TYPE_SOFTWARE,null);
        mPaint = new Paint();
    }
    
    @Override
    public boolean onTouchEvent(MotionEvent event) {

        if (mX != event.getX() || mY != mY) {
            mX = (int) event.getX();
            mY = (int) event.getY();

            setRadius(DEFAULT_RADIUS);
        }
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                return true;

            case MotionEvent.ACTION_UP:
            {
                if (mAnimator != null && mAnimator.isRunning()) {
                    mAnimator.cancel();
                }

                if (mAnimator == null) {
                    mAnimator = ObjectAnimator.ofInt(this,"radius",DEFAULT_RADIUS, getWidth());
                }

                mAnimator.setInterpolator(new AccelerateInterpolator());
                mAnimator.addListener(new Animator.AnimatorListener() {
                    @Override
                    public void onAnimationStart(Animator animation) {

                    }

                    @Override
                    public void onAnimationEnd(Animator animation) {
                        setRadius(0);
                    }

                    @Override
                    public void onAnimationCancel(Animator animation) {

                    }

                    @Override
                    public void onAnimationRepeat(Animator animation) {

                    }
                });
                mAnimator.start();
            }
        }

        return super.onTouchEvent(event);
    }

    public void setRadius(final int radius) {
        mCurRadius = radius;
        if (mCurRadius > 0) {
            mRadialGradient = new RadialGradient(mX, mY, mCurRadius, 0x00FFFFFF, 0xFF58FAAC, Shader.TileMode.CLAMP);
            mPaint.setShader(mRadialGradient);
        }
        postInvalidate();
    }


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

        canvas.drawCircle(mX, mY, mCurRadius, mPaint);
    }
}

代码分析:

首先我们看到onDraw里绘制了一个圆。然后我们看看mPaint是如何配置的。在onTouchEnvent的setRadius里为mPaint配置了一个“辐射性渐变”,圆心刚好是onTouchEvent触摸的x,y,  我们把setRadius代码摘取出来:

public void setRadius(final int radius) {
        mCurRadius = radius;
        if (mCurRadius > 0) {
            mRadialGradient = new RadialGradient(mX, mY, mCurRadius, 0x00FFFFFF, 0xFF58FAAC, Shader.TileMode.CLAMP);
            mPaint.setShader(mRadialGradient);
        }
        postInvalidate();
    }

mRadialGradient 是一个辐射渐变实例,圆心是mx,mY,即触摸点,颜色从0x00FFFFFF渐变到0xFF58FAAC,循环这个渐变直到延伸到整个半径,铺满这个圆,因为TileMode设置的是CLAMP.  最终调用postInvalidate触发onDraw的执行。也就是说我们每触摸一次按钮,就会重新配置一次mPaint,因为辐射渐变的圆心位置变了,进而会重新绘制一次。

 

在 case MotionEvent.ACTION_UP事件里,我们就会执行水波纹扩散动画:

if (mAnimator == null) {
                    mAnimator = ObjectAnimator.ofInt(this,"radius",DEFAULT_RADIUS, getWidth());
                }

这个动画使 “辐射渐变”的半径从 DEFAULT_RADIUS逐渐扩展到按钮的宽度getWidth().  

这里注意一下,"radius"这个属性值,是我们为RippleView这个自定义按钮自定义的属性。必须得有setRadius方法,否则属性“radius”不能识别,即不能被系统看做是RippleView的一个属性。

Ok,剩下的代码相信大家都能理解,“辐射圆渐变”实例就先讲解到这里。

“辐射圆渐变”源码:

3. BitmapShader实例

3.1 放大镜效果,如下git所示:

android渲染管线hwui 安卓图形渲染_android渲染管线hwui_16

放大镜 自定义控件ZoomView代码如下:

package com.cb.paint_gradient;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Shader;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.OvalShape;
import android.view.MotionEvent;
import android.view.View;

/**
 * 
 */

public class ZoomImageView extends View {

    //放大倍数
    private static final int FACTOR = 2;
    //放大镜的半径
    private static final int RADIUS  = 100;
    // 原图
    private Bitmap mBitmap;
    // 放大后的图
    private Bitmap mBitmapScale;
    // 制作的圆形的图片(放大的局部),盖在Canvas上面
    private ShapeDrawable mShapeDrawable;

    private Matrix mMatrix;

    public ZoomImageView(Context context) {
        super(context);

        mBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.hlw);
        mBitmapScale = mBitmap;
        //放大后的整个图片
        mBitmapScale = Bitmap.createScaledBitmap(mBitmapScale,mBitmapScale.getWidth() * FACTOR,
                mBitmapScale.getHeight() * FACTOR,true);
        BitmapShader bitmapShader = new BitmapShader(mBitmapScale, Shader.TileMode.CLAMP,
                Shader.TileMode.CLAMP);

        mShapeDrawable = new ShapeDrawable(new OvalShape());
        mShapeDrawable.getPaint().setShader(bitmapShader);
        // 切出矩形区域,用来画圆(内切圆)
        mShapeDrawable.setBounds(0,0,RADIUS * 2,RADIUS * 2);

        mMatrix = new Matrix();
    }

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

        // 1、画原图
        canvas.drawBitmap(mBitmap, 0 , 0 , null);

        // 2、画放大镜的图
        mShapeDrawable.draw(canvas);
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();

        // 将放大的图片往相反的方向挪动
        mMatrix.setTranslate(RADIUS - x * FACTOR, RADIUS - y *FACTOR);
        mShapeDrawable.getPaint().getShader().setLocalMatrix(mMatrix);
        // 切出手势区域点位置的圆
        mShapeDrawable.setBounds(x-RADIUS,y - RADIUS, x + RADIUS, y + RADIUS);
        invalidate();
        return true;
    }
}

然后,MainActivity.java里加载这个ZoomView控件,并显示,代码如下:

package com.cb.paint_gradient;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //setContentView(R.layout.activity_main);
        ZoomImageView view = new ZoomImageView(this);
        setContentView(view);
    }
}

3.2 文字跑马灯效果

android渲染管线hwui 安卓图形渲染_android_17

自定义View代码如下:

package com.cb.paint_gradient;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.LinearGradient;
import android.graphics.Matrix;
import android.graphics.Shader;
import android.support.annotation.Nullable;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.widget.TextView;

public class LinearGradientTextView extends TextView{
    private TextPaint mPaint;
    private LinearGradient mLinearGradient ;
    private Matrix mMatrix;
    private float mTranslate;
    private float DELTAX = 20;
    public LinearGradientTextView(Context context) {
        super(context);
    }
    public LinearGradientTextView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        // 拿到TextView的画笔
        mPaint = getPaint();
        String text = getText().toString();
        float textWith = mPaint.measureText(text);
        // 3个文字的宽度
        int gradientSize = (int) (textWith / text.length() * 3);

        // 从左边-gradientSize开始,即左边距离文字gradientSize开始渐变
        mLinearGradient = new LinearGradient(-gradientSize,0,0,0,new int[]{
                0x22ffffff, 0xffffffff, 0x22ffffff},null, Shader.TileMode.CLAMP
        );
        mPaint.setShader(mLinearGradient);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mTranslate += DELTAX;
        float textWidth = getPaint().measureText(getText().toString());
        if(mTranslate > textWidth + 1 || mTranslate < 1){
            DELTAX = - DELTAX;
        }

        mMatrix = new Matrix();
        mMatrix.setTranslate(mTranslate, 0);
        mLinearGradient.setLocalMatrix(mMatrix);
        postInvalidateDelayed(50);

    }
}

MainActivity.java:

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#000"
    >
    <com.cb.paint_gradient.LinearGradientTextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="30sp"
        android:textColor="#666666"
        android:text="武汉加油,中国加油"/>-

</android.support.constraint.ConstraintLayout>