114510t5k5dkueuqqikfi7.png
114510gvkjq4q4l4skg8qm.png
  可以看出,两图中的形状都可以认为是由两条平滑曲线(贝塞尔曲线)构成的。

  (本篇不深入贝塞尔曲线,大家只要知道贝塞尔曲线由起点、终点和N个控制点决定就好)

  假设蓝线和红线都以顶部为起点,以底部为终点,

  动画过程,其实就是两条曲线的起点下移,终点不动,控制点适当变化的过程。

  结合前面所说的,我们可以得到初步的方案:

  一套绘制代码:绘制两条贝塞尔曲线

  动画:贝塞尔曲线的起点、终点、控制点随progress值变化

  大思路有了,

  但是贝塞尔曲线的起点、终点、控制点是如何随progress值变化,才能实现不规则变形呢?

  对我而言,这个问题还是太复杂了,

  觉得复杂, 接着分解。

  规则的东西实现起来,总会简单一些,我们先想一想,如何实现规则变形,

  打破规则,也不难,我们在规则变形的基础上,破坏一些规则变形的条件,应该就能实现不规则变形。

  在进行下一步的思考之前,我们要先处理一个问题,

  上述思路,分析是合理的,但存在一个技术问题:两条贝塞尔曲线,是没法完美模拟一个圆的(没有深入调研,有兴趣的同学请搜索“贝塞尔曲线拟合圆”)。

  目前的结论是,四条贝塞尔曲线可以比较完美的模拟一个圆。

  所以我们的方案调整一下,如下图:

114510d9zsurqs9w99vgvj.png
  为了让大家看的更清晰,我给形状加上辅助点和辅助线(p.s. 辅助点和辅助线的思路来自Kitten的A-GUIDE-TO-iOS-ANIMATION),如下图:

1145101nzkvkavunkrlpaq.png
  每条曲线的起点、终点和两个控制点,应该比较清晰了。

  在处理变形之前,我们先看下,四条贝塞尔曲线怎么模拟出一个圆,如图:

114511s362g62s21vl336a.png 贝塞尔曲线拟合圆
  有兴趣的同学可以去找下相关的数学知识,可以搜“贝塞尔曲线拟合圆”。

  此处我们直接引用别人的结论,如图所示,第一个控制点和起点在连线与圆相切方向上,距离为半径r的1/1.8,第二个控制点和终点也是类似的。

  代码中定义的下述常量,大家就知道是什么意思了:

 

 

  1. let controlPointFactor: CGFloat = 1.8
复制代码
  圆模拟出来了,现在我们来看一下如何规则变形,

  简化一下,先考虑竖直方向的变形,我们以圆的底部为原点(0, 0),竖直变形,可以认为是各曲线的起点、终点和有需要的控制点的y坐标均乘于一个系数,本例中取0.8(竖直方向压扁),那么变形如下图:

none.gif 只竖直方向变形
  水平方向也类似,假设x方向系数为1.2(水平方向拉长),那么变形如下图:

none.gif 只水平方向变形
  两者结合起来就得到了圆的规则变形,如图(本篇中的规则变形可以认为是对称变形,圆未必变成了数学意义上的椭圆):

none.gif
  规则变化实现了,接下来就该破坏规则变形的条件了。

  大家跑的一样快,队形很整齐,想破坏队形,只要让一个人跑的比大家快或慢就行了。

  我们的动效中是顶部变形更明显,

  所以,我们让顶点y方向乘的系数小于0.8就可以了,也就说,顶点相对于其他点,y值变化的幅度更大,比0.8时的位置更接近原点(底点),如图:

none.gif
  至此,我们的效果就实现了。

  发散一下,

  顶点跑的慢:

none.gif
  左点不向左跑,反而向右跑:

none.gif
  不多举例了,大家可以看到,这种方案还是比较灵活的。

  复杂的形状可以由更多的贝塞尔曲线组成,只要我们找到贝塞尔曲线的起点、终点、控制点和progress的关系,就可以实现复杂可控的形状动画。

  具体代码实现,和本系列主线第一篇是类似的,采用的重绘方案,示意代码如下:

 

 

  1. // 创建CALayer子类
  2. class CircleIrregularTransformLayer: CALayer
  3.  
  4. // progress变化时,告知layer重绘自己
  5. override static func needsDisplayForKey(key: String) -> Bool {
  6. switch key {
  7. case progress:
  8. return true
  9. default:
  10. break
  11. }
  12.  
  13. return super.needsDisplayForKey(key)
  14. }
  15.  
  16. // 绘制代码
  17. override func drawInContext(ctx: CGContext) {
  18. let path = UIBezierPath()
  19.  
  20. // 以底点为原点
  21. let bottom = ...
  22. // 控制点偏移距离
  23. let controlOffsetDistance = radius / 1.8
  24.  
  25. // 各点变化系数
  26. let xFactor = ... // 根据progress计算
  27. let yFactor = ... // 根据progress计算
  28. // 顶点特殊的变化系数(破坏规则变形)
  29. let topYFactor = ... // 根据progress计算
  30.  
  31. // 右上弧
  32. path.addCurveToPoint(dest0, controlPoint1: control0A, controlPoint2: control0B)
  33.  
  34. // 左上弧
  35. path.addCurveToPoint(dest1, controlPoint1: control1A, controlPoint2: control1B)
  36.  
  37. // 左下弧
  38. path.addCurveToPoint(dest2, controlPoint1: control2A, controlPoint2: control2B)
  39.  
  40. // 右下弧
  41. path.addCurveToPoint(dest3, controlPoint1: control3A, controlPoint2: control3B)
  42.  
  43. CGContextAddPath(ctx, path.CGPath)
  44.  
  45. CGContextSetLineWidth(ctx, lineWidth)
  46. CGContextSetStrokeColorWithColor(ctx, UIColor.blueColor().CGColor)
  47. CGContextStrokePath(ctx)
  48.  
  49. // 辅助点
  50.  
  51. // 辅助线
  52. }
复制代码
  大家在看代码的时候,可能感觉各点的计算和文中提到的不完全一致,

  文中侧重思路,是以底点为坐标系原点(0, 0)、常规坐标系(x轴向右为正方向,y轴向上为正方向)来描述的,

  而代码中实现时,会使用UIKit的坐标系,底点在superView的坐标系中也不会是(0, 0),

  因此,请放心看代码,思路是一样的,不一样的只是实现上的细节。

  相关阅读:

  一款Loading动画的实现思路(一):复杂任务的拆分

  一款Loading动画的实现思路(二):stroke方案

  一款Loading动画的实现思路(三):利用经验分解动画

  总结:一款Loading动画的实现思路

锐亚教育

锐亚教育,游戏开发论坛|游戏制作人|游戏策划|游戏开发|独立游戏|游戏产业|游戏研发|游戏运营| unity|unity3d|unity3d官网|unity3d 教程|金融帝国3|8k8k8k|mcafee8.5i|游戏蛮牛|蛮牛 unity|蛮牛