Draw concentric circle using UIBezierPath

青春壹個敷衍的年華 提交于 2021-02-08 05:49:12

问题


I am trying to draw a concentric circle using UIBezierPath the expected result as in this .

but what I am actually getting is different like .

I have tried to make two different circles with different diameter and fill the smaller one.

  let path = UIBezierPath(ovalIn: CGRect(x: position.x - diameter / 2, y: position.y - diameter / 2, width: diameter, height: diameter))
  let path2 = UIBezierPath(ovalIn: CGRect(x: position.x - diameter / 2 + 2, y: position.y - diameter / 2 + 2, width: diameter - 2, height: diameter - 2))


        let shapeLayer = CAShapeLayer()
        shapeLayer.path = path.cgPath
        shapeLayer.strokeColor = color.cgColor
        shapeLayer.fillColor =  UIColor.white.cgColor
        shapeLayer.lineWidth = lineWidth

        let shapeLayer2 = CAShapeLayer()
        shapeLayer.path = path2.cgPath
        shapeLayer.strokeColor = color.cgColor
        shapeLayer.fillColor = isFilled ? color.cgColor : UIColor.white.cgColor
        shapeLayer.lineWidth = lineWidth


        view.layer.addSublayer(shapeLayer)
        view.layer.addSublayer(shapeLayer2)

回答1:


Here's exactly how to do it using layers.

There are indeed many advantages to doing it using layers rather than drawing it.

(By "drawing", I mean drawing it in draw#rect.)

To begin with animation is easier; and it is more reusable; and you don't have to concern yourself with the difficulty of "if the view moves/reshapes what exactly should I redraw". Everything is automatic with layers.

So here are the four major things to do when making layers:

1. As always, make the layers, and set their frames in layoutSubviews.

In iOS when you make layers you (A) make them (almost certainly with a lazy var) and (B) at layout time, of course you set the size of the frame.

Those are the two basic steps in using layers in iOS.

Note that you do not make them at layout time, and you do not set the frame at bringup time. You only make them at bringup time and only set the size of the frame at layout time.

I made a UIView, "Thingy" in storyboard and using constraints positioned it and made it 100x100.

I made the background color blue so you can see what is going on. (You'd probably want it to be clear.)

Here it is in the app:

We're going to need the thickness of the ring and of the gap:

class Thingy: UIView {

    var thicknessOfOuterRing: CGFloat = 20.0 {
        didSet{ setNeedsLayout() }
    }

    var thicknessOfTheGap: CGFloat = 20.0 {
        didSet{ setNeedsLayout() }
    }

A critical point to understand is that when you change those values, you will have to resize everything. Right?

That means you add "didSet .. setNeedsLayout" to those properties in the usual way.

(Similarly, if you want to be able to change say the color on the fly, you'd do that to the colors also. Anything you want to be able to "change", you need the "didSet .. setNeedsLayout" pattern.)

There are two simple calculations we will always need, the half-thickness, and the combined fatness of those. Simply use computed variables for these, to vastly simplify your code.

var halfT: CGFloat { return thicknessOfOuterRing / 2.0 }
var both: CGFloat { return thicknessOfOuterRing + thicknessOfTheGap }

OK! So two layers. The ring will be a shape layer, but the blob in the middle can simply be a layer:

private lazy var outerBand: CAShapeLayer = {
    let l = CAShapeLayer()
    layer.addSublayer(l)
    return l
}()

private lazy var centralBlob: CALayer = {
    let l = CALayer()
    layer.addSublayer(l)
    return l
}()

Now of course, you must set their frames at layout time:

override func layoutSubviews() {
    super.layoutSubviews()

    outerBand.frame = bounds
    centralBlob.frame = bounds
}

So that's the basic process of using layers in iOS.

(A) make the layers (almost certainly with a lazy var) and (B) at layout time, of course you set the size of the frame.

2. Set all "fixed" aspects of the layers, when they are made:

What qualities of the layers will never change?

Set those "never-changing" qualities in the lazy var:

private lazy var outerBand: CAShapeLayer = {
    let l = CAShapeLayer()
    l.fillColor = UIColor.clear.cgColor
    l.strokeColor = UIColor.black.cgColor
    layer.addSublayer(l)
    return l
}()

private lazy var centralBlob: CALayer = {
    let l = CALayer()
    l.backgroundColor = UIColor.black.cgColor
    layer.addSublayer(l)
    return l
}()

Once again, anything that never changes, shove it in the lazy vars.

Next ...

3. Set all "changing" aspect of the layers, in layout:

Regarding the central blob. It's very easy to make a coin shape from a square CALayer in iOS, you just do this:

someCALayer.cornerRadius = someCALayer.bounds.width / 2.0

Note that you COULD, and some programmers DO, actually use a shape layer, and go to the bother of making a circular shape and so on. But it's fine to just use a normal layer and simply set the cornerRadius.

Secondly regarding the inner blob. Note that it is simply smaller than the overall unit. In other words the frame of the inner blob has to be shrunk by some amount.

To achieve that use the critical inset(by: UIEdgeInsets call in iOS. Use it often!

You use inset(by: UIEdgeInsets very often in iOS.

Here's an important sub-tip. I really encourage you to use inset(by: UIEdgeInsets( ... where you lugubriously write out "top, left, bottom, right"

The other variation, insetBy#dx#dy tends to lead to confusion; it's unclear if you're doing double the quantity or what.

I personally recommend using the "long form" inset(by: UIEdgeInsets( ... as there is then absolutely no doubt what is happening.

So we are now totally done with the inner dot:

override func layoutSubviews() {
    super.layoutSubviews()

    outerBand.frame = bounds
    outerBand.lineWidth = thicknessOfOuterRing

    centralBlob.frame =
      bounds.inset(by: UIEdgeInsets(top: both, left: both, bottom: both, right: both))
    centralBlob.cornerRadius = centralBlob.bounds.width / 2.0
}

Finally, and this is the heart of your question:

The outer ring is made using a shape layer, not a normal layer.

And shape layers are defined by a path.

Here's the "one surprising trick" that is central to using shape layers and paths in iOS:

4. In iOS you make the paths of shape layers actually in layoutSubviews.

In my experience this confuses the hell out of programmers moving from other platforms to iOS, but that's how it goes down.

So let's make a computing variable that creates the path for us.

Note that you will definitely use the ever-handy inset(by: UIEdgeInsets( ... call.

Don't forget that by the time this is called, you should have already correctly set all the frames involved.

And finally Apple kindly give us the incredibly handy UIBezierPath#ovalIn call so you don't need to fool with arcs and Pi!

So it's this easy ...

var circlePathForCurrentLayout: UIBezierPath {
    var b = bounds
    b = b.inset(by: UIEdgeInsets(top: halfT, left: halfT, bottom: halfT, right: halfT))
    let p = UIBezierPath(ovalIn: b)
    return p
}

And then, as it says in bold letters, in iOS you surprisingly set the actual path of a shape layer, in layoutSubviews.

override func layoutSubviews() {
    super.layoutSubviews()

    outerBand.frame = bounds
    outerBand.lineWidth = thicknessOfOuterRing
    outerBand.path = circlePathForCurrentLayout.cgPath

    centralBlob.frame =
      bounds.inset(by: UIEdgeInsets(top: both, left: both, bottom: both, right: both))
    centralBlob.cornerRadius = centralBlob.bounds.width / 2.0
}

Recap:

1. As always, make the layers, and set their frames in layoutSubviews.

2. Set all "fixed" aspects of the layers, when they are made.

3. Set all "changing" aspect of the layers, in layoutSubviews.

4. Surprisingly in iOS you make the paths of shape layers actually in layoutSubviews.

And here's the whole thing to drop in:

//  Thingy.swift
//  Created for SO on 11/3/19.

import UIKit

class Thingy: UIView {

    var thicknessOfOuterRing: CGFloat = 20.0 {
        didSet{ setNeedsLayout() }
    }

    var thicknessOfTheGap: CGFloat = 20.0 {
        didSet{ setNeedsLayout() }
    }

    var halfT: CGFloat { return thicknessOfOuterRing / 2.0 }
    var both: CGFloat { return thicknessOfOuterRing + thicknessOfTheGap }

    var circlePathForCurrentLayout: UIBezierPath {
        var b = bounds
        b = b.inset(by:
          UIEdgeInsets(top: halfT, left: halfT, bottom: halfT, right: halfT))
        let p = UIBezierPath(ovalIn: b)
        return p
    }

    private lazy var outerBand: CAShapeLayer = {
        let l = CAShapeLayer()
        l.fillColor = UIColor.clear.cgColor
        l.strokeColor = UIColor.black.cgColor
        layer.addSublayer(l)
        return l
    }()

    private lazy var centralBlob: CALayer = {
        let l = CALayer()
        l.backgroundColor = UIColor.black.cgColor
        layer.addSublayer(l)
        return l
    }()

    override func layoutSubviews() {
        super.layoutSubviews()

        outerBand.frame = bounds
        outerBand.lineWidth = thicknessOfOuterRing
        outerBand.path = circlePathForCurrentLayout.cgPath

        centralBlob.frame = bounds.inset(by:
          UIEdgeInsets(top: both, left: both, bottom: both, right: both))
        centralBlob.cornerRadius = centralBlob.bounds.width / 2.0
    }
}

Next steps:

If you want to take on the challenge of having an image in the middle: link

If you want to, eek, take on shadows: link

Looking at gradients? link

Partial arcs: link




回答2:


You don't need layers for this. Just use UIBezierPaths:

override func draw(_ rect: CGRect) {
    // this is the line width of the outer circle
    let outerLineWidth = CGFloat(3) // adjust this however you like

    // if you didn't create a separate view for these concentric circles,
    // which I strongly recommend you do, replace "bounds" with the desired frame
    let outerCircle = UIBezierPath(ovalIn: bounds.insetBy(dx: outerLineWidth / 2, dy: outerLineWidth / 2))
    UIColor.white.setStroke()
    outerCircle.lineWidth = outerLineWidth
    outerCircle.stroke()

    // this is the distance between the outer and inner circle
    let inset = CGFloat(5); // you can adjust this too
    let innerCircle = UIBezierPath(ovalIn: bounds.insetBy(dx: inset, dy: inset))
    UIColor.white.setFill()
    innerCircle.fill()
}

As you can see, you don't need to calculate the frames manually, just use the insetBy(dx:dy:) method.



来源:https://stackoverflow.com/questions/58680306/draw-concentric-circle-using-uibezierpath

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!