我正在尝试使用CAShapeLayer绘制和制作圆形按钮,但只是绘图给我带来了很多麻烦 - 我似乎无法弄清楚如何将数据传递到我的班级。
这是我的设置: - 一类UIView类型,它将绘制CAShapeLayer - 视图在我的视图控制器中呈现,并使用自动布局约束构建
我尝试过使用layoutIfNeeded,但似乎传给数据的时间太晚,无法绘制视图。我也试过在vieWillLayoutSubviews()中重绘视图,但没有。示例代码如下。我做错了什么?
我是否过早/过晚传递数据? 我是否太晚了画bezierPath?
我非常感谢指针。
也许还有第二个跟进问题:是否有更简单的方法来绘制与其视图大小相关的圆形路径?
在我的视图控制器中:
import UIKit
class ViewController: UIViewController {
let buttonView: CircleButton = {
let view = CircleButton()
view.backgroundColor = .black
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override func viewWillLayoutSubviews() {
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(buttonView)
buttonView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
buttonView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
buttonView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.75).isActive = true
buttonView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.25).isActive = true
buttonView.layoutIfNeeded()
buttonView.arcCenter = buttonView.center
buttonView.radius = buttonView.frame.width/2
}
override func viewDidAppear(_ animated: Bool) {
print(buttonView.arcCenter)
print(buttonView.radius)
}
}
buttonView的类:
class CircleButton: UIView {
//Casting outer circular layers
let trackLayer = CAShapeLayer()
var arcCenter = CGPoint()
var radius = CGFloat()
//UIView Init
override init(frame: CGRect) {
super.init(frame: frame)
}
//UIView post init
override func layoutSubviews() {
super.layoutSubviews()
print("StudyButtonView arcCenter \(arcCenter)")
print("StudyButtonView radius \(radius)")
layer.addSublayer(trackLayer)
let outerCircularPath = UIBezierPath(arcCenter: arcCenter, radius: radius, startAngle: 0, endAngle: 2*CGFloat.pi, clockwise: true)
trackLayer.path = outerCircularPath.cgPath
trackLayer.strokeColor = UIColor.lightGray.cgColor
trackLayer.lineWidth = 5
trackLayer.strokeStart = 0
trackLayer.strokeEnd = 1
trackLayer.fillColor = UIColor.clear.cgColor
trackLayer.transform = CATransform3DMakeRotation(-CGFloat.pi/2, 0, 0, 1)
}
//Required for subclass
required init?(coder aDecoder: NSCoder) {
fatalError("has not been implemented")
}
}
答案 0 :(得分:4)
自动布局与CircleButton
类的正确实现之间确实没有任何关联。您的CircleButton
班级不知道或不关心是通过自动布局配置它还是它有一些固定的大小。
您的自动布局代码看起来不错(除了下面的第5点和第6点)。您的代码段中的大多数问题都在您的CircleButton
课程中。几点意见:
如果您要旋转形状图层,则必须设置其frame
,否则尺寸为.zero
,并且最终会围绕origin
旋转它视图的{1}}(并在视图的bounds
之外旋转,如果您剪切子视图,则尤其有问题)。在尝试旋转视图之前,请务必将frame
的{{1}}设置为视图的CAShapeLayer
。坦率地说,我会移除bounds
,但鉴于您正在使用transform
和strokeStart
,我猜您可能希望稍后更改这些值并让它开始在12点钟,在这种情况下变换是有道理的。
底线,如果旋转,首先设置strokeEnd
。如果没有,则设置图层的frame
是可选的。
如果您要更改视图的属性以更新形状图层,则需要确保frame
观察者对形状图层进行适当的更新(或致电didSet
)。您不希望视图控制器不必弄乱形状图层的内部,但您还希望确保这些更改确实反映在形状图层中。
这是一个小观察,但我建议在setNeedsLayout
期间添加形状图层,并仅配置并将其添加到视图层次结构中一次。这更有效率。因此,让各种init
方法调用您自己的init
方法。然后,在configure
中执行与大小相关的内容(例如更新路径)。最后,让属性观察者直接更新形状图层。这种分工更有效率。
如果需要,可以将此layoutSubviews
设置为项目中自己的目标。然后你可以在IB中添加它,看看它会是什么样子。您还可以制作所有各种属性@IBDesignable
,并且您也可以在IB中设置它们。如果您不想,您不必在视图控制器的代码中执行任何操作。 (但如果你愿意,请随意。)
一个小问题,但是当您以编程方式添加视图时,您无需致电@IBInspectable
。如果你要制作动画约束,你只需要这样做,你在这里没有做。添加约束(并修复上述问题)后,按钮将正确布局,不需要明确buttonView.layoutIfNeeded()
。
您的视图控制器有一行代码表示:
layoutIfNeeded
这会混淆buttonView.arcCenter = buttonView.center
(这是arcCenter
坐标空间内的坐标)和buttonView
(这是中按钮中心的坐标>视图控制器的根视图的坐标空间)。一个与另一个无关。就个人而言,我已经摆脱了buttonView.center
的手动设置,而是arcCenter
layoutSubviews
ButtonView
使用bounds.midX
和bounds.midY
动态处理此问题。
将所有这些结合在一起,你会得到类似的东西:
@IBDesignable
class CircleButton: UIView {
private let trackLayer = CAShapeLayer()
@IBInspectable var lineWidth: CGFloat = 5 { didSet { updatePath() } }
@IBInspectable var fillColor: UIColor = .clear { didSet { trackLayer.fillColor = fillColor.cgColor } }
@IBInspectable var strokeColor: UIColor = .lightGray { didSet { trackLayer.strokeColor = strokeColor.cgColor } }
@IBInspectable var strokeStart: CGFloat = 0 { didSet { trackLayer.strokeStart = strokeStart } }
@IBInspectable var strokeEnd: CGFloat = 1 { didSet { trackLayer.strokeEnd = strokeEnd } }
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
private func configure() {
trackLayer.fillColor = fillColor.cgColor
trackLayer.strokeColor = strokeColor.cgColor
trackLayer.strokeStart = strokeStart
trackLayer.strokeEnd = strokeEnd
layer.addSublayer(trackLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
updatePath()
}
private func updatePath() {
let arcCenter = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = (min(bounds.width, bounds.height) - lineWidth) / 2
trackLayer.lineWidth = lineWidth
trackLayer.path = UIBezierPath(arcCenter: arcCenter, radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: true).cgPath
// There's no need to rotate it if you're drawing a complete circle.
// But if you're going to transform, set the `frame`, too.
trackLayer.transform = CATransform3DIdentity
trackLayer.frame = bounds
trackLayer.transform = CATransform3DMakeRotation(-.pi / 2, 0, 0, 1)
}
}
产量:
或者您可以在IB中调整设置,您将看到它生效:
确保didSet
属性的所有ButtonView
观察者都更新路径或直接更新某个形状图层,视图控制器现在可以更新这些属性,它们会自动更新在ButtonView
。
答案 1 :(得分:1)
我在您的代码中看到的主要问题是您要在-layoutSubviews
内添加图层,在视图生命周期中会多次调用此方法。
如果您不想通过使用CAShapeLayer
属性使视图托管图层为layerClass
,则需要覆盖2个init方法(框架和编码器)并调用您实例化和添加的commonInit您的CAShape图层作为子图层
在-layoutSubviews
中,只需根据新视图大小设置它的frame属性和路径。