如何为 CALayer 的角半径设置动画

Posted

技术标签:

【中文标题】如何为 CALayer 的角半径设置动画【英文标题】:How can I animate the corner radius of a CALayer 【发布时间】:2021-02-12 16:52:42 【问题描述】:

我正在尝试创建一个动画,其中 CALayer 矩形从与视图的边框齐平变为具有圆角的左、右或两个角,并且矩形的宽度也发生了变化。

我遇到的问题是动画看起来失真。如果我在动画中禁用角半径更改,则不会发生失真。我怀疑这是因为当动画中的原始路径和最终路径具有不同数量的圆角时,路径具有不同数量的控制点,因此路径动画行为根据docs未定义。

我在想我应该尝试找到一种方法,使圆角矩形中的控制点数等于非圆角矩形中的数字,但我不确定我会如何做到这一点,因为我还没有找到一种方法来计算 CGPath/UIBezierPath 中控制点的数量。

这是我现在正在使用的代码,我正在为路径设置动画,但我愿意通过使用两个矩形或类似的东西将实现完全更改为“作弊”。

func setSelection(to color: UIColor, connectedLeft: Bool, connectedRight: Bool, animated: Bool) 
    self.connectedLeft = connectedLeft
    self.connectedRight = connectedRight
    self.color = color
    if animated 
        let pathAnimation = CABasicAnimation(keyPath: "path")
        let colorAnimation = CABasicAnimation(keyPath: "fillColor")
        self.configure(animation: pathAnimation)
        self.configure(animation: colorAnimation)
        pathAnimation.toValue = self.rectPath(connectedLeft: connectedLeft, connectedRight: connectedRight).cgPath
        colorAnimation.toValue = color.cgColor
        let group = CAAnimationGroup()
        group.animations = [pathAnimation, colorAnimation]
        self.configure(animation: group)
        group.delegate = self
        self.rectLayer.add(group, forKey: Constants.selectionChangeAnimationKey)
     else 
        self.rectLayer.fillColor = color.cgColor
        self.rectLayer.path = self.rectPath(connectedLeft: connectedLeft, connectedRight: connectedRight).cgPath
    


private func rectPath(connectedLeft: Bool, connectedRight: Bool) -> UIBezierPath 
    let spacing: CGFloat = 5
    var origin = self.bounds.origin
    var size = self.bounds.size
    var corners = UIRectCorner()
    if !connectedLeft 
        origin.x += spacing
        size.width -= spacing
        corners.formUnion([.topLeft, .bottomLeft])
    
    if !connectedRight 
        size.width -= spacing
        corners.formUnion([.topRight, .bottomRight])
    
    let path = UIBezierPath(roundedRect: .init(origin: origin, size: size), byRoundingCorners: corners, cornerRadii: .init(width: 8, height: 8))
    print(path.cgPath)
    return path

【问题讨论】:

这***.com/a/34842463/10352079 有帮助吗? 可以,我会试一试,但我需要能够只对一些角进行圆角处理。我可以通过使用重叠的矩形来破解它。 【参考方案1】:

您对动画无法正常工作的原因是正确的。

绘制弧线的核心图形/核心动画方法使用可变数量的三次贝塞尔曲线组成弧线,具体数量取决于绘制的角度。

我相信 90 度角被渲染为单个三次贝塞尔曲线。

根据我最近所做的一项测试,您似乎需要做的就是拥有与贝塞尔曲线相同数量的 端点 以获得尖角到圆角动画正确绘制。对于圆角,我很确定它会在圆角处绘制一条线段,然后将弧线绘制为一条贝塞尔曲线,然后是下一条线段。因此,您应该能够通过绘制每个尖角点两次来创建一个矩形。我没有尝试过,但我认为它会起作用。

(根据我对如何使用贝塞尔曲线绘制圆弧的理解,圆弧由绘制的每个四分之一圆或四分之一圆的一部分的三次贝塞尔曲线组成。所以对于 90度弯曲,你只需要2个控制点来代替圆角。如果角度> 90度但小于180,你需要在拐角处设置3个控制点,如果它> 180但小于270,你需要 4 个控制点。)

编辑:

上述为圆角矩形添加每个角点两次的方法应该适用于圆角矩形情况(每个角正好是 90 度),但它不适用于需要圆角的不规则多边形变化。我不知道如何处理这种情况。我已经创建了一个演示项目,它可以生成带有任意尖角和弯角混合的不规则多边形,因此我对其进行了调整以处理动画。

该项目现在包含一个函数,该函数生成带有可设置的圆角和尖角混合的CGPaths,它将动画化尖角和圆角之间的过渡。你要查看的函数叫做roundedRectPath(rect:byRoundingCorners:cornerRadius:)

这里是函数头和内嵌文档

/**
This function works like the UIBezierPath initializer `init(roundedRect:byRoundingCorners:cornerRadii:)` It returns a CGPath a rounded rectangle with a mixture of rounded and sharp corners, as specified by the options in `corners`.

 Unlike the UIBezierPath `init(roundedRect:byRoundingCorners:cornerRadii:` intitializer, The CGPath that is returned by this function will animate smoothly from rounded to non-rounded corners.

 - Parameter  rect: The starting rectangle who's corners you wish to round
 - Parameter corners: The corners to round
 - Parameter cornerRadius: The corner radius to use
 - Returns: A CGPath containing for the rounded rectangle.
*/
public func roundedRectPath(rect: CGRect, byRoundingCorners corners: UIRectCorner, cornerRadius: CGFloat) -> CGPath

您可以从 Github 下载项目并在this link 试用。

这是运行中的演示应用的 GIF:

【讨论】:

以上是关于如何为 CALayer 的角半径设置动画的主要内容,如果未能解决你的问题,请参考以下文章

UIView:如何用阴影为 CALayer 框架设置动画?

如何为一些子成员设置卡片视图的角半径?

Swift:如何为 CALayer 启用捏合缩放功能?

为啥动画自定义 CALayer 属性会导致其他属性在动画期间为零?

CALayer内边框圆角半径

UIVIew/CALayer 动画期间的回调