问题
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 UIBezierPath
s:
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