【问题标题】:How to make zoom in zoom out button animation on tap gesture in SwiftUI?如何在 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. 刻度值为1.0
  2. 用户触摸按钮
  3. 按钮刻度变为0.8
  4. 用户松开按钮
  5. 按钮刻度变为1.2
  6. 延迟0.1
  7. 按钮刻度恢复默认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))
    }
}

【问题讨论】:

    标签: ios swiftui


    【解决方案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()
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-05-03
      • 1970-01-01
      • 1970-01-01
      • 2021-06-04
      • 1970-01-01
      • 2011-11-12
      • 1970-01-01
      相关资源
      最近更新 更多