日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

SwiftUI之深入解析高级动画的几何效果GeometryEffect

發布時間:2024/5/28 编程问答 52 豆豆
生活随笔 收集整理的這篇文章主要介紹了 SwiftUI之深入解析高级动画的几何效果GeometryEffect 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

一、前言

  • 在我的博客 SwiftUI之深入解析高級動畫的路徑Paths 中,已經了解了 Animatable 的協議,以及如何使用它來動畫路徑。接下來,我們將使用相同的協議來動畫變換矩陣,使用一個新的工具:幾何效果。
  • 幾何效果是一個協議,符合 Animatable 和ViewModifier。為了符合幾何效果,需要實現以下方法:
func effectValue(size: CGSize) -> ProjectionTransform
  • 假設方法叫做 SkewEffect,為了將它應用到一個視圖,可以這樣使用它:
Text("Hello").modifier(SkewEfect(skewValue: 0.5))
  • Text(“Hello”) 將使用 SkewEfect.effectValue() 方法創建的矩陣進行轉換,就這么簡單。注意,這些更改將影響視圖,但不會影響其祖先或后代的布局,因為 GeometryEffect 也符合 Animatable,可以添加一個 animatableData 屬性,有一個 Animatable 效果。
  • 你可能沒有意識到,但你可能一直在使用幾何效果。如果你曾經使用過 .offset(),實際上使用的是幾何效果。如下所示,展示它是如何實現的:
public extension View {func offset(x: CGFloat, y: CGFloat) -> some View {return modifier(_OffsetEffect(offset: CGSize(width: x, height: y)))}func offset(_ offset: CGSize) -> some View {return modifier(_OffsetEffect(offset: offset))} }struct _OffsetEffect: GeometryEffect {var offset: CGSizevar animatableData: CGSize.AnimatableData {get { CGSize.AnimatableData(offset.width, offset.height) }set { offset = CGSize(width: newValue.first, height: newValue.second) }}public func effectValue(size: CGSize) -> ProjectionTransform {return ProjectionTransform(CGAffineTransform(translationX: offset.width, y: offset.height))} }

二、關鍵幀動畫 Animation Keyframes

  • 大多數動畫框架都有關鍵幀的概念,這是一種告訴動畫引擎將動畫劃分為塊的方法,雖然 SwiftUI 沒有這些特性,但我們可以模擬它。如下所示,創建一個效果,使視圖水平移動,但它在開始時傾斜,在結束時不傾斜:

  • 傾斜效果需要在動畫的前 20% 和后 20% 期間增加和減少,在中間傾斜效應將保持穩定,那么如何解決它呢?我們創建一個傾斜和移動我們的觀點的效果,不需要注意 20% 的要求,CGAffineTransform c 參數驅動傾,tx,x offset::

struct SkewedOffset: GeometryEffect {var offset: CGFloatvar skew: CGFloatvar animatableData: AnimatablePair<CGFloat, CGFloat> {get { AnimatablePair(offset, skew) }set {offset = newValue.firstskew = newValue.second}}func effectValue(size: CGSize) -> ProjectionTransform {return ProjectionTransform(CGAffineTransform(a: 1, b: 0, c: skew, d: 1, tx: offset, ty: 0))} }
  • 為了模擬關鍵幀,我們定義一個可動畫的參數,將其從 0 更改為 1,當該參數為 0.2 時,到達動畫的前 20%,當參數為 0.8 或更大時,處于動畫的最后 20%,代碼應該也隨之相應地改變效果,最重要的是,還會告訴效果是向右還是向左移動視圖,所以它可以向一邊或另一邊傾斜:
struct SkewedOffset: GeometryEffect {var offset: CGFloatvar pct: CGFloatlet goingRight: Boolinit(offset: CGFloat, pct: CGFloat, goingRight: Bool) {self.offset = offsetself.pct = pctself.goingRight = goingRight}var animatableData: AnimatablePair<CGFloat, CGFloat> {get { return AnimatablePair<CGFloat, CGFloat>(offset, pct) }set {offset = newValue.firstpct = newValue.second}}func effectValue(size: CGSize) -> ProjectionTransform {var skew: CGFloatif pct < 0.2 {skew = (pct * 5) * 0.5 * (goingRight ? -1 : 1)} else if pct > 0.8 {skew = ((1 - pct) * 5) * 0.5 * (goingRight ? -1 : 1)} else {skew = 0.5 * (goingRight ? -1 : 1)}return ProjectionTransform(CGAffineTransform(a: 1, b: 0, c: skew, d: 1, tx: offset, ty: 0))} }
  • 為了好玩,我們將把這個效果應用到多個視圖,它們的動畫會交錯,使用 .delay() 動畫修飾符。完整的代碼可以參考文末的完整示例,在實例 6 中可以找到:

三、動畫反饋

  • 現在來創建一個效果,執行 3d 旋轉,盡管 SwiftUI 已經有了一個修飾符,即 rotation3deeffect(),但這個修飾符比較特別,每當視圖旋轉到足夠顯示另一邊時,一個布爾綁定將被更新;通過對綁定變量的變化做出反應,我們將能夠替換正在旋轉動畫的過程中的視圖,這將創造一種錯覺,即視圖有兩個面,如下所示:

  • 你可能會注意到,三維旋轉變換可能與在核心動畫中的習慣略有不同。在 SwiftUI 中,默認的錨點是在視圖的前角,而在 Core Animation 中是在中心。雖然現有的 .rotrotingg3DEffect() 修飾符可以指定一個錨點,但我們正在建立我們自己的效果,這意味著必須自己處理它。由于不能改變錨點,我們需要在組合中加入一些轉換效果:
struct FlipEffect: GeometryEffect {var animatableData: Double {get { angle }set { angle = newValue }}@Binding var flipped: Boolvar angle: Doublelet axis: (x: CGFloat, y: CGFloat)func effectValue(size: CGSize) -> ProjectionTransform {// 我們把修改安排在視圖繪制完成后進行。// 否則,我們會收到一個運行時錯誤,表明我們正在改變// 視圖正在繪制時改變狀態。DispatchQueue.main.async {self.flipped = self.angle >= 90 && self.angle < 270}let a = CGFloat(Angle(degrees: angle).radians)var transform3d = CATransform3DIdentity;transform3d.m34 = -1/max(size.width, size.height)transform3d = CATransform3DRotate(transform3d, a, axis.x, axis.y, 0)transform3d = CATransform3DTranslate(transform3d, -size.width/2.0, -size.height/2.0, 0)let affineTransform = ProjectionTransform(CGAffineTransform(translationX: size.width/2.0, y: size.height / 2.0))return ProjectionTransform(transform3d).concatenating(affineTransform)} }
  • 通過查看幾何效果代碼,可以看到:我們用 @Bindingd 屬性 flipped 來向視圖報告,哪一面是面向用戶的。在視圖中,將使用 flipped 的值來有條件地顯示兩個視圖中的一個。然而,在示例中,我們將更多使用一個技巧,如果你仔細觀察上面的 gif 圖,你會發現這張牌一直在變化,雖然背面總是一樣的,但正面卻每次都在變化。因此,這不是簡單的為一面展示一個視圖,為另一面展示另一個視圖,我們不是基于 flipped 的值,而是要監測 flipped 的值的變化,然后每一個完整的回合,都將使用不同的牌。
  • 有一個圖像名稱的數組,想去逐一查看,為了做到這一點,我們將會使用一個自定義綁定變量(完整的代碼可以在文末完整示例中的實例 7 中找到):
struct RotatingCard: View {@State private var flipped = false@State private var animate3d = false@State private var rotate = false@State private var imgIndex = 0let images = ["diamonds-7", "clubs-8", "diamonds-6", "clubs-b", "hearts-2", "diamonds-b"]var body: some View {let binding = Binding<Bool>(get: { self.flipped }, set: { self.updateBinding($0) })return VStack {Spacer()Image(flipped ? "back" : images[imgIndex]).resizable().frame(width: 265, height: 400).modifier(FlipEffect(flipped: binding, angle: animate3d ? 360 : 0, axis: (x: 1, y: 5))).rotationEffect(Angle(degrees: rotate ? 0 : 360)).onAppear {withAnimation(Animation.linear(duration: 4.0).repeatForever(autoreverses: false)) {self.animate3d = true}withAnimation(Animation.linear(duration: 8.0).repeatForever(autoreverses: false)) {self.rotate = true}}Spacer()}}func updateBinding(_ value: Bool) {// If card was just flipped and at front, change the cardif flipped != value && !flipped {self.imgIndex = self.imgIndex+1 < self.images.count ? self.imgIndex+1 : 0}flipped = value} }
  • 如前所述,我們可能想使用兩個完全不同的視圖,而不是改變圖像名稱,這也是可以的:
Color.clear.overlay(ViewSwapper(showFront: flipped)).frame(width: 265, height: 400).modifier(FlipEffect(flipped: $flipped, angle: animate3d ? 360 : 0, axis: (x: 1, y: 5))) struct ViewSwapper: View {let showFront: Boolvar body: some View {Group {if showFront {FrontView()} else {BackView()}}} }

四、跟隨路徑創建視圖

  • 來一個完全不同的 GeometryEffect,我們的效果將通過一個任意的路徑移動一個視圖,需要注意兩個問題:
    • 如何獲取路徑中特定點的坐標;
    • 如何在通過路徑移動時確定視圖的方向。

  • 這個效果的可動畫參數將是 pct,它代表飛機在路徑中的位置。如果想讓飛機執行一個完整的轉彎,我們將使用 0 到 1 的值,對于一個 0.25 的值,它意味著飛機已經前進了 1/4 的路徑。

① 尋找路徑中的 x、y 位置

  • 為了獲得飛機在給定的 pct 值下的 x 和 y 位置,可以使用 Path 結構體的 .trimmedPath() 修飾符,給定一個起點和終點百分比,該方法返回一個 CGRect,它包含了該段路徑的邊界。
  • 根據我們的需求,只需用使用非常接近的起點和終點來調用它,它將返回一個非常小的矩形,將使用其中心作為 X 和 Y 位置。
func percentPoint(_ percent: CGFloat) -> CGPoint {// percent difference between pointslet diff: CGFloat = 0.001let comp: CGFloat = 1 - diff// handle limitslet pct = percent > 1 ? 0 : (percent < 0 ? 1 : percent)let f = pct > comp ? comp : pctlet t = pct > comp ? 1 : pct + difflet tp = path.trimmedPath(from: f, to: t)return CGPoint(x: tp.boundingRect.midX, y: tp.boundingRect.midY) }

② 尋找方向

  • 為了獲得平面的旋轉角度,需要使用一點三角函數,使用上面描述的技術,將得到兩點的 X 和 Y 的位置:當前位置和剛才的位置,通過創建一條假想線,可以計算出它的角度,這就是飛機的方向:
func calculateDirection(_ pt1: CGPoint,_ pt2: CGPoint) -> CGFloat {let a = pt2.x - pt1.xlet b = pt2.y - pt1.ylet angle = a < 0 ? atan(Double(b / a)) : atan(Double(b / a)) - Double.pireturn CGFloat(angle) }

③ 把所有的內容結合在一起

  • 知道實現目標所需的工具,我們來實現這種效果:
struct FollowEffect: GeometryEffect {var pct: CGFloat = 0let path: Pathvar rotate = truevar animatableData: CGFloat {get { return pct }set { pct = newValue }}func effectValue(size: CGSize) -> ProjectionTransform {if !rotate { // Skip rotation loginlet pt = percentPoint(pct)return ProjectionTransform(CGAffineTransform(translationX: pt.x, y: pt.y))} else {let pt1 = percentPoint(pct)let pt2 = percentPoint(pct - 0.01)let angle = calculateDirection(pt1, pt2)let transform = CGAffineTransform(translationX: pt1.x, y: pt1.y).rotated(by: angle)return ProjectionTransform(transform)}}func percentPoint(_ percent: CGFloat) -> CGPoint {// percent difference between pointslet diff: CGFloat = 0.001let comp: CGFloat = 1 - diff// handle limitslet pct = percent > 1 ? 0 : (percent < 0 ? 1 : percent)let f = pct > comp ? comp : pctlet t = pct > comp ? 1 : pct + difflet tp = path.trimmedPath(from: f, to: t)return CGPoint(x: tp.boundingRect.midX, y: tp.boundingRect.midY)}func calculateDirection(_ pt1: CGPoint,_ pt2: CGPoint) -> CGFloat {let a = pt2.x - pt1.xlet b = pt2.y - pt1.ylet angle = a < 0 ? atan(Double(b / a)) : atan(Double(b / a)) - Double.pireturn CGFloat(angle)} }

五、ignoredByLayout() 方法

  • 對 GeometryEffect 幾何效果的最后一個技巧是 .ignoredByLayout() 方法,看看文檔的描述:
Returns an effect producing the same geometry transform asself” but that will only be applied while rendering its view, not while the view is performing its layout calculations. This is often used to disable layout changes during transitions, but that will only be applied while rendering its view, not while the view is performing its layout calculations. This is often used to disable layout changes during transitions.
  • 大致意思為:返回一個產生與此效果相同的幾何變換的效果,只需要在渲染其視圖時應用該變換。使用此方法可以在轉換期間禁用布局更改,在視圖執行布局計算時,視圖將忽略此方法返回的變換。
  • 如下所示一個例子,使用 .ignoredByLayout() 有一些明顯的效果,將看到 GeometryReader 是如何報告不同的位置的,這取決于效果是如何被添加的(即,有或沒有 .ignoredByLayout() ):

struct ContentView: View {@State private var animate = falsevar body: some View {VStack {RoundedRectangle(cornerRadius: 5).foregroundColor(.green).frame(width: 300, height: 50).overlay(ShowSize()).modifier(MyEffect(x: animate ? -10 : 10))RoundedRectangle(cornerRadius: 5).foregroundColor(.blue).frame(width: 300, height: 50).overlay(ShowSize()).modifier(MyEffect(x: animate ? 10 : -10).ignoredByLayout())}.onAppear {withAnimation(Animation.easeInOut(duration: 1.0).repeatForever()) {self.animate = true}}} }struct MyEffect: GeometryEffect {var x: CGFloat = 0var animatableData: CGFloat {get { x }set { x = newValue }}func effectValue(size: CGSize) -> ProjectionTransform {return ProjectionTransform(CGAffineTransform(translationX: x, y: 0))} }struct ShowSize: View {var body: some View {GeometryReader { proxy inText("x = \(Int(proxy.frame(in: .global).minX))").foregroundColor(.white)}} }

六、完整示例

  • SwiftUI高級動畫之路徑Paths、幾何效果GeometryEffect與AnimatableModifier的效果實現。

總結

以上是生活随笔為你收集整理的SwiftUI之深入解析高级动画的几何效果GeometryEffect的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。