微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

平滑动画CAShapeLayer路径

如何解决平滑动画CAShapeLayer路径

我正在尝试制作一个Gauge UIView,以尽可能模仿以下图像

Goal

    func gradientBezierPath(percent: CGFloat) ->  UIBezierPath {
        // vary this to move the start of the arc
        let startAngle = CGFloat(180).toradians()//-CGFloat.pi / 2  // This corresponds to 12 0'clock
        // vary this to vary the size of the segment,in per cent
        let proportion = CGFloat(50 * percent)
        let centre = CGPoint (x: self.frame.size.width / 2,y: self.frame.size.height / 2)
        let radius = self.frame.size.height/4//self.frame.size.width / (CGFloat(130).toradians())
        let arc = CGFloat.pi * 2 * proportion / 100 // i.e. the proportion of a full circle

        // Start a mutable path
        let cPath = UIBezierPath()
        // Move to the centre
        cPath.move(to: centre)
        // Draw a line to the circumference
        cPath.addLine(to: CGPoint(x: centre.x + radius * cos(startAngle),y: centre.y + radius * sin(startAngle)))
        // Now draw the arc
        cPath.addArc(withCenter: centre,radius: radius,startAngle: startAngle,endAngle: arc + startAngle,clockwise: true)
        // Line back to the centre,where we started (or the stroke doesn't work,though the fill does)
        cPath.addLine(to: CGPoint(x: centre.x,y: centre.y))
        
        return cPath
    }
    
    override func draw(_ rect: CGRect) {
        
//        let endAngle = percent == 1.0 ? 0 : (percent * 180) + 180
        
        path = UIBezierPath(arcCenter: CGPoint(x: self.frame.size.width/2,y: self.frame.size.height/2),radius: self.frame.size.height/4,startAngle: CGFloat(180).toradians(),endAngle: CGFloat(0).toradians(),clockwise: true)
        
        percentPath = UIBezierPath(arcCenter: CGPoint(x: self.frame.size.width/2,clockwise: true)
        
        let shapeLayer = CAShapeLayer()
        shapeLayer.path = self.path.cgPath
        shapeLayer.strokeColor = UIColor(red: 110 / 255,green: 78 / 255,blue: 165 / 255,alpha: 1.0).cgColor
        shapeLayer.fillColor = UIColor.clear.cgColor
        shapeLayer.linewidth = 5.0
        shapeLayer.lineCap = .round
        self.layer.addSublayer(shapeLayer)
        
        percentLayer.path = self.percentPath.cgPath
        percentLayer.strokeColor = UIColor(red: 255 / 255,green: 93 / 255,blue: 41 / 255,alpha: 1.0).cgColor
        percentLayer.fillColor = UIColor.clear.cgColor
        percentLayer.linewidth = 8.0
//        percentLayer.strokeEnd = CGFloat(percent)
        percentLayer.lineCap = .round
        self.layer.addSublayer(percentLayer)
        
        // n.b. as @MartinR points out `cPath.close()` does the same!

        // circle shape
        circleShape.path = gradientBezierPath(percent: 1.0).cgPath//cPath.cgPath
        circleShape.strokeColor = UIColor.clear.cgColor
        circleShape.fillColor = UIColor.green.cgColor
        self.layer.addSublayer(circleShape)
        
        gradient.frame = frame
        gradient.mask = circleShape
        gradient.type = .radial
        gradient.colors = [UIColor(red: 255 / 255,alpha: 0.0).cgColor,UIColor(red: 255 / 255,alpha: 0.4).cgColor]
        gradient.locations = [0,0.35,1]
        gradient.startPoint = CGPoint(x: 0.49,y: 0.55) // increase Y adds more orange from top to bottom
        gradient.endPoint = CGPoint(x: 0.98,y: 1) // increase x pushes orange out more to edges
        self.layer.addSublayer(gradient)

        //myTextLayer.string = "\(Int(percent * 100))"
        myTextLayer.backgroundColor = UIColor.clear.cgColor
        myTextLayer.foregroundColor = UIColor.white.cgColor
        myTextLayer.fontSize = 85.0
        myTextLayer.frame = CGRect(x: (self.frame.size.width / 2) - (self.frame.size.width/8),y: (self.frame.size.height / 2) - self.frame.size.height/8,width: 120,height: 120)
        self.layer.addSublayer(myTextLayer)
        
    }

这会在一个与我要达到的目标非常接近的操场上产生以下内容

enter image description here

尝试对量规值的变化进行动画处理时出现问题。我可以通过修改percentLayer来为strokeEnd制作动画,但是如果量规的百分比值有较大变化,则对circleShape.path进行渐变动画会导致某些不平滑的动画。这是我用来设置两层动画的函数(现在每2秒在计时器上调用一次,以模拟量规值的变化)。

    func randomPercent() {
        let random = CGFloat.random(in: 0.0...1.0)
        
        // Animate the percent layer
        let animation = CABasicAnimation(keyPath: "strokeEnd")
        animation.fromValue = percentLayer.strokeEnd
        animation.tovalue = random
        animation.duration = 1.5
        percentLayer.strokeEnd = random
        percentLayer.add(animation,forKey: nil)
        
        // Animate the gradient layer
        let newShapePath = gradientBezierPath(percent: random)
        let gradientAnimation = CABasicAnimation(keyPath: "path")
        gradientAnimation.duration = 1.5
        gradientAnimation.tovalue = newShapePath
        gradientAnimation.fromValue = circleShape.path
        circleShape.path = newShapePath.cgPath
        self.circleShape.add(gradientAnimation,forKey: nil)
        
        myTextLayer.string = "\(Int(random * 100))"
    }

enter image description here

请注意,当动画的值进行很小的更改时,动画看起来很好。但是,当变化较大时,渐变动画看起来根本不自然。关于如何改善这一点的任何想法?也许可以设置不同的keyPath动画以获得更好的性能?任何帮助将不胜感激!

解决方法

您不能使用Bezier路径addArc()函数为圆弧设置动画并更改圆弧距离。

问题是控制点。为了使动画顺利运行,开始和结束形状必须具有相同数量和类型的控制点。在幕后,UIBezierPath(和CGPath)对象通过组合Bezier曲线创建近似于圆的弧(我不记得它是使用Quadratic还是Cubic Bezier曲线。)整个圆由多个连接的Bezier曲线组成(“ Bezier “数学样条函数,而不是UIBeizerPath,它是一个UIKit函数,该函数创建可以包含Bezier路径的形状。”我似乎记得一个圆的Bezier近似值由4个链接的三次Bezier曲线组成。 (如果您有兴趣,请参见this SO answer,以了解其外观。)

这是我对其运作方式的理解。 (我的细节可能有误,但是无论如何它都能说明问题。)当您从一个完整圆的 1/4时,圆弧函数将使用第一个1立方贝塞尔曲线截面,然后是2。在从 1/2圆的过渡处,它将移动到3个Bezier曲线,并且在从 3的过渡处圆的/ 4,将切换为4条贝塞尔曲线。

解决方案:

使用strokeEnd,您将走在正确的轨道上。始终将形状创建为完整的圆形,并将strokeEnd设置为小于1的值。这将使您成为圆形的一部分,但可以使 平滑地进行动画处理。 (您也可以为strokeStart设置动画。)

我已经像您使用CAShapeLayer和strokeEnd描述动画了圈一样(这是几年前的,所以它在Objective-C中。)我写了an article here on OS on using the approach to animate a mask on a UIImageView and create a "clock wipe" animation。如果您有整个阴影圆圈的图像,则可以在此处使用该确切方法。 (您应该能够将遮罩层添加到任何UIView的内容层或其他层,并像我的时钟擦除演示中那样对该层进行动画处理。让我知道您是否需要帮助来解密Objective-C。

这是我创建的示例时钟擦除动画:

Clock wipe animation

请注意,您可以使用此效果来遮盖任何层,而不仅仅是图像视图。

编辑:我用项目的Swift版本发布了时钟擦除动画问答的更新。

您可以直接在https://github.com/DuncanMC/ClockWipeSwift上找到新的仓库。

对于您的应用程序,我将量具的需要设置的部分设置为多层图层。然后,将基于CAShapeLayer的遮罩层附加到该复合层,并向该形状层添加圆弧路径,并为我的示例项目中所示的strokeEnd设置动画。我的时钟擦除动画可以像从表层中心扫过时针一样显示图像。在您的情况下,应将圆弧居中于图层的底部中心,而在形状图层中仅使用半圆弧。以这种方式使用蒙版将为您的复合层提供清晰的裁剪效果。您将失去红色弧形上的圆形端盖。要解决此问题,您必须为红色弧作为自己的形状图层设置动画(使用strokeEnd),并分别为渐变填充的弧strokeEnd设置动画。

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