欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

css动画反转_暂停和反转SwiftUI动画

程序员文章站 2024-03-24 12:24:46
...

css动画反转

Animations and transitions are decisively one of SwiftUI’s fortes. The framework allows us to go as deep as we want in handling specifics like timing function and duration parameters. Moreover it gives us sophisticated tools like the Animatable protocol to completely customize behavior. Here we are going to rely on the more constricted AnimatableModifier protocol to show how we can manually pause and resume and animation as well as adding the ability to reverse and loop over.

动画和过渡绝对是SwiftUI的特产之一。 该框架使我们能够按需深入处理诸如计时功能和持续时间参数之类的细节。 此外,它还提供了诸如Animatable协议之类的复杂工具来完全自定义行为。 在这里,我们将依靠更为严格的AnimatableModifier协议来展示如何手动暂停和恢复动画以及增加反向和循环播放的功能。

无聊的动画 (A Boring Animation)

To focus on function rather than visuals we are going to design what is probably the simplest animation possible: a counter. We will display a number on screen which can be counting up or down within a range like some kind of stopwatch.

为了专注于功能而不是视觉效果,我们将设计可能是最简单的动画:计数器。 我们将在屏幕上显示一个数字,该数字可以在某种秒表之类的范围内向上或向下计数。

css动画反转_暂停和反转SwiftUI动画
Simple stopwatch-like UI
简单的类似于秒表的UI

We’ll start by creating a basic ViewModifier containing a numeric value formatted to display as a zero-padded positive integer. The value is calculated as a percentage over a maximum which is taken as parameter.

我们将首先创建一个基本的ViewModifier,其中包含一个数值格式,该数值的格式设置为显示为零填充的正整数。 该值计算为相对于最大值的百分比,该最大值作为参数。

struct CountModifier: ViewModifier {  var maxValue: CGFloat // Maximum count value
var timeDuration: Double // How long it will take to reach max, in seconds private let percentValue: CGFloat = 0 // Percentage of the maximum count // Counter value as integer
var value: Int {
Int(percentValue * maxValue)
} func body(content: Content) -> some View {
Text("\(value, specifier: "%03d")") // Formatted count
.font(.system(.largeTitle, design: .monospaced))
.font(.largeTitle)
}}

Notice how in this case we are simply ignoring the input view and returning a fresh body. So we are not really modifying anything but rather replacing it entirely. Additionally we added a timeDuration argument which will be useful to customize the duration of the animation.

请注意,在这种情况下,我们是如何简单地忽略输入视图并返回一个新的正文。 因此,我们实际上并没有进行任何修改,而是完全替换了它。 另外,我们添加了timeDuration参数,该参数对于自定义动画的持续时间很有用。

添加外部控件 (Adding External Controls)

Outside of the numeric display we want to show a set of controls for pausing and reversing the count. These can be regular buttons which modify state variables defined on the main content view.

在数字显示之外,我们希望显示一组用于暂停和反转计数的控件。 这些可以是常规按钮,用于修改在主内容视图上定义的状态变量。

struct CountDownUp: View {  @State var isCounting = false // Whether we are counting (moving)
@State var isReversed = false // The direction of the count: up or down

var body: some View {
VStack {
EmptyView()
.modifier(
CountModifier(
maxValue: 100, // We'll count to 100
timeDuration: 10 // In 10 seconds
)
)
HStack {
Button(isCounting ? "⏸" : "▶️") {
isCounting.toggle() // Pause / resume
}
Button(isReversed ? "????" : "????") {
isReversed.toggle() // Count up / down
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
}
}}

As an added touch we made the buttons adapt to the current status of the counter thus providing an instant feedback.

作为增加的触摸,我们使按钮适应计数器的当前状态,从而提供即时反馈。

与修改器通信 (Communicating with the Modifier)

Because the animation will change depending on the state determined by our external controls we’ll necessarily have to pass these variables as bindings:

因为动画将根据外部控件确定的状态而变化,所以我们必须将这些变量作为绑定传递:

fileprivate struct CountModifier: ViewModifier {  var maxValue: CGFloat // Maximum count value
var timeDuration: Double
@Binding var percentage: CGFloat
@Binding var isCounting: Bool
@Binding var isReversed: Bool private var percentValue: CGFloat // Percentage of maximum count init(
maxValue: CGFloat,
timeDuration: Double,
percentage: Binding<CGFloat>,
isCounting: Binding<Bool>,
isReversed: Binding<Bool>
) {
self.maxValue = maxValue
self.timeDuration = timeDuration
_percentage = percentage // Bindings initialization
_isCounting = isCounting
_isReversed = isReversed
// The percent value is copied from the binding, later on it will be used for the animation
percentValue = percentage.wrappedValue
} var value: Int {
Int(percentValue * maxValue)
} func body(content: Content) -> some View {
Text("\(value, specifier: "%03d")")
.font(.system(.largeTitle, design: .monospaced))
.font(.largeTitle)
}}

We also included a binding to a percentage which indirectly initializes the internal percentValue variable by copying its current value. Remember this percentage drives the number on display at any given time during the course of the animation.

我们还包括一个绑定到percentage的绑定,该percentValue通过复制其当前值间接初始化内部percentValue变量。 请记住,在动画过程中的任何给定时间,此百分比都会驱动显示的数字。

The reason why this piece of state needs to be declared outside of the modifier will be revealed momentarily. For now let’s just add percentage to our root view:

需要暂时在修饰符之外声明此状态的原因将立即揭晓。 现在,让我们将percentage添加到我们的根视图中:

struct CountDownUp: View {  @State var percentage = CGFloat(0)


var body: some View {
VStack {
EmptyView()
.modifier(
CountModifier(

percentage: $percentage,

)

Great! We now have state shared between controls and display but there is still no actual animation happening.

大! 现在,我们已经在控件和显示之间共享了状态,但是仍然没有实际的动画发生。

动画柜台 (Animating the Counter)

The way AnimatableModifier works, in order to animate a certain value we need to declare a writable computed property named animatableData backed by some stored property. To turn CountModifier into an AnimatableModifier we’ll base our animatable data on the aforementioned percentage value.

AnimatableModifier的工作方式,为了使某个值具有动画效果,我们需要声明一个名为animatableData的可写计算属性, animatableData由某些存储的属性支持。 要打开CountModifierAnimatableModifier我们将立足于上述百分比值我们的动画数据。

struct CountModifier: AnimatableModifier {  var animatableData: CGFloat {
get { percentValue }
set { percentValue = newValue }
}

Apart from needing to use CGFloat as opposed to regular double/single precision floating point scalars which is required by the protocol, the implementation of the property is as trivial as it gets.

除了需要使用CGFloat而不是协议要求的常规双精度/单精度浮点标量之外,该属性的实现也变得微不足道。

触发动画 (Triggering the Animation)

To kick-start the counter we will be adding an onChange handler to watch over the contents of isCounting. This is an iOS 14+ only feature. In the accompanying source code (see working example) you’ll find a work-around for iOS 13 that uses PassthroughSubject to send the start signal.

为了启动计数器,我们将添加一个onChange处理程序来监视isCounting的内容。 这是仅限iOS 14+的功能。 在随附的源代码(请参见工作示例)中,您将找到iOS 13的变通方法,该变通方法使用PassthroughSubject发送启动信号。

struct CountModifier: AnimatableModifier {

func body(content: Content) -> some View {
Text("\(value, specifier: "%03d")")

.onChange(of: isCounting) { _ in
handleStart()
}
} func handleStart() -> () {
if isCounting {
withAnimation(.linear(duration: timeDuration)) {
self.percentage = 1
}
}
}

We handle isCounting being set by using that binding directly to set off the animation.

我们通过直接使用该绑定来设置动画来处理isCounting设置。

暂停和恢复 (Pausing and Resuming)

css动画反转_暂停和反转SwiftUI动画
Counter in action
反击行动

Now in order to pause the animation we need to enrich our change handler for the isCounting variable as it now can switch from on to off. We will also introduce a timeRemaining property that will tell us exactly how long we have left on the full duration of the count. Finally for the sake of semantics we’ll rename handleStart into the more descriptive handleStartStop:

现在,为了暂停动画,我们需要为isCounting变量充实更改处理程序,因为它现在可以从打开切换为关闭。 我们还将引入一个timeRemaining属性,该属性将确切告诉我们在整个计数持续时间内还剩下多长时间。 最后,出于语义的考虑,我们将handleStart重命名为更具描述性的handleStartStop

struct CountModifier: AnimatableModifier {

var timeRemaining: Double {
timeDuration * Double(1 - percentValue)
} func body(content: Content) -> some View {
Text("\(value, specifier: "%03d")")

.onChange(of: isCounting) { _ in
handleStartStop()
}
} func handleStartStop() -> () { // Formerly handleStart()
if isCounting {
withAnimation(.linear(duration: timeRemaining)) {
self.percentage = 1
}
} else {
withAnimation(.linear(duration: 0)) {
self.percentage = percentValue
}
}
}

The thing to notice here is that this time remaining approach only works for linear animations. More advanced timing functions would absolutely complicate the calculation that enables us to resume movement after pausing.

这里要注意的是,这种剩余时间的方法仅适用于线性动画。 更高级的计时功能将使计算复杂化,使我们能够在暂停后恢复运动。

循环播放 (Looping over)

It would be nice if our counter doesn’t just stop when it reaches the limit. For this we are forced into hijacking body() to check whether the percentage has reached its maximum value of 1, in which case we’ll force it back to zero.

如果我们的计数器在达到极限时不只是停止,那将是很好的。 为此,我们*劫持body()以检查百分比是否已达到其最大值1,在这种情况下,我们将其强制回零。

struct CountModifier: AnimatableModifier {

func body(content: Content) -> some View {
// Loop when we reach completion
if percentValue == 1 {
DispatchQueue.main.async {
self.percentage = 0
withAnimation(.linear(duration: self.timeDuration)) {
self.percentage = 1
}
}
}
return actualBody(content) // Original body of the view
}

// Moved the original body into a separate function
func actualBody(_ content: Content) -> some View {
Text("\(value, specifier: "%03d")")

}

After resetting the percentage we’ll kick it off again with an asynchronous call. To keep things clean we’ll move the previous body function into actualBody to keep taking advantage of the enhanced ViewBuilder syntax.

重置百分比后,我们将通过异步调用再次将其启动。 为了保持环境整洁,我们将先前的body函数移至actualBody以继续利用增强的ViewBuilder语法。

倒退 (Shift into Reverse)

Whether the counting is on or now, we want to be able to change the direction. For this we monitor isReversed for changes and handle the new value using the same technique as before. We’ll also need to alter the start/stop and the loop logic to take into consideration the current direction.

无论计数是现在还是现在,我们都希望能够更改方向。 为此,我们监视isReversed的更改并使用与以前相同的技术处理新值。 我们还需要更改启动/停止和循环逻辑以考虑当前方向。

struct CountModifier: AnimatableModifier {

var timeRemaining: Double {
isReversed
? timeDuration * Double(percentValue)
: timeDuration * Double(1 - percentValue)
} func body(content: Content) -> some View {
if (isReversed && percentValue == 0) || (!isReversed && percentValue == 1) {
DispatchQueue.main.async {
self.percentage = self.isReversed ? 1 : 0
withAnimation(.linear(duration: self.timeDuration)) {
self.percentage = self.isReversed ? 0 : 1
}
}
}
return actualBody(content)
}

func actualBody(_ content: Content) -> some View {
Text("\(value, specifier: "%03d")")

.onChange(of: isReversed) { _ in
handleReverse()
}
} func handleStartStop() -> () {
if isCounting {
withAnimation(.linear(duration: timeRemaining)) {
self.percentage = isReversed ? 0 : 1
}
} else {}
}
} func handleReverse() -> () {
if isCounting {
withAnimation(.linear(duration: 0)) {
self.percentage = percentValue
}
withAnimation(.linear(duration: timeRemaining)) {
self.percentage = isReversed ? 0 : 1
}
}
}}

Notably the time remaining was re-calculated by pondering the percentage or its inverse in the case of counting downwards.

值得注意的是,在考虑倒数的情况下,通过考虑百分比或其倒数来重新计算剩余时间。

最后的想法 (Final Thoughts)

This is a fun way of gaining control over events within a linear animation. It’s definitely not the only one and might not even the preferred way as it does involve some hacks. But on the other hand it has as an advantage the simplicity of not having to deal with the Animatable protocol directly.

这是一种控制线性动画中事件的有趣方式。 绝对不是唯一的方法,甚至可能不是首选的方法,因为它确实涉及一些hack 。 但是另一方面,它的优点是不必直接处理Animatable协议。

That’s it, as always check out the associated Working Example for the complete source code and interactive demo.

就这样,一如既往地查看相关的工作示例以获取完整的源代码和交互式演示。

FEATURED EXAMPLE

精选示例

css动画反转_暂停和反转SwiftUI动画
Count Down Up — Some kinda stopwatch倒计时—有点秒表

Originally published at https://swiftui.diegolavalle.com on August 12, 2020.

最初于 2020年8月12日 发布在 https://swiftui.diegolavalle.com 上。

翻译自: https://medium.com/swift-you-and-i/pausing-and-reversing-swiftui-animations-799c4188c38f

css动画反转

相关标签: css css3