Android高级UI开发(四十)shader渲染器介绍

举报
yd_57386892 发表于 2020/12/29 00:27:38 2020/12/29
【摘要】 Shader是什么,Canvas可以绘制图形(圆形、弧形、矩形等),Shader是为这些图形着色的,改变这些图形外观的,例如在一个圆形上将图片贴在圆形上,就可以实现圆形头像控件,在这里BitmapShader改变了圆形这个图形的外观,将图片内容附着到了图形上面。Shader不只有BitmapShader,它总共包括如下Shader:BitmapShader、LinearGra...

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

一、基础知识

1. BitmapShader

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


  
  1. package com.cb.paint_gradient;
  2. import android.content.Context;
  3. import android.graphics.Bitmap;
  4. import android.graphics.BitmapShader;
  5. import android.graphics.Canvas;
  6. import android.graphics.Color;
  7. import android.graphics.ComposeShader;
  8. import android.graphics.LinearGradient;
  9. import android.graphics.Matrix;
  10. import android.graphics.Paint;
  11. import android.graphics.RadialGradient;
  12. import android.graphics.Rect;
  13. import android.graphics.RectF;
  14. import android.graphics.Shader;
  15. import android.graphics.SweepGradient;
  16. import android.graphics.drawable.BitmapDrawable;
  17. import android.graphics.drawable.ShapeDrawable;
  18. import android.graphics.drawable.shapes.OvalShape;
  19. import android.support.annotation.Nullable;
  20. import android.util.AttributeSet;
  21. import android.view.View;
  22. /**
  23. * Created by xw.gao
  24. */
  25. public class MyShaderView extends View {
  26. private Paint mPaint;
  27. private Bitmap mBitMap = null;
  28. private int mWidth;
  29. private int mHeight;
  30. //gxw-private int[] mColors = {Color.RED,Color.GREEN,Color.BLUE,Color.YELLOW};
  31. public MyShaderView(Context context, @Nullable AttributeSet attrs) {
  32. super(context, attrs);
  33. mBitMap = ((BitmapDrawable)getResources().getDrawable(R.drawable.head)).getBitmap();
  34. mPaint = new Paint();
  35. mWidth = mBitMap.getWidth();
  36. mHeight = mBitMap.getHeight();
  37. }
  38. @Override
  39. protected void onDraw(Canvas canvas) {
  40. super.onDraw(canvas);
  41. canvas.drawColor(Color.WHITE);
  42. /**
  43. * TileMode.CLAMP 拉伸最后一个像素去铺满剩下的地方
  44. * TileMode.MIRROR 通过镜像翻转铺满剩下的地方。
  45. * TileMode.REPEAT 重复图片平铺整个画面(电脑设置壁纸)
  46. */
  47. BitmapShader bitMapShader = new BitmapShader(mBitMap, Shader.TileMode.MIRROR,
  48. Shader.TileMode.MIRROR);
  49. mPaint.setShader(bitMapShader);
  50. mPaint.setAntiAlias(true);
  51. canvas.drawCircle(500,900, 500 ,mPaint);
  52. }
  53. }

主要看以下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条直线上的颜色渐变。比如从一个矩形图形的左上角到右下角,颜色可以从绿色渐变为蓝色。我们实现这么一个例子:让一个矩形的颜色从左上角到右下角渐变。代码如下:


  
  1. package com.cb.paint_gradient;
  2. import android.content.Context;
  3. import android.graphics.Bitmap;
  4. import android.graphics.BitmapShader;
  5. import android.graphics.Canvas;
  6. import android.graphics.Color;
  7. import android.graphics.ComposeShader;
  8. import android.graphics.LinearGradient;
  9. import android.graphics.Matrix;
  10. import android.graphics.Paint;
  11. import android.graphics.RadialGradient;
  12. import android.graphics.Rect;
  13. import android.graphics.RectF;
  14. import android.graphics.Shader;
  15. import android.graphics.SweepGradient;
  16. import android.graphics.drawable.BitmapDrawable;
  17. import android.graphics.drawable.ShapeDrawable;
  18. import android.graphics.drawable.shapes.OvalShape;
  19. import android.support.annotation.Nullable;
  20. import android.util.AttributeSet;
  21. import android.view.View;
  22. /**
  23. * Created by xw.gao
  24. */
  25. public class MyShaderView extends View {
  26. private Paint mPaint;
  27. private Bitmap mBitMap = null;
  28. private int mWidth;
  29. private int mHeight;
  30. private int[] mColors = {Color.RED,Color.GREEN,Color.BLUE,Color.YELLOW};
  31. public MyShaderView(Context context, @Nullable AttributeSet attrs) {
  32. super(context, attrs);
  33. mBitMap = ((BitmapDrawable)getResources().getDrawable(R.drawable.head)).getBitmap();
  34. mPaint = new Paint();
  35. mWidth = mBitMap.getWidth();
  36. mHeight = mBitMap.getHeight();
  37. }
  38. @Override
  39. protected void onDraw(Canvas canvas) {
  40. super.onDraw(canvas);
  41. canvas.drawColor(Color.WHITE);
  42. /*gxw-s for BitmapShader
  43. * TileMode.CLAMP 拉伸最后一个像素去铺满剩下的地方
  44. * TileMode.MIRROR 通过镜像翻转铺满剩下的地方。
  45. * TileMode.REPEAT 重复图片平铺整个画面(电脑设置壁纸)
  46. BitmapShader bitMapShader = new BitmapShader(mBitMap, Shader.TileMode.MIRROR,Shader.TileMode.MIRROR);
  47. mPaint.setShader(bitMapShader);
  48. mPaint.setAntiAlias(true);
  49. //canvas.drawCircle(500,900, 500 ,mPaint);图形为圆形
  50. //canvas.drawRect(new Rect(0,0 , 1000, 1600),mPaint);图形为矩形
  51. //canvas.drawOval(new RectF(0 , 0, mWidth, mHeight),mPaint);图形为椭圆
  52. gxw-e for BitmapShader */
  53. /* 线性渐变
  54. * x0, y0, 起始点
  55. * x1, y1, 结束点
  56. * int[] mColors, 中间依次要出现的几个颜色
  57. * float[] positions,数组大小跟colors数组一样大,中间依次摆放的几个颜色分别放置在那个位置上(参考比例从左往右)
  58. * tile
  59. */
  60. LinearGradient linearGradient = new LinearGradient( 0, 0,800, 800, mColors, null, Shader.TileMode.CLAMP);
  61. // linearGradient = new LinearGradient(0, 0, 400, 400, mColors, null, Shader.TileMode.REPEAT);
  62. mPaint.setShader(linearGradient);
  63. canvas.drawRect(0, 0, 800, 800, mPaint);
  64. }
  65. }

还是看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 扫描式渐变渲染器

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


  
  1. package com.cb.paint_gradient;
  2. import android.content.Context;
  3. import android.graphics.Bitmap;
  4. import android.graphics.BitmapShader;
  5. import android.graphics.Canvas;
  6. import android.graphics.Color;
  7. import android.graphics.ComposeShader;
  8. import android.graphics.LinearGradient;
  9. import android.graphics.Matrix;
  10. import android.graphics.Paint;
  11. import android.graphics.RadialGradient;
  12. import android.graphics.Rect;
  13. import android.graphics.RectF;
  14. import android.graphics.Shader;
  15. import android.graphics.SweepGradient;
  16. import android.graphics.drawable.BitmapDrawable;
  17. import android.graphics.drawable.ShapeDrawable;
  18. import android.graphics.drawable.shapes.OvalShape;
  19. import android.support.annotation.Nullable;
  20. import android.util.AttributeSet;
  21. import android.view.View;
  22. /**
  23. * Created by xw.gao
  24. */
  25. public class MyShaderView extends View {
  26. private Paint mPaint;
  27. private Bitmap mBitMap = null;
  28. private int mWidth;
  29. private int mHeight;
  30. private int[] mColors = {Color.RED,Color.GREEN,Color.BLUE,Color.YELLOW};
  31. public MyShaderView(Context context, @Nullable AttributeSet attrs) {
  32. super(context, attrs);
  33. mBitMap = ((BitmapDrawable)getResources().getDrawable(R.drawable.head)).getBitmap();
  34. mPaint = new Paint();
  35. mWidth = mBitMap.getWidth();
  36. mHeight = mBitMap.getHeight();
  37. }
  38. @Override
  39. protected void onDraw(Canvas canvas) {
  40. super.onDraw(canvas);
  41. canvas.drawColor(Color.WHITE);
  42. SweepGradient mSweepGradient = new SweepGradient(300, 300, mColors, null);
  43. mPaint.setShader(mSweepGradient);
  44. canvas.drawCircle(300, 300, 300, mPaint);
  45. }
  46. }

还是看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


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

运行效果如下:

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

 

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


  
  1. RadialGradient mRadialGradient = new RadialGradient(300, 300, 100, mColors, null, Shader.TileMode.MIRROR);
  2. mPaint.setShader(mRadialGradient);
  3. 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的代码如下:


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

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

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

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

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

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

源码下载地址:https://download.csdn.net/download/gaoxiaoweiandy/12131538

二、实例开发

1. 雷达扫描

效果如下:

1.1 MainActivity.java代码:


  
  1. package com.xwgao.radarview;
  2. import android.os.Bundle;
  3. import android.support.v7.app.AppCompatActivity;
  4. import android.view.View;
  5. public class MainActivity extends AppCompatActivity {
  6. private RadarView mRadarView;
  7. @Override
  8. protected void onCreate(Bundle savedInstanceState) {
  9. super.onCreate(savedInstanceState);
  10. setContentView(R.layout.activity_main);
  11. mRadarView = (RadarView) findViewById(R.id.radarview);
  12. }
  13. public void start(View view){
  14. mRadarView.startScan();
  15. }
  16. public void stop(View view){
  17. mRadarView.stopScan();
  18. }
  19. }

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


  
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. xmlns:app="http://schemas.android.com/apk/res-auto"
  4. android:layout_width="match_parent"
  5. android:layout_height="match_parent"
  6. >
  7. <com.xwgao.radarview.RadarView
  8. android:id="@+id/radarview"
  9. android:layout_width="300dp"
  10. android:layout_height="300dp"
  11. android:layout_centerInParent="true"
  12. app:backgroundColor="#000000"
  13. app:circleNum="4"
  14. app:endColor="#aaff0000"
  15. app:lineColor="#00ff00"
  16. app:startColor="#aa0000ff"/>
  17. <Button
  18. android:layout_width="wrap_content"
  19. android:layout_height="wrap_content"
  20. android:layout_alignParentLeft="true"
  21. android:layout_alignParentBottom="true"
  22. android:onClick="start"
  23. android:text="开始" />
  24. <Button
  25. android:layout_width="wrap_content"
  26. android:layout_height="wrap_content"
  27. android:layout_alignParentRight="true"
  28. android:layout_alignParentBottom="true"
  29. android:onClick="stop"
  30. android:text="停止" />
  31. </RelativeLayout>

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

1.2 RadarView自定义控件

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


  
  1. package com.xwgao.radarview;
  2. import android.content.Context;
  3. import android.content.res.TypedArray;
  4. import android.graphics.Canvas;
  5. import android.graphics.Color;
  6. import android.graphics.Matrix;
  7. import android.graphics.Paint;
  8. import android.graphics.Shader;
  9. import android.graphics.SweepGradient;
  10. import android.os.Handler;
  11. import android.os.Message;
  12. import android.util.AttributeSet;
  13. import android.util.Log;
  14. import android.view.View;
  15. public class RadarView extends View {
  16. private final String TAG = "RadarView";
  17. private static final int MSG_WHAT = 1;
  18. private static final int DELAY_TIME = 20;
  19. //设置默认宽高,雷达一般都是圆形,所以我们下面取宽高会去Math.min(宽,高)
  20. private final int DEFAULT_WIDTH = 200;
  21. private final int DEFAULT_HEIGHT = 200;
  22. //雷达的半径
  23. private int mRadarRadius;
  24. //雷达画笔
  25. private Paint mRadarPaint;
  26. //雷达底色画笔
  27. private Paint mRadarBg;
  28. //雷达圆圈的个数,默认4个
  29. private int mCircleNum = 4;
  30. //雷达线条的颜色,默认为白色
  31. private int mCircleColor = Color.WHITE;
  32. //雷达圆圈背景色
  33. private int mRadarBgColor = Color.BLACK;
  34. //paintShader
  35. private Shader mRadarShader;
  36. //雷达扫描时候的起始和终止颜色
  37. private int mStartColor = 0x0000ff00;
  38. private int mEndColor = 0xaa00ff00;
  39. private Matrix mMatrix;
  40. //旋转的角度
  41. private int mRotate = 0;
  42. private Handler mHandler = new Handler() {
  43. @Override
  44. public void handleMessage(Message msg) {
  45. super.handleMessage(msg);
  46. mRotate += 3;
  47. postInvalidate();
  48. mMatrix.reset();
  49. mMatrix.preRotate(mRotate, 0, 0);
  50. mHandler.sendEmptyMessageDelayed(MSG_WHAT, DELAY_TIME);
  51. }
  52. };
  53. public RadarView(Context context) {
  54. this(context, null);
  55. }
  56. public RadarView(Context context, AttributeSet attrs) {
  57. this(context, attrs, 0);
  58. }
  59. public RadarView(Context context, AttributeSet attrs, int defStyleAttr) {
  60. super(context, attrs, defStyleAttr);
  61. init(context, attrs);
  62. mRadarBg = new Paint(Paint.ANTI_ALIAS_FLAG); //设置抗锯齿
  63. mRadarBg.setColor(mRadarBgColor); //画笔颜色
  64. mRadarBg.setStyle(Paint.Style.FILL); //画实心圆
  65. mRadarPaint = new Paint(Paint.ANTI_ALIAS_FLAG); //设置抗锯齿
  66. mRadarPaint.setColor(mCircleColor); //画笔颜色
  67. mRadarPaint.setStyle(Paint.Style.STROKE); //设置空心的画笔,只画圆边
  68. mRadarPaint.setStrokeWidth(2); //画笔宽度
  69. mRadarShader = new SweepGradient(0, 0, mStartColor, mEndColor);
  70. mMatrix = new Matrix();
  71. }
  72. //初始化,拓展可设置参数供布局使用
  73. private void init(Context context, AttributeSet attrs) {
  74. if (attrs != null) {
  75. TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.RadarView);
  76. mStartColor = ta.getColor(R.styleable.RadarView_startColor, mStartColor);
  77. mEndColor = ta.getColor(R.styleable.RadarView_endColor, mEndColor);
  78. mRadarBgColor = ta.getColor(R.styleable.RadarView_backgroundColor, mRadarBgColor);
  79. mCircleColor = ta.getColor(R.styleable.RadarView_lineColor, mCircleColor);
  80. mCircleNum = ta.getInteger(R.styleable.RadarView_circleNum, mCircleNum);
  81. ta.recycle();
  82. }
  83. }
  84. @Override
  85. protected void onSizeChanged(int w, int h, int oldw, int oldh) {
  86. super.onSizeChanged(w, h, oldw, oldh);
  87. mRadarRadius = Math.min(w / 2, h / 2);
  88. //Log.d(TAG, "onSizeChanged");
  89. }
  90. @Override
  91. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  92. super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  93. int width = measureSize(1, DEFAULT_WIDTH, widthMeasureSpec);
  94. int height = measureSize(0, DEFAULT_HEIGHT, heightMeasureSpec);
  95. Log.i(TAG,"width="+width+",height="+height);
  96. //取最大的 宽|高
  97. int measureSize = Math.max(width, height);
  98. setMeasuredDimension(measureSize, measureSize);
  99. }
  100. /**
  101. * 测绘measure
  102. *
  103. * @param specType 1为宽, 其他为高
  104. * @param contentSize 默认值
  105. */
  106. private int measureSize(int specType, int contentSize, int measureSpec) {
  107. int result = 0;
  108. //获取测量的模式和Size
  109. int specMode = MeasureSpec.getMode(measureSpec);
  110. int specSize = MeasureSpec.getSize(measureSpec);
  111. if (specMode == MeasureSpec.EXACTLY) {
  112. result = Math.max(contentSize, specSize);
  113. } else if (specMode == MeasureSpec.AT_MOST){
  114. result = contentSize;
  115. if (specType == 1) {
  116. // 根据传人方式计算宽
  117. result += (getPaddingLeft() + getPaddingRight());
  118. } else {
  119. // 根据传人方式计算高
  120. result += (getPaddingTop() + getPaddingBottom());
  121. }
  122. }
  123. return result;
  124. }
  125. @Override
  126. protected void onDraw(Canvas canvas) {
  127. super.onDraw(canvas);
  128. Log.d(TAG, "onDraw " + mRotate);
  129. mRadarBg.setShader(null);
  130. //将画板移动到屏幕的中心点
  131. canvas.translate(mRadarRadius, mRadarRadius);
  132. //绘制底色,让雷达的线看起来更清晰
  133. canvas.drawCircle(0, 0, mRadarRadius, mRadarBg);
  134. //画圆圈
  135. for (int i = 1; i <= mCircleNum; i++) {
  136. canvas.drawCircle(0, 0, (float) (i * 1.0 / mCircleNum * mRadarRadius), mRadarPaint);
  137. }
  138. //绘制雷达基线 x轴
  139. canvas.drawLine(-mRadarRadius, 0, mRadarRadius, 0, mRadarPaint);
  140. //绘制雷达基线 y轴
  141. canvas.drawLine(0, mRadarRadius, 0, -mRadarRadius, mRadarPaint);
  142. // canvas.rotate(mRotate,0,0);
  143. //设置颜色渐变从透明到不透明
  144. mRadarBg.setShader(mRadarShader);
  145. canvas.concat(mMatrix);
  146. canvas.drawCircle(0, 0, mRadarRadius, mRadarBg);
  147. }
  148. public void startScan() {
  149. mHandler.removeMessages(MSG_WHAT);
  150. mHandler.sendEmptyMessage(MSG_WHAT);
  151. }
  152. public void stopScan() {
  153. mHandler.removeMessages(MSG_WHAT);
  154. }
  155. }

 


  
  1. 我们主要看一下onDraw函数,
  2. (1)canvas.translate(mRadarRadius, mRadarRadius);这个将画布水平和垂直方向各移动 “半径长度”mRadarRadius。

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

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

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


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

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


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

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


  
  1. mRadarBg.setShader(mRadarShader);
  2. canvas.concat(mMatrix);
  3. 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,至此雷达扫描效果就先介绍到这里。

第(二)节,雷达扫描源码地址:https://download.csdn.net/download/gaoxiaoweiandy/12172599

第(一)节源码下载地址:https://download.csdn.net/download/gaoxiaoweiandy/12131538

2. 辐射渐变:RadiaGradient

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

 

2.1 原理:

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

2.2代码如下:


  
  1. package com.xiaowei.paint_radialgradient;
  2. import android.animation.Animator;
  3. import android.animation.ObjectAnimator;
  4. import android.content.Context;
  5. import android.graphics.Canvas;
  6. import android.graphics.Paint;
  7. import android.graphics.RadialGradient;
  8. import android.graphics.Shader;
  9. import android.util.AttributeSet;
  10. import android.view.MotionEvent;
  11. import android.view.animation.AccelerateInterpolator;
  12. import android.widget.Button;
  13. public class RippleView extends Button {
  14. // 点击位置
  15. private int mX, mY;
  16. private ObjectAnimator mAnimator;
  17. // 默认半径
  18. private int DEFAULT_RADIUS = 100;
  19. private int mCurRadius = 0;
  20. private RadialGradient mRadialGradient;
  21. private Paint mPaint;
  22. public RippleView(Context context) {
  23. super(context);
  24. init();
  25. }
  26. public RippleView(Context context, AttributeSet attrs) {
  27. super(context, attrs);
  28. init();
  29. }
  30. private void init() {
  31. // 禁用硬件加速
  32. setLayerType(LAYER_TYPE_SOFTWARE,null);
  33. mPaint = new Paint();
  34. }
  35. @Override
  36. public boolean onTouchEvent(MotionEvent event) {
  37. if (mX != event.getX() || mY != mY) {
  38. mX = (int) event.getX();
  39. mY = (int) event.getY();
  40. setRadius(DEFAULT_RADIUS);
  41. }
  42. switch (event.getAction()){
  43. case MotionEvent.ACTION_DOWN:
  44. return true;
  45. case MotionEvent.ACTION_UP:
  46. {
  47. if (mAnimator != null && mAnimator.isRunning()) {
  48. mAnimator.cancel();
  49. }
  50. if (mAnimator == null) {
  51. mAnimator = ObjectAnimator.ofInt(this,"radius",DEFAULT_RADIUS, getWidth());
  52. }
  53. mAnimator.setInterpolator(new AccelerateInterpolator());
  54. mAnimator.addListener(new Animator.AnimatorListener() {
  55. @Override
  56. public void onAnimationStart(Animator animation) {
  57. }
  58. @Override
  59. public void onAnimationEnd(Animator animation) {
  60. setRadius(0);
  61. }
  62. @Override
  63. public void onAnimationCancel(Animator animation) {
  64. }
  65. @Override
  66. public void onAnimationRepeat(Animator animation) {
  67. }
  68. });
  69. mAnimator.start();
  70. }
  71. }
  72. return super.onTouchEvent(event);
  73. }
  74. public void setRadius(final int radius) {
  75. mCurRadius = radius;
  76. if (mCurRadius > 0) {
  77. mRadialGradient = new RadialGradient(mX, mY, mCurRadius, 0x00FFFFFF, 0xFF58FAAC, Shader.TileMode.CLAMP);
  78. mPaint.setShader(mRadialGradient);
  79. }
  80. postInvalidate();
  81. }
  82. @Override
  83. protected void onDraw(Canvas canvas) {
  84. super.onDraw(canvas);
  85. canvas.drawCircle(mX, mY, mCurRadius, mPaint);
  86. }
  87. }

代码分析:

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


  
  1. public void setRadius(final int radius) {
  2. mCurRadius = radius;
  3. if (mCurRadius > 0) {
  4. mRadialGradient = new RadialGradient(mX, mY, mCurRadius, 0x00FFFFFF, 0xFF58FAAC, Shader.TileMode.CLAMP);
  5. mPaint.setShader(mRadialGradient);
  6. }
  7. postInvalidate();
  8. }

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

 

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


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

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

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

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

“辐射圆渐变”源码:https://download.csdn.net/download/gaoxiaoweiandy/12202323

3. BitmapShader实例

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

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


  
  1. package com.cb.paint_gradient;
  2. import android.content.Context;
  3. import android.graphics.Bitmap;
  4. import android.graphics.BitmapFactory;
  5. import android.graphics.BitmapShader;
  6. import android.graphics.Canvas;
  7. import android.graphics.Matrix;
  8. import android.graphics.Shader;
  9. import android.graphics.drawable.ShapeDrawable;
  10. import android.graphics.drawable.shapes.OvalShape;
  11. import android.view.MotionEvent;
  12. import android.view.View;
  13. /**
  14. *
  15. */
  16. public class ZoomImageView extends View {
  17. //放大倍数
  18. private static final int FACTOR = 2;
  19. //放大镜的半径
  20. private static final int RADIUS = 100;
  21. // 原图
  22. private Bitmap mBitmap;
  23. // 放大后的图
  24. private Bitmap mBitmapScale;
  25. // 制作的圆形的图片(放大的局部),盖在Canvas上面
  26. private ShapeDrawable mShapeDrawable;
  27. private Matrix mMatrix;
  28. public ZoomImageView(Context context) {
  29. super(context);
  30. mBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.hlw);
  31. mBitmapScale = mBitmap;
  32. //放大后的整个图片
  33. mBitmapScale = Bitmap.createScaledBitmap(mBitmapScale,mBitmapScale.getWidth() * FACTOR,
  34. mBitmapScale.getHeight() * FACTOR,true);
  35. BitmapShader bitmapShader = new BitmapShader(mBitmapScale, Shader.TileMode.CLAMP,
  36. Shader.TileMode.CLAMP);
  37. mShapeDrawable = new ShapeDrawable(new OvalShape());
  38. mShapeDrawable.getPaint().setShader(bitmapShader);
  39. // 切出矩形区域,用来画圆(内切圆)
  40. mShapeDrawable.setBounds(0,0,RADIUS * 2,RADIUS * 2);
  41. mMatrix = new Matrix();
  42. }
  43. @Override
  44. protected void onDraw(Canvas canvas) {
  45. super.onDraw(canvas);
  46. // 1、画原图
  47. canvas.drawBitmap(mBitmap, 0 , 0 , null);
  48. // 2、画放大镜的图
  49. mShapeDrawable.draw(canvas);
  50. }
  51. @Override
  52. public boolean onTouchEvent(MotionEvent event) {
  53. int x = (int) event.getX();
  54. int y = (int) event.getY();
  55. // 将放大的图片往相反的方向挪动
  56. mMatrix.setTranslate(RADIUS - x * FACTOR, RADIUS - y *FACTOR);
  57. mShapeDrawable.getPaint().getShader().setLocalMatrix(mMatrix);
  58. // 切出手势区域点位置的圆
  59. mShapeDrawable.setBounds(x-RADIUS,y - RADIUS, x + RADIUS, y + RADIUS);
  60. invalidate();
  61. return true;
  62. }
  63. }

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


  
  1. package com.cb.paint_gradient;
  2. import android.support.v7.app.AppCompatActivity;
  3. import android.os.Bundle;
  4. public class MainActivity extends AppCompatActivity {
  5. @Override
  6. protected void onCreate(Bundle savedInstanceState) {
  7. super.onCreate(savedInstanceState);
  8. //setContentView(R.layout.activity_main);
  9. ZoomImageView view = new ZoomImageView(this);
  10. setContentView(view);
  11. }
  12. }

3.2 文字跑马灯效果

自定义View代码如下:


  
  1. package com.cb.paint_gradient;
  2. import android.content.Context;
  3. import android.graphics.Canvas;
  4. import android.graphics.LinearGradient;
  5. import android.graphics.Matrix;
  6. import android.graphics.Shader;
  7. import android.support.annotation.Nullable;
  8. import android.text.TextPaint;
  9. import android.util.AttributeSet;
  10. import android.widget.TextView;
  11. public class LinearGradientTextView extends TextView{
  12. private TextPaint mPaint;
  13. private LinearGradient mLinearGradient ;
  14. private Matrix mMatrix;
  15. private float mTranslate;
  16. private float DELTAX = 20;
  17. public LinearGradientTextView(Context context) {
  18. super(context);
  19. }
  20. public LinearGradientTextView(Context context, @Nullable AttributeSet attrs) {
  21. super(context, attrs);
  22. }
  23. @Override
  24. protected void onSizeChanged(int w, int h, int oldw, int oldh) {
  25. super.onSizeChanged(w, h, oldw, oldh);
  26. // 拿到TextView的画笔
  27. mPaint = getPaint();
  28. String text = getText().toString();
  29. float textWith = mPaint.measureText(text);
  30. // 3个文字的宽度
  31. int gradientSize = (int) (textWith / text.length() * 3);
  32. // 从左边-gradientSize开始,即左边距离文字gradientSize开始渐变
  33. mLinearGradient = new LinearGradient(-gradientSize,0,0,0,new int[]{
  34. 0x22ffffff, 0xffffffff, 0x22ffffff},null, Shader.TileMode.CLAMP
  35. );
  36. mPaint.setShader(mLinearGradient);
  37. }
  38. @Override
  39. protected void onDraw(Canvas canvas) {
  40. super.onDraw(canvas);
  41. mTranslate += DELTAX;
  42. float textWidth = getPaint().measureText(getText().toString());
  43. if(mTranslate > textWidth + 1 || mTranslate < 1){
  44. DELTAX = - DELTAX;
  45. }
  46. mMatrix = new Matrix();
  47. mMatrix.setTranslate(mTranslate, 0);
  48. mLinearGradient.setLocalMatrix(mMatrix);
  49. postInvalidateDelayed(50);
  50. }
  51. }

MainActivity.java:


  
  1. public class MainActivity extends AppCompatActivity {
  2. @Override
  3. protected void onCreate(Bundle savedInstanceState) {
  4. super.onCreate(savedInstanceState);
  5. setContentView(R.layout.activity_main);
  6. }
  7. }

activity_main.xml:


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

 

文章来源: blog.csdn.net,作者:冉航--小虾米,版权归原作者所有,如需转载,请联系作者。

原文链接:blog.csdn.net/gaoxiaoweiandy/article/details/104127732

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。