如何在 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.2
或 1000
将具有相同的效果。也许这是一个错误????
所以我使用了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:
在我们等待苹果官方更新之前,实现touchStart
和touchEnd
两个事件的合适解决方案之一是基于@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 中点击手势时放大缩小按钮动画?的主要内容,如果未能解决你的问题,请参考以下文章
如何在 Android 视觉 CameraSource 中添加放大/缩小手势