How to make zoom in zoom out button animation on t

2020-07-13 09:30发布

Simple and regular approach to animate a bump effect for a button but not simple in SwiftUI.

I'm trying to change scale in tapGesture modifier, but it doesn't have any effect. I don't know how to make chain of animations, probably because SwiftUI doesn't have it. So my naive approach was:

@State private var scaleValue = CGFloat(1)

...

Button(action: {
    withAnimation {
        self.scaleValue = 1.5
    }

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

Obviously it doesn't work and buttons image get last scale value immediately.

My second thought was to change scale to 0.8 value on hold event and then after release event make scale to 1.2 and again after few mseconds change it to 1.0. I guess this algorithm should make nice and more natural bump effect. But I couldn't find suitable gesture struct in SwiftUI to handle hold-n-release event.

P.S. For ease understanding, I will describe the steps of the hold-n-release algorithm:

  1. Scale value is 1.0
  2. User touch the button
  3. The button scale becomes 0.8
  4. User release the button
  5. The button scale becomes 1.2
  6. Delay 0.1 sec
  7. The button scale go back to default 1.0

UPD: I found a simple solution using animation delay modifier. But I'm not sure it's right and clear. Also it doens't cover hold-n-release issue:

@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: I noticed in solution above it doesn't matter what value I pass as argument to delay modifier: 0.2 or 1000 will have same effect. Perhaps it's a bug

标签: ios swiftui
3条回答
姐就是有狂的资本
2楼-- · 2020-07-13 10:15

Yes, it looks like a bug but after my experimenting I found that you can do so

I've post a demo at 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()
    }
}

There is another strange thing if we move and scale the second Image to the first then it will not be shown without .frame(width: 100, height: 100).

查看更多
啃猪蹄的小仙女
3楼-- · 2020-07-13 10:28

Nice and clean swiftUI solution:

@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 } }
    )

however, you also need to add this code snippet to the project:

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))
    }
}
查看更多
Ridiculous、
4楼-- · 2020-07-13 10:33
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())
    }
}
查看更多
登录 后发表回答