【问题标题】:Delay a repeating animation in SwiftUI with between full autoreverse repeat cycles延迟 SwiftUI 中的重复动画,在完整的自动反转重复周期之间
【发布时间】:2020-12-30 15:57:51
【问题描述】:

我正在使用 SwiftUI 构建一个 Apple Watch 应用,它可以读取用户的心率并将其显示在心形符号旁边。

我有一个让心脏符号反复跳动的动画。因为我知道实际用户的心率,所以我想让它以与用户心率相同的速率跳动,每次心率变化时更新动画。

我可以通过将心率除以 60 来确定节拍之间的间隔秒数。例如,如果用户的心率为 80 BPM,则动画应该每 0.75 秒 (60/80) 发生一次。

这是我现在拥有的示例代码,其中currentBPM 是一个常量,但通常会更新。

struct SimpleBeatingView: View {
    
    // Once I get it working, this will come from a @Published Int that gets updated any time a new reading is avaliable.
    let currentBPM: Int = 80
    
    @State private var isBeating = false
    
    private let maxScale: CGFloat = 0.8
    
    var beatingAnimation: Animation {
        
        // The length of one beat
        let beatLength = 60 / Double(currentBPM)
        
        return Animation
            .easeInOut(duration: beatLength)
            .repeatForever()
    }
    
    var body: some View {
        Image(systemName: "heart.fill")
            .font(.largeTitle)
            .foregroundColor(.red)
            .scaleEffect(isBeating ? 1 : maxScale)
            .animation(beatingAnimation)
            .onAppear {
                self.isBeating = true
            }
    }
}

我想让这个动画表现得更像 Apple 的内置心率应用程序。与其让心脏不断变大或变小,我想让它跳动(两个方向的动画)然后暂停片刻,然后再次跳动(动画两个方向)然后再次暂停,依此类推。

当我添加一秒延迟时,例如,.delay(1).repeatForever() 之前,动画会在每个节拍中途暂停。例如,它变小,暂停,然后变大,然后暂停,等等。

我明白为什么会发生这种情况,但是如何在每个自动反转重复之间插入延迟,而不是在自动反转重复的两端插入?

我相信我可以计算出延迟应该多长以及每个节拍的长度以使一切正常进行,因此延迟长度可以是任意的,但我正在寻找的是帮助关于如何在动画循环之间实现暂停

我使用的一种方法是在每次获得新的心率 BPM 时将flatMapcurrentBPM 重复发布的Timers,这样我就可以尝试从中驱动动画,但我不确定如何实际上,我可以将其转换为 SwiftUI 中的动画,但根据我目前对 SwiftUI 的理解,当时间似乎应该由动画处理时,我不确定以这种方式手动驱动值是否是正确的方法。

【问题讨论】:

    标签: swift swiftui


    【解决方案1】:

    一种可能的解决方案是使用DispatchQueue.main.asyncAfter 链接单个片段动画。这使您可以控制何时延迟特定部分。

    这是一个演示:

    struct SimpleBeatingView: View {
        @State private var isBeating = false
        @State private var heartState: HeartState = .normal
    
        @State private var beatLength: TimeInterval = 1
        @State private var beatDelay: TimeInterval = 3
    
        var body: some View {
            VStack {
                Image(systemName: "heart.fill")
                    .imageScale(.large)
                    .font(.largeTitle)
                    .foregroundColor(.red)
                    .scaleEffect(heartState.scale)
                Button("isBeating: \(String(isBeating))") {
                    isBeating.toggle()
                }
                HStack {
                    Text("beatLength")
                    Slider(value: $beatLength, in: 0.25...2)
                }
                HStack {
                    Text("beatDelay")
                    Slider(value: $beatDelay, in: 0...5)
                }
            }
            .onChange(of: isBeating) { isBeating in
                if isBeating {
                    startAnimation()
                } else {
                    stopAnimation()
                }
            }
        }
    }
    
    private extension SimpleBeatingView {
        func startAnimation() {
            isBeating = true
            withAnimation(Animation.linear(duration: beatLength * 0.25)) {
                heartState = .large
            }
            DispatchQueue.main.asyncAfter(deadline: .now() + beatLength * 0.25) {
                withAnimation(Animation.linear(duration: beatLength * 0.5)) {
                    heartState = .small
                }
            }
            DispatchQueue.main.asyncAfter(deadline: .now() + beatLength * 0.75) {
                withAnimation(Animation.linear(duration: beatLength * 0.25)) {
                    heartState = .normal
                }
            }
            DispatchQueue.main.asyncAfter(deadline: .now() + beatLength + beatDelay) {
                withAnimation {
                    if isBeating {
                        startAnimation()
                    }
                }
            }
        }
    
        func stopAnimation() {
            isBeating = false
        }
    }
    
    enum HeartState {
        case small, normal, large
    
        var scale: CGFloat {
            switch self {
            case .small: return 0.5
            case .normal: return 0.75
            case .large: return 1
            }
        }
    }
    

    【讨论】:

      【解决方案2】:

      在 pawello2222 回答之前,我仍在尝试尝试使其正常工作,我想出了一个使用 Timer 发布者和组合框架的解决方案。

      我已经包含了下面的代码,但这不是一个好的解决方案,因为每次currentBPM 更改时都会在下一个动画开始之前添加一个新的延迟。 pawello2222 的回答更好,因为它总是允许当前的跳动动画完成,然后以更新的速率开始下一个循环。

      另外,我认为我在这里的回答不太好,因为很多动画工作是在数据存储对象中完成的,而不是封装在视图中,这可能更有意义。

      import SwiftUI
      import Combine
      
      class DataStore: ObservableObject {
          
          @Published var shouldBeSmall: Bool = false
          
          @Published var currentBPM: Int = 0
          
          private var cancellables = Set<AnyCancellable>()
          
          init() {
              
              let newLengthPublisher =
                  $currentBPM
                  .map { 60 / Double($0) }
                  .share()
              
              newLengthPublisher
                  .delay(for: .seconds(0.2),
                         scheduler: RunLoop.main)
                  .map { beatLength in
                      return Timer.publish(every: beatLength,
                                           on: .main,
                                           in: .common)
                          .autoconnect()
                  }
                  .switchToLatest()
                  .sink { timer in
                      self.shouldBeSmall = false
                  }
                  .store(in: &cancellables)
              
              newLengthPublisher
                  .map { beatLength in
                      return Timer.publish(every: beatLength,
                                           on: .main,
                                           in: .common)
                          .autoconnect()
                  }
                  .switchToLatest()
                  .sink { timer in
                      self.shouldBeSmall = true
                  }
                  .store(in: &cancellables)
              
              currentBPM = 75
          }
      }
      
      struct ContentView: View {
          
          @ObservedObject var store = DataStore()
          
          private let minScale: CGFloat = 0.8
          
          var body: some View {
              
              HStack {
                  
                  Image(systemName: "heart.fill")
                      .font(.largeTitle)
                      .foregroundColor(.red)
                      .scaleEffect(store.shouldBeSmall ? 1 : minScale)
                      .animation(.easeIn)
                  
                  Text("\(store.currentBPM)")
                      .font(.largeTitle)
                      .fontWeight(.bold)
              }
          }
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2019-05-31
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-12-04
        • 2012-12-02
        • 1970-01-01
        相关资源
        最近更新 更多