Reputation: 1626
I want to draw some items but leave a true alpha transparency cutout for a circular area. What I want to achieve:
Yellow is example background to show bleed through.
The cutout width is actually wider than the arc stroke, so they don't fully intersect. I need true cutout because I a saving to an image with transparency.
I thought maybe I could use setBlendMode() but I believe that would only work if I wanted my cutout to be exactly the same width as the arc. But there is the gist of how I was trying to go about it:
A Swift workbook follows. Any tips on achieving this are greatly appreciated.
import Foundation
import UIKit
var dimen: CGFloat = 200.0;
var strokeWidth: CGFloat = 20.0;
var cutoutWidth: CGFloat = 30.0;
class DonutView : UIView
{
override func draw(_ rect: CGRect)
{
// cutout
let cutoutColor = UIColor(red: 1, green: 0, blue: 0, alpha: 1)
cutoutColor.setFill()
let cutoutPath = UIBezierPath(ovalIn: CGRect(x: dimen-cutoutWidth, y: dimen/2-cutoutWidth/2, width: cutoutWidth, height: cutoutWidth))
cutoutPath.fill()
// let context = UIGraphicsGetCurrentContext()!
// context.setBlendMode(.sourceOut)
let ringOffset = cutoutWidth/2;
let circleWidth = dimen - ringOffset*2;
// ring
let ringPath = UIBezierPath(ovalIn: CGRect(x: ringOffset, y: ringOffset, width: circleWidth, height: circleWidth))
let ringColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.3)
ringColor.setStroke()
ringPath.lineWidth = strokeWidth
ringPath.stroke()
// arc
let arcRect = CGRect(x: ringOffset, y: ringOffset, width: circleWidth, height: circleWidth)
let arcPath = UIBezierPath()
arcPath.addArc(withCenter: CGPoint(x: arcRect.midX, y: arcRect.midY), radius: arcRect.width / 2, startAngle: -90 * CGFloat.pi/180, endAngle: 37 * CGFloat.pi/180, clockwise: true)
let arcColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.6)
arcColor.setStroke()
arcPath.lineWidth = strokeWidth
arcPath.stroke()
}
}
var view = DonutView(frame: CGRect.init(x: 0, y: 0, width: dimen, height: dimen))
view.backgroundColor = UIColor.yellow
// View these elements
view
(Edit: I should have stated this initially: this is to ultimately create a UIImage for WatchKit)
Upvotes: 0
Views: 482
Reputation: 77486
You can do this by using another CAShapeLayer
as a mask.
The portion(s) of the mask layer that are alpha = 1.0 will be fully transparent.
So...
If we make the Arc Layer a sublayer of the Ring Layer, we can then apply the Cutout Layer as a mask, resulting in:
Here is source for a Playground page:
class MyDonutView : UIView
{
let ringLayer = CAShapeLayer()
let arcLayer = CAShapeLayer()
let cutoutLayer = CAShapeLayer()
var strokeWidth: CGFloat = 20.0;
var cutoutWidth: CGFloat = 30.0;
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
// add arcLayer as a sublayer of ringLayer
ringLayer.addSublayer(arcLayer)
// add ringLayer as a sublayer of self.layer
layer.addSublayer(ringLayer)
// ring layer stroke is black at 0.3 alpha, fill is clear
ringLayer.strokeColor = UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.3).cgColor
ringLayer.fillColor = UIColor.clear.cgColor
ringLayer.lineWidth = strokeWidth
// arc layer stroke is black at 0.6 alpha, fill is clear
arcLayer.strokeColor = UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.6).cgColor
arcLayer.lineWidth = strokeWidth
arcLayer.fillColor = UIColor.clear.cgColor
// cutout layer stroke is black (although we're using Zero line width
// fill is black
cutoutLayer.strokeColor = UIColor.red.cgColor
cutoutLayer.lineWidth = 0
cutoutLayer.fillColor = UIColor.red.cgColor
}
override func layoutSubviews() {
super.layoutSubviews()
// define the "padding" around the ring
let ringOffset = cutoutWidth / 2.0
// define the diameter of the ring
let circleWidth = bounds.size.width - cutoutWidth;
// ring path
let ringPath = UIBezierPath(ovalIn: CGRect(x: ringOffset, y: ringOffset, width: circleWidth, height: circleWidth))
// arc path
let arcRect = CGRect(x: ringOffset, y: ringOffset, width: circleWidth, height: circleWidth)
let arcPath = UIBezierPath()
arcPath.addArc(withCenter: CGPoint(x: arcRect.midX, y: arcRect.midY), radius: arcRect.width / 2, startAngle: -90 * CGFloat.pi/180, endAngle: 37 * CGFloat.pi/180, clockwise: true)
// set ring layer path
ringLayer.path = ringPath.cgPath
// set arc layer path
arcLayer.path = arcPath.cgPath
// create a rect path the full size of bounds of self
let fullPath = UIBezierPath(rect: bounds)
// create a cutout path (the small circle to cut-out of the ring/arc)
let cutoutPath = UIBezierPath(ovalIn: CGRect(x: bounds.size.width-cutoutWidth, y: bounds.size.width/2-cutoutWidth/2, width: cutoutWidth, height: cutoutWidth))
// append the cutout path to the full rect path
fullPath.append(cutoutPath)
// even-odd winding rule
cutoutLayer.fillRule = CAShapeLayerFillRule.evenOdd
// set cutout layer path
cutoutLayer.path = fullPath.cgPath
// use cutout layer to mask ring layer
ringLayer.mask = cutoutLayer
}
}
class TestViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
// instantiate a MyDonutView
let myDonutView = MyDonutView()
// we can set the stroke and cutout widths here
myDonutView.strokeWidth = 20.0
myDonutView.cutoutWidth = 30.0
// we're using auto-layout
myDonutView.translatesAutoresizingMaskIntoConstraints = false
// background color yellow to see the frame
//myDonutView.backgroundColor = .yellow
// otherwise, it should be clear
myDonutView.backgroundColor = .clear
// add as subview
view.addSubview(myDonutView)
// constrain centerX and centerY
// width = 200, height = width
NSLayoutConstraint.activate([
myDonutView.widthAnchor.constraint(equalToConstant: 200.0),
myDonutView.heightAnchor.constraint(equalTo: myDonutView.widthAnchor),
myDonutView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
myDonutView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}
}
let vc = TestViewController()
PlaygroundPage.current.liveView = vc
Upvotes: 1
Reputation: 1626
With help from How to clear circle in CGContext in iOS
import Foundation
import UIKit
import PlaygroundSupport
var dimen: CGFloat = 200.0;
var strokeWidth: CGFloat = 20.0;
var cutoutWidth: CGFloat = 30.0;
class DonutView : UIImageView
{
override init(frame: CGRect) {
super.init(frame: frame)
UIGraphicsBeginImageContextWithOptions(CGSize(width: dimen, height: dimen), false, 1)
let context = UIGraphicsGetCurrentContext()!
let ringOffset = cutoutWidth/2;
let circleWidth = dimen - ringOffset*2;
// ring
let ringPath = UIBezierPath(ovalIn: CGRect(x: ringOffset, y: ringOffset, width: circleWidth, height: circleWidth))
let ringColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.3)
ringColor.setStroke()
ringPath.lineWidth = strokeWidth
ringPath.stroke()
// arc
let arcRect = CGRect(x: ringOffset, y: ringOffset, width: circleWidth, height: circleWidth)
let arcPath = UIBezierPath()
arcPath.addArc(withCenter: CGPoint(x: arcRect.midX, y: arcRect.midY), radius: arcRect.width / 2, startAngle: -90 * CGFloat.pi/180, endAngle: 37 * CGFloat.pi/180, clockwise: true)
let arcColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.6)
arcColor.setStroke()
arcPath.lineWidth = strokeWidth
arcPath.stroke()
// Cutout circle
context.setFillColor(UIColor.clear.cgColor)
context.setBlendMode(.clear)
context.addEllipse(in: CGRect(x: dimen-cutoutWidth, y: dimen/2-cutoutWidth/2, width: cutoutWidth, height: cutoutWidth))
context.drawPath(using: .fill)
context.setBlendMode(.normal)
image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
var view = DonutView(frame: CGRect.init(x: 0, y: 0, width: dimen, height: dimen))
view.backgroundColor = UIColor.yellow
// View these elements
view
Upvotes: 2