如何解决动画 UIBezierPath 六边形,如 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
}}
解决方法
您要么需要实现 draw(_:)
,要么需要使用 CAAnimation
,不能同时使用。
作为规则,不要为视图类实现 draw(_:)
。这会强制系统在 CPU 上完成所有渲染工作,并且不会利用 iOS 设备上基于磁贴的硬件加速渲染。相反,请使用 CALayer 和 CAAnimation,让硬件为您完成繁重的工作。
使用 CALayer 和 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,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,1)
CATransaction.commit()
// MARK: CATransaction end -
}
那个版本需要一个稍微不同的完成函数,因为它不使用 CAAnimation:
func animationDidStop(finished flag: Bool) {
delegate?.animationStepComplete(animationStepsRemaining)
if animating && animationStepsRemaining > 0 {
animateGradientRotationStep()
}
我编写了一个小示例应用程序来创建这样的动画。
您可以从 Github 的 this link 下载演示应用程序。
我不确定如何复制示例动画的一个部分是六边形的颜色在开始时似乎是亮白色,然后过渡到黄色。我的示例应用创建了一个动画,其中六边形是固定颜色并从不透明过渡到透明。
这是项目的自述文件:
PolarGradientMaskView
该项目说明了如何使用“圆锥”渐变来遮罩视图并创建圆形动画。
它使用 CAGradientLayer
类型的 .conic
,设置为大部分不透明,后半部分转换为透明。它将渐变图层作为蒙版安装在包含黄色六边形的形状图层上。
渐变层看起来像这样:
(在灰色棋盘格背景下以蓝色呈现,因此您可以看到从不透明到清晰的过渡。)
渐变的不透明(蓝色)部分使形状图层可见。渐变的透明部分隐藏(遮罩)了形状层的那些部分,渐变层的部分透明部分使形状层的那些部分部分透明。
动画只是围绕图层中心在 Z 轴上旋转渐变图层。它一次将图层旋转 1/4 圈,每次动画步骤完成时,它都会创建一个新动画,将蒙版再旋转 1/4 圈。
当您屏蔽六边形形状时,有点难以理解发生了什么。我创建了一个变体,在其中添加了一个图像视图作为自定义视图的子视图。动画看起来像这样:
应用程序的窗口如下所示:
,OP 的视频展示了一个动画,其中六边形形状的开头有一个白色的高光,然后过渡到黄色。
我创建了之前动画的变体,在动画的前缘添加了白色高光。它看起来像这样:
两个版本的动画都非常相似。
另一种全黄色动画使用一个带有圆锥渐变 CAGradientLayer
的单个形状图层作为遮罩层,这会导致六边形形状在最后 3 日左右淡出。动画只是围绕其中心旋转遮罩层。
主蒙版渐变如下所示:
(它在方格背景下以蓝色绘制,因此您可以更轻松地看到不透明和透明部分。
这个动画变体在第一个形状层之上添加了第二个形状层。我们称之为高光形状层。高光形状图层包含一个线宽略小的六边形,以白色绘制。高光形状图层也有一个圆锥形 CAGradientLayer
作为它的遮罩,但它的遮罩层除了六边形形状的开头外,还遮住了所有的东西。它只显示白色六边形的一小部分,并且永远不会完全不透明。
由于高光形状图层并非完全不透明,两个形状图层混合在一起,高光图层中不透明度较高的部分使组合图像中的像素看起来更白。
高光形状图层的渐变蒙版如下所示(在方格背景下再次显示为蓝色,以便您可以分辨蒙版的不透明度。)
这个版本的项目也在 github 上,地址是 https://github.com/DuncanMC/PolarGradientMaskView.git
但在名为“AddHighlightLayer”的分支中。
highlightGradientLayer
的设置代码如下:
highlightGradientLayer.type = .conic
highlightGradientLayer.colors = [UIColor.clear.cgColor,UIColor(red: 0,green: 0,blue: 1,alpha: 0.5).cgColor,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
,
问题是 - 代码正在渲染完整路径 - 每次从头到尾,所有动画的开始都相同。
加载器的想法是 - 在每个动画之后起点必须改变 - 类似 -
- 从角度 0 开始
- 向上到 45 度 /// 或 60 度,无论您希望它是什么
- 将起始角度更改为下一个逻辑步骤 - 比如说 30
- 然后最多渲染 75 /// 或 90,具体取决于您之前选择的内容
在这种安排中,您必须通过不断改变起点来不断绘制形状的某个部分。
在实践中,在不同的起始点值之间实现平滑过渡可能比看起来更困难。您可以在此处找到示例 - https://github.com/SVProgressHUD/SVProgressHUD/blob/master/SVProgressHUD/SVIndefiniteAnimatedView.m#L48-L102
更新
我在上面分享的链接中包含所有提示。该库使用图像作为蒙版,然后连续旋转。遮罩形状可以是您喜欢的任何形状 - 您已经有了相应的代码。
您只需要创建适合您的动画的图像。看看他们的 asset 长什么样
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。