前言
在前面幾篇關於SwiftUI的文章中,我們用一個具體的基本專案Demo來學習了下SwiftUI,裡面包含了常見的一些控制元件使用以及資料處理和地圖等等,有興趣的小夥伴可以去翻翻以前的文章,在前面總結的時候我有說過要具體說一下這個很有趣的官方示例的,這篇我們就好好的說說這個有意思的圖,我們具體要解析的內容圖如下:
最後出來的UI效果就是上面這個樣子,這個看過SwiftUI官方文件的朋友一定見過這張圖的,但不知道里面的程式碼具體的每一行或者思路是不是都讀懂了,下面我們就認真的分析一下它的實現思路和具體程式碼實際的作用。
解析實現
上面這張效果圖的實現我們把它分為三步走的方式,我們具體看看是那三步呢?然後我們就根據這三步具體的分析一下它的程式碼和實現。
1、畫出底部的背景。
2、畫單獨的箭頭型別圖。
3、把他們做一個組裝,組裝出我們現在看到的效果例項。
1、底部檢視該怎樣畫呢?
最主要的還是Path的下面兩個方法,
/// Appends a straight line segment from the current point to the specified /// point. public mutating func addLine(to p: CGPoint)
這個方法是 Path 類的劃線方法
/// Adds a quadratic Bézier curve to the path, with the specified end point /// and control point. public mutating func addQuadCurve(to p: CGPoint, control cp: CGPoint)
這個方法是 Path 類的畫貝塞爾曲線的方法,通過一個控制點從開始點到結束點畫一條曲線,
在通過這兩個主要方法畫出我們圖形的輪廓之後我們在通過 Shape 的fill 方法給填充一個線性漸變View( LinearGradient )就基本上有了底部檢視的效果。
/// Fills this shape with a color or gradient. /// /// - Parameters: /// - content: The color or gradient to use when filling this shape. /// - style: The style options that determine how the fill renders. /// - Returns: A shape filled with the color or gradient you supply. @inlinable public func fill<S>(_ content: S, style: FillStyle = FillStyle()) -> some View where S : ShapeStyle
那具體的程式碼如下面所示,程式碼註釋比較多,應該都能理解:
struct BadgeBackground: View { /// 漸變色的開始和結束的顏色 static let gradientStart = Color(red: 239.0 / 255, green: 120.0 / 255, blue: 221.0 / 255) static let gradientEnd = Color(red: 239.0 / 255, green: 172.0 / 255, blue: 120.0 / 255) /// var body: some View { /// geometry [dʒiˈɒmətri] 幾何學 /// 14之後改了它的對齊方式,向上對齊 GeometryReader { geometry in Path{path in /// 保證是個正方形 var width: CGFloat = min(geometry.size.width, geometry.size.height) let height = width /// 這個值越大 x的邊距越小 值越小 邊距越大 縮放係數 let xScale: CGFloat = 0.85 /// 定義的是x的邊距 let xOffset = (width * (1.0 - xScale)) / 2.0 width *= xScale /// 這個點事圖中 1 的位置 path.move(to: CGPoint( x: xOffset + width * 0.95 , y: height * (0.20 + HexagonParameters.adjustment)) ) /// 迴圈這個陣列 HexagonParameters.points.forEach { /// 從path開始的點到to指定的點新增一段直線 path.addLine( to:.init( /// useWidth: (1.00, 1.00, 1.00), /// xFactors: (0.60, 0.40, 0.50), x: xOffset + width * $0.useWidth.0 * $0.xFactors.0 , y: height * $0.useHeight.0 * $0.yFactors.0 ) ) /// 從開始的點到指定的點新增一個貝塞爾曲線 /// 這裡開始的點就是上面新增直線結束的點 path.addQuadCurve( to: .init( x: xOffset + width * $0.useWidth.1 * $0.xFactors.1, y: height * $0.useHeight.1 * $0.yFactors.1 ), control: .init( x: xOffset + width * $0.useWidth.2 * $0.xFactors.2, y: height * $0.useHeight.2 * $0.yFactors.2 ) ) } } /// 新增一個線性顏色漸變 .fill(LinearGradient( gradient:.init(colors: [Self.gradientStart, Self.gradientEnd]), /// 其實從 0.5 ,0 到 0.5 0.6 的漸變就是豎直方向的漸變 startPoint:.init(x: 0.5, y: 0), endPoint: .init(x: 0.5, y: 0.6) /// aspect 方向 Ratio 比率,比例 )) .aspectRatio(contentMode: .fit) } } }
這時候的效果圖如下所示:
接著我們在看看箭頭是怎麼畫出來的,具體的程式碼中是把它分成了上面兩部分來畫,然後通過控制各個點的連線畫出了圖案,這次使用的還是Path的方法,具體的是下面這個:
/// Adds a sequence of connected straight-line segments to the path. public mutating func addLines(_ lines: [CGPoint])
注意區分 addLine 和 addLines,不要把他們搞混淆了!一個傳遞的引數是一個點一個是點的集合,在沒有畫之前你可能會覺得難,但其實真正看程式碼還是比較簡單的,最後只需要填充一個你需要的顏色就可以,具體的程式碼我們也不細說了,應為比較簡單,如下:
struct BadgeSymbol: View { static let symbolColor = Color(red: 79.0 / 255, green: 79.0 / 255, blue: 191.0 / 255) var body: some View { GeometryReader { geometry in Path { path in let width = min(geometry.size.width, geometry.size.height) let height = width * 0.75 let spacing = width * 0.030 let middle = width / 2 let topWidth = 0.226 * width let topHeight = 0.488 * height /// 上面部分 path.addLines([ CGPoint(x: middle, y: spacing), CGPoint(x: middle - topWidth, y: topHeight - spacing), CGPoint(x: middle, y: topHeight / 2 + spacing), CGPoint(x: middle + topWidth, y: topHeight - spacing), CGPoint(x: middle, y: spacing) ]) /// path 移動到這個點重新開始繪製 其實這句沒啥影響 /// path.move(to: CGPoint(x: middle, y: topHeight / 2 + spacing * 3)) path.addLines([ CGPoint(x: middle - topWidth, y: topHeight + spacing), CGPoint(x: spacing, y: height - spacing), CGPoint(x: width - spacing, y: height - spacing), CGPoint(x: middle + topWidth, y: topHeight + spacing), CGPoint(x: middle, y: topHeight / 2 + spacing * 3) ]) } .fill(Self.symbolColor) } } }
這時候我們畫的效果如下:
組裝一下
通過上面的分析,我們把需要的基本上就都準備完畢了,然後我們需要的就是把它倆組一個組裝達到我們想要的效果,然後對這個箭頭再做一個簡單的封裝處理,按照上面的例子,需要對每一個箭頭做一個簡單的角度旋轉,旋轉的具體的資料也比較好計算,具體的程式碼如下所示:
/// 八個角度設定箭頭 static let rotationCount = 8 /// var badgeSymbols: some View { ForEach(0..<Badge.rotationCount) { i in RotatedBadgeSymbol( /// degrees 度數 八等分制 angle: .degrees(Double(i) / Double(Badge.rotationCount)) * 360.0 ) } .opacity(0.5) /// opacity 透明度 }
簡單的封裝了下箭頭,程式碼:
struct RotatedBadgeSymbol: View { /// 角度 let angle: Angle /// var body: some View { BadgeSymbol() .padding(-60) /// 旋轉角度 .rotationEffect(angle, anchor: .bottom) } }
最後一步也比較簡單,這種某檢視在另一個製圖之上的需要用到 ZStack ,前面的文章中我們有介紹和使用過 HStack 和 VStack,這次在這裡就用到了 VStack,他們之間沒有啥特備大的區別,理解檢視與檢視之間的層級和位置關係就沒問題。
首先肯定是背景在下面,然後箭頭檢視在上面,把它經過一個迴圈和旋轉角度新增,最後處理一下它的大小和透明底就有了我們需要的效果,具體的程式碼如下:
var body: some View { /// Z 軸 在底部背景之上 ZStack { BadgeBackground() GeometryReader { geometry in self.badgeSymbols /// 縮放比例 .scaleEffect(1.0 / 4.0, anchor: .top) /// position 說的是badgeSymbols的位置 /// GeometryReader可以幫助我們獲取父檢視的size .position(x: geometry.size.width / 2.0, y: (3.0 / 4.0) * geometry.size.height) } } .scaledToFit() }
最後附一份畫圖時候的點的資料方便大家學習:
struct HexagonParameters { struct Segment { let useWidth: (CGFloat, CGFloat, CGFloat) let xFactors: (CGFloat, CGFloat, CGFloat) let useHeight: (CGFloat, CGFloat, CGFloat) let yFactors: (CGFloat, CGFloat, CGFloat) } static let adjustment: CGFloat = 0.085 static let points = [ Segment( useWidth: (1.00, 1.00, 1.00), xFactors: (0.60, 0.40, 0.50), useHeight: (1.00, 1.00, 0.00), yFactors: (0.05, 0.05, 0.00) ), Segment( useWidth: (1.00, 1.00, 0.00), xFactors: (0.05, 0.00, 0.00), useHeight: (1.00, 1.00, 1.00), yFactors: (0.20 + adjustment, 0.30 + adjustment, 0.25 + adjustment) ), Segment( useWidth: (1.00, 1.00, 0.00), xFactors: (0.00, 0.05, 0.00), useHeight: (1.00, 1.00, 1.00), yFactors: (0.70 - adjustment, 0.80 - adjustment, 0.75 - adjustment) ), Segment( useWidth: (1.00, 1.00, 1.00), xFactors: (0.40, 0.60, 0.50), useHeight: (1.00, 1.00, 1.00), yFactors: (0.95, 0.95, 1.00) ), Segment( useWidth: (1.00, 1.00, 1.00), xFactors: (0.95, 1.00, 1.00), useHeight: (1.00, 1.00, 1.00), yFactors: (0.80 - adjustment, 0.70 - adjustment, 0.75 - adjustment) ), Segment( useWidth: (1.00, 1.00, 1.00), xFactors: (1.00, 0.95, 1.00), useHeight: (1.00, 1.00, 1.00), yFactors: (0.30 + adjustment, 0.20 + adjustment, 0.25 + adjustment) ) ] }