SwiftUI之深入解析高级动画的AnimatableModifier使用
Posted Serendipity·y
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SwiftUI之深入解析高级动画的AnimatableModifier使用相关的知识,希望对你有一定的参考价值。
一、前言
- 在我之前的 SwiftUI之深入解析高级动画的路径Paths 和 SwiftUI之深入解析高级动画的几何效果GeometryEffect 两篇博客中,已经对 Animatable 协议使用做详细的分析,本文将分析 AnimatableModifier,使用它可以完成更多的动画工作。
- AnimatableModifier 是一个 ViewModifier,符合 Animatable 协议。
二、动画文本
- 首先需要制作一些文字动画,如下所示,创建一个进度加载指示器:
- 可能很多人都认为应该使用动画路径实现,但是,内部标签就无法设置动画,使用 AnimatableModifier 可以实现,关键代码如下(完整代码请参考文末的完整示例的示例 10):
struct PercentageIndicator: AnimatableModifier
var pct: CGFloat = 0
var animatableData: CGFloat
get pct
set pct = newValue
func body(content: Content) -> some View
content
.overlay(ArcShape(pct: pct).foregroundColor(.red))
.overlay(LabelView(pct: pct))
struct ArcShape: Shape
let pct: CGFloat
func path(in rect: CGRect) -> Path
var p = Path()
p.addArc(center: CGPoint(x: rect.width / 2.0, y:rect.height / 2.0),
radius: rect.height / 2.0 + 5.0,
startAngle: .degrees(0),
endAngle: .degrees(360.0 * Double(pct)), clockwise: false)
return p.strokedPath(.init(lineWidth: 10, dash: [6, 3], dashPhase: 10))
struct LabelView: View
let pct: CGFloat
var body: some View
Text("\\(Int(pct * 100)) %")
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(.white)
- 在示例中,可以看到没有使 ArcShape animatable,因为 modifier 已经多次创建形状,具有不同的 pct 值。
三、动画渐变
- 在实现渐变动画时,会遇到一些限制,比如,可以为起点和终点设置动画,但是不能为渐变颜色设置动画。使用 AnimatableModifier 就可以避免出现:
- 很容易就可以实现这个功能,在这个基础上可以实现更多复杂的动画。如果需要插入中间颜色,只需要计算 RGB 值的平均值。另外需要注意,modifier 假设输入颜色数组都包含相同数量的颜色。关键代码如下(完整代码请参考文末的完整示例的示例 11):
struct AnimatableGradient: AnimatableModifier
let from: [UIColor]
let to: [UIColor]
var pct: CGFloat = 0
var animatableData: CGFloat
get pct
set pct = newValue
func body(content: Content) -> some View
var gColors = [Color]()
for i in 0..<from.count
gColors.append(colorMixer(c1: from[i], c2: to[i], pct: pct))
return RoundedRectangle(cornerRadius: 15)
.fill(LinearGradient(gradient: Gradient(colors: gColors),
startPoint: UnitPoint(x: 0, y: 0),
endPoint: UnitPoint(x: 1, y: 1)))
.frame(width: 200, height: 200)
// This is a very basic implementation of a color interpolation
// between two values.
func colorMixer(c1: UIColor, c2: UIColor, pct: CGFloat) -> Color
guard let cc1 = c1.cgColor.components else return Color(c1)
guard let cc2 = c2.cgColor.components else return Color(c1)
let r = (cc1[0] + (cc2[0] - cc1[0]) * pct)
let g = (cc1[1] + (cc2[1] - cc1[1]) * pct)
let b = (cc1[2] + (cc2[2] - cc1[2]) * pct)
return Color(red: Double(r), green: Double(g), blue: Double(b))
四、更多文本动画
- 再次实现一个文本动画,逐步进行,一次放大一个字符,如下所示:
- 关键代码如下(完整代码请参考文末的完整示例的示例 12):
struct WaveTextModifier: AnimatableModifier
let text: String
let waveWidth: Int
var pct: Double
var size: CGFloat
var animatableData: Double
get pct
set pct = newValue
func body(content: Content) -> some View
HStack(spacing: 0)
ForEach(Array(text.enumerated()), id: \\.0) (n, ch) in
Text(String(ch))
.font(Font.custom("Menlo", size: self.size).bold())
.scaleEffect(self.effect(self.pct, n, self.text.count, Double(self.waveWidth)))
func effect(_ pct: Double, _ n: Int, _ total: Int, _ waveWidth: Double) -> CGFloat
let n = Double(n)
let total = Double(total)
return CGFloat(1 + valueInCurve(pct: pct, total: total, x: n/total, waveWidth: waveWidth))
func valueInCurve(pct: Double, total: Double, x: Double, waveWidth: Double) -> Double
let chunk = waveWidth / total
let m = 1 / chunk
let offset = (chunk - (1 / total)) * pct
let lowerLimit = (pct - chunk) + offset
let upperLimit = (pct) + offset
guard x >= lowerLimit && x < upperLimit else return 0
let angle = ((x - pct - offset) * m)*360-90
return (sin(angle.rad) + 1) / 2
extension Double
var rad: Double return self * .pi / 180
var deg: Double return self * 180 / .pi
五、计数器动画
- 如下所示,如何创建一个计数器动画:
- 其实很简单,就是为每个数字使用 5 个 Text 视图,并通过 .spring() 动画上下移动它们,此外还需要使用 .clipshape() 修饰符,来隐藏绘制边框外的部分。
- 为了更好地理解它是如何工作的,可以给. clipshape() 加注释并大大降低动画的速度(完整代码请参考文末的完整示例的示例 13),关键代码如下:
struct MovingCounterModifier: AnimatableModifier
@State private var height: CGFloat = 0
var number: Double
var animatableData: Double
get number
set number = newValue
func body(content: Content) -> some View
let n = self.number + 1
let tOffset: CGFloat = getOffsetForTensDigit(n)
let uOffset: CGFloat = getOffsetForUnitDigit(n)
let u = [n - 2, n - 1, n + 0, n + 1, n + 2].map getUnitDigit($0)
let x = getTensDigit(n)
var t = [abs(x - 2), abs(x - 1), abs(x + 0), abs(x + 1), abs(x + 2)]
t = t.map getUnitDigit(Double($0))
let font = Font.custom("Menlo", size: 34).bold()
return HStack(alignment: .top, spacing: 0)
VStack
Text("\\(t[0])").font(font)
Text("\\(t[1])").font(font)
Text("\\(t[2])").font(font)
Text("\\(t[3])").font(font)
Text("\\(t[4])").font(font)
.foregroundColor(.green).modifier(ShiftEffect(pct: tOffset))
VStack
Text("\\(u[0])").font(font)
Text("\\(u[1])").font(font)
Text("\\(u[2])").font(font)
Text("\\(u[3])").font(font)
Text("\\(u[4])").font(font)
.foregroundColor(.green).modifier(ShiftEffect(pct: uOffset))
.clipShape(ClipShape())
.overlay(CounterBorder(height: $height))
.background(CounterBackground(height: $height))
func getUnitDigit(_ number: Double) -> Int
return abs(Int(number) - ((Int(number) / 10) * 10))
func getTensDigit(_ number: Double) -> Int
return abs(Int(number) / 10)
func getOffsetForUnitDigit(_ number: Double) -> CGFloat
return 1 - CGFloat(number - Double(Int(number)))
func getOffsetForTensDigit(_ number: Double) -> CGFloat
if getUnitDigit(number) == 0
return 1 - CGFloat(number - Double(Int(number)))
else
return 0
六、动画文本颜色
- 通常情况下是通过 .foregroundColor() 为动画添加颜色,但是在文本类动画中使用没有效果。然而,如果需要动画文本的颜色,可以使用 AnimatableModifier 实现:
- 关键如下所示(完整代码请参考文末的完整示例的示例 14):
struct AnimatableColorText: View
let from: UIColor
let to: UIColor
let pct: CGFloat
let text: () -> Text
var body: some View
let textView = text()
return textView.foregroundColor(Color.clear)
.overlay(Color.clear.modifier(AnimatableColorTextModifier(from: from, to: to, pct: pct, text: textView)))
struct AnimatableColorTextModifier: AnimatableModifier
let from: UIColor
let to: UIColor
var pct: CGFloat
let text: Text
var animatableData: CGFloat
get pct
set pct = newValue
func body(content: Content) -> some View
return text.foregroundColor(colorMixer(c1: from, c2: to, pct: pct))
// This is a very basic implementation of a color interpolation
// between two values.
func colorMixer(c1: UIColor, c2: UIColor, pct: CGFloat) -> Color
guard let cc1 = c1.cgColor.components else return Color(c1)
guard let cc2 = c2.cgColor.components else return Color(c1)
let r = (cc1[0] + (cc2[0] - cc1[0]) * pct)
let g = (cc1[1] + (cc2[1] - cc1[1]) * pct)
let b = (cc1[2] + (cc2[2] - cc1[2]) * pct)
return Color(red: Double(r), green: Double(g), blue: Double(b))
七、AnimatableModifier 无法实现动画问题
- 如果是第一次使用 AnimatableModifier,可能会遇到问题,试下一个简单的动画,但是没有动画效果,如下所示,在 VStack 中就没有动画效果:
VStack SwiftUI之深入解析高级动画的路径Paths
SwiftUI之深入解析高级动画的时间轴TimelineView