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

问题描述

我正在尝试创建一个动画,其中 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
}

解决方法

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

绘制弧线的 Core Graphics/Core Animation 方法根据绘制的角度使用可变数量的三次贝塞尔曲线组成弧线。

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

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

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

编辑:

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

该项目现在包含一个函数,该函数生成具有圆角和尖角的可设置混合的 CGPath,可以为尖角和圆角之间的过渡设置动画。您要查看的函数名为 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:

enter image description here

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...