一款Loading动画的实现思路(二):stroke方案
感谢大家对本系列第一篇的关注和肯定,这让我更有动力和信心与大家分享心得。现在,本系列的第二篇正式开始。
有些同学可能是第一次阅读本系列文章,没看过之前的内容。这里我贴出动画的整体效果(我实现的简单版本),原动效及前阶段动效的实现,请点击本系列第一篇:一款Loading动画的实现思路(一):复杂任务的拆分。
阶段回顾与新阶段介绍
先来回顾一下,第1阶段的最终效果如下(此处应插入第1阶段最终效果的图片)。
现在进入第2阶段,其效果如下图所示(为了清晰展示,我将之前阶段设置为灰色正常速度播放,而当前阶段设置为蓝色慢速播放)(此处应插入第2阶段效果的图片)。
看到这个运动的小蓝条,大家应该能发现,它是一段起点和终点不停变化的弧。看过本系列第一篇的同学可能会想到,完全可以用第1阶段重绘弧的方式来实现这个效果。大家可以对比下面两张图,前者是第1阶段,后者是第2阶段(此处应插入第1阶段和第2阶段对比的图片)。
图中蓝色部分是我们的弧,由于弧较短,我们用灰色的线来表示弧的运动轨迹,这样能更清晰地观察。虽然具体的节点数值未标出,但可以看出,第2阶段可以通过重绘弧的方式实现,而且看上去比第1阶段还要简单一些。
如果在实际项目中,为了降低项目的维护成本,减少Bug出现的可能性,同时降低项目中新人的学习成本,第2阶段可能会选用重绘弧方案。毕竟方案越多,潜在的问题也可能越多。
然而,我们现在处于学习阶段,追求的是探索更多的实现方式,方案自然是越多越好。接下来,我们将换一种方式来实现第2阶段动画,有兴趣的同学可以自己尝试用重绘弧的方式实现,以加深印象。
换一种思路
请再次观察这张图(此处应插入相关图片)。我们可以这样理解,灰色部分是一条弧,弧OD只是它的一部分,这部分被涂上了蓝色。进一步说,如果将灰色改成透明,那么可以认为弧本身是透明的,我们将弧上O和D之间的部分涂上了蓝色。如果不断改变O和D的位置,弧上涂上蓝色部分的位置也会不断变化,看上去就像一段蓝色弧在移动。基于这个思路,我们可以提出一种新方案——stroke方案。
stroke方案
看到“stroke”,有过绘制经验的同学可能会联想到“strokeColor”“lineWidth”等概念,没错,就是这个“stroke”。没用过的同学也不用担心,后文会详细介绍。
我们可以将弧看作是一条路径(后文统一用“path”表示,例如第1阶段我们使用的“UIBezierPath”),“stroke”就是沿着“path”进行涂色。重绘弧的方案可以理解为,我们不断创建新的蓝色“path”,每条“path”的起点和终点不同,从而形成动画。而“stroke”方案则是,我们只创建一条透明的“path”,通过不断改变其涂蓝色的起点和终点来形成动画。
假设我们将涂蓝色的起点和终点分别命名为“SS(strokeStart)”和“SE(strokeEnd)”,并且用0 - 1之间的值来表示它们在“path”上的位置(例如0.1表示距离“path”起点的10%处)。下面我们用一条直线“path”来举例,依然用灰色代表透明部分,蓝色代表“stroke”部分,请看下图(此处应插入不同SS、SE取值下path样子的图片),观察SS、SE取值对“path”外观的影响。
图中展示了4种情况下,SS、SE取值形成的“path”的样子。接下来,我们通过几个示例来进一步说明SS、SE动态变化时“path”的效果:
- 假设SS = 0不变,SE从0变化到1。由于SS与SE之间的部分是要涂色的部分,看上去就是涂色的部分从“path”的起点开始,逐渐接近“path”的终点,直到到达终点,效果就是“path”从无到有(此处应插入相应的动画或图片)。
- 假设SE = 1不变,SS从0变化到1。初始时SS = 0,SE = 1,此时“path”整体被涂色。在动画过程中,SS逐渐变大,直到SS和SE重合为一个点。由于涂色范围是SS到SE之间,两者重合时就没有可涂色的部分了,效果就是“path”从有到无(此处应插入相应的动画或图片)。
- 假设SS从0变到0.9,SE从0.1变到1。在变化过程中,SS和SE的相对位置始终保持不变,即涂色的长度不变。这种情况下,看上去就像一段固定长度的蓝色部分在“path”上移动(此处应插入相应的动画或图片)。
再看第2阶段的效果图(此处应插入第2阶段效果图),可以发现,它与上述直线“path”的情况本质是一样的,只是这里是一段弧“path”的SS和SE在变化。假设第2阶段蓝色部分的长度是弧长(即“path”长度)的1/10,那么我们只需要找到这段弧的“path”,然后让它的SS从0变到0.9(9/10),SE从0.1(1/10)变到1即可,SS与SE始终相差0.1,也就是“path”长度的1/10。
接下来,我们需要找到这段弧。先复习一下画弧的API,画弧通常需要以下4个值:弧的圆心(arcCenter)、弧的半径(arcRadius)、开始角度(我们在前文中用O点代表)、结束角度(用D点代表)。
为了简化计算,我让弧的终点在圆正上方,且与圆顶部的距离正好是圆的半径。请看下图(此处应插入相关图片),灰色的圆是第1阶段动画形成的圆,蓝色的弧就是我们要找的弧。
图中左下方有一个“d”,代表x轴上弧圆心到圆左侧的距离。由此可以得出变量的表达式(代码如下,代码中的“kRadius”是小圆的半径):
// 此处应补充具体代码
可以看到,表达式中唯一未知的就是“d”。接下来思考“d”的表达式,图中红色的两条线都是弧的半径,所以长度相等。注意到左上的这条是一个直角三角形的斜边,因此可以得出下面的表达式(此处应补充表达式)。
思路已经明确,弧也找到了,现在可以开始写代码了。
写代码
这次我们要使用“CAShapeLayer”,它是“CALayer”的子类。“CAShapeLayer”有三个重要的属性(此处应插入展示属性的图片),这些属性正是我们所需要的。以下是实现代码:
// 此处应补充具体代码
代码逻辑清晰,而且代码量也不多。这说明现在的系统库和第三方库功能非常强大,只要有清晰的思路,实现起来通常不会太难。
发散思考
“stroke”方案非常适合处理沿“path”变化的动效。例如前文中的这张图(此处应插入相关图片),它其实是自定义进度条的雏形。如果“path”是直线,那就是直线进度条;如果是圆,那就是圆形进度条;如果是独特的形状,那就是独特的进度条。
此外,还有很多使用“CAShapeLayer”的“path”和“stroke”实现的动效。由于“path”具有无限可能性,因此“stroke”方案能实现很多炫酷的动效,比如动画写字效果,非常酷炫(此处应插入相关动画或图片)。