iOS框架使用:Lottie 动画特效

原创:有趣知识点摸索型文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

目录

  • 一、Lottie 介绍
  • 二、Lottie 使用
  • 三、Lottie 优势
  • 四、Lottie 适用场景
  • 五、Lottie 使用注意

一、Lottie 介绍

Lottie 是一个可应用于Andriod和iOS的动画库,它通过bodymovin插件来解析Adobe After Effects 动画并导出为json文件,通过手机端原生的方式或者通过React Native的方式渲染出矢量动画。

这是前所未有的方式,设计师可以创作并且运行优美的动画而不需要工程师煞费苦心地通过手动调整的方式来重现动画。由于动画是通过json来加载的,使得动画源文件只需占用极小的空间就能完成相当复杂的效果!Lottie可以用于播放动画、调整尺寸、循环播放、加速、减速、甚至是精致的交互。

换句话说,你也可以通过设计器直接把JSON文件放入Xcode project,让Lottie帮你下载动画。当然别误会,你还是需要为你的动画写一些代码,但是你会发现Lottie的确将为你的动画编码节省大量的时间。

替补方案
  • 手动创建动画对于设计师以及iOS、Android工程师而言意味着付出巨额的时间。通常很难,甚至不可能证明花费这么多时间来获得动画是正确的。
  • Facebook Keyframes是专门用来构建用户界面的, 是FaceBook的一个很棒,很新的库。但是Keyframes不支持一些Lottie所能支持的特性,比如: 遮罩,蒙版,裁切路径,虚线样式还有很多。
  • Gifs占用的大小是bodymovin生成的JSON大小的2倍还多,并且渲染的尺寸是固定的,并不能放大来适应更大更高分辨率的屏幕。
  • Png序列桢动画 甚至比gifs更糟糕,它们的文件大小通常是bodymovin json文件大小的30-50倍,并且也不能被放大。

二、Lottie 使用

资源加载方式

最基本的方式是用AnimationView来使用它:

// someJSONFileName 指的就是用 AE 导出的动画本地 JSON 文件名
let animationView = AnimationView(name: "someJSONFileName")
    
// 可以使用 frame 也可以使用自动布局
animationView.frame = CGRect(x: 100, y: 100, width: 100, height: 100)
    
view.addSubview(animationView)
    
animationView.play { (isFinished) in
    
    // 动画执行完成后的回调
    // Do Something
}

如果你使用到了跨bundleJSON文件,你可以这么做:

let animationView = AnimationView(name: "someJSONFileName", bundle: YOUR_BUNDLE)

从本地支持的JSON文件加载Lottie动画的完整方法是:

/**
- Parameter name: JSON文件名.
- Parameter bundle: 动画所在的包.
- Parameter imageProvider: 加载动画需要的图片资源(有些动画需要图片配合【可以是本地图片资源,也可以是网络图片资源,实现该协议返回对应的CGImage】).
- Parameter animationCache: 缓存机制【需要自己实现缓存机制,Lottie本身不支持】).
*/
convenience init(name: String,
                      bundle: Bundle = Bundle.main,
                      imageProvider: AnimationImageProvider? = nil,
                      animationCache: AnimationCacheProvider? = LRUAnimationCache.sharedCache) { ... }

或者你可以使用磁盘路径和从服务器加载:

// 从磁盘路径加载动画
convenience init(filePath: String,
                          imageProvider: AnimationImageProvider? = nil,
                          animationCache: AnimationCacheProvider? = LRUAnimationCache.sharedCache) { ... }
// 从网络加载
convenience init(url: URL,
                  imageProvider: AnimationImageProvider? = nil,
                  closure: @escaping AnimationView.DownloadClosure,
                  animationCache: AnimationCacheProvider? = LRUAnimationCache.sharedCache) { ... }

填充模式

Lottie 支持iOS中的UIView.ContentModescaleAspectFit, scaleAspectFillscaleToFill这些属性。

let animationView = AnimationView(name: "someJSONFileName")
animationView.contentMode = .scaleToFill
...

播放控制

Lottie 动画的播放控制,除了常规的控制,还支持进度播放、帧播放。

播放、暂停、停止:

let animationView = AnimationView(name: "someJSONFileName")

// 从上一次的动画位置开始播放
animationView.play()
// 暂停动画播放
animationView.pause()
// 停止动画播放,此时动画进度重置为0
animationView.stop() 

控制进度播放:参考DEMO中的 FrameAnimationViewController

/**
 播放动画,进度(0 ~ 1).
 
 - Parameter fromProgress: 动画的开始进度。 如果是'nil`,动画将从当前进度开始。
 - Parameter toProgress: 动画的结束进度。
 - Parameter loopMode: 动画的循环行为。 如果是`nil`,将使用视图的`loopMode`属性。默认是 .playOnce
 - Parameter completion: 动画停止时要调用的可选完成闭包。
 */
public func play(fromProgress: AnimationProgressTime? = nil,
                 toProgress: AnimationProgressTime,
                 loopMode: LottieLoopMode? = nil,
                 completion: LottieCompletionBlock? = nil)
animationView.play(fromProgress: 0, toProgress: 1, loopMode: .playOnce) { (isFinished) in
    // 播放完成后的回调闭包
}
// 设置当前进度
animationView.currentProgress = 0.5

控制帧播放:参考DEMO中的 FrameAnimationViewController

/**
 使用帧的方式播放动画
 
 - Parameter fromProgress: 动画的开始进度。 如果是'nil`,动画将从当前进度开始。
 - Parameter toProgress: 动画的结束进度
 - Parameter loopMode: 动画的循环行为。 如果是`nil`,将使用视图的`loopMode`属性。
 - Parameter completion: 动画停止时要调用的可选完成闭包。
 */
public func play(fromFrame: AnimationFrameTime? = nil,
                 toFrame: AnimationFrameTime,
                 loopMode: LottieLoopMode? = nil,
                 completion: LottieCompletionBlock? = nil)
animationView.play(fromFrame: 50, toFrame: 80, loopMode: .loop) { (isFinished) in
    // 播放完成后的回调闭包
}
// 设置当前帧
animationView.currentFrame = 65

动画的循环模式。设置play调用的循环行为, 默认为playOnce

public enum ottieLoopMode {
  /// 动画播放一次然后停止。
  case playOnce
  /// 动画将从头到尾循环直到停止。
  case loop
  /// 动画将向前播放,然后向后播放并循环直至停止。
  case autoReverse
}

// 循环模式
animationView.loopMode = .playOnce

动画到后台的行为模式。到后台时AnimationView的行为,默认为暂停。 回调会以false调用完成。

public enum LottieBackgroundBehavior {
    /// 停止动画并将其重置为当前播放时间的开头。 调用完成回调。
    case stop
    /// 暂停动画,回调会以“false”调用完成。
    case pause
    /// 暂停动画并在应到前台时重新启动它,在动画完成时调用回调
    case pauseAndRestore
}
        
// 到后台的行为模式
animationView.backgroundBehavior = .pause

编辑某帧的动画对象的属性:

let redValueProvider = ColorValueProvider(Color(r: 1, g: 0.2, b: 0.3, a: 1))
animationView.setValueProvider(redValueProvider, keypath: AnimationKeypath(keypath: "BG-On.Group 1.Fill 1.Color"))
    
let otherValueProvider = ColorValueProvider(Color(r: 0.3, g: 0.2, b: 0.3, a: 1))
animationView.setValueProvider(otherValueProvider, keypath: AnimationKeypath(keypath: "BG-Off.Group 1.Fill 1.Color"))

AnimationView常用的属性和方法:

/// 动画属性
public var animation: Animation? { ... }
/// 程序到后台动画的行为,上面有详细解释
public var backgroundBehavior: LottieBackgroundBehavior = .pause
/// 如果动画需要图片资源的支持,需要设定该协议
public var imageProvider: AnimationImageProvider { ... }
/// 动画是否正在播放
public var isAnimationPlaying: Bool { ... }
/// 循环模式,上面有详细解释
public var loopMode: LottieLoopMode = .playOnce { ... }
/// 当前的播放进度(取值范围 0 ~ 1)
public var currentProgress: AnimationProgressTime { ... }
/// 当前的播放时间
public var currentTime: TimeInterval { ... }
/// 当前帧数
public var currentFrame: AnimationFrameTime { ... }
/// 动画的播放速度
public var animationSpeed: CGFloat = 1 { ... }
/// 判断是否正在播放动画
public var isAnimationPlaying: Bool { get }
/// 播放方法,带可选完成回调
public func play(completion: LottieCompletionBlock? = nil) { ... }

/// 从一个进度到另一个进度,上面有详细解释
public func play(fromProgress: AnimationProgressTime? = nil,
                   toProgress: AnimationProgressTime,
                   loopMode: LottieLoopMode? = nil,
                   completion: LottieCompletionBlock? = nil) { ... }

/// 从一个帧到另一个帧,上面有详细解释                   
public func play(fromFrame: AnimationFrameTime? = nil,
                   toFrame: AnimationFrameTime,
                   loopMode: LottieLoopMode? = nil,
                   completion: LottieCompletionBlock? = nil) { ... }  
/// 停止
public func stop() 
/// 暂停
public func pause()  
/// 打印所有的层级属性
public func logHierarchyKeypaths()   
/// 强制AnimationView重绘其内容
public func forceDisplayUpdate() { ... }  

三、Lottie 优势

  • 开发成本低。设计师导出 json 文件后,扔给开发同学即可,可以放在本地,也支持放在服务器。原本要1天甚至更久的动画实现,现在只要不到一小时甚至更少时间了。
  • 动画的实现成功率高了。设计师的成果可以最大程度得到实现,试错成本也低了。
  • 支持服务端 URL 方式创建。所以可以通过服务端配置 json 文件,随时替换客户端的动画,不用通过发版本就可以做到了。比如 app 启动动画可以根据活动需要进行变换了。
  • 性能。可以替代原来需要使用帧图完成的动画。节省了客户端的空间和加载的内存。对硬件性能好一些。
  • 跨平台。iOS、安卓平台可以使用一套文件。省时省力,动画一致。不用设计师跑去两边去跟着微调确认了。

四、Lottie 适用场景

  • 首次启动引导页:这个要做比较好的效果,也比较复杂,DEMO中暂时没有这个,有兴趣可以尝试~~
  • 启动(splash)动画:典型场景是APPlogo动画的播放
  • 上下拉刷新动画:所有APP都必备的功能,利用Lottie 可以做的更加简单酷炫了
  • 加载(loading)动画:典型场景是网络请求的loading动画
  • 提示(tips)动画:典型场景是空白页的提示
  • 按钮(button)动画:典型场景如switch按钮、编辑按钮、播放按钮等按钮的切换过 渡动画
  • 礼物(gift)动画:典型场景是直播类APP的高级动画播放

五、Lottie 使用注意

Lottie是基于CALayer的动画, 所有的路径预先在AE中计算好, 转换为Json文件, 然后自动转换为Layer的动画, 所以性能理论上是非常不错的。

有些需要展示动画的AnimationView,可能也会有点击事件,如果需要添加点击事件的话,就需要一个个地加,比较麻烦。

/// 关联属性 key
private var kUIViewTouchEventKey = "kUIViewTouchEventKey"

/// 点击事件闭包
public typealias UIViewTouchEvent = (AnyObject) -> ()

extension UIView {
    ......
}
private var touchEvent: UIViewTouchEvent? {
    set {
        objc_setAssociatedObject(self, &kUIViewTouchEventKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
    }
    get {
        if let event = objc_getAssociatedObject(self, &kUIViewTouchEventKey) as? UIViewTouchEvent {
            return event
        }
        return nil
    }
}

添加点击事件:

func addTouchEvent(event: @escaping UIViewTouchEvent) {
    self.touchEvent = event
    // 先判断当前是否有交互事件
    // 所有gesture的交互事件都会被添加进gestureRecognizers中
    if (self.gestureRecognizers == nil) {
        self.isUserInteractionEnabled = true
        // 添加单击事件
        let tapEvent = UITapGestureRecognizer.init(target: self, action: #selector(touchedAciton))
        self.addGestureRecognizer(tapEvent)
    }
}

点击事件处理:

@objc private func touchedAciton() {
    guard let touchEvent = self.touchEvent else {
        return
    }
    touchEvent(self)
}

参考文献

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

推荐阅读更多精彩内容