动画 UIBezierPath 六边形,如 UIActivityIndi​​catorview

如何解决动画 UIBezierPath 六边形,如 UIActivityIndi​​catorview

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

enter image description here

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

enter image description here

这是我的 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 可以获得如下效果:

Hexagon animation

我建议您执行以下操作:

  • 创建一个完整的圆形六边形作为 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,设置为大部分不透明,后半部分转换为透明。它将渐变图层作为蒙版安装在包含黄色六边形的形状图层上。

渐变层看起来像这样:

enter image description here

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

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

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

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

enter image description here

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

enter image description here

,

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

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

enter image description here

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

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

主蒙版渐变如下所示:

Conical gradient

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

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

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

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

enter image description here

这个版本的项目也在 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
,

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

加载器的想法是 - 在每个动画之后起点必须改变 - 类似 -

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

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

在实践中,在不同的起始点值之间实现平滑过渡可能比看起来更困难。您可以在此处找到示例 - https://github.com/SVProgressHUD/SVProgressHUD/blob/master/SVProgressHUD/SVIndefiniteAnimatedView.m#L48-L102

更新


我在上面分享的链接中包含所有提示。该库使用图像作为蒙版,然后连续旋转。遮罩形状可以是您喜欢的任何形状 - 您已经有了相应的代码。

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

并且在掩盖他们的动画看起来像之后 - enter image description here


版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


使用本地python环境可以成功执行 import pandas as pd import matplotlib.pyplot as plt # 设置字体 plt.rcParams[&#39;font.sans-serif&#39;] = [&#39;SimHei&#39;] # 能正确显示负号 p
错误1:Request method ‘DELETE‘ not supported 错误还原:controller层有一个接口,访问该接口时报错:Request method ‘DELETE‘ not supported 错误原因:没有接收到前端传入的参数,修改为如下 参考 错误2:cannot r
错误1:启动docker镜像时报错:Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen 解决方法:重启docker -&gt; systemctl r
错误1:private field ‘xxx‘ is never assigned 按Altʾnter快捷键,选择第2项 参考:https://blog.csdn.net/shi_hong_fei_hei/article/details/88814070 错误2:启动时报错,不能找到主启动类 #
报错如下,通过源不能下载,最后警告pip需升级版本 Requirement already satisfied: pip in c:\users\ychen\appdata\local\programs\python\python310\lib\site-packages (22.0.4) Coll
错误1:maven打包报错 错误还原:使用maven打包项目时报错如下 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources)
错误1:服务调用时报错 服务消费者模块assess通过openFeign调用服务提供者模块hires 如下为服务提供者模块hires的控制层接口 @RestController @RequestMapping(&quot;/hires&quot;) public class FeignControl
错误1:运行项目后报如下错误 解决方案 报错2:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project sb 解决方案:在pom.
参考 错误原因 过滤器或拦截器在生效时,redisTemplate还没有注入 解决方案:在注入容器时就生效 @Component //项目运行时就注入Spring容器 public class RedisBean { @Resource private RedisTemplate&lt;String
使用vite构建项目报错 C:\Users\ychen\work&gt;npm init @vitejs/app @vitejs/create-app is deprecated, use npm init vite instead C:\Users\ychen\AppData\Local\npm-
参考1 参考2 解决方案 # 点击安装源 协议选择 http:// 路径填写 mirrors.aliyun.com/centos/8.3.2011/BaseOS/x86_64/os URL类型 软件库URL 其他路径 # 版本 7 mirrors.aliyun.com/centos/7/os/x86
报错1 [root@slave1 data_mocker]# kafka-console-consumer.sh --bootstrap-server slave1:9092 --topic topic_db [2023-12-19 18:31:12,770] WARN [Consumer clie
错误1 # 重写数据 hive (edu)&gt; insert overwrite table dwd_trade_cart_add_inc &gt; select data.id, &gt; data.user_id, &gt; data.course_id, &gt; date_format(
错误1 hive (edu)&gt; insert into huanhuan values(1,&#39;haoge&#39;); Query ID = root_20240110071417_fe1517ad-3607-41f4-bdcf-d00b98ac443e Total jobs = 1
报错1:执行到如下就不执行了,没有显示Successfully registered new MBean. [root@slave1 bin]# /usr/local/software/flume-1.9.0/bin/flume-ng agent -n a1 -c /usr/local/softwa
虚拟及没有启动任何服务器查看jps会显示jps,如果没有显示任何东西 [root@slave2 ~]# jps 9647 Jps 解决方案 # 进入/tmp查看 [root@slave1 dfs]# cd /tmp [root@slave1 tmp]# ll 总用量 48 drwxr-xr-x. 2
报错1 hive&gt; show databases; OK Failed with exception java.io.IOException:java.lang.RuntimeException: Error in configuring object Time taken: 0.474 se
报错1 [root@localhost ~]# vim -bash: vim: 未找到命令 安装vim yum -y install vim* # 查看是否安装成功 [root@hadoop01 hadoop]# rpm -qa |grep vim vim-X11-7.4.629-8.el7_9.x
修改hadoop配置 vi /usr/local/software/hadoop-2.9.2/etc/hadoop/yarn-site.xml # 添加如下 &lt;configuration&gt; &lt;property&gt; &lt;name&gt;yarn.nodemanager.res