生活随笔
收集整理的這篇文章主要介紹了
SwiftUI之深入解析高级动画的几何效果GeometryEffect
小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.
一、前言
在我的博客 SwiftUI之深入解析高級動畫的路徑Paths 中,已經了解了 Animatable 的協(xié)議,以及如何使用它來動畫路徑。接下來,我們將使用相同的協(xié)議來動畫變換矩陣,使用一個新的工具:幾何效果。 幾何效果是一個協(xié)議,符合 Animatable 和ViewModifier。為了符合幾何效果,需要實現(xiàn)以下方法:
func effectValue ( size
: CGSize ) - > ProjectionTransform
假設方法叫做 SkewEffect,為了將它應用到一個視圖,可以這樣使用它:
Text ( "Hello" ) . modifier ( SkewEfect ( skewValue
: 0.5 ) )
Text(“Hello”) 將使用 SkewEfect.effectValue() 方法創(chuàng)建的矩陣進行轉換,就這么簡單。注意,這些更改將影響視圖,但不會影響其祖先或后代的布局,因為 GeometryEffect 也符合 Animatable,可以添加一個 animatableData 屬性,有一個 Animatable 效果。 你可能沒有意識到,但你可能一直在使用幾何效果。如果你曾經使用過 .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
: CGSize var 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 沒有這些特性,但我們可以模擬它。如下所示,創(chuàng)建一個效果,使視圖水平移動,但它在開始時傾斜,在結束時不傾斜:
傾斜效果需要在動畫的前 20% 和后 20% 期間增加和減少,在中間傾斜效應將保持穩(wěn)定,那么如何解決它呢?我們創(chuàng)建一個傾斜和移動我們的觀點的效果,不需要注意 20% 的要求,CGAffineTransform c 參數驅動傾,tx,x offset::
struct SkewedOffset : GeometryEffect { var offset
: CGFloat var skew
: CGFloat var animatableData
: AnimatablePair < CGFloat , CGFloat > { get { AnimatablePair ( offset
, skew
) } set { offset
= newValue
. first skew
= 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
: CGFloat var pct
: CGFloat let goingRight
: Bool init ( offset
: CGFloat , pct
: CGFloat , goingRight
: Bool ) { self . offset
= offset
self . pct
= pct
self . goingRight
= goingRight
} var animatableData
: AnimatablePair < CGFloat , CGFloat > { get { return AnimatablePair < CGFloat , CGFloat > ( offset
, pct
) } set { offset
= newValue
. first pct
= newValue
. second
} } func effectValue ( size
: CGSize ) - > ProjectionTransform { var skew
: CGFloat if 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 中可以找到:
三、動畫反饋
現(xiàn)在來創(chuàng)建一個效果,執(zhí)行 3d 旋轉,盡管 SwiftUI 已經有了一個修飾符,即 rotation3deeffect(),但這個修飾符比較特別,每當視圖旋轉到足夠顯示另一邊時,一個布爾綁定將被更新;通過對綁定變量的變化做出反應,我們將能夠替換正在旋轉動畫的過程中的視圖,這將創(chuàng)造一種錯覺,即視圖有兩個面,如下所示:
你可能會注意到,三維旋轉變換可能與在核心動畫中的習慣略有不同。在 SwiftUI 中,默認的錨點是在視圖的前角,而在 Core Animation 中是在中心。雖然現(xiàn)有的 .rotrotingg3DEffect() 修飾符可以指定一個錨點,但我們正在建立我們自己的效果,這意味著必須自己處理它。由于不能改變錨點,我們需要在組合中加入一些轉換效果:
struct FlipEffect : GeometryEffect { var animatableData
: Double { get { angle
} set { angle
= newValue
} } @
Binding var flipped
: Bool var angle
: Double let 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 圖,你會發(fā)現(xiàn)這張牌一直在變化,雖然背面總是一樣的,但正面卻每次都在變化。因此,這不是簡單的為一面展示一個視圖,為另一面展示另一個視圖,我們不是基于 flipped 的值,而是要監(jiān)測 flipped 的值的變化,然后每一個完整的回合,都將使用不同的牌。 有一個圖像名稱的數組,想去逐一查看,為了做到這一點,我們將會使用一個自定義綁定變量(完整的代碼可以在文末完整示例中的實例 7 中找到):
struct RotatingCard : View { @
State private var flipped
= false @
State private var animate3d
= false @
State private var rotate
= false @
State private var imgIndex
= 0 let 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 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
: Bool var body
: some
View { Group { if showFront
{ FrontView ( ) } else { BackView ( ) } } }
}
四、跟隨路徑創(chuàng)建視圖
來一個完全不同的 GeometryEffect,我們的效果將通過一個任意的路徑移動一個視圖,需要注意兩個問題:
這個效果的可動畫參數將是 pct,它代表飛機在路徑中的位置。如果想讓飛機執(zhí)行一個完整的轉彎,我們將使用 0 到 1 的值,對于一個 0.25 的值,它意味著飛機已經前進了 1/4 的路徑。
① 尋找路徑中的 x、y 位置
為了獲得飛機在給定的 pct 值下的 x 和 y 位置,可以使用 Path 結構體的 .trimmedPath() 修飾符,給定一個起點和終點百分比,該方法返回一個 CGRect,它包含了該段路徑的邊界。 根據我們的需求,只需用使用非常接近的起點和終點來調用它,它將返回一個非常小的矩形,將使用其中心作為 X 和 Y 位置。
func percentPoint ( _ percent
: CGFloat ) - > CGPoint { let diff
: CGFloat = 0.001 let comp
: CGFloat = 1 - diff
let pct
= percent
> 1 ? 0 : ( percent
< 0 ? 1 : percent
) let f
= pct
> comp
? comp
: pct
let t
= pct
> comp
? 1 : pct
+ diff
let tp
= path
. trimmedPath ( from
: f
, to
: t
) return CGPoint ( x
: tp
. boundingRect
. midX
, y
: tp
. boundingRect
. midY
)
}
② 尋找方向
為了獲得平面的旋轉角度,需要使用一點三角函數,使用上面描述的技術,將得到兩點的 X 和 Y 的位置:當前位置和剛才的位置,通過創(chuàng)建一條假想線,可以計算出它的角度,這就是飛機的方向:
func calculateDirection ( _ pt1
: CGPoint , _ pt2
: CGPoint ) - > CGFloat { let a
= pt2
. x
- pt1
. x
let b
= pt2
. y
- pt1
. y
let angle
= a
< 0 ? atan ( Double ( b
/ a
) ) : atan ( Double ( b
/ a
) ) - Double . pi
return CGFloat ( angle
)
}
③ 把所有的內容結合在一起
知道實現(xiàn)目標所需的工具,我們來實現(xiàn)這種效果:
struct FollowEffect : GeometryEffect { var pct
: CGFloat = 0 let path
: Path var rotate
= true var animatableData
: CGFloat { get { return pct
} set { pct
= newValue
} } func effectValue ( size
: CGSize ) - > ProjectionTransform { if ! rotate
{ let 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 { let diff
: CGFloat = 0.001 let comp
: CGFloat = 1 - diff
let pct
= percent
> 1 ? 0 : ( percent
< 0 ? 1 : percent
) let f
= pct
> comp
? comp
: pct
let t
= pct
> comp
? 1 : pct
+ diff
let 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
. x
let b
= pt2
. y
- pt1
. y
let angle
= a
< 0 ? atan ( Double ( b
/ a
) ) : atan ( Double ( b
/ a
) ) - Double . pi
return CGFloat ( angle
) }
}
五、ignoredByLayout() 方法
對 GeometryEffect 幾何效果的最后一個技巧是 .ignoredByLayout() 方法,看看文檔的描述:
Returns an effect producing the same geometry transform
as “
self ” 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
.
大致意思為:返回一個產生與此效果相同的幾何變換的效果,只需要在渲染其視圖時應用該變換。使用此方法可以在轉換期間禁用布局更改,在視圖執(zhí)行布局計算時,視圖將忽略此方法返回的變換。 如下所示一個例子,使用 .ignoredByLayout() 有一些明顯的效果,將看到 GeometryReader 是如何報告不同的位置的,這取決于效果是如何被添加的(即,有或沒有 .ignoredByLayout() ):
struct ContentView : View { @
State private var animate
= false var 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 = 0 var 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
in Text ( "x = \( Int ( proxy. frame ( in : . global) . minX) )" ) . foregroundColor ( . white
) } }
}
六、完整示例
SwiftUI高級動畫之路徑Paths、幾何效果GeometryEffect與AnimatableModifier的效果實現(xiàn)。
總結
以上是生活随笔 為你收集整理的SwiftUI之深入解析高级动画的几何效果GeometryEffect 的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網站內容還不錯,歡迎將生活随笔 推薦給好友。