【问题标题】:Drawing a CAShapeLayer using autolayout使用自动布局绘制 CAShapeLayer
【发布时间】:2017-12-19 03:51:05
【问题描述】:

我正在尝试使用 CAShapeLayer 绘制一个圆形按钮并为其设置动画,但只是绘图让我很头疼 - 我似乎无法弄清楚如何将数据传递给我的班级。

这是我的设置: - 将绘制 CAShapeLayer 的 UIView 类型的类 - 视图在我的视图控制器中呈现并使用自动布局约束构建

我曾尝试使用 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")
    }
}

【问题讨论】:

    标签: swift autolayout uibezierpath


    【解决方案1】:

    自动布局与您的CircleButton 类的正确实现之间确实没有任何关联。您的 CircleButton 类不知道也不关心它是通过自动布局配置还是具有固定大小。

    您的自动布局代码看起来不错(除了下面的第 5 点和第 6 点)。您的代码 sn-p 中的大多数问题都存在于您的 CircleButton 类中。几点观察:

    1. 如果你要旋转形状层,你也必须设置它的frame,否则大小是.zero,它最终会围绕视图的origin旋转(和旋转到视图的bounds 之外,如果您正在剪切子视图,则尤其成问题)。确保在尝试旋转视图之前将CAShapeLayerframe 设置为视图的bounds。坦率地说,我会删除transform,但考虑到您正在使用strokeStartstrokeEnd,我猜您可能想稍后更改这些值并让它从12 点开始,在这种情况下,转换是有意义的。

      底线,如果旋转,首先设置frame。如果没有,设置图层的frame 是可选的。

    2. 如果您要更改视图的属性以更新形状层,您需要确保didSet 观察者对形状层进行适当的更新(或调用setNeedsLayout)。您不希望您的视图控制器不得不处理形状层的内部结构,但您还希望确保这些更改确实反映在形状层中。

    3. 这是一个小观察,但我建议在init 期间添加形状层,并且只配置一次并将其添加到视图层次结构中。这样更有效率。因此,让各种init 方法调用您自己的configure 方法。然后,在layoutSubviews 中做与大小相关的事情(比如更新路径)。最后,拥有直接更新形状层的属性观察器。这种分工更有效率。

    4. 如果你愿意,你可以把这个@IBDesignable 放到你项目中自己的目标中。然后你可以直接在 IB 中添加它,看看它会是什么样子。您还可以创建所有各种属性@IBInspectable,并且您也可以在 IB 中正确设置它们。如果您不想这样做,则无需在视图控制器的代码中执行任何操作。 (但如果您愿意,请随意。)

    5. 一个小问题,但是当您以编程方式添加视图时,您不需要调用buttonView.layoutIfNeeded()。只有在为约束设置动画时才需要这样做,而这里没有这样做。添加约束(并修复上述问题)后,按钮将正确布局,无需显式 layoutIfNeeded

    6. 您的视图控制器有一行代码:

      buttonView.arcCenter = buttonView.center
      

      这将arcCenter(这是buttonView 坐标空间内的坐标)和buttonView.center(这是按钮中心的坐标在视图控制器的根视图内坐标空间)。一个与另一个无关。就个人而言,我会摆脱 arcCenter 的手动设置,而是让 layoutSubviews 中的 ButtonView 动态处理这个问题,使用 bounds.midXbounds.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 中调整设置,您会看到它生效:

    在确保 ButtonView 属性的所有 didSet 观察者更新路径或直接更新某些形状层后,视图控制器现在可以更新这些属性,它们将自动呈现在ButtonView.

    【讨论】:

    • 哇,非常感谢。这正是我试图实现的目标,并且缺乏正确的方法来做到这一点。我刚刚运行了您的代码(同时删除了 IB 部分,因为我正在以编程方式进行所有操作)并且它完美地工作! bounds.midX 是我不知道我能得到的东西之一 - 超级有帮助!两个后续问题:1)为什么在所需的初始化过程中第二次调用 configure() ? 2) 我不是 100% 确定我理解 didSet 观察者——这到底是做什么的?你提到另一种方法是要求 layoutifNeeded - 你会怎么做?非常感谢!
    • 哦,如果感兴趣的话:我旋转圆形路径的原因是我可以从 12 点钟开始动画,而不是默认的 3 点钟位置。
    • @moopoints - “你为什么在需要的初始化过程中第二次调用 configure()?” ...我不会第二次调用它。 init(coder:) 在通过情节提要或状态恢复创建视图时被调用。 init(frame:) 在您以编程方式创建它时(或在 IB 中的实时取景期间)被调用。它们是两种完全不同的视图创建方式,无论您碰巧采用哪种方式,我们都希望它能够正常工作。
    • @moopoints - “我不是 100% 确定我理解 didSet 观察者 - 那到底是做什么的?” ...见The Swift Programming Language: Property Observers。最重要的是,每当您更改(但不是在您第一次初始化时)属性时,它都是一些代码。当视图控制器或您拥有的属性更改时,您正在“观察”。
    • @moopoints - “你提到另一种选择是要求layoutIfNeeded - 你会怎么做?” ...我不确定你指的是什么。我唯一一次使用layoutIfNeeded 是在为由约束定义的视图设置动画时。但是你在这里没有这样做,所以我们不需要它。我唯一谈论的是在didSet 观察者中可选地调用setNeedsLayout。例如。在我的lineWidth 观察者中,我可以调用setNeedsLayout,这将导致layoutSubviews 被调用。在这种情况下,调用updatePath 更简洁,但它是一种替代方法。
    【解决方案2】:

    我在您的代码中看到的主要问题是您要在 -layoutSubviews 中添加层,此方法在视图生命周期中被多次调用。
    如果您不想通过使用layerClass 属性将视图托管层设置为CAShapeLayer,则需要覆盖2个init方法(框架和编码器)并调用commonInit,在其中实例化并将CAShape层添加为一个子层。
    -layoutSubviews 中只需根据新的视图大小设置它的框架属性和路径。

    【讨论】:

    • layoutSubviews中重新添加层在技术上没有问题,但它的效率低得可以忽略不计,并且表明对过程的误解。所以我同意你的建议,但这不是 OP 问题的根源。
    • 谢谢,安德里亚!我不确定我是否完全理解你的提议。在 VC 和课堂上,你究竟会处理什么?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-12-18
    • 2011-08-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多