SVGAExPlayer是一个基于SVGAPlayer重构的增强版。
Demo地址:SVGAPlayer_Optimized
Feature:
✅ 内置SVGA解析器;
✅ 带有播放状态且可控制;
✅ 可自定义下载器&加载器;
✅ 防止重复加载;
✅ 可随时设置静音;
✅ 可随时反转播放;
✅ 可随时设置播放区间;
✅ 兼容 OC & Swift;
✅ API简单易用。

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继承自SVGARePlayer(SVGARePlayer是参考原版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)
SVGAExPlayerDelegate的方法都是可选的,具体的使用可以看看SVGAPlayer_Optimized。
如果已经有现成的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)
- 由于调用播放的方法并不会立马就播放,如果在加载的过程中再次调用播放的方法,但
fromFrame和isAutoPlay不一样,那么fromFrame和isAutoPlay会以最新的设置来进行后续的操作。
可定制化的设置:
/// 是否带动画过渡(默认为`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停止播放后,若
isHideWhenStopped为false则alpha会被设置为1,反之为0(所以可能与外部修改的值不符)。 - 如需修改请确保
isAnimated为false。
- 注意:SVGA停止播放后,若
既然不想使用原来的API,为什么要使用继承SVGARePlayer的方式?
- 首先为了让基本设置跟之前一样;
- 其次希望跟之前一样当做一个UIView来使用,但不想再套一层UIView,为了尽可能减少图层的数量。
最后
介绍就这么多了,总的来说SVGAPlayer对比Lottie轻量一些,比较适合动画数量少的场景。但如果需要很多且复杂的场景,我个人更加推荐Lottie,毕竟架构和性能要比SVGAPlayer好很多,最重要是Lottie一直都有维护和更新,而SVGAPlayer已经不更新了。
我的SVGARePlayer是基于SVGAPlayer的重构版,而SVGAExPlayer则是SVGARePlayer的加强版,除了保留原有功能外,主要对其做了「加载防重」和「API简化」。
如果需要使用,可以去SVGAPlayer_Optimized中的SVGAPlayer_Optimized文件夹下:
直接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'