Shader是什么,Canvas可以绘制图形(圆形、弧形、矩形等),Shader是为这些图形着色的,改变这些图形外观的,例如在一个圆形上将图片贴在圆形上,就可以实现圆形头像控件,在这里BitmapShader改变了圆形这个图形的外观,将图片内容附着到了图形上面。Shader不只有BitmapShader,它总共包括如下Shader:BitmapShader、LinearGradient、SweepGradient、RadialGradient、ComposeShader。接下来我们会详细讲解这几个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。所以显示效果就如下图所示:
把上述canvas.drawCircle这行代码改为如下代码,可以将图形设置为矩形,然后把图片贴在矩形里。
canvas.drawRect(new Rect(0,0 , 1000, 1600),mPaint);
那么这时的效果图如下:
再把上述canvas.drawRect代码换成如下代码,则是绘制一个椭圆,然后图片贴在椭圆里。
canvas.drawOval(new RectF(0 , 0, mWidth, mHeight),mPaint);
这里的mWidth和mHeight是图片的宽和高。也就是说这个椭圆的外切矩形的宽高就是图片的宽高。这时图片贴上去,一张图片就可以铺满这个椭圆。效果如下图(由于我们这张图片的宽和高比较相近,所以呈现出来的就是一个近乎圆形的头像):
至此,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如下:
图1
position参数不为null时,例如float[]position = {0,0.3f,0.8f,1};
红色渐变到绿色时,这2种颜色的跨度为对角线的30%。然后再另起一个绿色,渐变为蓝色,这2种颜色的跨度为对角线的30%到80%这一部分,也就是总跨度站了对角线的一半。最后蓝色渐变为黄色,这一段渐变效果在对角线上的跨度占了20%
注意这里position数组的元素个数应该和color数组的元素个数相同。具体效果图如下图2所示。
图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扫描式渐变渲染器
何为扫描式渐变渲染器,直观的给大家展示一下,如下图所示:
图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%。
图4
注意:这次没有Shader.TileMode这个填充模式参数了。因为既然是圆周扫描,肯定要和图形(圆形)的大小一样。
(2)canvas.drawCircle(300, 300, 300, mPaint);,
绘制一个图形:“圆”;然后用带有SweepGradient渲染器的paint去渲染这个图形。这个图形的圆心(第1、2参数)和SweepGradient的圆心一样。第3个参数是圆的半径,决定了这个四彩缤纷的圆的大小,第4个参数是画笔。
至此SweepGradient (扫描式渐变渲染器)就介绍到这里。
4. RadialGradient环形(从中心向外辐射)渐变渲染
何为环形渐变,就是颜色从圆心向外不断扩散,最终成为多个颜色渐变的圆环,如下图效果:
图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:
图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);
运行效果如下:
我们发现当半径为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);
效果如下:
我们发现 【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渐变渲染器着色为了绿色。
“心”图片为:
最终组合bitmapShader和bitmapShader,使矩形变为了如下图所示:
上述使用组合渲染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. 雷达扫描
效果如下:
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环形(从中心向外辐射)渐变渲染”。
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所示:
放大镜 自定义控件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 文字跑马灯效果
自定义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>