SwiftUI學習100天(Day45 - 項目 9,第三部分)

原創(chuàng)鏈接:https://www.hackingwithswift.com/100/swiftui
以下內容僅供學習參考:

今天,我們將通過觀察特效和動畫,將你的繪畫技巧發(fā)揮到極致。由于我們正處于繪圖的前沿,可以公平地說這些技能不太可能用于日常編碼,但正如拉爾夫沃爾多愛默生曾經說過的那樣,“我們的目標是超越目標,達到目標?!?/p>
在學習今天的主題時,你將學習如何為形狀設置動畫,這是 SwiftUI 感覺有點像魔術的另一個實例。不過,正如你之前所見,它真的不是魔法——SwiftUI 只是響應我們配置視圖的方式。這有點像 Rube Goldberg 機器:我們把事情設置得完全正確,讓整個機器運轉起來,然后觀察正確的輸出結果。
控制動畫也不例外:我們不希望body
每秒重新調用視圖的屬性 60 或 120 次以獲得流暢的動畫,因此我們只是提供說明隨著動畫的進行應該改變什么。它不是很容易被發(fā)現(xiàn)——也就是說,你不可能偶然發(fā)現(xiàn)這個解決方案——但我希望你會同意它使用起來很簡單。
今天你有三個主題要完成,如果你有勇氣的話,再加一個額外的主題。你將了解混合模式、animatableData
、AnimatablePair
等。

SwiftUI 中的特殊效果:模糊、混合等
SwiftUI 使我們能夠非凡地控制視圖的渲染方式,包括應用實時模糊、混合模式、飽和度調整等的能力。
混合模式允許我們控制一個視圖在另一個視圖之上的渲染方式。默認模式是.normal
,它只是將新視圖中的像素繪制到后面的任何內容上,但是有很多選項可以控制顏色和不透明度。
例如,我們可以在ZStack
中繪制一個圖像
,然后在頂部添加一個紅色矩形,該矩形是使用乘法混合模式繪制的:
“乘法”之所以如此命名,是因為它將每個源像素顏色與目標像素顏色相乘——在我們的例子中,圖像的每個像素和頂部矩形的每個像素。每個像素都有 RGBA 的顏色值,范圍從 0(沒有該顏色)到 1(所有該顏色),因此最高的結果顏色將為 1x1,最低的為 0x0。
對純色使用 multiply 會應用一種非常常見的色調效果:黑色保持黑色(因為它們的顏色值為 0,所以無論你在上面放什么,乘以 0 都會產生 0),而較淺的顏色會變成各種陰影著色。
事實上,乘法是如此常見以至于有一個快捷修飾符意味著我們可以避免使用ZStack
:
還有許多其他混合模式可供選擇,值得花一些時間試驗看看它們是如何工作的。另一種流行的效果稱為screen,它與 multiply 相反:它反轉顏色,執(zhí)行 multiply,然后再次反轉它們,從而產生更亮的圖像而不是更暗的圖像。
例如,我們可以在ZStack
中的不同位置渲染三個圓
,然后使用滑塊來控制它們的大小和重疊:
如果你特別細心,你可能會注意到中間完全混合的顏色不是很白——它是一種非常淡的淡紫色。原因是Color.red
、Color.green
和Color.blue
不完全是那些顏色;使用Color.red
時你看不到純紅色
。相反,你看到的是 SwiftUI 的自適應顏色,旨在在暗模式和亮模式下看起來都不錯,因此它們是紅色、綠色和藍色的自定義混合,而不是純色調。
如果你想看到混合紅色、綠色和藍色的完整效果,你應該使用像這三種自定義顏色:
我們可以應用許多其他實時效果,我們已經在項目 3 中回顧過blur()
。所以,在我們繼續(xù)之前,讓我們再看一個:saturation()
,它調整視圖內使用的顏色量。給它一個介于 0(無顏色,只有灰度)和 1(全色)之間的值。
我們可以編寫一些代碼來在同一視圖中演示blur()
和
saturation()
,如下所示
:
使用該代碼,將滑塊設置為 0 意味著圖像模糊且無色,但是當你將滑塊向右移動時,它會獲得顏色并變得清晰——所有這些都以閃電般的速度呈現(xiàn)。



使用 animatableData 為簡單的形狀制作動畫
我們現(xiàn)在已經涵蓋了各種與繪圖相關的任務,并且在項目 6 中我們研究了動畫,所以現(xiàn)在我想看看將這兩件事放在一起。
首先,讓我們構建一個可以用作示例的自定義形狀——這里是梯形的代碼,它是一個四邊形,有直邊,其中一對對邊平行:
我們現(xiàn)在可以在視圖中使用它,為其插入量傳入一些本地狀態(tài),以便我們可以在運行時修改值:
每次點擊梯形時,insetAmount
都會設置為一個新值,導致重新繪制形狀。
如果我們可以為插圖中的變化設置動畫不是很好嗎?當然可以——嘗試將onTapGesture()
閉包更改為:
現(xiàn)在再次運行它,并且……什么都沒有改變。我們已經要求動畫,但我們沒有得到動畫——這是怎么回事?
之前看動畫的時候,讓你在body
屬性里面添加一個調用
print()
,然后是這樣說的:
”你應該看到它打印出 2.0、3.0、4.0 等等。同時,按鈕平滑地向上或向下縮放——它不只是直接跳到縮放 2、3 和 4。這里實際發(fā)生的是 SwiftUI 在綁定更改之前檢查我們視圖的狀態(tài),檢查綁定更改后視圖的目標狀態(tài),然后應用動畫從 A 點到達 B 點?!?/p>
因此,一旦insetAmount
設置為新的隨機值,它會立即跳轉到該值并將其直接傳遞給Trapezoid
- 它不會在動畫發(fā)生時傳遞大量中間值。這就是為什么我們的梯形從一個插圖跳到另一個插圖;它甚至不知道動畫正在發(fā)生。
我們只需四行代碼就可以解決這個問題,其中一行只是一個右括號。然而,盡管這段代碼很簡單,但它的工作方式可能會讓你費盡心思。
首先,代碼——現(xiàn)在將這個新的計算屬性添加到Trapezoid
結構中:
你現(xiàn)在可以再次運行該應用程序并看到我們的梯形以流暢的動畫改變形狀。
這里發(fā)生的事情非常復雜:當我們使用withAnimation()
時
,SwiftUI 會立即將我們的狀態(tài)屬性更改為其新值,但在后臺它還會跟蹤隨時間變化的值作為動畫的一部分。隨著動畫的進行,SwiftUI 會將animatableData
我們形狀的屬性設置為最新值,由我們來決定這意味著什么——在我們的例子中,我們直接將它分配給insetAmount
,因為這是我們想要動畫的東西。
請記住,SwiftUI 在應用動畫之前評估我們的視圖狀態(tài),然后再次評估。它可以看到我們最初的代碼評估為Trapezoid(insetAmount: 50)
,但是在選擇了一個隨機數(shù)之后我們最終得到了(例如)Trapezoid(insetAmount: 62)
。因此,它將在我們的動畫長度內插值 50 到 62 之間,每次都將animatableData
我們形狀的屬性設置為最新的插值值——51、52、53 等等,直到達到 62。



使用 AnimatablePair 為復雜形狀制作動畫
SwiftUI 使用一個animatableData
屬性讓我們對形狀的變化進行動畫處理,但是當我們想要兩個、三個、四個或更多屬性進行動畫處理時會發(fā)生什么?animatableData
是一個屬性,這意味著它必須始終是一個值,但是我們可以決定它是什么類型的值:它可能是單個Double
,也可能是包含在名為 的特殊包裝器中的兩個值AnimatablePair
。
為了嘗試這一點,讓我們看看一個名為 Checkerboard
的新形狀
,它必須使用一定數(shù)量的行和列來創(chuàng)建:
我們現(xiàn)在可以在 SwiftUI 視圖中創(chuàng)建一個 4x4 棋盤,使用一些我們可以使用點擊手勢更改的狀態(tài)屬性:
當它運行時,你應該能夠點擊黑色方塊以查看棋盤從 4x4 跳到 8x16,沒有動畫,即使變化是在一個withAnimation()
塊內。
與更簡單的形狀一樣,這里的解決方案是實現(xiàn)一個animatableData
屬性,該屬性將隨著動畫的進行而設置為中間值。不過,這里有兩個問題:
我們有兩個要設置動畫的屬性,而不是一個。
我們的
row
和column
屬性是整數(shù),SwiftUI 不能插入整數(shù)。
為了解決第一個問題,我們將使用一種名為AnimatablePair
.?顧名思義,它包含一對可設置動畫的值,并且因為它的兩個值都可以設置動畫,AnimatablePair
所以它本身也可以設置動畫。我們可以使用.first
和.second
從對中讀取單個值
。
為了解決第二個問題,我們只需要進行一些類型轉換:我們可以使用 .Int(someDouble)
將 一個
Double
使用
?,而反過來轉換
為一個Int
Double(someInt)。
因此,要使我們的棋盤動畫顯示行數(shù)和列數(shù)的變化,請?zhí)砑哟藢傩裕?/p>
現(xiàn)在,當你運行該應用程序時,你應該會發(fā)現(xiàn)變化很順利地發(fā)生了——或者正如你所期望的那樣順利,因為我們將數(shù)字四舍五入為整數(shù)。
當然,下一個問題是:我們如何為三個屬性設置動畫?還是四個?
為了回答這個問題,讓我向你展示SwiftUI的EdgeInsets
類型的animatableData
屬性:
是的,他們使用三個獨立的動畫對,然后使用諸如newValue.second.second.first
.
我不會聲稱這是最優(yōu)雅的解決方案,但我希望你能理解它存在的原因:因為 SwiftUI 可以讀取和寫入形狀的可動畫數(shù)據(jù),而不管數(shù)據(jù)是什么或它意味著什么,它不會body
在動畫期間,每秒需要 60 次甚至 120 次重新調用我們視圖的屬性——它只會更改實際正在更改的部分。



使用 SwiftUI 創(chuàng)建呼吸描記器
為了完成真正通過繪圖進入城鎮(zhèn)的東西,我將引導你使用 SwiftUI 創(chuàng)建一個簡單的螺旋儀?!癝pirograph”是一種玩具的商標名稱,你可以將一支鉛筆放在一個圓圈內,然后繞著另一個圓圈的圓周旋轉,從而創(chuàng)造出各種幾何圖案,這些圖案被稱為輪盤賭——就像賭場游戲一樣。
此代碼涉及一個非常具體的方程式。我將對其進行解釋,但如果你不感興趣,完全可以跳過本章——這只是為了好玩,這里沒有介紹新的 Swift 或 SwiftUI。
我們的算法有四個輸入:
內圓的半徑。
外圓的半徑。
虛擬筆距外圈圓心的距離。
抽取多少輪盤賭。這是可選的,但我認為它確實有助于顯示算法工作時發(fā)生的情況。
那么,讓我們從這個開始:
然后我們從該數(shù)據(jù)準備三個值,從內半徑和外半徑的最大公約數(shù) (GCD) 開始。計算兩個數(shù)的 GCD 通常使用 Euclid 算法完成,其稍微簡化的形式如下所示:
請將該方法添加到Spirograph
結構中。
其他兩個值是內半徑和外半徑之間的差值,以及繪制輪盤需要執(zhí)行多少步——這是 360 度乘以外半徑除以最大公約數(shù),再乘以我們輸入的數(shù)量。我們所有的輸入在以整數(shù)形式提供時效果最佳,但在繪制輪盤賭時我們需要使用Double
,因此我們還將創(chuàng)建Double
輸入的副本。
現(xiàn)在將此path(in:)
方法添加到Spirograph
結構中:
最后,我們可以通過從 0 到終點循環(huán)并將點放置在精確的 X/Y 坐標處來繪制輪盤賭本身。計算該循環(huán)中給定點的 X/Y 坐標(稱為“theta”)是真正的數(shù)學用武之地,但老實說,我只是將標準方程式從維基百科轉換為 Swift——這不是我夢想記住的東西!
X等于半徑差乘以theta的余弦,加上距離乘以半徑差的余弦除以外半徑乘以theta。
Y等于半徑差乘以theta的正弦,減去距離乘以半徑差的正弦除以外半徑乘以theta。
這是核心算法,但我們要做兩個小改動:我們將分別向 X 和 Y 添加繪圖矩形寬度或高度的一半,以便它在我們的繪圖空間中居中,并且如果 theta 為 0 – 也就是說,如果這是我們輪盤賭中被抽取的第一個點 – 我們的為路徑調用move(to:)
而不是addLine(to:)
。
這是該方法的最終代碼path(in:)
——將// more code to come
注釋替換為:
我意識到這是大量繁重的數(shù)學運算,但回報即將到來:我們現(xiàn)在可以在視圖中使用該形狀,添加各種滑塊來控制內半徑、外半徑、距離、數(shù)量,甚至顏色:
那是很多代碼,但我希望你花時間運行該應用程序并欣賞輪盤賭的美妙之處。你所看到的實際上只是輪盤賭的一種形式,稱為次擺線——通過對算法進行小幅調整,你可以生成外擺線等等,它們以不同的方式呈現(xiàn)出美麗。
在我結束之前,我想提醒你這里使用的參數(shù)方程是數(shù)學標準,而不是我剛剛發(fā)明的東西——我真的去了維基百科關于 hypotrochoids 的頁面 (?https://en.wikipedia.org/wiki/Hypotrochoid?)并將它們轉換為 Swift。


