Try   HackMD

CS193P - Lecture 7: ViewModifier Animation - 70 mins

summary

  • animation
    • how does it work?
  • viewModifier
    • what exactly are functions like foregroundColor, font, padding, etc. doing?
  • animation
    • implicit vs explicit animation
    • animation views (via their viewModifiers which can implement the animatable protoocl)
    • transitions (animation the appreance/disapperance of views by specifying viewModifier)
    • match geometry effect
    • animating shapes (via the animatable protocol)

00:30 Animation

兩種方法可以做動畫:

  1. animating a Shape
  2. using ViewModifier

withAnimation { ... } ??

1:07 ViewModifier

ViewModifier 是什麼東西?

Text(card.content) .font(font(in: geometry.size)) .padding(DrawingConstants.circlePadding) .aspectRatio(2/3)

這邊的 AspectModifier 是一個 ViewModifier,view 套用 ViewModifier 後再被送出去

extension View { func aspectRatio(_ ratio: CGFloat) -> some View { self.modifier(AspectModifier(ratio)) } }

而 AspectModifier 這個 ViewModifier 需要 conform Modifier protocol
only job is to create a new View based on the thing passed to it.

protocol ViewModifier { typealias Content func body(content: Content) -> some View { return some View that almost certanly contains the View content } }
  • ViewModifier 跟一般的 View 程式碼看起來非常像,差異是
    • View 需要 var body: some View { get }
    • ViewModifier 需要 func body(content: Content) -> some View
    • 他們倆是不一樣的東西,ViewModifier 無法當成 View 使用

ViewModifier 做的事需要把 View 丟進去,然後他會處理後再丟出來

先丟進去 再丟出來
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

6:17 Demo

自製 ViewModifier - Cardify

讓「卡片」這個 View 透過傳入的 isFaceUp 決定是正面(卡片內容)或反面(色塊)

struct Cardify: ViewModifier { var isFaceUp: Bool func body(content: Content) -> some View { ZStack { let shape = RoundedRectangle(cornerRadius: DrawingConstants.cornerRadius) if isFaceUp { shape.fill().foregroundColor(.white) shape.strokeBorder(lineWidth: DrawingConstants.lineWidth) content } else { shape.fill() } } } private struct DrawingConstants { static let cornerRadius: CGFloat = 10 static let lineWidth: CGFloat = 2.5 } } extension View { func cardify(isFaceUp: Bool) -> some View { return self.modifier(Cardify(isFaceUp: isFaceUp)) } }

把卡片內容替換成任意 View 也有相同的卡片樣式

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

16:09 Animation

  • 只有在東西「被改變了」,才會去觸發動畫
    1. ViewModifier arguments
    2. Shapes
    3. the existance (or not) of a View in the UI
  • 動畫是反應狀態 已經 被改變了的事實

ViewModifier are the primary "change agent" in the UI. A change to a ViewModifier's arguments has to happen after the View is initially put in the UI. In other words, only changes in a ViewModifier's arguments since it joined the UI are animated. Not all ViewModifier arguments are animatable (e.g. .font's are not), but most are.

How do you make an animation "go"?

  1. implicitly, using the view modifier .animation(.linear)
  2. explicit, withAnimation(.linear) { ... }
  3. by making Views be inclued or excluded from the UI

21:17 Implicit Animation 隱式動畫

Text("👻")
    .opacity(scary ? 1 : 0)
    .rotationEffect(Angle.degree(upsideDown ? 180 : 0))
    .animation(Animation.easeInOut)    // 加上這行即可獲得隱式動畫
  • 上面這樣子寫,在 scary 變化時,opacity 會以動畫的方式跟著變化
  • 要注意的是,寫在 .animation() 後面的屬性變化不會被以動畫呈現
  • 不建議將 .animation() 用在像 ZStack 這樣子的 container,因為他會把動畫傳遞進去他的子 views
  • 當你對某個 container 加上 .animation() 時,你並不是在為 container 本身設定動畫;你是在說「我想要讓 container 內的所有 subviews 做這個動畫」

The .animation modifier does not work how you might think on a container. A container just propagates the .animation modifier to all the Views it contains. In other words, .animation does not work on like .padding, it works mpre like .font.

設定 animation 的一些細節: duration, delay, repeat, linear, easeInOut

28:19 Explicit Animation

  • 更常見的動畫需求是我們操作 model 內的變數,然後期待他會造成什麼動畫出現
  • 或是使用者做了什麼互動操作,app 要跟著響應,這時我們使用 Explicit Animation(顯式動畫)
  • 大部分時候,我們會在 viewModel 內的 intent function 裡呼叫
  • 不過會換一個說法,不會直接指定哪些 view 要做什麼,而是類似「進入 editing mode」
  • Explicit animations do not override an Implicit animation
withAnimation(.linear(duration: 2)) { // do something that will cause ViewModifier/Shape arguments to change somewhere }

30:08 Transitions

Transition sprcify how to animation the arrival/departure of Views.
Only works for Views that are inside CTAAOS(Containers That Are Already On-Screen)

Under the covers, a tranisiton is nothing more than a pair of ViewModifiers. One of the modifiers is the "before" modification of the View that's on the move. The other modifier is the "after" modification if the View that's on the move. Thus a transition is just a version of a "changes in arguments to ViewModifiers" animation.

An asymmetric transition has 2 pairs of ViewModifiers. One pair for when the View appears (insertion). And another pair for when the View disappears (removal). Example: a View fades in when it appears, but then flies across the screen then it disappears.

Mostly we use "pre-canned" transitios (opacity, scaling, moving across the screen). They are static vars/funcs on the AnyTransition struct.

31:35 Transitions

all the transition API is "type erased"

37:50 matched Geometry Effect

  • 如果你要移動的 View 的新位置是在同一個 container 底下,那麼直接使用 .position() 即可
  • 如果是要做跨 container 的位移 -> that is really not posible
  • 請用 .matchedGeometryEffect(id: ID, in: namespace)

45:10 Shape and ViewModifier Animation

all actual animation happens in shapes and ViewModifiers.
(even transition and matchedGeometryEffects are just "paired ViewModifiers")

so how do they actually do their animation?
the communication with the animation system happens (both ways) with a single var. This var is the only thing in the Animatable protocol. Shape and ViewModifier that want to be animatable must implement this protocol.

public protocol Animatable { /// The type defining the data to animate. associatedtype AnimatableData : VectorArithmetic /// The data to animate. var animatableData: Self.AnimatableData { get set } }

47:54 Demo

寫這樣的話,isMatched 改變後的動畫會瞬間就完成,所以看不到過程。

Text(card.content) .rotationEffect(Angle(degrees: card.isMatched ? 360 : 0))

多加一行表示我們想要 implicit animation。

Text(card.content) .rotationEffect(Angle(degrees: card .isMatched ? 360 : 0)) .animation(Animation.easeInOut(duration: 2))

但有問題出現了

點了的第二張卡片沒有旋轉動畫 裝置旋轉後會有奇怪的位移動畫

58:18 處理旋轉動畫會因為裝置旋轉後被重複套用的問題

因為 .font() 這個 ViewModifier 不是 animatable
所以我們用另一種方式來達到調整字體大小的目的 .scaleEffect()

1:03:20 再次提醒 ViewModifier 的順序十分重要

.animation() 放置的位置很重要,他會套用到該行以前的 View

所以應該要寫成

Text() .rotateEffect(...) .animation(...) .font(...) .scaleEffect(...)

而不是

Text() .rotateEffect(...) .font(...) .scaleEffect(...) .animation(...) // 這樣 .animation 就會套用前面幾行的效果

1:03:44 處理第二張翻開的卡片沒有旋轉動畫的問題

animation 只會被用在被改變的圖