Loading动画外篇·圆的不规则变形

一款Loading动画的实现思路系列已经结束了,非常感谢大家的捧场。

看过本系列的同学可能还记得,我对原动效做了简化,
为了让大家回忆一下,也让新来的同学有点印象,我先贴一下原动画效果图:

可以看到,圆被上方的竖线压扁的时候,发生了不规则的变形,
具体来说,圆的顶部比底部变形明显。

这个很好理解,我们把球放到地上,拿手指去按它,手指按下的地方,肯定要比球和地面接触的地方变形更明显。

在Loading系列中我做了简化,圆只是简单的变成了椭圆,如下图:

虽然效果也不错,但还是有点遗憾,
所以今天我们一起看一下,圆的不规则变形的一种实现方案,
效果如图:

好,我们开始吧。

看上去,这个动画就是从一个形状变成了另一个形状,
熟悉CAShapeLayer的同学,可能想到了它的path属性,没错,path属性是支持动画的,
那我们用UIBezierPath分别画出动画初始、结束的形状,作为path动画的from、to值,应该就可以了吧。

思路看上去没有问题,我们来测试一下,示意代码如下:
(p.s. 从本篇开始,我在文章示例中使用swift代码,GitHub上会上传swift、OC两个版本)

@IBAction func startAnimation(sender: AnyObject) {
    // reset
    animationLayer.removeAllAnimations()

    // 初始
    let fromPath = ... // 圆
    // 结束
    let toPath = ... // 圆变形后的形状

    // end status
    animationLayer.path = toPath.CGPath

    // animation
    let animation = CABasicAnimation(keyPath: "path")
    animation.duration = 3
    animation.fromValue = fromPath.CGPath
    animation.toValue = toPath.CGPath
    animationLayer.addAnimation(animation, forKey: nil)
}

测试之前我们最好有个用例,或者有个非正式的预期

比如我对这段代码的预期是这样的:

测试一下,
结果是这样的:

很明显,测试结果和我们的预期不一样;
由此,我们得出一个不严谨的结论:path动画的效果是不可控的。

也许有的同学会说,换一种绘制方式,动画效果可能就达到要求了,
这是可能的,大家可以试一试,
但本篇中,我就不去猜怎么绘制才能达到要求了,我尝试找一个可控的方案。

所谓可控,就是动画的每一步,形状的样子我们都知道。

如果我们能建立起形状和动画进度(用progress代替,取值0.0~1.0)的关系,那么progress变化时,我们重绘形状,应该就可以了。

思考一下,形状和progress建立关系的难点在哪?

初始我们绘制了一个圆,结束时我们绘制了一个不规则的形状,
它们的绘制逻辑是不一样的,从代码层面讲,它们各自有一套绘制代码。

两套绘制代码,听着不太符合直觉。

比较符合直觉的是,我们只有一套绘制代码,progress是这套代码的参数,progress为0时,绘制的是圆,progress为1时,绘制的是不规则图形。

一套代码可以做到吗?
可以的,前提是我们要将形状进行分解
看上去不一样的东西,经过分解后,很可能发现共同点

请看下面的两张图:

可以看出,两图中的形状都可以认为是由两条平滑曲线(贝塞尔曲线)构成的。
(本篇不深入贝塞尔曲线,大家只要知道贝塞尔曲线由起点、终点和N个控制点决定就好)

假设蓝线和红线都以顶部为起点,以底部为终点,
动画过程,其实就是两条曲线的起点下移,终点不动,控制点适当变化的过程。

结合前面所说的,我们可以得到初步的方案:
一套绘制代码:绘制两条贝塞尔曲线
动画:贝塞尔曲线的起点、终点、控制点随progress值变化

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

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

觉得复杂, 接着分解

规则的东西实现起来,总会简单一些,我们先想一想,如何实现规则变形,
打破规则,也不难,我们在规则变形的基础上,破坏一些规则变形的条件,应该就能实现不规则变形。

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

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

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


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

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

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


贝塞尔曲线拟合圆

有兴趣的同学可以去找下相关的数学知识,可以搜“贝塞尔曲线拟合圆”。
此处我们直接引用别人的结论,如图所示,第一个控制点和起点在连线与圆相切方向上,距离为半径r的1/1.8,第二个控制点和终点也是类似的。

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

let controlPointFactor: CGFloat = 1.8

圆模拟出来了,现在我们来看一下如何规则变形,

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

只竖直方向变形

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


只水平方向变形

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


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

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

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


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

发散一下,

顶点跑的慢:


左点不向左跑,反而向右跑:


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

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

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

// 创建CALayer子类
class CircleIrregularTransformLayer: CALayer

// progress变化时,告知layer重绘自己
override static func needsDisplayForKey(key: String) -> Bool {
    switch key {
    case "progress":
        return true
    default:
        break
    }

    return super.needsDisplayForKey(key)
}

// 绘制代码
override func drawInContext(ctx: CGContext) {
    let path = UIBezierPath()

    // 以底点为原点
    let bottom = ...
    // 控制点偏移距离
    let controlOffsetDistance = radius / 1.8

    // 各点变化系数
    let xFactor = ... // 根据progress计算
    let yFactor = ... // 根据progress计算
    // 顶点特殊的变化系数(破坏规则变形)
    let topYFactor = ... // 根据progress计算

    // 右上弧
    path.addCurveToPoint(dest0, controlPoint1: control0A, controlPoint2: control0B)

    // 左上弧
    path.addCurveToPoint(dest1, controlPoint1: control1A, controlPoint2: control1B)

    // 左下弧
    path.addCurveToPoint(dest2, controlPoint1: control2A, controlPoint2: control2B)

    // 右下弧
    path.addCurveToPoint(dest3, controlPoint1: control3A, controlPoint2: control3B)

    CGContextAddPath(ctx, path.CGPath)

    CGContextSetLineWidth(ctx, lineWidth)
    CGContextSetStrokeColorWithColor(ctx, UIColor.blueColor().CGColor)
    CGContextStrokePath(ctx)

    // 辅助点

    // 辅助线
}

大家在看代码的时候,可能感觉各点的计算和文中提到的不完全一致,
文中侧重思路,是以底点为坐标系原点(0, 0)、常规坐标系(x轴向右为正方向,y轴向上为正方向)来描述的,
而代码中实现时,会使用UIKit的坐标系,底点在superView的坐标系中也不会是(0, 0),
因此,请放心看代码,思路是一样的,不一样的只是实现上的细节。

本篇作为一款Loading动画系列的补充,到这就这结束了,非常感谢大家的捧场!

大家,下个系列见。

完整代码

请参考GitHub上OneLoadingAnimation工程中Swift、OC目录下的CircleIrregularTransform。

本系列的�传送门

鸣谢及推荐

相关链接

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,400评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,016评论 4 62
  • (一) 我是一条活在海里的鱼 存在的意义是在海里遨游 他说,你好自由 你拥有辽阔无边的海洋 他说,你好幸福 生来,...
    梓杨说阅读 281评论 0 0
  • 我大概是去年十一之后入手的kindle,希望自己督促自己利用碎片时间多看书,刚才突然想起算算入手也有半年了,这半年...
    适说心语阅读 340评论 0 0