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

640?wx_fmt=gif


温馨提示

请拖动到文章末尾,长按识别「抽奖」小程序。现金红包等你来拿。


作者:猛猛的小盆友

链接:

https://juejin.im/post/5cc3d0686fb9a031f4160713

本文由作者授权发布。

1


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


前言


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


老夫的少女心


640?wx_fmt=gif


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


如何画图


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


640?wx_fmt=other


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


  1. 一张纸(Android 中的 canvas):用来承载我们绘制的内容。

  2. 一支笔(Android 中的 paint):负责绘制内容的轨迹。

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

1、绘图坐标系


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


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


640?wx_fmt=other

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

640?wx_fmt=other

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


2、视图坐标系


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


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


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


640?wx_fmt=other

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);

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

效果图


640?wx_fmt=other


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


 
 
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);

效果图



640?wx_fmt=other



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


 
 
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


640?wx_fmt=other


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


640?wx_fmt=other


(1)DIFFERENCE


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

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


640?wx_fmt=other


(2)INTERSECT


描述: A和B交集的形状

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


640?wx_fmt=other


(3)UNION


描述: A和B的全集

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


640?wx_fmt=other


(4)XOR


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

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


640?wx_fmt=other


(5)REVERSE_DIFFERENCE


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

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


640?wx_fmt=other



(6)REPLACE


描述: 只显示B的形状

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


640?wx_fmt=other


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


 
 
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的作用就是裁剪,这小节我们就把它使用起来。


老夫的少女心


效果图


640?wx_fmt=gif


源码地址:https://github.com/zincPower/UI2018


编码思路


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


640?wx_fmt=other


这里为了视觉效果易于讲解,红色即为我们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地址:


https://github.com/zincPower/UI2018


如果喜欢的话给我一个star吧?


推荐阅读


LinearLayout和RelativeLayout性能对比,你认为谁的效率更高?


Handler如何有效地避免内存泄漏?


640?wx_fmt=png长按识别小程序,参与抽奖

640?wx_fmt=png


更多现金红包,请长按二维码640?wx_fmt=other 640?wx_fmt=png

目前100000+人已关注加入我们

640?wx_fmt=gif 640?wx_fmt=gif 640?wx_fmt=gif 640?wx_fmt=gif 640?wx_fmt=gif 640?wx_fmt=gif 640?wx_fmt=gif 640?wx_fmt=gif

640?wx_fmt=gif 640?wx_fmt=gif 640?wx_fmt=gif 640?wx_fmt=gif 640?wx_fmt=gif 640?wx_fmt=gif 640?wx_fmt=gif 640?wx_fmt=gif


微信扫描参与抽奖


640?wx_fmt=png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值