简体   繁体   English

使用 SwiftUI 的按钮闪烁动画

[英]Button blink animation with SwiftUI

How to make border color changing animation in SwiftUI.如何在 SwiftUI 中制作边框颜色变化动画。 Here is the code with UIKit这是 UIKit 的代码

extension UIButton{
    func blink(setColor: UIColor, repeatCount: Float, duration: Double) {
        self.layer.borderWidth = 1.0
        let animation: CABasicAnimation = CABasicAnimation(keyPath: "borderColor")
        animation.fromValue = UIColor.clear.cgColor
        animation.toValue = setColor.cgColor
        animation.duration = duration
        animation.autoreverses = true
        animation.repeatCount = repeatCount
        self.layer.borderColor = UIColor.clear.cgColor
        self.layer.add(animation, forKey: "")
    }
}

I had a similar problem to implement a repeating text with my SwiftUI project.我在 SwiftUI 项目中实现重复文本时遇到了类似的问题。 And the answer looks too advanced for me to implement.答案看起来太先进了,我无法实施。 After some search and research.经过一番搜索和研究。 I managed to repeatedly blink my text.我设法反复闪烁我的文字。 For someone who sees this post later, you may try this approach using withAnimation{} and .animation() .对于稍后看到这篇文章的人,您可以使用withAnimation{}.animation()尝试这种方法。

Swift 5斯威夫特 5

@State private var myRed = 0.2
@State private var myGreen = 0.2
@State private var myBlue = 0.2

var body:some View{
    
Button(action:{
 //
}){
 Text("blahblahblah")
}
.border(Color(red: myRed,green: myGreen,blue: myBlue))
.onAppear{
    withAnimation{
       myRed = 0.5
       myGreen = 0.5
       myBlue = 0
    }
}
.animation(Animation.easeInOut(duration:2).repeatForever(autoreverses:true))
}

Update: Xcode 13.4 / iOS 15.5更新:Xcode 13.4 / iOS 15.5

A proposed solution still works with some minimal tuning.建议的解决方案仍然可以通过一些最小的调整来工作。

Updated code and demo is here 更新的代码和演示在这里

Original:原来的:

Hope the following approach would be helpful.希望以下方法会有所帮助。 It is based on ViewModifier and can be controlled by binding.它基于 ViewModifier,可以通过绑定来控制。 Speed of animation as well as animation kind itself can be easily changed by needs.动画速度以及动画种类本身可以根据需要轻松更改。

Note: Although there are some observed drawbacks: due to no didFinish callback provided by API for Animation it is used some trick to workaround it;注意:虽然有一些观察到的缺点:由于动画 API 没有提供 didFinish 回调,因此使用了一些技巧来解决它; also it is observed some strange handling of Animation.repeatCount, but this looks like a SwiftUI issue.还观察到对 Animation.repeatCount 的一些奇怪处理,但这看起来像是一个 SwiftUI 问题。

Anyway, here is a demo (screen flash at start is launch of Preview): a) activating blink in onAppear b) force activating by some action, in this case by button无论如何,这是一个演示(开始时的屏幕闪烁是预览的启动):a)在 onAppear 中激活闪烁 b)通过某些操作强制激活,在这种情况下通过按钮

边框闪烁

struct BlinkingBorderModifier: ViewModifier {
    let state: Binding<Bool>
    let color: Color
    let repeatCount: Int
    let duration: Double

    // internal wrapper is needed because there is no didFinish of Animation now
    private var blinking: Binding<Bool> {
        Binding<Bool>(get: {
            DispatchQueue.main.asyncAfter(deadline: .now() + self.duration) {
                self.state.wrappedValue = false
            }
            return self.state.wrappedValue }, set: {
            self.state.wrappedValue = $0
        })
    }
    
    func body(content: Content) -> some View
    {
        content
            .border(self.blinking.wrappedValue ? self.color : Color.clear, width: 1.0)
            .animation( // Kind of animation can be changed per needs
                Animation.linear(duration:self.duration).repeatCount(self.repeatCount)
            )
    }
}

extension View {
    func blinkBorder(on state: Binding<Bool>, color: Color,
                     repeatCount: Int = 1, duration: Double = 0.5) -> some View {
        self.modifier(BlinkingBorderModifier(state: state, color: color,
                                             repeatCount: repeatCount, duration: duration))
    }
}

struct TestBlinkingBorder: View {
    @State  var blink = false
    var body: some View {
        VStack {
            Button(action: { self.blink = true }) {
                Text("Force Blinking")
            }
            Divider()
            Text("Hello, World!").padding()
                .blinkBorder(on: $blink, color: Color.red, repeatCount: 5, duration: 0.5)
        }
        .onAppear {
            self.blink = true
        }
    }
}

This is so much easy.这很容易。 First create a ViewModifier , so that we can use it easily anywhere.首先创建一个ViewModifier ,以便我们可以在任何地方轻松使用它。

import SwiftUI

struct BlinkViewModifier: ViewModifier {
    
    let duration: Double
    @State private var blinking: Bool = false
    
    func body(content: Content) -> some View {
        content
            .opacity(blinking ? 0 : 1)
            .animation(.easeOut(duration: duration).repeatForever())
            .onAppear {
                withAnimation {
                    blinking = true
                }
            }
    }
}

extension View {
    func blinking(duration: Double = 0.75) -> some View {
        modifier(BlinkViewModifier(duration: duration))
    }
}

Then use this like,然后像这样使用,

// with duration 

Text("Hello, World!")
    .foregroundColor(.white)
    .padding()
    .background(Color.blue)
    .blinking(duration: 0.75) // here duration is optional. This is blinking time

// or (default is 0.75)

Text("Hello, World!")
    .foregroundColor(.white)
    .padding()
    .background(Color.blue)
    .blinking() 

After a lot of research on this topic, I found two ways to solve this thing.在对这个话题进行了大量研究之后,我找到了两种解决这个问题的方法。 Each has its advantages and disadvantages.每个都有其优点和缺点。

The Animation way动画方式

There is a direct answer to your question.你的问题有一个直接的答案。 It's not elegant as it relies on you putting in the timing in a redundant way.它并不优雅,因为它依赖于您以冗余方式输入时间。

Add a reverse function to Animation like this:像这样向Animation添加reverse函数:

extension Animation {
    func reverse(on: Binding<Bool>, delay: Double) -> Self {
        DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
            on.wrappedValue = false /// Switch off after `delay` time
        }
        return self
    }
}

With this extension, you can create a text, that scales up and back again after a button was pressed like this:使用此扩展程序,您可以创建一个文本,在按下按钮后按比例放大和返回,如下所示:

struct BlinkingText: View {
    @State private var isBlinking: Bool = false

    var body: some View {
        VStack {
            Button {
                isBlinking = true
            } label: {
                Text("Let it blink")
            }
            .padding()

            Text("Blink!")
                .font(.largeTitle)
                .foregroundColor(.red)
                .scaleEffect(isBlinking ? 2.0 : 1.0)
                .animation(Animation.easeInOut(duration: 0.5).reverse(on: $isBlinking, delay: 0.5))
        }
    }
}

It's not perfect so I did more research.它并不完美,所以我做了更多的研究。

The Transition way过渡方式

Actually, SwiftUI provides two ways to get from one look (visual representation, ... you name it) to another smoothly.实际上,SwiftUI 提供了两种方式来平滑地从一种外观(视觉表示,......你的名字)到另一种外观。

  1. Animations are especially designed to get from one View to another look of the same View.(same = same struct, different instance)动画专门设计用于从一个视图到同一视图的另一种外观。(相同 = 相同的结构,不同的实例)
  2. Transitions are made to get from one view to another view by transitioning out the old View and transition in another one.通过过渡出旧视图并过渡到另一个视图来进行从一个视图到另一个视图的过渡。

So, here's another code snippet using transitions.所以,这是另一个使用转换的代码片段。 The hacky part is the if-else which ensures, that one View disappears and another one appears.骇人听闻的部分是if-else确保一个视图消失而另一个视图出现。

struct LetItBlink: View {
    @State var count: Int

    var body: some View {
        VStack {
            Button {
                count += 1
            } label: {
                Text("Let it blink: \(count)")
            }
            .padding()

            if count % 2 == 0 {
                BlinkingText(text: "Blink Blink 1!")
            } else {
                BlinkingText(text: "Blink Blink 2!")
            }
        }
        .animation(.default)
    }
}

private struct BlinkingText: View {
    let text: String

    var body: some View {
        Text(text)
            .foregroundColor(.red)
            .font(.largeTitle)
            .padding()
            .transition(AnyTransition.scale(scale: 1.5).combined(with: .opacity))
    }
}

You can create nice and interesting "animations" by combining transitions.您可以通过组合过渡来创建漂亮而有趣的“动画”。

What's my personal opinion?我个人的看法是什么?

  • Both are not perfectly elegant and look somehow "hacky".两者都不是非常优雅,看起来有点“hacky”。 Either because of the delay management or because of the if-else .要么是因为延迟管理,要么是因为if-else Adding the possibility to SwiftUI to chain Animations would help.为 SwiftUI 添加链接动画的可能性会有所帮助。
  • Transitions look more customisable, but this depends on the actual requirement.过渡看起来更可定制,但这取决于实际需求。
  • Both are necessary.两者都是必要的。 Adding a default animation is one of the first things I do, because they make the app look and feel smooth.添加默认动画是我做的第一件事,因为它们使应用程序看起来和感觉流畅。

This is some code I came up with for a blinking button in SwiftUI 2, it might help someone.这是我为 SwiftUI 2 中的闪烁按钮提出的一些代码,它可能对某人有所帮助。 It's a toggle button that blinks a capsule shaped overlay around the button.这是一个切换按钮,可在按钮周围闪烁胶囊形覆盖层。 It works but personally, I don't like my function blink() that calls itself.它有效,但就我个人而言,我不喜欢调用自身的函数 blink()。

    struct BlinkingButton:View{
    @Binding var val:Bool
    var label:String
    @State private var blinkState:Bool = false
    
    var body: some View{
        Button(label){
            val.toggle()
            if val{
                blink()
            }
        }
        .padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
        .foregroundColor(.white)
        .background(val ? Color.blue:Color.gray)
        .clipShape(Capsule())
        .padding(.all,8)
        .overlay(Capsule().stroke( blinkState && val ? Color.red:Color.clear,lineWidth: 3))

    }
     func blink(){
        blinkState.toggle()
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5){
            if val{
                blink()
            }
        }
    }
   
}

In use it looks like this:在使用中它看起来像这样:

    struct ContentView: View {
    @State private var togVal:Bool = false
    var body: some View {
        VStack{
            Text("\(togVal ? "ON":"OFF")")
            BlinkingButton(val: $togVal, label: "tap me")
        }
    }
}

Please use below SwiftUI code to implement border blinking animation. 请使用下面的SwiftUI代码实现边框闪烁动画。

extension AnyTransition {
    static func repeating<T: ViewModifier>(from: T, to: T, duration: Double = 1) -> AnyTransition {
       .asymmetric(
            insertion: AnyTransition
                .modifier(active: from, identity: to)
                .animation(Animation.easeInOut(duration: duration).repeatForever())
                .combined(with: .opacity),
            removal: .opacity
        )
    }
}

struct BorderColor: ViewModifier {
    private let borderColor: Color
    init(_ borderColor: Color) {
        self.borderColor = borderColor
    }

    func body(content: Content) -> some View {
        content.border(borderColor, width: 2.0)
    }
}

struct ContentView: View {
    @State var showBlinkingView: Bool = false

    var body: some View {
        VStack {
            if showBlinkingView {

                Text("I am blinking")
                    .transition(.repeating(from: BorderColor(.red), to: BorderColor(.green)))
            }
            Spacer()
            Button(action: {
                self.showBlinkingView.toggle()
            }, label: {
                Text("Toggle blinking view")
            })
        }.padding(.vertical, 50)
    }
}

Please have a look reference video 请看参考视频

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM