像 UIActivityIndi​​catorview 一样对 UIBezierPath 六边形进行动画处理

gov*_*mar 2 cabasicanimation cashapelayer ios uibezierpath swift

我正在尝试实现与下面所示完全相同的动画

在此输入图像描述

我使用 UIBezierPath 和 CABasicAnimation 的输出如下。

在此输入图像描述

这是我的LoaderView代码

class LoaderView: UIView {

private let lineWidth : CGFloat = 5
internal var backgroundMask = CAShapeLayer()


override init(frame: CGRect) {
    super.init(frame: frame)
    setUpLayers()
    createAnimation()
}


required init?(coder: NSCoder) {
    super.init(coder: coder)
    setUpLayers()
    createAnimation()
}

func setUpLayers()
{
    backgroundMask.lineWidth = lineWidth
    backgroundMask.fillColor = nil
    backgroundMask.strokeColor = UIColor.blue.cgColor
    layer.mask = backgroundMask
    layer.addSublayer(backgroundMask)
}

func createAnimation()
{
    let animation = CABasicAnimation(keyPath: "strokeEnd")
    animation.fromValue = 0
    animation.duration = 1
    animation.repeatCount = .infinity
    backgroundMask.add(animation, forKey: "MyAnimation")
}

override func draw(_ rect: CGRect) {
    let sides = 6
    let rect = self.bounds
    let path = UIBezierPath()
    
    let cornerRadius : CGFloat = 10
    let rotationOffset = CGFloat(.pi / 2.0)
    
    let theta: CGFloat = CGFloat(2.0 * .pi) / CGFloat(sides) // How much to turn at every corner
    let width = min(rect.size.width, rect.size.height)        // Width of the square
    
    let center = CGPoint(x: rect.origin.x + width / 2.0, y: rect.origin.y + width / 2.0)
    
    // Radius of the circle that encircles the polygon
    // Notice that the radius is adjusted for the corners, that way the largest outer
    // dimension of the resulting shape is always exactly the width - linewidth
    let radius = (width - lineWidth + cornerRadius - (cos(theta) * cornerRadius)) / 2.0
    
    
    // Start drawing at a point, which by default is at the right hand edge
    // but can be offset
    var angle = CGFloat(rotationOffset)
    
    let corner = CGPoint(x: center.x + (radius - cornerRadius) * cos(angle), y: center.y + (radius - cornerRadius) * sin(angle))
    path.move(to: CGPoint(x: corner.x + cornerRadius * cos(angle + theta), y: corner.y + cornerRadius * sin(angle + theta)))
    
    for _ in 0..<sides {
        angle += theta
        
        let corner = CGPoint(x: center.x + (radius - cornerRadius) * cos(angle), y: center.y + (radius - cornerRadius) * sin(angle))
        let tip = CGPoint(x: center.x + radius * cos(angle), y: center.y + radius * sin(angle))
        let start = CGPoint(x: corner.x + cornerRadius * cos(angle - theta), y: corner.y + cornerRadius * sin(angle - theta))
        let end = CGPoint(x: corner.x + cornerRadius * cos(angle + theta), y: corner.y + cornerRadius * sin(angle + theta))
        
        path.addLine(to: start)
        path.addQuadCurve(to: end, controlPoint: tip)
        
    }
    path.close()
    backgroundMask.path = path.cgPath
}}
Run Code Online (Sandbox Code Playgroud)

Dun*_*n C 5

您需要实现draw(_:)或使用CAAnimation,而不是两者都需要。

作为规则,不要实施draw(_:)为视图类实现。这迫使系统在 CPU 上完成所有渲染,并且不利用 iOS 设备上基于图块的硬件加速渲染。相反,使用 CALayers 和 CAAnimation,让硬件为您完成繁重的工作。

使用 CALayers 和 CAAnimation 你可以得到这样的效果:

六边形动画

我建议执行以下操作:

  • 创建一个完整的圆形六边形形状作为CAShapeLayer. (您的方法中的代码draw()已经生成了一个六边形路径。您可以轻松地调整它以将六边形路径安装到 . 中CAShapeLayer。)

  • 将该形状图层添加为视图的子图层。

  • 创建一个CAGradientLayer以图层中心为起点、以顶部中心为终点的“圆锥曲线”。

  • 将从透明颜色到任何不透明颜色的颜色添加到渐变层,使用一组根据locations需要羽化渐变的颜色。

  • 在六边形图层上安装渐变图层作为蒙版。

  • 创建一个 CABasicAnimation,使渐变层绕 Z 轴每次旋转 1/4 圈。不断运行该动画,直到完成动画。

创建渐变层的代码可能如下所示:

    let gradientLayer = CAGradientLayer()
    gradientLayer.frame = self.bounds
    gradientLayer.type = .conic
    gradientLayer.colors = [UIColor.clear.cgColor,
                            UIColor.clear.cgColor,
                            UIColor.white.cgColor,
                            UIColor.white.cgColor]
    let center = CGPoint(x: 0.5, y: 0.5)
    gradientLayer.locations = [0, 0.3, 0.7, 0.9]
    gradientLayer.startPoint = center
    gradientLayer.endPoint = CGPoint(x: 0.5, y: 0)
Run Code Online (Sandbox Code Playgroud)

(如果所属视图的边界发生变化,您将需要更新渐变层的边界。)

旋转渐变层的代码可能如下所示:

private func animateGradientRotationStep() {
    let rotation = CABasicAnimation(keyPath: "transform.rotation.z")
    animationStepsRemaining -= 1
    rotation.fromValue =  rotationAngle
    rotationAngle += CGFloat.pi / 2
    rotation.toValue =  rotationAngle
    rotation.duration = 0.5
    rotation.delegate = self
    gradientLayer.add(rotation, forKey: nil)

    // After a tiny delay, set the layer's transform to the state at the end of the animation
    // so it doesnt jump back once the animation is complete.
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {

        // You have to wrap this step in a CATransaction with setDisableActions(true)
        // So you don't get an implicit animation
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        self.gradientLayer.transform = CATransform3DMakeRotation(self.rotationAngle, 0, 0, 1)
        CATransaction.commit()
    }
}
Run Code Online (Sandbox Code Playgroud)

并且您需要您的视图符合协议CAAnimationDelegate

extension GradientLayerView: CAAnimationDelegate {
    func animationDidStop(_ anim: CAAnimation,
                          finished flag: Bool) {
        if animating && animationStepsRemaining > 0 {
            animateGradientRotation()
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,图层的变换属性是“隐式动画”的,这意味着默认情况下系统会生成变化的动画。我们可以利用这一事实,对隐式动画进行一些调整。这使得动画功能更简单:

// This version of the function takes advantage of the fact
// that a layer's transform property is implicitly animated
private func animateGradientRotationStep() {
    animationStepsRemaining -= 1
    rotationAngle += CGFloat.pi / 2
    // MARK: - CATransaction begin
    // Use a CATransaction to set the animation duration, timing function, and completion block
    CATransaction.begin()
    CATransaction.setAnimationDuration(0.5)
    CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: .linear))
    CATransaction.setCompletionBlock {
        self.animationDidStop(finished:true)
    }
    self.gradientLayer.transform = CATransform3DMakeRotation(self.rotationAngle, 0, 0, 1)
    CATransaction.commit()
    // MARK: CATransaction end -
}
Run Code Online (Sandbox Code Playgroud)

该版本需要稍微不同的完成函数,因为它不使用 CAAnimation:

func animationDidStop(finished flag: Bool) {
    delegate?.animationStepComplete(animationStepsRemaining)
    if animating && animationStepsRemaining > 0 {
        animateGradientRotationStep()
    }
Run Code Online (Sandbox Code Playgroud)

我制作了一个创建此类动画的小示例应用程序。

您可以通过此链接从 Github 下载演示应用程序。

我不确定如何复制示例动画的一部分是六边形的颜色一开始似乎是亮白色,然后过渡到黄色。我的示例应用程序创建了一个动画,其中六边形是固定颜色并从不透明过渡到透明。

这是该项目的自述文件:


极地渐变遮罩视图

该项目演示了如何使用“圆锥”渐变来遮罩视图并创建圆形动画。

它使用CAGradientLayerof 类型.conic,设置为大部分不透明,后半部分过渡为透明。它将渐变层作为蒙版安装在包含黄色六边形的形状层上。

Gadient 层如下所示:

在此输入图像描述

(在灰色棋盘背景下以蓝色渲染,以便您可以看到从不透明到透明的过渡。)

渐变的不透明(蓝色)部分使形状图层可见。渐变的透明部分隐藏(遮罩)形状图层的那些部分,渐变图层的部分透明部分使形状图层的那些部分部分透明。

动画只是围绕图层中心在 Z 轴上旋转渐变图层。它一次将图层旋转 1/4 圈,每次动画步骤完成时,它都会创建一个新动画,将蒙版再旋转 1/4 圈。

当你掩盖六边形形状时,有点难以理解发生了什么。我创建了一个变体,其中添加了图像视图作为自定义视图的子视图。其动画如下所示:

在此输入图像描述

该应用程序的窗口如下所示:

在此输入图像描述

  • @aheze 我刚刚编辑了我的答案以包含指向存储库的链接。 (2认同)