如何在 SwiftUI 中点击手势时放大缩小按钮动画?

Posted

技术标签:

【中文标题】如何在 SwiftUI 中点击手势时放大缩小按钮动画?【英文标题】:How to make zoom in zoom out button animation on tap gesture in SwiftUI? 【发布时间】:2019-09-21 03:48:02 【问题描述】:

为按钮设置凹凸效果的简单而常规的方法,但在 SwiftUI 中并不简单。

我正在尝试在 tapGesture 修饰符中更改 scale,但它没有任何效果。我不知道如何制作动画链,可能是因为 SwiftUI 没有它。所以我幼稚的做法是:

@State private var scaleValue = CGFloat(1)

...

Button(action: 
    withAnimation 
        self.scaleValue = 1.5
    

    withAnimation 
        self.scaleValue = 1.0
    
) 
    Image("button1")
        .scaleEffect(self.scaleValue)


显然它不起作用,按钮图像立即获得最后一个比例值。

我的第二个想法是在hold 事件上将比例更改为0.8 值,然后在release 事件之后将比例更改为1.2,并在几毫秒后再次将其更改为1.0。我想这个算法应该会产生更好更自然的 bump 效果。但是我在 SwiftUI 中找不到合适的 gesture 结构来处理 hold-n-release 事件。

附:为了便于理解,我将描述hold-n-release算法的步骤:

    刻度值为1.0 用户触摸按钮 按钮刻度变为0.8 用户松开按钮 按钮刻度变为1.2 延迟0.1秒 按钮刻度恢复默认1.0

UPD:我找到了一个使用动画delay 修饰符的简单解决方案。但我不确定它是否正确和清晰。它也不涵盖hold-n-release 问题:

@State private var scaleValue = CGFloat(1)

...

Button(action: 
    withAnimation 
        self.scaleValue = 1.5
    

    //
    // Using delay for second animation block
    //
    withAnimation(Animation.linear.delay(0.2)) 
        self.scaleValue = 1.0
    
) 
    Image("button1")
        .scaleEffect(self.scaleValue)


UPD 2: 我注意到在上面的解决方案中,我将什么值作为参数传递给 delay 修饰符并不重要:0.21000 将具有相同的效果。也许这是一个错误????

所以我使用了Timer 实例而不是delay 动画修饰符。现在它按预期工作:

...

Button(action: 
    withAnimation 
        self.scaleValue = 1.5
    

    //
    // Replace it
    //
    // withAnimation(Animation.linear.delay(0.2)) 
    //    self.scaleValue = 1.0
    // 
    //
    // by Timer with 0.5 msec delay
    //
    Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false)  _ in
        withAnimation 
            self.scaleValue = 1.0
        
    
) 
...

UPD 3: 在我们等待苹果官方更新之前,实现touchStarttouchEnd 两个事件的合适解决方案之一是基于@average Joe answer:

import SwiftUI

struct TouchGestureViewModifier: ViewModifier 

    let minimumDistance: CGFloat
    let touchBegan: () -> Void
    let touchEnd: (Bool) -> Void

    @State private var hasBegun = false
    @State private var hasEnded = false

    init(minimumDistance: CGFloat, touchBegan: @escaping () -> Void, touchEnd: @escaping (Bool) -> Void) 
        self.minimumDistance = minimumDistance
        self.touchBegan = touchBegan
        self.touchEnd = touchEnd
    

    private func isTooFar(_ translation: CGSize) -> Bool 
        let distance = sqrt(pow(translation.width, 2) + pow(translation.height, 2))
        return distance >= minimumDistance
    

    func body(content: Content) -> some View 
        content.gesture(DragGesture(minimumDistance: 0)
            .onChanged  event in
                guard !self.hasEnded else  return 

                if self.hasBegun == false 
                    self.hasBegun = true
                    self.touchBegan()
                 else if self.isTooFar(event.translation) 
                    self.hasEnded = true
                    self.touchEnd(false)
                
            
            .onEnded  event in
                if !self.hasEnded 
                    let success = !self.isTooFar(event.translation)
                    self.touchEnd(success)
                
                self.hasBegun = false
                self.hasEnded = false
            
        )
    


extension View 
    func onTouchGesture(minimumDistance: CGFloat = 20.0,
                        touchBegan: @escaping () -> Void,
                        touchEnd: @escaping (Bool) -> Void) -> some View 
        modifier(TouchGestureViewModifier(minimumDistance: minimumDistance, touchBegan: touchBegan, touchEnd: touchEnd))
    


【问题讨论】:

【参考方案1】:
struct ScaleButtonStyle: ButtonStyle 
    func makeBody(configuration: Self.Configuration) -> some View 
        configuration.label
            .scaleEffect(configuration.isPressed ? 2 : 1)
    


struct Test2View: View 
    var body: some View 
        Button(action: ) 
            Image("button1")
        .buttonStyle(ScaleButtonStyle())
    

【讨论】:

太棒了!谢谢!你是怎么知道这个方法的? :] 在电报频道:SwiftUI 注意:出于某种奇怪的原因,如果您在 UIVisualEffectView 中使用自定义 ButtonStyle(通过 UIHostingViewController),您会注意到活力无法按预期工作。这是一个值得注意的奇怪副作用。 在 SwiftUI 中是否有任何已知的方法可以在没有按钮的情况下实现相同的缩放效果?【参考方案2】:

干净整洁的swiftUI解决方案:

@State private var scaleValue = CGFloat(1)

...

Image("button1")
    .scaleEffect(self.scaleValue)
    .onTouchGesture(
        touchBegan:  withAnimation  self.scaleValue = 1.5  ,
        touchEnd:  _ in withAnimation  self.scaleValue = 1.0  
    )

不过,你还需要将这段代码sn-p添加到项目中:

struct TouchGestureViewModifier: ViewModifier 
    let touchBegan: () -> Void
    let touchEnd: (Bool) -> Void

    @State private var hasBegun = false
    @State private var hasEnded = false

    private func isTooFar(_ translation: CGSize) -> Bool 
        let distance = sqrt(pow(translation.width, 2) + pow(translation.height, 2))
        return distance >= 20.0
    

    func body(content: Content) -> some View 
        content.gesture(DragGesture(minimumDistance: 0)
                .onChanged  event in
                    guard !self.hasEnded else  return 

                    if self.hasBegun == false 
                        self.hasBegun = true
                        self.touchBegan()
                     else if self.isTooFar(event.translation) 
                        self.hasEnded = true
                        self.touchEnd(false)
                    
                
                .onEnded  event in
                    if !self.hasEnded 
                        let success = !self.isTooFar(event.translation)
                        self.touchEnd(success)
                    
                    self.hasBegun = false
                    self.hasEnded = false
                )
    


extension View 
    func onTouchGesture(touchBegan: @escaping () -> Void,
                      touchEnd: @escaping (Bool) -> Void) -> some View 
        modifier(TouchGestureViewModifier(touchBegan: touchBegan, touchEnd: touchEnd))
    

【讨论】:

我猜有点笨拙,但看起来很棒!感谢您的解决方案!我决定将它包含在我的问题中,并添加一些内容。我将minimumDistance 参数添加到ViewModifier 结构的构造函数中。 我想指出 DragGesture 不适用于 Button 所以 onTouchGesture 放在 extension View 里面没有意义【参考方案3】:

是的,它看起来像一个错误,但经过我的实验,我发现你可以这样做

我在https://youtu.be/kw4EIOCp78g发布了一个演示

struct TestView: View 
    @State private var scaleValue = CGFloat(1)

    var body: some View 
        ZStack 
            CustomButton(
                touchBegan: 
                    withAnimation 
                        self.scaleValue = 2
                    
                ,
                touchEnd: 
                   withAnimation 
                        self.scaleValue = 1
                   
                
            )
                Image("button1")
            .frame(width: 100, height: 100)
            Image("button1").opacity(scaleValue > 1 ? 1 : 0).scaleEffect(self.scaleValue)
        
    


struct CustomButton<Content: View>: UIViewControllerRepresentable 
    var content: Content
    var touchBegan: () -> ()
    var touchEnd: () -> ()

    typealias UIViewControllerType = CustomButtonController<Content>

    init(touchBegan: @escaping () -> (), touchEnd: @escaping () -> (), @ViewBuilder content: @escaping () -> Content) 
        self.touchBegan = touchBegan
        self.touchEnd = touchEnd
        self.content = content()
    


    func makeUIViewController(context: Context) -> UIViewControllerType 
        CustomButtonController(rootView: self.content, touchBegan: touchBegan, touchEnd: touchEnd)
    

    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) 

    



class CustomButtonController<Content: View>: UIHostingController<Content> 
    var touchBegan: () -> ()
    var touchEnd: () -> ()

    init(rootView: Content, touchBegan: @escaping () -> (), touchEnd: @escaping () -> ()) 
        self.touchBegan = touchBegan
        self.touchEnd = touchEnd
        super.init(rootView: rootView)
        self.view.isMultipleTouchEnabled = true
    

    @objc required dynamic init?(coder aDecoder: NSCoder) 
        fatalError("init(coder:) has not been implemented")
    

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) 
        super.touchesBegan(touches, with: event)
        self.touchBegan()
    

    override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) 
        super.touchesCancelled(touches, with: event)
        self.touchEnd()
    

    //touchesEnded only works if the user moves his finger beyond the bound of the image and releases
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) 
        super.touchesEnded(touches, with: event)
        self.touchEnd()
    

还有一件奇怪的事情,如果我们将第二个图像移动并缩放到第一个图像,那么如果没有.frame(width: 100, height: 100),它将不会显示。

【讨论】:

感谢您的回答!抱歉,您的代码看起来像个把戏。在我看来,这个问题应该有一个更简单的解决方案。 我添加了另一个解决方案,我希望它应该很简单【参考方案4】:

ios 15 的答案升级代码(但在 iOS 13 中也可用)。 Animation() 修饰符的单参数形式现在已被正式弃用,主要是因为它会导致各种意外行为(例如在 Lazy 堆栈中:lazy(v/h)scroll、lazyvgrid):按钮动画意外(跳跃)在滚动期间。

public struct ScaleButtonStyle: ButtonStyle 
    public init() 
    
    public func makeBody(configuration: Self.Configuration) -> some View 
        configuration.label
            .scaleEffect(configuration.isPressed ? 0.95 : 1)
            .animation(.linear(duration: 0.2), value: configuration.isPressed)
            .brightness(configuration.isPressed ? -0.05 : 0)
    


public extension ButtonStyle where Self == ScaleButtonStyle 
    static var scale: ScaleButtonStyle 
        ScaleButtonStyle()
    

【讨论】:

以上是关于如何在 SwiftUI 中点击手势时放大缩小按钮动画?的主要内容,如果未能解决你的问题,请参考以下文章

iOS Swift 仿微信朋友圈实现图片点击放大查看

如何在 Android 视觉 CameraSource 中添加放大/缩小手势

ScrollView 的 SwiftUI 动画

捏合和点击手势无法正常工作

为啥在 SwiftUI 中点击按钮的手势甚至在它的框架之外也会被触发?

如何制作 SwiftUI 手势,在按下视图时继续运行代码