美文网首页iOS Developer - AnimationiOS动画控件封装
Loading动画外篇·圆的不规则变形

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

作者: 柯烂 | 来源:发表于2016-01-10 23:29 被阅读3565次

    一款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。

    本系列的�传送门

    鸣谢及推荐

    相关链接

    相关文章

      网友评论

      • 若末lan:望而却步,学AE,直接做GIF:joy:
      • visual_:Cool
      • d9557f883fd8:写动画是慢活啊,计算,调效果。。。这效果我以前看到过,写了一半实在算的烦放弃了,哈哈,楼主可以的。明天好好看看
        柯烂:@zhnnnnn 是啊哈哈
      • Smiling雨花石:内容很详细,作者用心了,,, :+1:
      • 畅雨潇潇:5篇文章全部耐心的看完了,作者很耐心,很细致,学到很多东西,现在也在做iOS,请问作者一般用什么沟通,我还有一些问题需要请教。
        柯烂:@畅雨潇潇 谢谢鼓励 ,可以发我的邮箱 ,一起进步,为防止广告邮件,我私信给你 :smiley:
      • 07d93406ec39:請問有rss或blog來訂閱嗎?
        柯烂:@漩渦貓_Lanaya 你好,暂时没有 :smiley: 刚开始写字,还没有建立自己的blog
      • 90f6592757ae:谢谢!一样看到更多的这种技术贴
        柯烂:@Nerostone 谢谢 :smile: 大家一起进步
      • 昊囧:作者很耐心啊,希望能多发一点动画的教程
        点赞!
        柯烂:@昊囧 谢谢鼓励 :smiley: 也在学习的路上,会努力多更新
      • Fe_Zn:看来要学动画,数学知识还是必不可少哈,点赞!!
        柯烂:@JsonChan 谢谢,数学知识可能不是必须,但还是有帮助的:smiley:
      • Shumin_Wu:支持
        柯烂:@Shumin_Wu 谢谢 :smiley:

      本文标题:Loading动画外篇·圆的不规则变形

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