前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >SwiftUI 官方画图实例详细解析

SwiftUI 官方画图实例详细解析

作者头像
Mr.RisingSun
发布2021-01-20 19:38:50
9520
发布2021-01-20 19:38:50
举报
文章被收录于专栏:移动端开发移动端开发

前言


在前面几篇关于SwiftUI的文章中,我们用一个具体的基本项目Demo来学习了下SwiftUI,里面包含了常见的一些控件使用以及数据处理和地图等等,有兴趣的小伙伴可以去翻翻以前的文章,在前面总结的时候我有说过要具体说一下这个很有趣的官方示例的,这篇我们就好好的说说这个有意思的图,我们具体要解析的内容图如下:

最后出来的UI效果就是上面这个样子,这个看过SwiftUI官方文档的朋友一定见过这张图的,但不知道里面的代码具体的每一行或者思路是不是都读懂了,下面我们就认真的分析一下它的实现思路和具体代码实际的作用。

解析实现


上面这张效果图的实现我们把它分为三步走的方式,我们具体看看是那三步呢?然后我们就根据这三步具体的分析一下它的代码和实现。

1、画出底部的背景。

2、画单独的箭头类型图。

3、把他们做一个组装,组装出我们现在看到的效果实例。

1、底部视图该怎样画呢?

最主要的还是Path的下面两个方法,

代码语言:javascript
复制
/// Appends a straight line segment from the current point to the specified
/// point.
public mutating func addLine(to p: CGPoint)

这个方法是 Path 类的划线方法

代码语言:javascript
复制
/// 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 )就基本上有了底部视图的效果。

代码语言:javascript
复制
/// 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

那具体的代码如下面所示,代码注释比较多,应该都能理解:

代码语言:javascript
复制
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的方法,具体的是下面这个:

代码语言:javascript
复制
/// Adds a sequence of connected straight-line segments to the path.
public mutating func addLines(_ lines: [CGPoint])

注意区分 addLine 和 addLines,不要把他们搞混淆了!一个传递的参数是一个点一个是点的集合,在没有画之前你可能会觉得难,但其实真正看代码还是比较简单的,最后只需要填充一个你需要的颜色就可以,具体的代码我们也不细说了,应为比较简单,如下:

代码语言:javascript
复制
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)
        }
    }
}

这时候我们画的效果如下:

组装一下


通过上面的分析,我们把需要的基本上就都准备完毕了,然后我们需要的就是把它俩组一个组装达到我们想要的效果,然后对这个箭头再做一个简单的封装处理,按照上面的例子,需要对每一个箭头做一个简单的角度旋转,旋转的具体的数据也比较好计算,具体的代码如下所示:

代码语言:javascript
复制
/// 八个角度设置箭头
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 透明度
}

简单的封装了下箭头,代码:

代码语言:javascript
复制
struct RotatedBadgeSymbol: View {
    
    /// 角度
    let angle: Angle
    ///
    var body: some View {
        
        BadgeSymbol()
            .padding(-60)
            /// 旋转角度
            .rotationEffect(angle, anchor: .bottom)
    }
}

最后一步也比较简单,这种某视图在另一个制图之上的需要用到 ZStack ,前面的文章中我们有介绍和使用过 HStack 和 VStack,这次在这里就用到了 VStack,他们之间没有啥特备大的区别,理解视图与视图之间的层级和位置关系就没问题。

首先肯定是背景在下面,然后箭头视图在上面,把它经过一个循环和旋转角度添加,最后处理一下它的大小和透明底就有了我们需要的效果,具体的代码如下:

代码语言:javascript
复制
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()
}

最后附一份画图时候的点的数据方便大家学习:

代码语言:javascript
复制
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)
        )
    ]
}
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2021-01-19 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档