因此,我创建了一个演示项目来回答这里的问题,关于如何绘制侧面带有圆角矩形作为指针的气泡形状(有点像聊天气泡。)
代码创建这样的形状:
它使用
CGMutablePath
方法addArc(tangent1End:tangent2End:radius:transform:)
来绘制指针矩形的圆角。
我在形状图层的边界周围添加了边框,以便您可以看到形状的限制。
理想情况下,左侧三角形的圆角应一直延伸到边界的左边缘。我不知道如何计算圆角与三角形圆角尖端的距离。
如果我知道如何计算圆角被切除的量,我可以将形状的整个左边缘移动该量,并使圆角向右移动到边界矩形的左边缘。
这是生成路径的代码:
var path: CGPath {
let cgPath = CGMutablePath()
cgPath.move(to: CGPoint(x: triangleWidth, y: shapeLayer.bounds.height/2 + triangleHeight/2))
cgPath.addArc(center: CGPoint(x: triangleWidth + radius, y: shapeLayer.bounds.height - radius),
radius: radius,
startAngle: CGFloat.pi,
endAngle: CGFloat.pi / 2,
clockwise: true,
transform: .identity)
cgPath.addArc(center: CGPoint(x: shapeLayer.bounds.width - radius, y: shapeLayer.bounds.height - radius),
radius: radius,
startAngle: CGFloat.pi / 2,
endAngle: 0,
clockwise: true,
transform: .identity)
cgPath.addArc(center: CGPoint(x: shapeLayer.bounds.width - radius, y: radius),
radius: radius,
startAngle: 0,
endAngle: 3 * CGFloat.pi / 2,
clockwise: true,
transform: .identity)
cgPath.addArc(center: CGPoint(x: triangleWidth + radius, y: radius),
radius: radius,
startAngle: 3 * CGFloat.pi / 2,
endAngle: CGFloat.pi,
clockwise: true,
transform: .identity)
cgPath.addLine(to: CGPoint(x: triangleWidth, y: shapeLayer.bounds.height / 2 - triangleHeight / 2))
cgPath.addArc(tangent1End: CGPoint(x: 0, y: shapeLayer.bounds.height / 2), tangent2End: CGPoint(x: triangleWidth, y: shapeLayer.bounds.height / 2 + triangleHeight / 2), radius: triangleRadius)
cgPath.addLine(to: CGPoint(x: triangleWidth, y: shapeLayer.bounds.height / 2 + triangleHeight / 2))
return cgPath
}
(最后一条弧线是创建矩形圆角的弧线。)
我想要一个适用于任何三角形高度、宽度和角半径的通用解决方案,但我无法找出可以让我计算三角形被剪切量的三角函数。
这是全班同学:
//
// CustomBubbleView.swift
// CustomBubbleView
//
// Created by Duncan Champney on 11/21/23.
//
import UIKit
import CoreGraphics
class CustomBubbleView: UIView {
let radius: CGFloat = 10.0
let triangleRadius = 5.0
let triangleWidth: CGFloat = 30
let triangleHeight: CGFloat = 30
var shapeLayer = CAShapeLayer()
override var bounds: CGRect {
didSet {
print("In didSet")
shapeLayer.frame = bounds
// shapeLayer.frame = bounds.insetBy(dx: 10, dy: 10)
shapeLayer.path = path
}
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
// Custom initialization
backgroundColor = UIColor(red: 0.95, green: 0.95, blue: 0.95, alpha: 1)
layer.borderWidth = 1
layer.borderColor = UIColor.blue.cgColor
layer.addSublayer(shapeLayer)
shapeLayer.fillColor = UIColor.white.cgColor
shapeLayer.strokeColor = UIColor.lightGray.cgColor
shapeLayer.lineWidth = 2
}
var path: CGPath {
let cgPath = CGMutablePath()
cgPath.move(to: CGPoint(x: triangleWidth, y: shapeLayer.bounds.height/2 + triangleHeight/2))
cgPath.addArc(center: CGPoint(x: triangleWidth + radius, y: shapeLayer.bounds.height - radius),
radius: radius,
startAngle: CGFloat.pi,
endAngle: CGFloat.pi / 2,
clockwise: true,
transform: .identity)
cgPath.addArc(center: CGPoint(x: shapeLayer.bounds.width - radius, y: shapeLayer.bounds.height - radius),
radius: radius,
startAngle: CGFloat.pi / 2,
endAngle: 0,
clockwise: true,
transform: .identity)
cgPath.addArc(center: CGPoint(x: shapeLayer.bounds.width - radius, y: radius),
radius: radius,
startAngle: 0,
endAngle: 3 * CGFloat.pi / 2,
clockwise: true,
transform: .identity)
cgPath.addArc(center: CGPoint(x: triangleWidth + radius, y: radius),
radius: radius,
startAngle: 3 * CGFloat.pi / 2,
endAngle: CGFloat.pi,
clockwise: true,
transform: .identity)
cgPath.addLine(to: CGPoint(x: triangleWidth, y: shapeLayer.bounds.height / 2 - triangleHeight / 2))
cgPath.addArc(tangent1End: CGPoint(x: 0, y: shapeLayer.bounds.height / 2), tangent2End: CGPoint(x: triangleWidth, y: shapeLayer.bounds.height / 2 + triangleHeight / 2), radius: triangleRadius)
cgPath.addLine(to: CGPoint(x: triangleWidth, y: shapeLayer.bounds.height / 2 + triangleHeight / 2))
return cgPath
}
/*
override func draw(_ rect: CGRect) {
let radius: CGFloat = 10
let triangleWidth: CGFloat = 20
let triangleHeight: CGFloat = 20
// Create the path for the triangle and the rounded rectangle
let path = UIBezierPath()
// Start with the triangle part
path.move(to: CGPoint(x: triangleWidth, y: rect.height / 2 - triangleHeight / 2))
path.addLine(to: CGPoint(x: 0, y: rect.height / 2))
path.addLine(to: CGPoint(x: triangleWidth, y: rect.height / 2 + triangleHeight / 2))
// Continue with the rounded rectangle part
path.addLine(to: CGPoint(x: triangleWidth, y: rect.height - radius))
path.addArc(withCenter: CGPoint(x: triangleWidth + radius, y: rect.height - radius),
radius: radius,
startAngle: CGFloat.pi,
endAngle: CGFloat.pi / 2,
clockwise: false)
path.addLine(to: CGPoint(x: rect.width - radius, y: rect.height))
path.addArc(withCenter: CGPoint(x: rect.width - radius, y: rect.height - radius),
radius: radius,
startAngle: CGFloat.pi / 2,
endAngle: 0,
clockwise: false)
path.addLine(to: CGPoint(x: rect.width, y: radius))
path.addArc(withCenter: CGPoint(x: rect.width - radius, y: radius),
radius: radius,
startAngle: 0,
endAngle: -CGFloat.pi / 2,
clockwise: false)
path.addLine(to: CGPoint(x: triangleWidth + radius, y: 0))
path.addArc(withCenter: CGPoint(x: triangleWidth + radius, y: radius),
radius: radius,
startAngle: -CGFloat.pi / 2,
endAngle: -CGFloat.pi,
clockwise: false)
path.close()
// Set the stroke color to gray
UIColor.gray.setStroke()
// Set the fill color to white
UIColor.white.setFill()
// Draw the filled path
path.fill()
// Draw the path with stroke
path.stroke()
}
*/
}
还有 Github 上的项目:
可以在这篇 Math.SE 帖子中找到相关数学知识。从该帖子中,您可以找到圆弧中心与切线交点之间的距离(该帖子中称为 QA)。从中减去三角形半径就是您需要移动的量。
QA 等于半径除以 sin(alpha/2),其中 alpha 是切线所成的角度。
func calculateShift(width: CGFloat, height: CGFloat, cornerRadius: CGFloat) -> CGFloat {
// here the CGSizes represent 2D vectors
let v1 = CGSize(width: -width, height: height / 2)
let v2 = CGSize(width: -width, height: -height / 2)
let alpha = angle(between: v1, and: v2)
let qa = cornerRadius / sin(alpha / 2)
return qa - cornerRadius
}
func cross(_ v1: CGSize, _ v2: CGSize) -> CGFloat {
v1.width * v2.height - v1.height * v2.width
}
func dot(_ v1: CGSize, _ v2: CGSize) -> CGFloat {
v1.width * v2.width + v1.height * v2.height
}
func angle(between v1: CGSize, and v2: CGSize) -> CGFloat {
atan2(cross(v1, v2), dot(v1, v2))
}
然后路径可以轻松地向左移动:
let cgPath = CGMutablePath()
let shiftAmount = calculateShift(width: triangleWidth, height: triangleHeight, cornerRadius: triangleRadius)
cgPath.move(to: CGPoint(x: triangleWidth - shiftAmount, y: shapeLayer.bounds.height/2 + triangleHeight/2))
cgPath.addArc(center: CGPoint(x: triangleWidth + radius - shiftAmount, y: shapeLayer.bounds.height - radius),
radius: radius,
startAngle: CGFloat.pi,
endAngle: CGFloat.pi / 2,
clockwise: true,
transform: .identity)
cgPath.addArc(center: CGPoint(x: shapeLayer.bounds.width - radius, y: shapeLayer.bounds.height - radius),
radius: radius,
startAngle: CGFloat.pi / 2,
endAngle: 0,
clockwise: true,
transform: .identity)
cgPath.addArc(center: CGPoint(x: shapeLayer.bounds.width - radius, y: radius),
radius: radius,
startAngle: 0,
endAngle: 3 * CGFloat.pi / 2,
clockwise: true,
transform: .identity)
cgPath.addArc(center: CGPoint(x: triangleWidth + radius - shiftAmount, y: radius),
radius: radius,
startAngle: 3 * CGFloat.pi / 2,
endAngle: CGFloat.pi,
clockwise: true,
transform: .identity)
cgPath.addLine(to: CGPoint(x: triangleWidth - shiftAmount, y: shapeLayer.bounds.height / 2 - triangleHeight / 2))
cgPath.addArc(
tangent1End: CGPoint(x: -shiftAmount, y: shapeLayer.bounds.height / 2),
tangent2End: CGPoint(x: triangleWidth - shiftAmount, y: shapeLayer.bounds.height / 2 + triangleHeight / 2),
radius: triangleRadius
)
cgPath.addLine(to: CGPoint(x: triangleWidth - shiftAmount, y: shapeLayer.bounds.height / 2 + triangleHeight / 2))
return cgPath