【iOS】GIF小组件的巧妙实现

2,341 阅读3分钟

本文的技术实现来自SwingAnimationDynamicWidget,这里只是简单讲解和应用(主要是DynamicWidget)。

📢📢📢 使用clockHandRotationEffect这个API的小伙伴们要注意啦,在最新版的 Xcode 26.1.1 中该API会失效!也就是小组件无法动起来了!🤯 不过好在旧版Xcode(例如26.0.1)运行的项目还是能使用的。私有API还是有风险啊,只好等专业人士去适配了😵。

Demo地址:OneDay

gif_widget.gif

Github上有位大佬已经分享了一个可以让小组件执行动画的Modifier --- SwingAnimation,该Modifier应该是使用了苹果的私有api --- clockHandRotationEffect,这个估计是实现系统时钟小组件的动画组件。

我一直都想在小组件上播放gif,曾想过clockHandRotationEffect是不是也能对Z轴进行动画,通过不断变换zIndex实现gif的播放,但是clockHandRotationEffect貌似只能实现旋转的动画,底层代码也没公开,这就整不会了,只能暂且作罢。

直到最近发现有另一位大佬使用clockHandRotationEffect实现了gif的播放:DynamicWidget

首先感谢两位大佬的分享,看了下DynamicWidget的代码,作者使用了一种十分巧妙的方式实现了多张图片叠加并轮播展示,从而实现了gif效果。

实现

核心代码如下:

ZStack {
    ForEach(1...gifImages.count, id: \.self) { index in
        Image(uiImage: gifImages[index-1])
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width: width, height: height)
            .mask(
                ArcView(arcStartAngle: angle * Double(index - 1),
                        arcEndAngle: angle * Double(index),
                        arcRadius: arcRadius)
                .stroke(style: .init(lineWidth: arcWidth, lineCap: .square, lineJoin: .miter))
                .frame(width: width, height: height)
                .clockHandRotationEffect(period: .custom(duration))
                .offset(y: arcRadius) // ⚠️ 需要先进行旋转,再设置offset
            )
    }
}
.frame(width: width, height: height)
  • 其中ArcView是一个Shape,绘制的是一段圆弧,用来作为Image的mask图层(跟UIKit的mask一样,限制展示的区域)。

直接说原理吧,作者是先对gif进行解码,对每一帧图片都加了一层mask图层,然后对全部mask图层执行旋转动画,以控制当前帧要展示的图片

用文字表达有点难,简单起见,选取3张图片,将其缩小再缩小,并垂直铺开,这样就一目了然:

Pasted Graphic 1_副本.jpg

  • PS:这里为了演示,圆弧没有作为Image的mask,而是添加在其上面并进行了染色处理。
  1. Image能展示的区域就是圆弧跟图片重合的区域,因此ArcView边宽要设置成Image的最大边,确保圆弧经过Image时能覆盖整个图片。

Pasted Graphic_副本.jpg

  1. 每一个ImageArcView360° / 图片总数的弧度,确保全部叠加起来能刚好围成一个圆。

  2. 最后把全部Image叠加起来,并对全部ArcView执行旋转动画,就能做到「每一段圆弧滑过自己的Image」后能立马接上「下一个Image的圆弧」,实现无缝衔接

jp_gif_file 2.GIF

  • 图片位置不变,图片的展示取决于圆弧的位置,我们只能看到圆弧跟图片重合的区域

从Z轴上看如同无间断切换图片,实现gif播放😄。

注意

  1. 当gif的图片数超过360张,超出部分可能就会丢失了(跟开头图片的弧度重合导致被覆盖),因此要注意好这个图片数。
  2. 另外图片数过少(圆弧过长)、时间较长(圆弧转动变慢)的gif,可能会看到图片“撕裂”的现象,这是因为在两个相连的mask圆弧交接处,此时是同时展示了两张图片:

Pasted Graphic 1.png

  • 不过一般很少有这种gif吧,这也跟苹果的帧率有关,放心的是作者把该圆弧设置得很大很大,基本很难会看到这种现象。

分析就这么多了,本人技术有限无法再深入探讨🙃。

最后

我把gif效果也收录到我的OneDay中,方便自己选择相册的gif作为小组件,需要的话可以去试一下:

jp_gif_file 3.GIF

  • 桌面小组件

jp_gif_file 4.GIF

  • 充电小组件

最后再次感谢两位大佬的分享!SwingAnimationDynamicWidget