上一篇文章我們了解了如何使用.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}
}
最后,希望能夠幫助到有需要的朋友,如果覺得有幫助,還望點個贊,添加個關注,筆者也會不斷地努力,寫出更多更好用的文章。