上一篇文章我们了解了如何使用.gesture
修饰符和@GestureState
属性包装器,让我们看看另一种常见的手势:DragGesture
拖拽手势。
下面先看个效果图:
这个效果中,我们实现了一个Text
文本,并添加了拖拽手势,可以拖动到屏幕的任意位置,松手后停留在目标位置,而非回到原来的起点位置。
UI组件就不多说了,我们在Text
组件上添加了.gesture
修饰符,并在该修饰符内部添加了DragGesture
手势,然后给DragGesture
添加了.updating
和.onEnded
修饰符。
关于.updating
修饰符上一篇文章已经介绍过了(SwiftUI中的手势(TapGesture、LongPressGesture、GestureState的使用)),这里创建了一个@GestureState
修饰的dragOffset
变量,用于绑定到.updating
修饰符上的参数上。在.updating
修饰符的body
中进行了赋值操作,这样在拖拽过程中,state
将不断地被赋予新的值。
@GestureState private var dragOffset: CGSize = .zero
.updating($dragOffset, body: { value, state, _ instate = value.translation
})
同时我们对Text
组件添加了.offset(dragOffset)
修饰符,并传入dragOffset
变量,到此就可以拖拽Text
组件了,但是松手后,它会自动地回到原来的位置。
.offset(dragOffset)
为了解决这个问题,我们还需要再来一个变量position
记录松手时的位置信息。
@State private var position: CGSize = .zero
并且在.onEnded
修饰符的闭包中,给position
赋值。该闭包中返回了当前的手势信息变量,而移动信息储存在该变量的translation
中。
.onEnded({ value inposition.width += value.translation.widthposition.height += value.translation.height
})
这里更新了position
信息,这里主要注意用了+=
运算符,因为拖动不止一次,上一次的结束位置即是下一次的起点位置。如果说拖拽松手后想回到原点,那就移出关于position
的相关代码。
最后在给Text
添加一个.offset(position)
修饰符,并传入position
变量。
.offset(position)
到现在已经添加了两个.offset()
修饰符,也可以添加一个,但是要将position
和dragOffset
两个变量加起来传入进去,比如这样:
.offset(CGSize(width: dragOffset.width + position.width, height: dragOffset.height + position.height))
至此,该动画就全部完成了。另外上面的视图中,在界面的顶部加了两个Text
,用来显示dragOffset
和position
的数值,可以看出,每次动作结束@GestureState
修饰的dragOffset
都恢复了初始值。
完整代码如下:
struct DragGestureDemo: View {@GestureState private var dragOffset: CGSize = .zero@State private var position: CGSize = .zerovar body: some View {ZStack {VStack {Text("dragOffset: \(dragOffset)")Text("position: \(position)")Spacer()}Text("Drag me!").font(.title).padding().background(Color.cyan).cornerRadius(10.0).offset(dragOffset).offset(position).gesture(DragGesture().updating($dragOffset, body: { value, state, _ instate = value.translation}).onEnded({ value inposition.width += value.translation.widthposition.height += value.translation.height}))}}
}
可能有人会说,我习惯用了.onChange()
修饰符,不习惯用.updating()
修饰符,下面这个就是用.onChange()
修饰符实现的拖动。
这里需要两个@State
修饰的变量来记录信息,dragOffset
记录相对于最初位置的偏移量,position
记录松手后相对于最初位置的偏移量。
给Text
设置一个.offset()
修饰符即可,要传入dragOffset
,dragOffset
是被@State
修饰器修饰的,在松手后该值不会重置。
在.onChange()
修饰符闭包内,计算dragOffset
的值,dragOffset = 上一次位置偏移量 + 手势偏移量。
在.onEnded()
修饰符闭包内,计算position
的值,position = dragOffset,因为dragOffset
不会重置,且也是手离开的时候的偏移量。给position
赋值,是为了在.onChange()
修饰符闭包内给dragOffset
赋值。
最终代码如下:
struct DragGestureDemo2: View {@State private var dragOffset: CGSize = .zero@State private var position: CGSize = .zerovar body: some View {ZStack {VStack {Text("dragOffset: \(dragOffset)")Text("position: \(position)")Spacer()}Text("Drag me!").font(.title).padding().background(Color.cyan).cornerRadius(10.0).offset(dragOffset).gesture(DragGesture().onChanged({ value indragOffset.width = position.width + value.translation.widthdragOffset.height = position.height + value.translation.height}).onEnded({ _ inposition = dragOffset}))}}
}
如果想要在松手后回到原点,那就不需要position
记录位置了,直接修改成下面代码即可:
.gesture(DragGesture().onChanged({ value indragOffset = value.translation}).onEnded({ _ indragOffset = .zero})
)
以上就是两种方式实现的拖拽动画,下面看一个比较酷的左右滑动卡片的拖拽动画。
关于这个拖拽动画效果,不做过多的说明了,附上源码,有问题可以留言。
struct DragGestureDemo3: View {@State private var offset: CGSize = .zerovar body: some View {Image("liuyifei").resizable().frame(width: 260).aspectRatio(1.0, contentMode: .fit).offset(offset).scaleEffect(getScale()).rotationEffect(Angle(degrees: getRotation())).gesture(DragGesture().onChanged({ value inwithAnimation(.spring()) {offset = value.translation}}).onEnded({ _ inwithAnimation(.spring()) {offset = .zero}}))}func getScale() -> CGFloat {let max = UIScreen.main.bounds.width / 2let current = abs(offset.width)let percentage = current / maxreturn 1.0 - min(percentage, 0.5) * 0.5}func getRotation() -> Double {let max = UIScreen.main.bounds.width / 2let current = offset.widthlet percentage = Double(current / max)let maxAngle: Double = 10return percentage * maxAngle}
}
最后,希望能够帮助到有需要的朋友,如果觉得有帮助,还望点个赞,添加个关注,笔者也会不断地努力,写出更多更好用的文章。