美文网首页android 开发View绘制安卓开发常用知识
Android显示框架:自定义View实践之绘制篇

Android显示框架:自定义View实践之绘制篇

作者: 江湖再见2024 | 来源:发表于2017-09-15 10:26 被阅读1899次

    Android显示框架:自定义View实践之绘制篇

    关于作者

    郭孝星,程序员,吉他手,主要从事Android平台基础架构方面的工作,欢迎交流技术方面的问题,可以去我的Github提issue或者发邮件至guoxiaoxingse@163.com与我交流。

    文章目录

    • 一 View
    • 二 Paint
      • 2.1 颜色处理
      • 2.2 文字处理
      • 2.3 特殊处理
    • 三 Canvas
      • 3.1 界面绘制
      • 3.2 范围裁切
      • 3.3 集合变换
    • 四 Path
      • 4.1 添加图形
      • 4.3 画线(直线或曲线)
      • 4.3 辅助设置和计算

    文章源码

    本文还提供了三个综合性的完整实例来辅助理解。

    • View绘制 - 图片标签效果实现
    • Canvas绘图 - 水面涟漪效果实现
    • 二阶贝塞尔曲线的应用 - 杯中倒水效果实现

    第一次阅览本系列文章,请参见导读,更多文章请参见文章目录

    本篇文章我们来分析View绘制方面的实践。

    一个简单的自定义View

    public class DrawView extends View {
    
        Paint paint = new Paint();
    
        public DrawView(Context context) {
            super(context);
        }
    
        public DrawView(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
        }
    
        public DrawView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            paint.setColor(Color.BLACK);
            canvas.drawCircle(150, 150, 150, paint);
        }
    }
    

    它在屏幕上绘制了一个圆形,如图:

    image.png

    在处理绘制的时候有以下几个关键点:

    • 处理绘制需要重写绘制方法,常用的是View的onDraw(),当然我们也可以使用其他的绘制方法来处理遮盖关系。
    • 完成绘制的是Canvas类,该类提供了绘制系列方法drawXXX()。裁剪系列方法clipXXX()以及几何变换方法translate()方法,还有辅助绘制的Path与Matrix。
    • 定制绘制的是Paint类,该类是绘制所用的画笔,可以实现特殊的绘制效果。

    我们分别来看看这个关键的角色。

    一 View

    我们讨论的第一个问题就是View/ViewGroup的绘制顺序问题,绘制在View.draw()方法里调用的,具体的执行顺序是:

    1. drawBackground():绘制背景,不能重写。
    2. onDraw():绘制主体。
    3. dispatchDraw():绘制子View
    4. onDrawForeground():绘制滑动边缘渐变、滚动条和前景。
    image.png
    public class LabelImageView extends AppCompatImageView {
    
        /**
         * 梯形距离左上角的长度
         */
        private static final int LABEL_LENGTH = 100;
        /**
         * 梯形斜边的长度
         */
        private static final int LABEL_HYPOTENUSE_LENGTH = 100;
    
        private Paint textPaint;
        private Paint backgroundPaint;
        private Path pathText;
        private Path pathBackground;
    
    
        public LabelImageView(Context context) {
            super(context);
            init();
        }
    
        public LabelImageView(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
            init();
        }
    
        public LabelImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            init();
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            //计算路径
            calculatePath(getMeasuredWidth(), getMeasuredHeight());
            canvas.drawPath(pathBackground, backgroundPaint);
            canvas.drawTextOnPath("Hot", pathText, 100, -20, textPaint);
        }
    
        @Override
        public void onDrawForeground(Canvas canvas) {
            super.onDrawForeground(canvas);
        }
    
        /**
         * 计算路径              x1   x2
         * ................................    distance(标签离右上角的垂直距离)
         * .                      .    .  .
         * .                        .    .. y1
         * .                          .   .
         * .                            . .
         * .                              . y2    height(标签垂直高度)
         * .                              .
         * ................................
         */
        private void calculatePath(int measuredWidth, int measuredHeight) {
    
            int top = 185;
            int right = measuredWidth;
    
            float x1 = right - LABEL_LENGTH - LABEL_HYPOTENUSE_LENGTH;
            float x2 = right - LABEL_HYPOTENUSE_LENGTH;
            float y1 = top + LABEL_LENGTH;
            float y2 = top + LABEL_LENGTH + LABEL_HYPOTENUSE_LENGTH;
    
            pathText.reset();
            pathText.moveTo(x1, top);
            pathText.lineTo(right, y2);
            pathText.close();
    
            pathBackground.reset();
            pathBackground.moveTo(x1, top);
            pathBackground.lineTo(x2, top);
            pathBackground.lineTo(right, y1);
            pathBackground.lineTo(right, y2);
            pathBackground.close();
        }
    
        private void init() {
            pathText = new Path();
            pathBackground = new Path();
    
            textPaint = new Paint();
            textPaint.setTextSize(50);
            textPaint.setFakeBoldText(true);
            textPaint.setColor(Color.WHITE);
    
            backgroundPaint = new Paint();
            backgroundPaint.setColor(Color.RED);
            backgroundPaint.setStyle(Paint.Style.FILL);
        }
    }
    

    所以你可以看到,当我们继承了一个View,根据需求的不同可以选择性重写我们需要的方法,在super前插入代码和在super后插入代码,效果是不一样的。

    • draw():super.draw()之前,被背景盖住;super.draw()后,盖住前景;
    • onDraw():super.onDraw()之前,背景与主体内容之前;super.onDraw()之后,主体内容和子View之间;
    • dispatchDraw():super.dispatchDraw()之前,主体内容和子View之间;super.dispatchDraw()之后,子View和前景之间;
    • onDrawForeground():super.onDrawForeground()之前,子View和前景之间;super.onDrawForeground()之后,盖住前景;

    二 Paint

    Paint:顾名思义,画笔,通过Paint可以对绘制行为进行控制。

    Paint有三种构造方法

    public class Paint {
          //空的构造方法
          public Paint() {
              this(0);
          }
      
          //传入flags来构造Paint,flags用来控制Paint的行为,例如:抗锯齿等
          public Paint(int flags) {
              mNativePaint = nInit();
              NoImagePreloadHolder.sRegistry.registerNativeAllocation(this, mNativePaint);
              setFlags(flags | HIDDEN_DEFAULT_PAINT_FLAGS);
              // TODO: Turning off hinting has undesirable side effects, we need to
              //       revisit hinting once we add support for subpixel positioning
              // setHinting(DisplayMetrics.DENSITY_DEVICE >= DisplayMetrics.DENSITY_TV
              //        ? HINTING_OFF : HINTING_ON);
              mCompatScaling = mInvCompatScaling = 1;
              setTextLocales(LocaleList.getAdjustedDefault());
          }
      
          //传入另外一个Paint来构造新的Paint
          public Paint(Paint paint) {
              mNativePaint = nInitWithPaint(paint.getNativeInstance());
              NoImagePreloadHolder.sRegistry.registerNativeAllocation(this, mNativePaint);
              setClassVariablesFrom(paint);
          }  
    }
    

    2.1 颜色处理类

    在Paint类中,处理颜色主要有三个方法。

    • setShader(Shader shader):用来处理颜色渐变
    • setColorFilter(ColorFilter filter):用来基于颜色进行过滤处理;
    • setXfermode(Xfermode xfermode) 用来处理源图像和 View 已有内容的关系

    setShader(Shader shader)

    着色器是图像领域的一个通用概念,它提供的是一套着色规则。

    public Shader setShader(Shader shader) 
    

    着色器具体由Shader的子类实现:

    LinearGradient - 线性渐变

    public LinearGradient(float x0, float y0, float x1, float y1, int color0, int color1, TileMode tile)
    
    • x0 y0 x1 y1:渐变的两个端点的位置
    • color0 color1 是端点的颜色
    • tile:端点范围之外的着色规则,类型是 TileMode。TileMode 一共有 3 个值可选: CLAMP, MIRROR 和 REPEAT。CLAMP

    举例

    image.png
    //线性渐变
    Shader shader1 = new LinearGradient(0, 100, 200, 100, Color.RED, Color.BLUE, Shader.TileMode.CLAMP);
    paint1.setShader(shader1);
    
    Shader shader2 = new LinearGradient(0, 600, 200, 600, Color.RED, Color.BLUE, Shader.TileMode.MIRROR);
    paint2.setShader(shader2);
    
    Shader shader3 = new LinearGradient(0, 1100, 200, 1100, Color.RED, Color.BLUE, Shader.TileMode.REPEAT);
    paint3.setShader(shader3);
    
    canvas.drawRect(0, 100, 1000, 500, paint1);
    canvas.drawRect(0, 600, 1000, 1000, paint2);
    canvas.drawRect(0, 1100, 1000, 1500, paint3);
    

    SweepGradient - 辐射渐变

    public RadialGradient(float centerX, float centerY, float radius, int centerColor, int edgeColor, @NonNull TileMode tileMode) 
    
    • centerX centerY:辐射中心的坐标
    • radius:辐射半径
    • centerColor:辐射中心的颜色
    • edgeColor:辐射边缘的颜色
    • tileMode:辐射范围之外的着色模式

    举例

    image.png
    //辐射渐变
    Shader shader1 = new RadialGradient(0, 100, 200, Color.RED, Color.BLUE, Shader.TileMode.CLAMP);
    paint1.setShader(shader1);
    
    Shader shader2 = new RadialGradient(0, 600, 200, Color.RED, Color.BLUE, Shader.TileMode.MIRROR);
    paint2.setShader(shader2);
    
    Shader shader3 = new RadialGradient(0, 1100, 200, Color.RED, Color.BLUE, Shader.TileMode.REPEAT);
    paint3.setShader(shader3);
    
    canvas.drawRect(0, 100, 1000, 500, paint1);
    canvas.drawRect(0, 600, 1000, 1000, paint2);
    

    BitmapShader - 位图着色

    使用位图的像素来填充图形或者文字。

     public BitmapShader(@NonNull Bitmap bitmap, TileMode tileX, TileMode tileY)
    
    • bitmap:用来做模板的 Bitmap 对象
    • tileX:横向的 TileMode
    • tileY:纵向的 TileMode。

    举例

    BitmapShader是一个很有用的类,可以利用该类做各种各样的图片裁剪。

    image.png
    //位图着色
    Shader shader1 = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
    paint1.setShader(shader1);
    
    //绘制圆形
    canvas.drawCircle(500, 500, 300, paint1);
    

    ComposeShader - 组合Shader

    ComposeShader可以将连个Shader组合在一起。

    public ComposeShader(Shader shaderA, Shader shaderB, PorterDuff.Mode mode) 
    
    • shaderA, shaderB:两个相继使用的 Shader
    • mode: 两个 Shader 的叠加模式,即 shaderA 和 shaderB 应该怎样共同绘制。它的类型是PorterDuff.Mode。

    PorterDuff.Mode用来指定两个Shader叠加时颜色的绘制策略,它有很多种策略,也就是以一种怎样的模式来与原图像进行合成,具体如下:

    蓝色矩形为原图像,红色圆形为目标图像。

    image.png
    //颜色过滤器
    ColorFilter colorFilter1 = new LightingColorFilter(Color.RED, Color.BLUE);
    paint2.setColorFilter(colorFilter1);
    
    canvas.drawBitmap(bitmapTimo, null, rect1, paint1);
    canvas.drawBitmap(bitmapTimo, null, rect2, paint2);
    

    PorterDuffColorFilter - 模拟颜色混合效果

    public PorterDuffColorFilter(@ColorInt int color, @NonNull PorterDuff.Mode mode) 
    

    PorterDuffColorFilter指定一种颜色和PorterDuff.Mode来与源图像就行合成,也就是以一种怎样的模式来与原图像进行合成,我们在上面已经讲过这个内容。

    举例

    //我们在使用Xfermode的时候也是使用它的子类PorterDuffXfermode
    Xfermode xfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
    canvas.drawBitmap(rectBitmap, 0, 0, paint); // 画方  
    paint.setXfermode(xfermode); // 设置 Xfermode  
    canvas.drawBitmap(circleBitmap, 0, 0, paint); // 画圆  
    paint.setXfermode(null); // 用完及时清除 Xfermode  
    

    ColorMatrixColorFilter - 颜色矩阵过滤

    ColorMatrixColorFilter使用一个颜色矩阵ColorMatrix来对象图像进行处理。

    public ColorMatrixColorFilter(ColorMatrix matrix)
    

    ColorMatrix是一个4x5的矩阵

    [ a, b, c, d, e,
      f, g, h, i, j,
      k, l, m, n, o,
      p, q, r, s, t ]
    

    通过计算,ColorMatrix可以对要绘制的像素进行转换,如下:

    R’ = a*R + b*G + c*B + d*A + e;  
    G’ = f*R + g*G + h*B + i*A + j;  
    B’ = k*R + l*G + m*B + n*A + o;  
    A’ = p*R + q*G + r*B + s*A + t;  
    

    利用ColorMatrixColorFilter(可以实现很多炫酷的滤镜效果。

    setXfermode(Xfermode xfermode)

    Paint.setXfermode(Xfermode xfermode)方法,它也是一种混合图像的方法。

    Xfermode 指的是你要绘制的内容和 Canvas 的目标位置的内容应该怎样结合计算出最终的颜色。但通俗地说,其实就是要你以绘制的内容作为源图像,以View中已有的内
    容作为目标图像,选取一个PorterDuff.Mode作为绘制内容的颜色处理方案。

    小结

    关于PorterDuff.Mode,我们已经提到

    • ComposeShader:混合两个Shader
    • PorterDuffColorFilter:增加一个单色的ColorFilter
    • Xfermode:指定原图像与目标图像的混合模式

    这三种以不同的方式来使用PorterDuff.Mode,但是原理都是一样的。

    2.2 文字处理类

    Paint里有大量方法来设置文字的绘制属性,事实上文字在Android底层是被当做图片来处理的。

    • setTextSize(float textSize):设置文字大小
    • setTypeface(Typeface typeface):设置文字字体
    • setFakeBoldText(boolean fakeBoldText):是否使用伪粗体(并不是提到size,而是在运行时描粗的)
    • setStrikeThruText(boolean strikeThruText):是否添加删除线
    • setUnderlineText(boolean underlineText):是否添加下划线
    • setTextSkewX(float skewX):设置文字倾斜度
    • setTextScaleX(float scaleX):设置文字横向缩放
    • setLetterSpacing(float letterSpacing):设置文字间距
    • setFontFeatureSettings(String settings):使用CSS的font-feature-settings的方式来设置文字。
    • setTextAlign(Paint.Align align):设置文字对齐方式
    • setTextLocale(Locale locale):设置文字Local
    • setHinting(int mode):设置字体Hinting(微调),过向字体中加入 hinting 信息,让矢量字体在尺寸过小的时候得到针对性的修正,从而提高显示效果。
    • setSubpixelText(boolean subpixelText):设置次像素级抗锯齿,根据程序所运行的设备的屏幕类型,来进行针对性的次像素级的抗锯齿计算,从而达到更好的抗锯齿效果。

    2.3 特殊效果类

    setAntiAlias (boolean aa)

    设置抗锯齿,默认关闭,用来是图像的绘制更加圆润。我们还可以在初始化的时候设置Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);。

    setStyle(Paint.Style style)

    设置填充风格,

    • FILL 模式,填充
    • STROKE 模式,画线
    • FILL_AND_STROKE 模式,填充 + 画线

    如果是划线模式,我们针对线条还可以有多种设置。

    setStrokeWidth(float width) - 设置线条粗细

    setStrokeCap(Paint.Cap cap) - 设置线头的形状,默认为 BUTT

    • UTT 平头
    • ROUND 圆头
    • SQUARE 方头

    setStrokeJoin(Paint.Join join) - 设置拐角的形状。默认为 MITER

    • MITER 尖角
    • BEVEL 平角
    • ROUND 圆角

    setStrokeMiter(float miter)- 设置 MITER 型拐角的延长线的最大值

    setDither(boolean dither)

    设置图像的抖动。

    image.png

    抖动是指把图像从较高色彩深度(即可用的颜色数)向较低色彩深度的区域绘制时,在图像中有意地插入噪点,通过有规律地扰乱图像来让图像对于肉眼更加真实的做法。

    当然这个效果旨在低位色的时候比较有用,例如,ARGB_4444 或者 RGB_565,不过现在Android默认的色彩深度都是32位的ARGB_8888,这个方法的效果没有那么明显。

    setFilterBitmap(boolean filter)

    设置是否使用双线性过滤来绘制 Bitmap 。

    image.png

    图像在放大绘制的时候,默认使用的是最近邻插值过滤,这种算法简单,但会出现马赛克现象;而如果开启了双线性过滤,就可以让结果图像显得更加平滑。

    etPathEffect(PathEffect effect)

    设置图形的轮廓效果。Android有六种PathEffect:

    • CornerPathEffect:将拐角绘制成圆角
    • DiscretePathEffect:将线条进行随机偏离
    • DashPathEffect:绘制虚线
    • PathDashPathEffect:使用指定的Path来绘制虚线
    • SumPathEffect:组合两个PathEffect,叠加应用。
    • ComposePathEffect:组合两个PathEffect,叠加应用。

    CornerPathEffect(float radius)

    • float radius圆角半径

    DiscretePathEffect(float segmentLength, float deviation)

    • float segmentLength:用来拼接每个线段的长度,
    • float deviation:偏离量

    DashPathEffect(float[] intervals, float phase)

    • float[] intervals:指定了虚线的格式,数组中元素必须为偶数(最少是 2 个),按照「画线长度、空白长度、画线长度、空白长度」……的顺序排列
    • float phase:虚线的偏移量

    PathDashPathEffect(Path shape, float advance, float phase, PathDashPathEffect.Style style)

    • Path shape:用来绘制的Path
    • float advance:两个相邻Path段起点间的间隔
    • float phase:虚线的偏移量
    • PathDashPathEffect.Style style:指定拐弯改变的时候 shape 的转换方式:TRANSLATE:位移、ROTATE:旋转、MORPH:变体

    SumPathEffect(PathEffect first, PathEffect second)

    • PathEffect first:同时应用的PathEffect
    • PathEffect second:同时应用的PathEffect

    ComposePathEffect(PathEffect outerpe, PathEffect innerpe)

    • PathEffect outerpe:后应用的PathEffect
    • PathEffect innerpe:先应用的PathEffect

    举例

    image.png
    //图形轮廓效果
    //绘制圆角
    PathEffect cornerPathEffect = new CornerPathEffect(20);
    paint1.setStyle(Paint.Style.STROKE);
    paint1.setStrokeWidth(5);
    paint1.setPathEffect(cornerPathEffect);
    
    //绘制尖角
    PathEffect discretePathEffect = new DiscretePathEffect(20, 5);
    paint2.setStyle(Paint.Style.STROKE);
    paint2.setStrokeWidth(5);
    paint2.setPathEffect(discretePathEffect);
    
    //绘制虚线
    PathEffect dashPathEffect = new DashPathEffect(new float[]{20,10, 5, 10}, 0);
    paint3.setStyle(Paint.Style.STROKE);
    paint3.setStrokeWidth(5);
    paint3.setPathEffect(dashPathEffect);
    
    //使用path来绘制虚线
    Path path = new Path();//画一个三角来填充虚线
    path.lineTo(40, 40);
    path.lineTo(0, 40);
    path.close();
    PathEffect pathDashPathEffect = new PathDashPathEffect(path, 40, 0, PathDashPathEffect.Style.TRANSLATE);
    paint4.setStyle(Paint.Style.STROKE);
    paint4.setStrokeWidth(5);
    paint4.setPathEffect(pathDashPathEffect);
    

    setShadowLayer(float radius, float dx, float dy, int shadowColor)

    设置阴影图层,处于目标下层图层。

    • float radius:阴影半径
    • float dx:阴影偏移量
    • float dy:阴影偏移量
    • int shadowColor:阴影颜色

    举例

    image.png
    paint1.setTextSize(200);
    paint1.setShadowLayer(10, 0, 0, Color.RED);
    canvas.drawText("Android", 80, 300 ,paint1);
    

    注:在硬件加速开启的情况下, setShadowLayer() 只支持文字的绘制,文字之外的绘制必须关闭硬件加速才能正常绘制阴影。如果 shadowColor 是半透明的,阴影的透明度就使用 shadowColor 自己
    的透明度;而如果 shadowColor 是不透明的,阴影的透明度就使用 paint 的透明度。

    setMaskFilter(MaskFilter maskfilter)

    设置图层遮罩层,处于目标上层图层。

    MaskFilter有两个子类:

    • BlurMaskFilter:模糊效果
    • BlurMaskFilter:浮雕效果

    举例

    模糊效果

    • BlurMaskFilter.Blur.NORMAL
    • BlurMaskFilter.Blur.SOLD
    • BlurMaskFilter.Blur.INNER
    • BlurMaskFilter.Blur.OUTTER

    分别为:

    image.png image.png image.png image.png
    //设置遮罩图层,处于目标上层图层
    //关闭硬件加速
    setLayerType(View.LAYER_TYPE_SOFTWARE, null);
    MaskFilter blurMaskFilter = new BlurMaskFilter(200, BlurMaskFilter.Blur.NORMAL);
    paint2.setMaskFilter(blurMaskFilter);
    
    canvas.drawBitmap(bitmapTimo, null, rect1, paint1);
    canvas.drawBitmap(bitmapTimo, null, rect2, paint2);
    

    注:在硬件加速开启的情况下, setMaskFilter(MaskFilter maskfilter)只支持文字的绘制,文字之外的绘制必须关闭硬件加速才能正常绘制阴影。关闭硬件加速可以调用
    View.setLayerType(View.LAYER_TYPE_SOFTWARE, null)或者在Activity标签里设置android:hardwareAccelerated="false"。

    三 Canvas

    Canvas实现了Android 2D图形的绘制,底层基于Skia实现。

    3.1 界面绘制

    Canvas提供了丰富的对象绘制方法,一般都以drawXXX()打头,绘制的对象包括:

    • 弧线(Arcs)
    • 颜色(Argb、Color)
    • 位图(Bitmap)
    • 圆(Circle)
    • 点(Point)
    • 线(Line)
    • 矩形(Rect)
    • 图片(Picture)
    • 圆角矩形(RoundRect)
    • 文本(Text)
    • 顶点(Vertices)
    • 路径(Path)

    这里的方法大都很简单,我们来描述下期中比较复杂的方法。

    弧线

    public void drawArc(float left, float top, float right, float bottom, float startAngle,
            float sweepAngle, boolean useCenter, @NonNull Paint paint) {
        native_drawArc(mNativeCanvasWrapper, left, top, right, bottom, startAngle, sweepAngle,
                useCenter, paint.getNativeInstance());
    }
    
    • float left, float top, float right, float bottom:左、上、右、下的坐标。
    • float startAngle:弧形起始角度,Android坐标系x轴正右的方向是0度的位置,顺时针为正角度,逆时针为负角度。
    • float sweepAngle:弧形划过的角度。
    • boolean useCenter:是否连接到圆心。如果不连接到圆心就是弧形,如果连接到圆心,就是扇形。

    例如

    image.png
    paint.setStyle(Paint.Style.FILL);//填充模式
    canvas.drawArc(200, 100, 800, 500, -110, 100, true, paint);
    canvas.drawArc(200, 100, 800, 500, 20, 140, false, paint);
    paint.setStyle(Paint.Style.STROKE);//画线模式
    paint.setStrokeWidth(5);
    canvas.drawArc(200, 100, 800, 500, 180, 60, false, paint);
    

    位图

    • public void drawBitmap(@NonNull Bitmap bitmap, float left, float top, @Nullable Paint paint) - 绘制位图
    • public void drawBitmapMesh(@NonNull Bitmap bitmap, int meshWidth, int meshHeight,
      @NonNull float[] verts, int vertOffset, @Nullable int[] colors, int colorOffset,
      @Nullable Paint paint) - 绘制拉伸位图

    第一个方法很简单,就是在指定的坐标处开始绘制位图。我们着重来看看第二个方法,这个方法不是很常用(可能是计算比较复杂的锅😓),但这并不影响它强大的功能。

    drawBitmapMesh()方法将位图分为若干网格,然后对每个网格进行扭曲处理。我们先来看看这个方法的参数:

    • @NonNull Bitmap bitmap:源位图
    • int meshWidth:横向上将源位图划分成多少格
    • int meshHeight:纵向上将源位图划分成多少格
    • @NonNull float[] verts:网格顶点坐标数组,记录扭曲后图片各顶点的坐标,数组大小为 (meshWidth+1) * (meshHeight+1) * 2 + vertOffset
    • int vertOffset:记录verts数组从第几个数组元素开始扭曲
    • @Nullable int[] colors:设置网格顶点的颜色,该颜色会和位图对应像素的颜色叠加,数组大小为 (meshWidth+1) * (meshHeight+1) + colorOffset
    • int colorOffset:记录colors从几个数组元素开始取色
    • @Nullable Paint paint:画笔

    我们来用drawBitmapMesh()方法实现一个水面涟漪效果。

    ripple.gif

    举例

    /**
     * 利用Canvas.drawBitmapMeshC()方法对图像做扭曲处理,模拟水波效果。
     * <p>
     * For more information, you can visit https://github.com/guoxiaoxing or contact me by
     * guoxiaoxingse@163.com
     *
     * @author guoxiaoxing
     * @since 2017/9/12 下午3:44
     */
    public class RippleLayout extends FrameLayout {
    
        /**
         * 图片横向、纵向的格树
         */
        private final int MESH_WIDTH = 20;
        private final int MESH_HEIGHT = 20;
    
        /**
         * 图片顶点数
         */
        private final int VERTS_COUNT = (MESH_WIDTH + 1) * (MESH_HEIGHT + 1);
    
        /**
         * 原坐标数组
         */
        private final float[] originVerts = new float[VERTS_COUNT * 2];
    
        /**
         * 转换后的坐标数组
         */
        private final float[] targetVerts = new float[VERTS_COUNT * 2];
    
        /**
         * 当前空间的图像
         */
        private Bitmap bitmap;
    
        /**
         * 水波宽度的一半
         */
        private float rippleWidth = 100f;
    
        /**
         * 水波扩展的速度
         */
        private float rippleRadius = 15f;
    
        /**
         * 水波半径
         */
        private float rippleSpeed = 15f;
    
        /**
         * 水波动画是否在进行中
         */
        private boolean isRippling;
    
        public RippleLayout(@NonNull Context context) {
            super(context);
        }
    
        public RippleLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
        }
    
        public RippleLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
        @Override
        protected void dispatchDraw(Canvas canvas) {
            if (isRippling && bitmap != null) {
                canvas.drawBitmapMesh(bitmap, MESH_WIDTH, MESH_HEIGHT, targetVerts, 0, null, 0, null);
            } else {
                super.dispatchDraw(canvas);
            }
        }
    
        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            switch (ev.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    showRipple(ev.getX(), ev.getY());
                    break;
            }
            return super.dispatchTouchEvent(ev);
        }
    
        /**
         * 显示水波动画
         *
         * @param originX 原点 x 坐标
         * @param originY 原点 y 坐标
         */
        public void showRipple(final float originX, final float originY) {
            if (isRippling) {
                return;
            }
            initData();
            if (bitmap == null) {
                return;
            }
            isRippling = true;
            //循环次数,通过控件对角线距离计算,确保水波纹完全消失
            int viewLength = (int) getLength(bitmap.getWidth(), bitmap.getHeight());
            final int count = (int) ((viewLength + rippleWidth) / rippleSpeed);
            Observable.interval(0, 10, TimeUnit.MILLISECONDS)
                    .subscribeOn(Schedulers.io())
                    .observeOn(AndroidSchedulers.mainThread())
                    .take(count + 1)
                    .subscribe(new Consumer<Long>() {
                        @Override
                        public void accept(@NonNull Long aLong) throws Exception {
                            rippleRadius = aLong * rippleSpeed;
                            warp(originX, originY);
                            if (aLong == count) {
                                isRippling = false;
                            }
                        }
                    });
        }
    
        /**
         * 初始化 Bitmap 及对应数组
         */
        private void initData() {
            bitmap = getCacheBitmapFromView(this);
            if (bitmap == null) {
                return;
            }
            float bitmapWidth = bitmap.getWidth();
            float bitmapHeight = bitmap.getHeight();
            int index = 0;
            for (int height = 0; height <= MESH_HEIGHT; height++) {
                float y = bitmapHeight * height / MESH_HEIGHT;
                for (int width = 0; width <= MESH_WIDTH; width++) {
                    float x = bitmapWidth * width / MESH_WIDTH;
                    originVerts[index * 2] = targetVerts[index * 2] = x;
                    originVerts[index * 2 + 1] = targetVerts[index * 2 + 1] = y;
                    index += 1;
                }
            }
        }
    
        /**
         * 图片转换
         *
         * @param originX 原点 x 坐标
         * @param originY 原点 y 坐标
         */
        private void warp(float originX, float originY) {
            for (int i = 0; i < VERTS_COUNT * 2; i += 2) {
                float staticX = originVerts[i];
                float staticY = originVerts[i + 1];
                float length = getLength(staticX - originX, staticY - originY);
                if (length > rippleRadius - rippleWidth && length < rippleRadius + rippleWidth) {
                    PointF point = getRipplePoint(originX, originY, staticX, staticY);
                    targetVerts[i] = point.x;
                    targetVerts[i + 1] = point.y;
                } else {
                    //复原
                    targetVerts[i] = originVerts[i];
                    targetVerts[i + 1] = originVerts[i + 1];
                }
            }
            invalidate();
        }
    
        /**
         * 获取水波的偏移坐标
         *
         * @param originX 原点 x 坐标
         * @param originY 原点 y 坐标
         * @param staticX 待偏移顶点的原 x 坐标
         * @param staticY 待偏移顶点的原 y 坐标
         * @return 偏移后坐标
         */
        private PointF getRipplePoint(float originX, float originY, float staticX, float staticY) {
            float length = getLength(staticX - originX, staticY - originY);
            //偏移点与原点间的角度
            float angle = (float) Math.atan(Math.abs((staticY - originY) / (staticX - originX)));
            //计算偏移距离
            float rate = (length - rippleRadius) / rippleWidth;
            float offset = (float) Math.cos(rate) * 10f;
            float offsetX = offset * (float) Math.cos(angle);
            float offsetY = offset * (float) Math.sin(angle);
            //计算偏移后的坐标
            float targetX;
            float targetY;
            if (length < rippleRadius + rippleWidth && length > rippleRadius) {
                //波峰外的偏移坐标
                if (staticX > originX) {
                    targetX = staticX + offsetX;
                } else {
                    targetX = staticX - offsetX;
                }
                if (staticY > originY) {
                    targetY = staticY + offsetY;
                } else {
                    targetY = staticY - offsetY;
                }
            } else {
                //波峰内的偏移坐标
                if (staticX > originY) {
                    targetX = staticX - offsetX;
                } else {
                    targetX = staticX + offsetX;
                }
                if (staticY > originY) {
                    targetY = staticY - offsetY;
                } else {
                    targetY = staticY + offsetY;
                }
            }
            return new PointF(targetX, targetY);
        }
    
        /**
         * 根据宽高,获取对角线距离
         *
         * @param width  宽
         * @param height 高
         * @return 距离
         */
        private float getLength(float width, float height) {
            return (float) Math.sqrt(width * width + height * height);
        }
    
        /**
         * 获取 View 的缓存视图
         *
         * @param view 对应的View
         * @return 对应View的缓存视图
         */
        private Bitmap getCacheBitmapFromView(View view) {
            view.setDrawingCacheEnabled(true);
            view.buildDrawingCache(true);
            final Bitmap drawingCache = view.getDrawingCache();
            Bitmap bitmap;
            if (drawingCache != null) {
                bitmap = Bitmap.createBitmap(drawingCache);
                view.setDrawingCacheEnabled(false);
            } else {
                bitmap = null;
            }
            return bitmap;
        }
    }
    

    路径

    public void drawPath(@NonNull Path path, @NonNull Paint paint) {
        if (path.isSimplePath && path.rects != null) {
            native_drawRegion(mNativeCanvasWrapper, path.rects.mNativeRegion, paint.getNativeInstance());
        } else {
            native_drawPath(mNativeCanvasWrapper, path.readOnlyNI(), paint.getNativeInstance());
        }
    }
    

    drawPath()可以绘制自定义图形,图形的路径用Path对象来描述。

    Path对象可以描述很多图形,具体说来:

    • 直线
    • 二次曲线
    • 三次曲线
    • 椭圆
    • 弧形
    • 矩形
    • 圆角矩形

    3.2 范围裁切

    Canvas里的范围裁切主要有两类方法:

    • clipReact():按路径裁切
    • clipPath():按坐标裁切

    举例

    clipReact

    image.png

    clipPath

    image.png
    //范围裁切
    canvas.save();//保存画布
    canvas.clipRect(200, 200, 900, 900);
    canvas.drawBitmap(bitmapTimo, 100, 100, paint1);
    canvas.restore();//恢复画布
    
    canvas.save();//保存画布
    path.addCircle(500, 500, 300, Path.Direction.CW);
    canvas.clipPath(path);
    canvas.drawBitmap(bitmapTimo, 100, 100, paint1);
    canvas.restore();//恢复画布
    

    3.3 几何变换

    关于几何变换有三种实现方式:

    • Canvas:常规几何变换
    • Matrix:自定义几何变换
    • Camera:三维变换

    Canvas常规几何变换

    Canvas还提供了对象的位置变换的方法,其中包括:

    • translate(float dx, float dy):平移
    • rotate(float degrees):旋转,可以设置旋转圆点,默认在原点位置。
    • scale(float sx, float sy):缩放
    • skew(float sx, float sy):扭曲

    举例

    image.png
    canvas.save();//保存画布
    canvas.skew(0, 0.5f);
    canvas.drawBitmap(bitmapTimo, null, rect1, paint1);
    canvas.restore();//恢复画布
    
    canvas.save();//保存画布
    canvas.rotate(45, 750, 750);
    canvas.drawBitmap(bitmapTimo, null, rect2, paint1);
    canvas.restore();//恢复画布
    

    注:1 为了不影响其他绘制操作,在进行变换之前需要调用canvas.save()保存画布,变换完成以后再调用canvas.restore()来恢复画布。
    2 Canvas几何变换的顺序是相反的,例如我们在代码写了:canvas.skew(0, 0.5f); canvas.rotate(45, 750, 750); 它的实际调用顺序是canvas.rotate(45, 750, 750); -> canvas.skew(0, 0.5f)

    Matrix自定义几何变换

    Matrix也实现了Canvas里的四种常规变换,它的实现流程如下:

    1. 创建 Matrix 对象;
    2. 调用 Matrix 的 pre/postTranslate/Rotate/Scale/Skew() 方法来设置几何变换;
    3. 使用 Canvas.setMatrix(matrix) 或 Canvas.concat(matrix) 来把几何变换应用到 Canvas。

    Canvas.concat(matrix):用 Canvas 当前的变换矩阵和 Matrix 相乘,即基于 Canvas 当前的变换,叠加上 Matrix 中的变换。

    举例

    image.png
    //Matrix几何变换
    canvas.save();//保存画布
    matrix.preSkew(0, 0.5f);
    canvas.concat(matrix);
    canvas.drawBitmap(bitmapTimo, null, rect1, paint1);
    canvas.restore();//恢复画布
    
    canvas.save();//保存画布
    matrix.reset();
    matrix.preRotate(45, 750, 750);
    canvas.concat(matrix);
    canvas.drawBitmap(bitmapTimo, null, rect2, paint1);
    canvas.restore();//恢复画布
    

    Matrix除了四种基本的几何变换,还可以自定义几何变换。

    • setPolyToPoly(float[] src, int srcIndex, float[] dst, int dstIndex, int pointCount)
    • setRectToRect(RectF src, RectF dst, ScaleToFit stf)

    这两个方法都是通过多点的映射的方式来直接设置变换,把指定的点移动到给出的位置,从而发生形变。

    举例

    image.png
    //Matrix几何变换
    canvas.save();//保存画布
    matrix.setPolyToPoly(src, 0, dst, 0, 2);
    canvas.concat(matrix);
    canvas.drawBitmap(bitmapTimo, 0, 0, paint1);
    canvas.restore();//恢复画布
    

    Camera三维变换

    在讲解Camera的三维变换之前,我们需要先理解Camera的坐标系系统。

    我们前面说过,Canvas使用的是二维坐标系。

    image.png

    而Camera使用的是三维坐标系,这里偷个懒😊,借用凯哥的图来描述一下。

    image.png
    //Camera三维变换
    canvas.save();//保存画布
    
    camera.save();//保存camera
    camera.rotateX(45);
    canvas.translate(500, 750);//camera也是默认在原点(0, 0)位置,所以我们要把画布平移到图片中心(500, 750)
    camera.applyToCanvas(canvas);
    canvas.translate(-500, -750);//翻转完图片,再将画布从图片中心(500, 750)平移到原点(0, 0)
    camera.restore();//恢复camera
    
    canvas.drawBitmap(bitmapTimo, null, rect, paint1);
    canvas.restore();//恢复画布
    

    平移

    image.png
    //Camera三维变换
    canvas.save();//保存画布
    
    camera.save();//保存camera
    camera.translate(500, 500, 500);
    canvas.translate(500, 750);//camera也是默认在原点(0, 0)位置,所以我们要把画布平移到图片中心(500, 750)
    camera.applyToCanvas(canvas);
    canvas.translate(-500, -750);//翻转完图片,再将画布从图片中心(500, 750)平移到原点(0, 0)
    camera.restore();//恢复camera
    
    canvas.drawBitmap(bitmapTimo, null, rect, paint1);
    canvas.restore();//恢复画布
    

    移动相机

    image.png
    //Camera三维变换
    canvas.save();//保存画布
    
    camera.save();//保存camera
    camera.setLocation(0, 0, - 1000);//相机往前移动,图像变小
    canvas.translate(500, 750);//camera也是默认在原点(0, 0)位置,所以我们要把画布平移到图片中心(500, 750)
    camera.applyToCanvas(canvas);
    canvas.translate(-500, -750);//翻转完图片,再将画布从图片中心(500, 750)平移到原点(0, 0)
    camera.restore();//恢复camera
    
    canvas.drawBitmap(bitmapTimo, null, rect, paint1);
    canvas.restore();//恢复画布
    

    四 Path

    Path描述了绘制路径,用它可以完成很多复杂的图形绘制。

    我们再来看看Path里的方法。

    4.1 添加图形

    例如:addCircle(float x, float y, float radius, Direction dir)

    public void addCircle(float x, float y, float radius, Direction dir) {
        isSimplePath = false;
        native_addCircle(mNativePath, x, y, radius, dir.nativeInt);
    }
    

    该方法的参数含义:

    • float x:圆心x轴坐标
    • float y:圆心y轴坐标
    • float radius:圆半径
    • Direction dir:画圆的路径的方向,顺时针Direction.CN,逆时针Direction.CCN,它们在填充图形(Paint.Style 为 FILL 或 FILL_AND_STROKE)且图形出现相交的时候
      用来判断填充范围。

    其他的方法都是这个方法类似。

    4.2 画线(直线或者曲线)

    直线

    //从当前位置,向目标位置画一条直线,该方法使用相对于原点的绝对坐标
    public void lineTo(float x, float y) {
        isSimplePath = false;
        native_lineTo(mNativePath, x, y);
    }
    
    //从当前位置,向目标位置画一条直线,该方法使用相对于当前位置的相对坐标
    public void rLineTo(float dx, float dy) {
        isSimplePath = false;
        native_rLineTo(mNativePath, dx, dy);
    }
    

    当前位置:当前位置指的是最后一次盗用Path的方法的终点位置,初始原点为(0, 0)

    这里说到当前位置,我们再提一个方法Path.moveTo(float x, float y),它可以移动当前位置到一个新的位置。

    举例

    ![image.png](https://img.haomeiwen.com/i1067252/b461f9836207c9ac.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/250
    ![Upload bezier_wave.gif failed. Please try again.]
    )

    paint.setStyle(Paint.Style.STROKE);
    paint.setStrokeWidth(5);
    path.lineTo(300, 400);// 由当前位置 (0, 0) 向 (300, 400) 画一条直线
    path.rLineTo(400, 0);// 由当前位置 (300, 400) 向正右方400像素的位置画一条直线
    canvas.drawPath(path, paint);
    

    贝塞尔曲线

    贝塞尔曲线:贝塞尔曲线是几何上的一种曲线。它通过起点、控制点和终点来描述一条曲线,主要用于计算机图形学。简单来说,贝塞尔曲线就是将任意一条曲线转换为精确的数学公式。

    在贝塞尔曲线中,有两类点:

    • 数据点:一般指一条路径的起点与终点。
    • 控制点:控制点决定了路径的弯曲轨迹,根据控制点的个数,贝塞尔曲线分为:一阶贝塞尔曲线(0个控制点),二阶贝塞尔曲线(1个控制点),三阶贝塞尔曲线(2个控制点)等。

    一阶贝塞尔曲线

    B(t)为时间为t时的坐标,P0为起点,P1为终点。

    二阶贝塞尔曲线

    三阶贝塞尔曲线

    贝塞尔曲线的模拟可以使用bezier-curve

    我们再来看看Path类提供的关于贝塞尔曲线的方法。

    
    //二阶贝塞尔曲线,绝对坐标,(x1, y1)表示控制点,(x2, y2)表示终点
    public void quadTo(float x1, float y1, float x2, float y2) {
        isSimplePath = false;
        native_quadTo(mNativePath, x1, y1, x2, y2);
    }
    
    //二阶贝塞尔曲线,相对坐标
    public void rQuadTo(float dx1, float dy1, float dx2, float dy2) {
        isSimplePath = false;
        native_rQuadTo(mNativePath, dx1, dy1, dx2, dy2);
    }
    
    //三阶贝塞尔曲线,绝对坐标,(x1, y1)、(x2, y2)表示控制点,(x3, y3)表示终点
    public void cubicTo(float x1, float y1, float x2, float y2,
                        float x3, float y3) {
        isSimplePath = false;
        native_cubicTo(mNativePath, x1, y1, x2, y2, x3, y3);
    }
    
    //三阶贝塞尔曲线,相对坐标
    public void rCubicTo(float x1, float y1, float x2, float y2,
                         float x3, float y3) {
        isSimplePath = false;
        native_rCubicTo(mNativePath, x1, y1, x2, y2, x3, y3);
    }
    

    我们来用贝塞尔曲线实现一个波浪效果。

    举例

    /**
     * 控制点的X坐标不断左右移动,形成波浪效果。
     * <p>
     * For more information, you can visit https://github.com/guoxiaoxing or contact me by
     * guoxiaoxingse@163.com
     *
     * @author guoxiaoxing
     * @since 2017/9/11 下午6:11
     */
    public class WaveView extends View {
    
        private static final String TAG = "WaveView";
    
        /**
         * 波浪从屏幕外开始,在屏幕外结束,这样效果更真实
         */
        private static final float EXTRA_DISTANCE = 200;
    
        private Path mPath;
        private Paint mPaint;
    
        /**
         * 控件宽高
         */
        private int mWidth;
        private int mHeight;
    
        /**
         * 控制点坐标
         */
        private float mControlX;
        private float mControlY;
    
        /**
         * 波浪峰值
         */
        private float mWaveY;
    
        /**
         * 是否移动控制点
         */
        private boolean mMoveControl = true;
    
        public WaveView(Context context) {
            super(context);
            init();
        }
    
        public WaveView(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
            init();
        }
    
        public WaveView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            init();
        }
    
        @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
            super.onSizeChanged(w, h, oldw, oldh);
            mWidth = w;
            mHeight = h;
    
            mControlY = mHeight - mHeight / 8;
            mWaveY = mHeight - mHeight / 32;
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
    
            //波浪从屏幕外开始,效果更真实
            mPath.moveTo(-EXTRA_DISTANCE, mWaveY);
            //二阶贝塞尔曲线
            mPath.quadTo(mControlX, mControlY, mWidth + EXTRA_DISTANCE, mWaveY);
            //闭合曲线
            mPath.lineTo(mWidth, mHeight);
            mPath.lineTo(0, mHeight);
            mPath.close();
            canvas.drawPath(mPath, mPaint);
    
            //mControlX坐标在 -EXTRA_DISTANCE ~ mWidth + EXTRA_DISTANCE 范围内,先自增再自减,左右移动
            //形成波浪效果
            if (mControlX <= -EXTRA_DISTANCE) {
                mMoveControl = true;
            } else if (mControlX >= mWidth + EXTRA_DISTANCE) {
                mMoveControl = false;
            }
            mControlX = mMoveControl ? mControlX + 20 : mControlX - 20;
    
            //水面不断上升
            if (mControlY >= 0) {
                mControlY -= 2;
                mWaveY -= 2;
            }
    
            Log.d(TAG, "mControlX: " + mControlX + " mControlY: " + mControlY + " mWaveY: " + mWaveY);
    
            mPath.reset();
            invalidate();
        }
    
    
        private void init() {
            mPath = new Path();
            mPaint = new Paint();
            mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
            mPaint.setColor(Color.parseColor("#4CAF50"));
        }
    }
    

    弧线

    
    //画弧线
    public void arcTo(float left, float top, float right, float bottom, float startAngle,
            float sweepAngle, boolean forceMoveTo) {
        isSimplePath = false;
        native_arcTo(mNativePath, left, top, right, bottom, startAngle, sweepAngle, forceMoveTo);
    }
    

    我们来看看这个方法的参数:

    • float left, float top, float right, float bottom:左、上、右、下的坐标。
    • float startAngle:弧形起始角度,Android坐标系x轴正右的方向是0度的位置,顺时针为正角度,逆时针为负角度。
    • float sweepAngle:弧形划过的角度。
    • boolean forceMoveTo):是否留下移动的痕迹file

    注:可以发现,这个方法与同样用来画弧线的方法Canvas.drawArc()少了个boolean useCenter参数,这是因为arcTo()方法只用来画弧线。

    4.3 辅助设置和计算

    public void setFillType(FillType ft) - 设置填充方式

    方法用来设置填充方式,填充的方式有四种:

    • WINDING:non-zero winding rule,非零环绕数原则,
    • EVEN_ODD:even-odd rule,奇偶原则
    • INVERSE_WINDING:WINDING的反转
    • INVERSE_EVEN_ODD:EVEN_ODD的反转

    WINDING:non-zero winding rule,非零环绕数原则,该原则基于所有图形的绘制都有绘制方向(前面提到的Direction描述的顺时针与逆向时针),对于平面上的任意一点,向任意方向射出一条射线,射线遇到每个顺时针
    的交点则加1,遇到逆时针的交点则减1,最后的结果如果不为0,则认为该点在图形内部,染色。如果结果为0,则认为该点在图形外部,不染色。

    EVEN_ODD:even-odd rule,奇偶原则,对于平面上的任意一点,向任意方向射出一条射线,这条射线与图形相交(不是相切)的次数为奇数则说明这个点在图形内部,则进行染色。若为偶数则认为在图形外部,不进行染色。
    这是一中交叉染色的情况。

    相关文章

      网友评论

      本文标题:Android显示框架:自定义View实践之绘制篇

      本文链接:https://www.haomeiwen.com/subject/xyhssxtx.html