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

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

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

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

一、前言

  • 在我的博客 SwiftUI之深入解析高級動畫的路徑Paths 中,已經(jīng)了解了 Animatable 的協(xié)議,以及如何使用它來動畫路徑。接下來,我們將使用相同的協(xié)議來動畫變換矩陣,使用一個新的工具:幾何效果。
  • 幾何效果是一個協(xié)議,符合 Animatable 和ViewModifier。為了符合幾何效果,需要實現(xiàn)以下方法:
func effectValue(size: CGSize) -> ProjectionTransform
  • 假設(shè)方法叫做 SkewEffect,為了將它應(yīng)用到一個視圖,可以這樣使用它:
Text("Hello").modifier(SkewEfect(skewValue: 0.5))
  • Text(“Hello”) 將使用 SkewEfect.effectValue() 方法創(chuàng)建的矩陣進(jìn)行轉(zhuǎn)換,就這么簡單。注意,這些更改將影響視圖,但不會影響其祖先或后代的布局,因為 GeometryEffect 也符合 Animatable,可以添加一個 animatableData 屬性,有一個 Animatable 效果。
  • 你可能沒有意識到,但你可能一直在使用幾何效果。如果你曾經(jīng)使用過 .offset(),實際上使用的是幾何效果。如下所示,展示它是如何實現(xiàn)的:
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))} }

二、關(guān)鍵幀動畫 Animation Keyframes

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

  • 傾斜效果需要在動畫的前 20% 和后 20% 期間增加和減少,在中間傾斜效應(yīng)將保持穩(wěn)定,那么如何解決它呢?我們創(chuàng)建一個傾斜和移動我們的觀點的效果,不需要注意 20% 的要求,CGAffineTransform c 參數(shù)驅(qū)動傾,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))} }
  • 為了模擬關(guān)鍵幀,我們定義一個可動畫的參數(shù),將其從 0 更改為 1,當(dāng)該參數(shù)為 0.2 時,到達(dá)動畫的前 20%,當(dāng)參數(shù)為 0.8 或更大時,處于動畫的最后 20%,代碼應(yīng)該也隨之相應(yīng)地改變效果,最重要的是,還會告訴效果是向右還是向左移動視圖,所以它可以向一邊或另一邊傾斜:
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))} }
  • 為了好玩,我們將把這個效果應(yīng)用到多個視圖,它們的動畫會交錯,使用 .delay() 動畫修飾符。完整的代碼可以參考文末的完整示例,在實例 6 中可以找到:

三、動畫反饋

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

  • 你可能會注意到,三維旋轉(zhuǎn)變換可能與在核心動畫中的習(xí)慣略有不同。在 SwiftUI 中,默認(rèn)的錨點是在視圖的前角,而在 Core Animation 中是在中心。雖然現(xiàn)有的 .rotrotingg3DEffect() 修飾符可以指定一個錨點,但我們正在建立我們自己的效果,這意味著必須自己處理它。由于不能改變錨點,我們需要在組合中加入一些轉(zhuǎn)換效果:
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 {// 我們把修改安排在視圖繪制完成后進(jìn)行。// 否則,我們會收到一個運(yùn)行時錯誤,表明我們正在改變// 視圖正在繪制時改變狀態(tài)。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 的值來有條件地顯示兩個視圖中的一個。然而,在示例中,我們將更多使用一個技巧,如果你仔細(xì)觀察上面的 gif 圖,你會發(fā)現(xiàn)這張牌一直在變化,雖然背面總是一樣的,但正面卻每次都在變化。因此,這不是簡單的為一面展示一個視圖,為另一面展示另一個視圖,我們不是基于 flipped 的值,而是要監(jiān)測 flipped 的值的變化,然后每一個完整的回合,都將使用不同的牌。
  • 有一個圖像名稱的數(shù)組,想去逐一查看,為了做到這一點,我們將會使用一個自定義綁定變量(完整的代碼可以在文末完整示例中的實例 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()}}} }

四、跟隨路徑創(chuàng)建視圖

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

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

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

  • 為了獲得飛機(jī)在給定的 pct 值下的 x 和 y 位置,可以使用 Path 結(jié)構(gòu)體的 .trimmedPath() 修飾符,給定一個起點和終點百分比,該方法返回一個 CGRect,它包含了該段路徑的邊界。
  • 根據(jù)我們的需求,只需用使用非常接近的起點和終點來調(diào)用它,它將返回一個非常小的矩形,將使用其中心作為 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) }

② 尋找方向

  • 為了獲得平面的旋轉(zhuǎn)角度,需要使用一點三角函數(shù),使用上面描述的技術(shù),將得到兩點的 X 和 Y 的位置:當(dāng)前位置和剛才的位置,通過創(chuàng)建一條假想線,可以計算出它的角度,這就是飛機(jī)的方向:
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) }

③ 把所有的內(nèi)容結(jié)合在一起

  • 知道實現(xiàn)目標(biāo)所需的工具,我們來實現(xiàn)這種效果:
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.
  • 大致意思為:返回一個產(chǎn)生與此效果相同的幾何變換的效果,只需要在渲染其視圖時應(yīng)用該變換。使用此方法可以在轉(zhuǎn)換期間禁用布局更改,在視圖執(zhí)行布局計算時,視圖將忽略此方法返回的變換。
  • 如下所示一個例子,使用 .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的效果實現(xiàn)。

總結(jié)

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

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。