美文网首页IT@程序员猿媛半栈工程师Android开发
Canvas中的裁剪师讲解与实战——Android高级UI

Canvas中的裁剪师讲解与实战——Android高级UI

作者: 9dfaf364d57f | 来源:发表于2019-04-27 12:00 被阅读38次

    目录
    一、前言
    二、如何画图
    1、绘图坐标系
    2、视图坐标系
    3、小结
    三、Canvas的剪刀手API
    1、clipPath
    2、clipOutPath
    3、clipPath
    四、实战
    五、写在最后

    一、前言

    从今天开始我们聊一聊 Canvas 的API,因为Canvas的API较多,所以我们分为几次分享,首先分享的是裁剪类型的API使用。话不多说,先上实战图。

    老夫的少女心

    源码地址文末会给出,了解原理才能更好地驾驭。

    二、如何画图

    分享前,我们先来聊聊,在我们生活中如何绘制一张如下的图。



    我们需要两样东西来绘制:

    1. 一张纸(Android 中的 canvas):用来承载我们绘制的内容。
    2. 一支笔(Android 中的 paint):负责绘制内容的轨迹。

    有了这两样,我们就能在现实的场景中开始绘制了。

    1、绘图坐标系

    但在 Android 的体系中,我们所谓的 “笔Paint” 和 “纸Canvas” 都是由App持有的,所以我们在绘制时就出现一个问题:我们怎么“告诉”App,确定我们想要绘制图形的落笔点?当然需要一个坐标系来进行交流。

    而这个 坐标系 便是我们经常所说的 绘图坐标系。初始状态下,Canvas的左上角为原点,如下图的蓝色点所示。此时我们想画图中的红点,就非常的容易,只需要“告诉” App 在坐标(200,500)处画一个红点,这就达到了画图的效果了。
    所以我们可以明确的一点是 我们所有的画图坐标都是根据原点进行确定。


    所以我们可以移动原点,达到整体坐标点的移动,例如还是画刚才的红点,我们可以先将原点水平移动100,垂直移动400。然后在进行绘制,这时红点的坐标就变为(100,100),具体如下图所示。

    经过上面的简单讲述,我们可以知道,绘图过程中,我们的绘图坐标永远是跟随当前的原点,而画布的原点可以进行移动。

    2、视图坐标系

    理论上 Canvas 这张纸是没有边界的,但是我们的手机屏幕是有界的。我们可以理解为我们透过一个方形的洞(手机屏幕)看一张巨画(Canvas)。

    而这里我们就又存在一个问题了,因为刚才的移动,我们是移动的原点,也就是说我们的画布是静止不动的,只是落笔点一直在变动,这就导致我们绘制的图对于用户来说是看不全的,所以我们需要进行移动 方形的洞 来查看这幅画。

    举个例子,我们要查看最开始所说的画,可以通过移动 Screen框来查看这幅画,而这里又出现了一个坐标系,这一坐标系则为 视图坐标系,通过 scrollerToscrollerBy 进行移动该Screen框,正数则往正半轴,负数则往负半轴。

    3、小结

    自定义控件中存在两个坐标系需要明确,用一句话总结如下:

    1. 绘图坐标系:决定我们的绘制的坐标
    2. 视图坐标系:决定我们所看到的画布范围

    三、Canvas的剪刀手API

    Canvas 中以 clip开头 的公有方法,用于裁剪画布的内容。 我们抽取比较好玩的参数类型为Path的方法来分享,其余的都可以一一映射进来。

    1、clipPath

    public boolean clipPath(@NonNull Path path)
    

    描述: 只留下 path内 的画布区域,而处于path范围之外的则不显示。

    举个例子:
    我们先准备好一个心形的路径Path,然后调用 clipPath 从画布中将此路径内的区域 “裁剪” 下来,最后为了我们观察,使用drawColor “染”上酒红色。

    // 第一步:创建 心形路径 mPath
    ....省略,具体请移步github
    
    // 第二步:从画布 canvas 裁剪下心形路径之内的区域
    canvas.clipPath(mPath);
    
    // 第三步:涂酒红色
    canvas.drawColor(mBgColor);
    

    如果想了解如何绘制心形轨迹,请移步小盆友的另一篇博文:自带美感的贝塞尔曲线原理与实战

    效果图


    此类型的方法还有以下这几个,但他们的裁剪范围均为矩形
    public boolean clipRect(float left, float top, float right, float bottom)
    public boolean clipRect(int left, int top, int right, int bottom)
    public boolean clipRect(@NonNull Rect rect)
    public boolean clipRect(@NonNull RectF rect)
    

    2、clipOutPath

    public boolean clipOutPath(@NonNull Path path)
    

    描述: 只留下 path外 的画布区域,而处于path范围之内的则不显示。(与clipPath的作用范围正好相反)

    值得注意的是,该方法只能在API26版本以上调用。 低版本我们使用下一小节介绍的方法

    举个例子:

    我们先准备好一个心形的路径Path,然后调用 clipOutPath 从画布中将此路径之外的区域 “裁剪” 下来,最后为了我们观察,使用 drawColor “染”上酒红色。

    // 第一步:创建 心形路径 mPath
    ....省略,具体请移步github
    
    // 第二步:从画布 canvas 裁剪下心形路径之外的区域
    canvas.clipOutPath(mPath);
    
    // 第三步:涂酒红色
    canvas.drawColor(mBgColor);
    

    效果图

    此类型的方法还有以下这几个,但他们的裁剪范围均为矩形

    public boolean clipOutRect(float left, float top, float right, float bottom)
    public boolean clipOutRect(int left, int top, int right, int bottom)
    public boolean clipOutRect(@NonNull Rect rect)
    public boolean clipOutRect(@NonNull RectF rect)
    

    3、clipPath

    public boolean clipPath(@NonNull Path path, @NonNull Region.Op op)
    

    描述: 在画布上进行使用 path 路径进行操作,至于其作用由 op 决定。

    描述比较抽象,我们通过例子来体会。但在上例子前,我们需要先了解下 Region.Op 这个枚举类型,具体内容代码如下

    public enum Op {
        // A: 为我们先裁剪的路径
        // B: 为我们后裁剪的路径
    
        // A形状中不同于B的部分显示出来
        DIFFERENCE(0),
        // A和B交集的形状
        INTERSECT(1),
        // A和B的全集
        UNION(2),
        // A和B的全集形状,去除交集形状之后的部分
        XOR(3),
        // B形状中不同于A的部分显示出来
        REVERSE_DIFFERENCE(4),
        // 只显示B的形状
        REPLACE(5);
    
        // ...省略不相关代码
    }
    

    通过源码可以知道共有六种类型。值得一提的有以下两点:

    1)clipOutPath 方法中使用的类型就是 DIFFERENCE,换而言之,我们可以使用以下代码代替,解决在API26 以下无法使用的问题clipOutPath 方法的问题

    clipPath(mPath, Region.Op.DIFFERENCE)
    

    2)clipPath 方法中使用的类型就是 INTERSECT,换而言之,我们可以使用以下代码代替

    clipPath(mPath, Region.Op.INTERSECT)
    

    举些例子:

    接下来我们一个个讲解这六种类型,两次裁剪比较能体现出 Region.Op 参数的作用,所以我们接下来的例子需要使用两个路径:

    1、心形路径 (下列例子中的 A


    2、圆路径(下列例子中的 B

    (1)DIFFERENCE

    描述: A形状中不同于B的部分显示出来

    效果图: 红色即为最终裁剪留下区域

    (2)INTERSECT

    描述: A和B交集的形状

    效果图: 红色即为最终裁剪留下区域

    (3)UNION

    描述: A和B的全集

    效果图: 红色即为最终裁剪留下区域

    (4)XOR

    描述: A和B的全集形状,去除交集形状之后的部分

    效果图: 红色即为最终裁剪留下区域

    (5)REVERSE_DIFFERENCE

    描述: B形状中不同于A的部分显示出来

    效果图: 红色即为最终裁剪留下区域

    (6)REPLACE

    描述: 只显示B的形状

    效果图: 红色即为最终裁剪留下区域


    此类型的方法还有以下这几个,但他们的 裁剪范围均为矩形
    public boolean clipRect(float left, float top, float right, float bottom,
                @NonNull Region.Op op) 
    public boolean clipRect(@NonNull Rect rect, @NonNull Region.Op op)
    public boolean clipRect(@NonNull RectF rect, @NonNull Region.Op op)
    

    四、实战

    上一小节我们已经了解了这几些API的作用就是裁剪,这小节我们就把它使用起来。

    老夫的少女心

    效果图

    Github入口:传送门

    编码思路

    我们借助下面这张小盆友手绘的思路图(看看能不能达到一图胜千言😄)

    这里为了视觉效果易于讲解,红色即为我们demo中的粉色,蓝色即为我们demo中青色,橘色就是最终的渐变色

    第一步(绿色心形部分): 我们先在画布裁剪下心形区域,这就奠定了最后呈现给用户所看到的画布区域为一个“心”。

    第二步(红色部分): 我们用将画布染成红色,然后在画布的中心用蓝色写上 “猛猛的小盆友” ,最后使用图中红色框(即上边是横线,下边是用贝塞尔曲线绘制的Path红色区域)将画布的上半部分裁剪下来,放置最终呈现的画布中。

    第三步(蓝色部分): 与第二步正好相反,我们用将画布染成蓝色,然后在画布的中心用红色写上 “猛猛的小盆友” ,最后使用图中蓝色框(即上边是用贝塞尔曲线绘制,下边是横线的Path懒色区域)将画布的下半部分裁剪下来,放置最终呈现的画布中。

    第四步: 经过前三步,我们的图案已经形成了右边的图像。我们开启动画,其实就是控制中间贝塞尔曲线的y轴坐标,令其从底部上升至顶部,则呈现出了灌满心形的动画效果,所以我们可以通过让画布偏移一定的值达到该效果,同时让贝塞尔曲线做水平的运动,有一种波动感。

    核心代码

    // 第一步
    canvas.clipPath(mHeartPath);
    
    // ======== 第二步start ==============
    canvas.save();
    
    // 第四步
    canvas.translate(-mCurOffset, mCurPos);
    
    canvas.clipPath(mTopPath);
    mPaint.setColor(mTopBgColor);
    mPaint.setStyle(Paint.Style.FILL);
    canvas.drawPath(mTopPath, mPaint);
    
    canvas.translate(mCurOffset, -mCurPos);
    drawText(canvas, mBottomBgColor);
    canvas.restore();
    // ======== 第二步end ==============
    
    // ======== 第三步start ==============
    canvas.save();
    
    // 第四步
    canvas.translate(-mCurOffset, mCurPos);
    
    canvas.clipPath(mBottomPath);
    mPaint.setColor(ContextCompat.getColor(getContext(), R.color.canvas_light_blue_color));
    mPaint.setStyle(Paint.Style.FILL);
    canvas.drawPath(mBottomPath, mPaint);
    
    canvas.translate(mCurOffset, -mCurPos);
    drawText(canvas, mTopBgColor);
    canvas.restore();
    // ======== 第三步end ==============
    

    五、写在最后

    Canvas 中的API挺多,涉及的小知识也比较零碎,本来想在一篇文章中分享完所有的API,但写的过于宽泛,纠结再三,小盆友最终还是选择回归初心,按照自己的理解分享好每个知识点,将canvas的分享拆分为几次。如果觉得文章对你有所启发,请给我个赞吧,如果发现有那些欠妥的地方,请留言区与我讨论,我们共同进步。

    高级UI系列的Github地址:请进入传送门,如果喜欢的话给我一个star吧😄

    欢迎加我微信,我们可以进行更多更有趣的交流


    相关文章

      网友评论

        本文标题:Canvas中的裁剪师讲解与实战——Android高级UI

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