我通过
UIBezierPath
和 CAShapeLayer
有一个圆角矩形进度条。进度笔划动画当前从顶部中心开始顺时针绘制 360 度。
我目前的设置是笔划从 315 度开始,但在顶部中心结束,而且我承认我迷路了。我的目标是以 315 度开始/结束划水。任何指导将不胜感激!
class ProgressBarView: UIView {
let progressLayer = CAShapeLayer()
let cornerRadius: CGFloat = 20
override init(frame: CGRect) {
super.init(frame: frame)
setupProgressLayer()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupProgressLayer()
}
private func setupProgressLayer() {
progressLayer.lineWidth = 6
progressLayer.fillColor = nil
progressLayer.strokeColor = Constants.style.offWhite.cgColor
progressLayer.strokeStart = 135 / 360
progressLayer.lineCap = .round
let lineWidth: CGFloat = 6
let radius = bounds.height / 2 - lineWidth / 2
let progressPath = UIBezierPath()
progressPath.move(to: CGPoint(x: lineWidth / 2 + cornerRadius, y: lineWidth / 2))
progressPath.addLine(to: CGPoint(x: bounds.width - lineWidth / 2 - cornerRadius, y: lineWidth / 2))
progressPath.addArc(withCenter: CGPoint(x: bounds.width - lineWidth / 2 - cornerRadius, y: lineWidth / 2 + cornerRadius), radius: cornerRadius, startAngle: -CGFloat.pi / 2, endAngle: 0, clockwise: true)
progressPath.addLine(to: CGPoint(x: bounds.width - lineWidth / 2, y: bounds.height - lineWidth / 2 - cornerRadius))
progressPath.addArc(withCenter: CGPoint(x: bounds.width - lineWidth / 2 - cornerRadius, y: bounds.height - lineWidth / 2 - cornerRadius), radius: cornerRadius, startAngle: 0, endAngle: CGFloat.pi / 2, clockwise: true)
progressPath.addLine(to: CGPoint(x: lineWidth / 2 + cornerRadius, y: bounds.height - lineWidth / 2))
progressPath.addArc(withCenter: CGPoint(x: lineWidth / 2 + cornerRadius, y: bounds.height - lineWidth / 2 - cornerRadius), radius: cornerRadius, startAngle: CGFloat.pi / 2, endAngle: CGFloat.pi, clockwise: true)
progressPath.addLine(to: CGPoint(x: lineWidth / 2, y: lineWidth / 2 + cornerRadius))
progressPath.addArc(withCenter: CGPoint(x: lineWidth / 2 + cornerRadius, y: lineWidth / 2 + cornerRadius), radius: cornerRadius, startAngle: CGFloat.pi, endAngle: -CGFloat.pi / 2, clockwise: true)
progressPath.close()
progressLayer.path = progressPath.cgPath
layer.addSublayer(progressLayer)
}
func setProgress(_ progress: CGFloat) {
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = progressLayer.strokeStart
animation.toValue = progress
animation.duration = 1
progressLayer.add(animation, forKey: "progressAnimation")
progressLayer.strokeEnd = progress
}
}
如果您愿意放弃圆形线帽,一种写法是使用遮罩层。这个想法是你的主层的路径总是完整的圆角矩形,而遮罩层是一个圆圈,其中一个楔子被取出来隐藏圆角矩形的一部分。它看起来像这样:
这是我修改后的
ProgressBarView
代码。请注意,绘制圆角矩形的代码要简单得多,因为我总是绘制整个东西,而且因为我使用 insetBy(dx:dy:)
而不是重复添加/减去 lineWidth / 2
.
class ProgressBarView: UIView {
var progress: CGFloat = 0.5 {
didSet {
setMaskShape()
}
}
let maskLayer = CAShapeLayer()
var cornerRadius: CGFloat { 20 }
var lineWidth: CGFloat { 6 }
override class var layerClass: AnyClass { CAShapeLayer.self }
override func layoutSubviews() {
super.layoutSubviews()
let layer = self.layer as! CAShapeLayer
if layer.mask != maskLayer {
layer.lineWidth = lineWidth
layer.fillColor = nil
layer.strokeColor = UIColor.gray.cgColor
// layer.strokeStart = 135 / 360
layer.mask = maskLayer
}
layer.path = CGPath(
roundedRect: bounds.insetBy(dx: 0.5 * lineWidth, dy: 0.5 * lineWidth),
cornerWidth: cornerRadius,
cornerHeight: cornerRadius,
transform: nil
)
maskLayer.frame = bounds
setMaskShape()
}
private func setMaskShape() {
let bounds = maskLayer.bounds
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let path = CGMutablePath()
path.move(to: center)
path.addRelativeArc(
center: center,
radius: bounds.size.width + bounds.size.height,
startAngle: 0.625 * 2 * .pi,
delta: progress * 2 * .pi
)
path.closeSubpath()
maskLayer.path = path
}
}
这是我用来测试它的 SwiftUI 代码:
struct ContentView: View {
@State var progress: CGFloat = 0.5
var body: some View {
VStack {
ProgressBarViewRep(progress: progress)
.aspectRatio(1, contentMode: .fit)
Slider(value: $progress, in: 0 ... 1)
}
.padding()
}
}
struct ProgressBarViewRep: UIViewRepresentable {
var progress: CGFloat
func makeUIView(context: Context) -> ProgressBarView {
return ProgressBarView()
}
func updateUIView(_ uiView: ProgressBarView, context: Context) {
uiView.progress = progress
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.padding()
}
}
如果您真的想要圆形线帽,或者您希望能够使用
UIView
或 CALayer
动画轻松地动画化进度,您需要绘制路径,使其以 315° 开始和结束。
因为315°在其中一个圆角的中间,所以你必须从一个跨越45°的圆弧开始绘图,并以另一个45°的圆弧结束它。但是,您可以使用专门为绘制圆角设计的
CGMutablePath
不同方法绘制其他三个角,从而简化代码。
class ProgressBarView: UIView {
var progress: CGFloat {
get { (layer as! CAShapeLayer).strokeEnd }
set { (layer as! CAShapeLayer).strokeEnd = newValue }
}
var cornerRadius: CGFloat { 20 }
var lineWidth: CGFloat { 6 }
override class var layerClass: AnyClass { CAShapeLayer.self }
override func layoutSubviews() {
super.layoutSubviews()
let layer = layer as! CAShapeLayer
layer.lineCap = .round
layer.lineWidth = lineWidth
layer.fillColor = nil
layer.strokeColor = UIColor.gray.cgColor
let rect = layer.bounds.insetBy(dx: 0.5 * lineWidth, dy: 0.5 * lineWidth)
let (x0, y0, x1, y1) = (rect.minX, rect.minY, rect.maxX, rect.maxY)
let r = cornerRadius
let path = CGMutablePath()
// No prior move, so path automatically moves to the start of this arc.
path.addRelativeArc(
center: CGPoint(x: x0 + r, y: y0 + r),
radius: r,
startAngle: 0.625 * 2 * .pi, // 45° above -x axis
delta: 0.125 * 2 * .pi // 45°
)
var current = CGPoint(x: x1, y: y0)
for next in [
CGPoint(x: x1, y: y1),
CGPoint(x: x0, y: y1),
CGPoint(x: x0, y: y0)
] {
path.addArc(tangent1End: current, tangent2End: next, radius: r)
current = next
}
path.addRelativeArc(
center: CGPoint(x: x0 + r, y: y0 + r),
radius: r,
startAngle: 0.5 * 2 * .pi, // -x axis
delta: 0.125 * 2 * .pi // 45°
)
path.closeSubpath()
layer.path = path
}
}
我没有将
CAShapeLayer
设置为视图层的子层,而是告诉视图直接使用 CAShapeLayer
。这样做的好处是,如果我们在 UIView.animate
块中进行这些更改,UIKit 将为图层属性的更改设置动画。这是演示代码。请注意,当我更新视图的 progress
属性时,我是如何在动画块内进行操作的。我不必直接创建CABasicAnimation
。
struct ContentView: View {
@State var isComplete = true
var body: some View {
VStack {
ProgressBarViewRep(progress: isComplete ? 1 : 0)
.aspectRatio(1, contentMode: .fit)
Toggle("Complete", isOn: $isComplete)
.toggleStyle(.button)
.fixedSize()
}
.padding()
}
}
struct ProgressBarViewRep: UIViewRepresentable {
var progress: CGFloat
func makeUIView(context: Context) -> ProgressBarView {
let view = ProgressBarView()
view.progress = progress
return view
}
func updateUIView(_ uiView: ProgressBarView, context: Context) {
UIView.animate(withDuration: 1, delay: 0, options: .curveLinear) {
uiView.progress = progress
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.padding()
}
}