英文:
Drawing rounded rectangle progress bar, start/end at 315°
问题
我有一个通过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
}
}
英文:
I have a rounded rectangular progress bar via a UIBezierPath
and CAShapeLayer
. The progress stroke animated currently draws 360 degrees clockwise beginning from top center.
My current setup has the stroke starting at 315 degrees, but ends at top center, and am admittedly lost. My goal is to start/end the stroke at 315 degrees. Any guidance would be appreciated!
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
}
}
答案1
得分: 0
以下是您要翻译的代码部分:
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.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
}
}
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()
}
}
希望这能帮助您。如果您需要进一步的帮助,请随时告诉我。
英文:
One way to write this, if you're willing to forgo the rounded line caps, is to use a mask layer. The idea is that your main layer's path is always the full rounded rect, and the mask layer is a circle with a wedge taken out to hide part of the rounded rect. It looks like this:
Here's my revised ProgressBarView
code. Note that the code for drawing the rounded rect is considerably simpler because I'm always drawing the whole thing, and because I used insetBy(dx:dy:)
instead of repeatedly adding/subtracting 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
}
}
And here is the SwiftUI code I used to test it out:
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()
}
}
答案2
得分: 0
如果您确实想要圆角线帽,或者希望能够轻松使用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()
// 无先前移动,因此路径自动移至此圆弧的起点。
path.addRelativeArc(
center: CGPoint(x: x0 + r, y: y0 + r),
radius: r,
startAngle: 0.625 * 2 * .pi, // 45度在-x轴上方
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轴
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()
}
}
英文:
If you really want rounded line caps, or you want to be able to easily animate the progress using UIView
or CALayer
animation, you need to draw the path so that it starts and ends at 315°.
Since 315° is in the middle of one of the rounded corners, you have to start the drawing with a circular arc spanning 45°, and end it with another arc of 45°. However, you can draw the other three corners using a different method of CGMutablePath
designed specifically for drawing rounded corners, which simplifies the code.
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
}
}
Instead of setting up the CAShapeLayer
as a sublayer of the view's layer, I tell the view to use a CAShapeLayer
directly. This has the advantage that UIKit will set up animations for changes to the layer's properties, if we make those changes inside a UIView.animate
block. Here's the demo code. Notice how when I update the view's progress
property, I do it inside an animation block. I don't have to create a CABasicAnimation
directly.
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()
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论