动画 UIBezierPath 六边形像 UIActivityIndi​​catorview

Animate a UIBezierPath hexagon like UIActivityIndicatorview

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

.

下面是我使用 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
}}

问题是 - 代码正在渲染完整的路径 - 每次都从头到尾,所有动画的开始都是相同的。

加载程序的想法是 - 每次动画后起点必须改变 - 类似于 -

  1. 从角度 0 开始
  2. 向上倾斜 45 /// 或 60,无论你想要什么
  3. 将开始角度更改为下一个逻辑步骤 - 例如 30
  4. 然后渲染最多 75 /// 或 90,具体取决于您之前选择的内容

在这种安排中,你必须不断地改变起点来绘制形状的某个部分。

在实践中,实现不同起点值之间的平滑过渡可能比看起来困难。你可以在这里找到一个例子 - https://github.com/SVProgressHUD/SVProgressHUD/blob/master/SVProgressHUD/SVIndefiniteAnimatedView.m#L48-L102

更新


我上面分享的link里面有所有提示。该库使用图像作为遮罩,然后连续旋转。面具形状可以是任何你喜欢的形状 - 你已经有了相应的代码。

您只需创建适合您的动画的图像。看看他们的 asset 长什么样

并且在屏蔽他们的动画看起来像之后 -


您需要实施 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)

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

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

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()
    }
}

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

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

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

// 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 -
}

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

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

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

您可以从 Github this link 下载演示应用程序。

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

这是项目的自述文件:


PolarGradientMaskView

此项目说明了如何使用“圆锥”渐变来遮盖视图并创建圆形动画。

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

渐变层看起来像这样:

(在灰色棋盘格背景下以蓝色呈现,因此您可以看到从不透明到透明的过渡。)

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

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

当您遮盖六边形时,有点难以理解发生了什么。我创建了一个变体,其中我添加了一个图像视图作为自定义视图的子视图。动画看起来像这样:

应用程序的 window 如下所示:

OP 的视频显示了一个动画,其中六边形在开始时有一个白色高光,然后过渡到黄色。

我创建了以前动画的变体,在动画的前缘添加了白色高光。它看起来像这样:

两个版本的动画非常相似。

另一个全黄色动画使用一个带有圆锥渐变 CAGradientLayer 的单一形状层作为遮罩层,导致六边形在最后 3 日左右淡出。动画只是围绕它的中心旋转遮罩层。

主要遮罩渐变如下所示:

(它在方格背景下以蓝色绘制,因此您可以更容易地看到不透明和透明部分。

动画变体在第一个形状图层之上添加了第二个形状图层。我们称它为高光形状层。高光形状图层包含一个六边形,线宽略小,以白色绘制。高光形状层也有一个圆锥形 CAGradientLayer 作为它的遮罩,但它的遮罩层遮住了除六边形形状的开头以外的所有部分。它只显示白色六边形的一小部分,并且永远不会完全不透明。

由于高光形状层不是完全不透明,两个形状层混合在一起,高光层不透明度较高的部分使组合图像中的像素看起来更白。

高光形状图层的渐变蒙版如下所示(在方格背景下再次显示为蓝色,因此您可以分辨蒙版的不透明度。)

此版本的项目也在 github https://github.com/DuncanMC/PolarGradientMaskView.git

但在名为“AddHighlightLayer”的分支中。

highlightGradientLayer的设置代码如下:

    highlightGradientLayer.type = .conic
    highlightGradientLayer.colors = [UIColor.clear.cgColor,
                                     UIColor.clear.cgColor,
                                     UIColor(red: 0, green: 0, blue: 1, alpha: 0.5).cgColor,
                                     UIColor(red: 0, green: 0, blue: 1, alpha: 0.9).cgColor,
                            ]
    highlightGradientLayer.locations = [0.00, 0.85, 0.90, 1.00]
    highlightGradientLayer.startPoint = center
    highlightGradientLayer.endPoint = CGPoint(x: 0.5, y: 0)
    self.layer.addSublayer(highlightShapeLayer)
    highlightShapeLayer.mask = highlightGradientLayer