SwiftUI 官方畫圖例項詳細解析

zxRisingSun發表於2021-01-19

 

前言


 

      在前面幾篇關於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)
        )
    ]
}

 

相關文章