SwiftUI:如何绘制填充和描边的形状?

时间:2019-06-27 07:34:53

标签: swift swiftui

在UIKit中,绘制描边和填充的路径/形状非常简单。

例如,下面的代码绘制了一个蓝色的红色圆圈。

override func draw(_ rect: CGRect) {
    guard let ctx = UIGraphicsGetCurrentContext() else { return }

    let center = CGPoint(x: rect.midX, y: rect.midY)

    ctx.setFillColor(UIColor.red.cgColor)
    ctx.setStrokeColor(UIColor.blue.cgColor)

    let arc = UIBezierPath(arcCenter: center, radius: rect.width/2, startAngle: 0, endAngle: CGFloat.pi * 2, clockwise: true)

    arc.stroke()
    arc.fill()
}

如何使用SwiftUI做到这一点?

Swift UI似乎支持:

Circle().stroke(Color.blue)
// and/or
Circle().fill(Color.red)

但不是

Circle().fill(Color.red).stroke(Color.blue) // Value of type 'ShapeView<StrokedShape<Circle>, Color>' has no member 'fill'
// or 
Circle().stroke(Color.blue).fill(Color.red) // Value of type 'ShapeView<Circle, Color>' has no member 'stroke'

我应该只将ZStack圈两个吗?看起来有点傻。

9 个答案:

答案 0 :(得分:23)

您还可以使用strokeBorder和背景组合。

代码:

         Circle()
            .strokeBorder(Color.blue,lineWidth: 4)
            .background(Circle().foregroundColor(Color.red))

结果:

enter image description here

答案 1 :(得分:9)

我的解决方法:

import SwiftUI

extension Shape {
    /// fills and strokes a shape
    public func fill<S:ShapeStyle>(
        _ fillContent: S, 
        stroke       : StrokeStyle
    ) -> some View {
        ZStack {
            self.fill(fillContent)
            self.stroke(style:stroke)
        }
    }
}

示例:


struct ContentView: View {
    // fill gradient
    let gradient = RadialGradient(
        gradient   : Gradient(colors: [.yellow, .red]), 
        center     : UnitPoint(x: 0.25, y: 0.25), 
        startRadius: 0.2, 
        endRadius  : 200
    )
    // stroke line width, dash
    let w: CGFloat   = 6       
    let d: [CGFloat] = [20,10]
    // view body
    var body: some View {
        HStack {
            Circle()
                // ⭐️ Shape.fill(_:stroke:)
                .fill(Color.red, stroke: StrokeStyle(lineWidth:w, dash:d))
            Circle()
                .fill(gradient, stroke: StrokeStyle(lineWidth:w, dash:d))
        }.padding().frame(height: 300)
    }
}

结果:

fill and stroke circles

答案 2 :(得分:4)

我根据以上答案将以下包装材料放在一起。它使此操作更容易,代码也更易于阅读。

struct FillAndStroke<Content:Shape> : View
{
  let fill : Color
  let stroke : Color
  let content : () -> Content

  init(fill : Color, stroke : Color, @ViewBuilder content : @escaping () -> Content)
  {
    self.fill = fill
    self.stroke = stroke
    self.content = content
  }

  var body : some View
  {
    ZStack
    {
      content().fill(self.fill)
      content().stroke(self.stroke)
    }
  }
}

可以这样使用:

FillAndStroke(fill : Color.red, stroke : Color.yellow)
{
  Circle()
}

希望苹果将来会找到一种支持形状的填充和笔触的方法。

答案 3 :(得分:1)

为了将来参考,@ Imran的解决方案有效,但是您还需要通过填充在总帧中考虑笔划宽度:

struct Foo: View {
    private let lineWidth: CGFloat = 12
    var body: some View {
        Circle()
            .stroke(Color.purple, lineWidth: self.lineWidth)
        .overlay(
            Circle()
                .fill(Color.yellow)
        )
        .padding(self.lineWidth)
    }
}

enter image description here

答案 4 :(得分:1)

如果我们希望有一个带有no moved边框效果的圆,我们可以看到使用ZStack { Circle().fill(), Circle().stroke }

我准备了如下内容:

第一步

我们正在创建一个新的Shape

struct CircleShape: Shape {
    
    // MARK: - Variables
    var radius: CGFloat
    
    func path(in rect: CGRect) -> Path {
        let centerX: CGFloat = rect.width / 2
        let centerY: CGFloat = rect.height / 2
        var path = Path()
        path.addArc(center: CGPoint(x: centerX, y: centerY), radius: radius, startAngle: Angle(degrees: .zero)
            , endAngle: Angle(degrees: 360), clockwise: true)
        
        return path
    }
}

第二步

我们正在创建一个新的ButtonStyle

struct LikeButtonStyle: ButtonStyle {
        
        // MARK: Constants
        private struct Const {
            static let yHeartOffset: CGFloat = 1
            static let pressedScale: CGFloat = 0.8
            static let borderWidth: CGFloat = 1
        }
        
        // MARK: - Variables
        var radius: CGFloat
        var isSelected: Bool
        
        func makeBody(configuration: Self.Configuration) -> some View {
            ZStack {
                if isSelected {
                    CircleShape(radius: radius)
                        .stroke(Color.red)
                        .animation(.easeOut)
                }
                CircleShape(radius: radius - Const.borderWidth)
                    .fill(Color.white)
                configuration.label
                    .offset(x: .zero, y: Const.yHeartOffset)
                    .foregroundColor(Color.red)
                    .scaleEffect(configuration.isPressed ? Const.pressedScale : 1.0)
            }
        }
    }

最后一步

我们正在创建一个新的View

struct LikeButtonView: View {
    
    // MARK: - Typealias
    typealias LikeButtonCompletion = (Bool) -> Void
    
    // MARK: - Constants
    private struct Const {
        static let selectedImage = Image(systemName: "heart.fill")
        static let unselectedImage = Image(systemName: "heart")
        static let textMultiplier: CGFloat = 0.57
        static var textSize: CGFloat { 30 * textMultiplier }
    }
    
    // MARK: - Variables
    @State var isSelected: Bool = false
    private var radius: CGFloat = 15.0
    private var completion: LikeButtonCompletion?
    
    init(isSelected: Bool, completion: LikeButtonCompletion? = nil) {
        _isSelected = State(initialValue: isSelected)
        self.completion = completion
    }
    
    var body: some View {
        ZStack {
            Button(action: {
                withAnimation {
                    self.isSelected.toggle()
                    self.completion?(self.isSelected)
                }
            }, label: {
                setIcon()
                    .font(Font.system(size: Const.textSize))
                
            })
                .buttonStyle(LikeButtonStyle(radius: radius, isSelected: isSelected))
        }
    }
    
    // MARK: - Private methods
    private func setIcon() -> some View {
        isSelected ? Const.selectedImage : Const.unselectedImage
    }
}

输出(选中和未选中状态):

enter image description here

enter image description here

答案 5 :(得分:1)

以lochiwei的先前答案为基础...

public func fill<S:ShapeStyle>(_ fillContent: S,
                                   opacity: Double,
                                   strokeWidth: CGFloat,
                                   strokeColor: S) -> some View
    {
        ZStack {
            self.fill(fillContent).opacity(opacity)
            self.stroke(strokeColor, lineWidth: strokeWidth)
        }
    }

Shape对象上使用:

struct SelectionIndicator : Shape {
    let parentWidth: CGFloat
    let parentHeight: CGFloat
    let radius: CGFloat
    let sectorAngle: Double


    func path(in rect: CGRect) -> Path { ... }
}

SelectionIndicator(parentWidth: g.size.width,
                        parentHeight: g.size.height,
                        radius: self.radius + 10,
                        sectorAngle: self.pathNodes[0].sectorAngle.degrees)
                    .fill(Color.yellow, opacity: 0.2, strokeWidth: 3, strokeColor: Color.white)

答案 6 :(得分:1)

另一个更简单的选项是将笔触与ZStack一起填充在填充上方

    ZStack{
        Circle().fill()
            .foregroundColor(.red)
        Circle()
            .strokeBorder(Color.blue, lineWidth: 4)
    }

The result is

答案 7 :(得分:0)

目前看来是ZStack.overlay

根据Xcode,视图层次结构几乎相同。

struct ContentView: View {

    var body: some View {

        VStack {
            Circle().fill(Color.red)
                .overlay(Circle().stroke(Color.blue))
            ZStack {
                 Circle().fill(Color.red)
                 Circle().stroke(Color.blue)
            }
        }

    }

}

输出

enter image description here


查看层次结构

enter image description here

答案 8 :(得分:0)

您在实心圆的顶部绘制一个带有笔触的圆

struct ContentView: View {
    var body: some View {
        Circle()
        .overlay(
            Circle()
           .stroke(Color.green,lineWidth: 5)
          ).foregroundColor(Color.red)
   }
}

enter image description here