【iOS】SVGAExPlayer - 增强版SVGA播放器

3,555 阅读9分钟

SVGAExPlayer是一个基于SVGAPlayer重构的增强版。

Demo地址:SVGAPlayer_Optimized

Feature:
    ✅ 内置SVGA解析器;
    ✅ 带有播放状态且可控制;
    ✅ 可自定义下载器&加载器;
    ✅ 防止重复加载;
    ✅ 可随时设置静音;
    ✅ 可随时反转播放;
    ✅ 可随时设置播放区间;
    ✅ 兼容 OC & Swift;
    ✅ API简单易用。

example.gif

SVGAPlayer是个很老的第三方库了,作者很久没有更新,使用起来挺麻烦的,原来的使用方式:

let player = SVGAPlayer()

override func viewDidLoad() {
    super.viewDidLoad()
    
    player.frame = CGRect(x: 100, y: 100, width: 100, height: 100)
    view.addSubview(player)

    // 创建 SVGA 动画解析器
    let parser = SVGAParser()
    
    // 加载 SVGA 动画文件
    parser.parse(withNamed: "your_animation_file", in: nil) { [weak self] videoItem in
        guard let self, videoItem else { return }
        
        // 将 SVGA 动画加载到播放器中
        self.player.videoItem = videoItem
        
        // 开始播放动画
        self.player.startAnimation()
    }
}

老实说,性能没有Lottie好,但是没办法,项目要用。为了使用起来更加方便,于是乎在此基础上进行二次封装。

基本使用

SVGAExPlayer继承自SVGARePlayerSVGARePlayer是参考原版SVGAPlayer完全重写的新播放器,API基本保持一致,内部进行了代码优化),基本设置跟父类一样即可,主要是API的使用不一样,变得更加易用。

加载并播放:

player.play("your_animation_path", fromFrame: 0, isAutoPlay: true)
  • fromFrame: 从第几帧开始
  • isAutoPlay: 加载完成后是否自动开始播放

内部会自动调用SVGAParser进行「远程/本地」SVGA资源的加载,所以调用该方法后并不会立马播放,会有加载的过程。

加载后可以选择是否自动播放,具体的状态可以遵守SVGAExPlayerDelegate,可以收到状态发生改变的回调:

/// 状态发生改变【状态更新】
@objc optional
func svgaExPlayer(_ player: SVGAExPlayer,
                  statusDidChanged status: SVGAExPlayerStatus,
                  oldStatus: SVGAExPlayerStatus)

另外加载的失败和完成SVGAExPlayerDelegate也有对应的回调:

/// SVGA未知来源【无法播放】
@objc optional
func svgaExPlayer(_ player: SVGAExPlayer,
                  unknownSvga source: String)

/// SVGA资源加载失败【无法播放】
@objc optional
func svgaExPlayer(_ player: SVGAExPlayer,
                  svga source: String,
                  dataLoadFailed error: Error)

/// 加载的SVGA资源解析失败【无法播放】
@objc optional
func svgaExPlayer(_ player: SVGAExPlayer,
                  svga source: String,
                  dataParseFailed error: Error)

/// 本地SVGA资源解析失败【无法播放】
@objc optional
func svgaExPlayer(_ player: SVGAExPlayer,
                  svga source: String,
                  assetParseFailed error: Error)

/// SVGA资源无效【无法播放】
@objc optional
func svgaExPlayer(_ player: SVGAExPlayer,
                  svga source: String,
                  entity: SVGAVideoEntity,
                  invalid error: SVGAVideoEntityError)

/// SVGA资源解析成功【可以播放】
@objc optional
func svgaExPlayer(_ player: SVGAExPlayer,
                  svga source: String,
                  parseDone entity: SVGAVideoEntity)

当然播放相关的回调也会提供:

/// SVGA动画(本地/远程资源)已准备就绪即可播放【即将播放】
/// - Parameters:
///   - isNewSource: 是否为新的资源(播放的资源需要加载、或者切换不同的`entity`则该值为`true`)
///   - fromFrame: 从第几帧开始
///   - isWillPlay: 是否即将开始播放
///   - resetHandler: 用于重置「从第几帧开始」和「是否开始播放」,如需更改调用该闭包并传入新值即可
@objc optional
func svgaExPlayer(_ player: SVGAExPlayer,
                  svga source: String,
                  readyForPlay isNewSource: Bool,
                  fromFrame: Int,
                  isWillPlay: Bool,
                  resetHandler: @escaping (_ newFrame: Int, _ isPlay: Bool) -> Void)

/// SVGA动画执行回调【正在播放】
@objc optional
func svgaExPlayer(_ player: SVGAExPlayer,
                  svga source: String,
                  animationPlaying currentFrame: Int)

/// SVGA动画完成一次播放【正在播放】
/// - Note: 每一次动画的完成(无论是否循环播放)都会回调;若是「用户手动停止」则不会回调。
@objc optional
func svgaExPlayer(_ player: SVGAExPlayer,
                  svga source: String,
                  animationDidFinishedOnce loopCount: Int)

/// SVGA动画完成所有播放【结束播放】
/// - Note: 设置了`loops > 0`并且达到次数才会回调;若是「用户手动停止」或`loops = 0`则不会回调。
@objc optional
func svgaExPlayer(_ player: SVGAExPlayer,
                  svga source: String,
                  animationDidFinishedAll loopCount: Int)

/// SVGA动画播放失败的回调【播放失败】
/// - Note: 尝试播放时发现「没有SVGA资源」或「没有父视图」、SVGA资源只有一帧可播放帧(无法形成动画)就会触发该回调。
@objc optional
func svgaExPlayer(_ player: SVGAExPlayer,
                  svga source: String,
                  animationPlayFailed error: SVGARePlayerPlayError)

如果已经有现成的SVGAVideoEntity对象,就可以直接使用该对象进行播放:

let entity: SVGAVideoEntity = ...
player.play(with: entity, fromFrame: 0, isAutoPlay: true)
  • 使用这种方式播放的话,svgaSource则为该SVGAVideoEntity对象的内存地址。

加载优化

自定义远程资源下载器

加载远程的SVGA资源,内部是使用SVGAParser自带的下载方法,如果需要自己定义下载方式(例如加载缓存的资源),可以自定义下载器:

SVGAExPlayer.downloader = { svgaSource, success, failure in
    guard let url = URL(string: svgaSource) else {
        failure(NSError(domain: "SVGAExPlayer", code: -1, userInfo: [NSLocalizedDescriptionKey: "路径错误"]))
        return
    }
    
    Task {
        do {
            let (data, _) = try await URLSession.shared.data(from: url)
            await MainActor.run { success(data) }
        } catch {
            await MainActor.run { failure(error) }
        }
    }
}
  • 实现SVGAExPlayer.downloader这个闭包即可。

说明一点,如果播放相同的资源路径,并且该资源正在加载或者已经加载好了,是不会重复去加载的。内部是根据资源路径来判断是否同一个SVGA资源,除非换成新的资源路径,就会清除上一个资源去加载新的资源,这是为了确保同一个资源不会做重复的加载操作

📢 注意:内部是通过判定资源路径是否带有http://https://的前缀,才去调用下载器进行下载的,否则就会使用本地资源加载的方式。

💡 除了全局静态属性外,你也可以在每个 SVGAExPlayer 实例上设置自定义下载器,以便根据不同播放器单独配置:

let player = SVGAExPlayer()
player.downloader = { svgaSource, success, failure in
    // 在这里编写实例级别的下载逻辑
}

自定义资源加载器

如果想完全由自己控制加载方式,可以去实现SVGAExPlayer.loader这个闭包:

SVGAExPlayer.loader = { svgaSource, success, failure, forwardDownload, forwardLoadAsset in
    // 判断是不是磁盘的SVGA
    guard FileManager.default.fileExists(atPath: svgaSource) else {
        if svgaSource.hasPrefix("http://") || svgaSource.hasPrefix("https://") {
            // 调用SVGAParsePlayer内部的远程加载方法(如果实现了SVGAParsePlayer.downloader就调用该闭包)
            forwardDownload(svgaSource)
        } else {
            // 调用SVGAParsePlayer内部的本地资源加载方法
            forwardLoadAsset(svgaSource)
        }
        return
    }
            
    // 加载磁盘的SVGA
    do {
        let data = try Data(contentsOf: URL(fileURLWithPath: svgaSource))
        success(data)
    } catch {
        failure(error)
    }
}
  • forwardDownload:原本的SVGAExPlayer内部的远程加载方法(如果实现了SVGAExPlayer.downloader就调用该闭包)
  • forwardLoadAsset::原本的SVGAExPlayer内部的本地资源加载方法

💡 同样地,每个 SVGAExPlayer 实例也支持自定义加载器,可以更精细地控制加载逻辑,而不会影响其他播放器:

let player = SVGAExPlayer()
player.loader = { svgaSource, success, failure, forwardDownload, forwardLoadAsset in
    // 在这里编写实例级别的加载逻辑
}

自定义缓存键生成器

加载成功后,默认会使用原来的缓存方式进行缓存(NSCache),使用的key是该SVGA的路径。如果需要自定义缓存key,可以去实现SVGAExPlayer.cacheKeyGenerator这个闭包:

SVGAExPlayer.cacheKeyGenerator = { svgaSource in
    return svgaSource.md5 // 使用md5进行加密
}

💡 你也可以在实例级别定义缓存键生成器,从而为不同播放器单独定制缓存策略:

let player = SVGAExPlayer()
player.cacheKeyGenerator = { svgaSource in
    return svgaSource.md5
}

其他的API和设置

/// 播放当前SVGA(从当前所在帧开始)
func play()

/// 播放当前SVGA
/// - Parameters:
///  - fromFrame: 从第几帧开始
///  - isAutoPlay: 是否自动开始播放
func play(fromFrame: Int, isAutoPlay: Bool) 

/// 重置当前SVGA(回到开头,重置完成次数)
/// 如果设置过`startFrame`或`endFrame`,则从`leadingFrame`开始
/// - Parameters:
///   - isAutoPlay: 是否自动开始播放
func reset(isAutoPlay: Bool = true) 

/// 暂停
func pause() 

/// 停止
/// - Parameters:
///   - scene: 停止后的场景
///     - clearLayers: 清空图层
///     - stepToTrailing: 去到尾帧
///     - stepToLeading: 回到头帧
func stop(then scene: SVGARePlayerStoppedScene, completion: UserStopCompletion? = nil)
    
/// 停止
/// - 等同于:`stop(then: userStoppedScene, completion: completion)`
func stop(completion: UserStopCompletion? = nil)
    
/// 清空
func clean(completion: UserStopCompletion? = nil)
  • 由于调用播放的方法并不会立马就播放,如果在加载的过程中再次调用播放的方法,但fromFrameisAutoPlay不一样,那么fromFrameisAutoPlay会以最新的设置来进行后续的操作。

可定制化的设置:

/// 是否带动画过渡(默认为`false`)
/// - 为`true`则会在「更换SVGA」和「播放/停止」的场景中带有淡入淡出的效果
public var isAnimated = false

/// 是否在【非播放/暂停】状态时隐藏自身(默认为`false`)
public var isHidesWhenStopped = false

/// 是否在【停止】状态时重置`loopCount`(默认为`true`)
public var isResetLoopCountWhenStopped = true

/// 是否启用内存缓存(主要是给到`SVGAParser`使用,默认为`false`)
public var isEnabledMemoryCache = false

/// 是否打印调试日志(仅限`DEBUG`环境,默认为`false`)
public var isDebugLog = false

互斥的API

因为SVGAExPlayer本身继承自SVGARePlayer,为了避免发生错误,不要调用SVGARePlayer原来的这些API:

/// 原代理已被`self`遵守,请使用`exDelegate`来进行监听
@property (nonatomic, weak) id<SVGAOptimizedPlayerDelegate> delegate;

/// 内部会自行修改`alpha`,以此控制「展示」与「隐藏」,并实现淡入淡出的效果,因此请尽量不要在外部修改`alpha`
@property (nonatomic) CGFloat alpha;

/// 不允许外部设置`videoItem`,内部已为其设置
- (void)setVideoItem:(nullable SVGAVideoEntity *)videoItem
        currentFrame:(NSInteger)currentFrame;
- (void)setVideoItem:(nullable SVGAVideoEntity *)videoItem
          startFrame:(NSInteger)startFrame
            endFrame:(NSInteger)endFrame;
- (void)setVideoItem:(nullable SVGAVideoEntity *)videoItem
          startFrame:(NSInteger)startFrame 
            endFrame:(NSInteger)endFrame
        currentFrame:(NSInteger)currentFrame;

/// 与原播放逻辑互斥,请使用`play`开头的API进行加载和播放
- (BOOL)startAnimation;
- (BOOL)stepToFrame:(NSInteger)frame;
- (BOOL)stepToFrame:(NSInteger)frame andPlay:(BOOL)andPlay;

/// 与原播放逻辑互斥,请使用`pause()`进行暂停
- (void)pauseAnimation;

/// 与原播放逻辑互斥,请使用`stop(with scene: SVGARePlayerStoppedScene)`进行停止
- (void)stopAnimation;
- (void)stopAnimation:(SVGARePlayerStoppedScene)scene;
  • delegate: 原本的代理给到自身遵守了,如果需要监听以前的代理方法,那就使用exDelegate

    • 也就是SVGAExPlayerDelegate,包括了原本delegate的方法还有上面的那几个回调方法。
  • alpha: 请尽量不要在外部修改,因为内部会自行修改alpha,以此控制「展示」与「隐藏」,并实现淡入淡出的效果。

    • 注意:SVGA停止播放后,若isHideWhenStoppedfalsealpha会被设置为1,反之为0(所以可能与外部修改的值不符)。
    • 如需修改请确保isAnimatedfalse

既然不想使用原来的API,为什么要使用继承SVGARePlayer的方式?

  1. 首先为了让基本设置跟之前一样;
  2. 其次希望跟之前一样当做一个UIView来使用,但不想再套一层UIView,为了尽可能减少图层的数量。

最后

介绍就这么多了,总的来说SVGAPlayer对比Lottie轻量一些,比较适合动画数量少的场景。但如果需要很多且复杂的场景,我个人更加推荐Lottie,毕竟架构和性能要比SVGAPlayer好很多,最重要是Lottie一直都有维护和更新,而SVGAPlayer已经不更新了。

我的SVGARePlayer是基于SVGAPlayer的重构版,而SVGAExPlayer则是SVGARePlayer的加强版,除了保留原有功能外,主要对其做了「加载防重」和「API简化」。

如果需要使用,可以去SVGAPlayer_Optimized中的SVGAPlayer_Optimized文件夹下:

image.jpg

直接CV这几个文件到自己工程里面即可。

同时也支持pod导入:

pod 'SVGAPlayer_Optimized', :git => 'https://github.com/Rogue24/SVGAPlayer_Optimized.git', :tag => '0.1.1'

PS:由于依赖SVGAPlayer,并且作者已经不维护了,而发布到CocoaPods公有库需要最低版本支持iOS 12以上(目前SVGAPlayer最低支持iOS 7),这种情况只能fork一份SVGAPlayer更改成新的个人库并上传,这就超出常规维护的范围了。

不过我还真的fork了一份SVGAPlayer,除了指定最低版本支持iOS 12以上,另外也将其依赖的Protobuf升级到3.29.5并重新生成Svga.pbobjc,符合较新版本的pb格式(防止同一项目下如果有新版Protobuf导致编译错误)。

如果需要也可以完整pod导入:

pod 'SVGAPlayer', :git => 'https://github.com/Rogue24/SVGAPlayer-iOS.git', :tag => '2.5.8'
pod 'SVGAPlayer_Optimized', :git => 'https://github.com/Rogue24/SVGAPlayer_Optimized.git', :tag => '0.1.2'