簡體   English   中英

平滑地動畫 CAShapeLayer 路徑

[英]Animating CAShapeLayer path smoothly

我正在嘗試制作一個 Gauge UIView 來盡可能接近地模仿以下圖像目標

    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, y: self.frame.size.height/2),
                                   radius: self.frame.size.height/4,
                                   startAngle: CGFloat(180).toRadians(),
                                   endAngle: CGFloat(0).toRadians(),
                                   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, green: 93 / 255, blue: 41 / 255, alpha: 0.0).cgColor,
                           UIColor(red: 255 / 255, green: 93 / 255, blue: 41 / 255, alpha: 0.0).cgColor,
                           UIColor(red: 255 / 255, green: 93 / 255, blue: 41 / 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)
        
    }

這會在操場上產生以下內容,這與我的目標非常接近: 在此處輸入圖片說明

嘗試為儀表值的變化設置動畫時會出現問題。 我可以動畫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))"
    }

在此處輸入圖片說明

請注意當動畫完成時值的微小變化時,動畫看起來不錯。 然而,當有很大的變化時,漸變動畫看起來一點也不自然。 關於如何改進這一點的任何想法? 或者是否可以為不同的 keyPath 設置動畫以獲得更好的性能? 任何幫助將不勝感激!

您不能使用 Bezier 路徑addArc()函數為圓弧設置動畫並更改圓弧距離。

問題是控制點。 為了使動畫順利運行,開始和結束形狀必須具有相同數量和類型的控制點。 在幕后,UIBezierPath(和 CGPath)對象通過組合 Bezier 曲線(我不記得它是使用 Quadratic 還是 Cubic Bezier 曲線)創建近似圓的弧。整個圓由多個連接的 Bezier 曲線(“Bezier “數學樣條函數,而不是 UIBeizerPath,它是一個 UIKit 函數,可以創建可以包含 Bezier 路徑的形狀。)我似乎記得圓的 Bezier 近似值由 4 個鏈接的三次 Bezier 曲線組成。 (如果您有興趣,請參閱此 SO 答案以討論其外觀。)

這是我對其工作原理的理解。 (我的細節可能有誤,但無論如何它都說明了問題。)當您從 <= 1/4 of a full circle 移動到 > 1/4 of a full circle 時,arc 函數將使用第一個 1 三次貝塞爾部分,然后 2。在從 <= 1/2 圓到 > 1/2 圓的過渡處,它將變成 3 條貝塞爾曲線,在從 <= 3/4 圓到 > 3 的過渡處/4 一個圓,它將切換到 4 條貝塞爾曲線。

解決方案:

使用strokeEnd,您走在正確的軌道上。 始終將您的形狀創建為完整的圓形,並將 strokeEnd 設置為小於 1 的值。這將為您提供圓形的一部分,但以一種您可以平滑動畫的方式。 (您也可以為 strokeStart 設置動畫。)

我已經使用 CAShapeLayer 和 strokeEnd 對圓圈進行了動畫處理(這是幾年前的事情,所以它是在 Objective-C 中的。)我在 OS 上了一篇關於使用這種方法在 UIImageView 和創建“時鍾擦除”動畫 如果您有完整陰影圓圈的圖像,則可以在此處使用該確切方法。 (您應該能夠向任何 UIView 的內容層或其他層添加遮罩層,並像在我的時鍾擦除演示中一樣為該層設置動畫。如果您需要幫助破譯 Objective-C,請告訴我。

這是我創建的示例時鍾擦除動畫:

時鍾擦除動畫

請注意,您可以使用此效果來遮罩任何圖層,而不僅僅是圖像視圖。

編輯:我使用該項目的 Swift 版本發布了我的時鍾擦除動畫問題和答案的更新。

您可以直接通過https://github.com/DuncanMC/ClockWipeSwift 訪問新的存儲庫。

對於您的應用程序,我會將您需要動畫化的儀表部分設置為圖層的組合。 然后將基於 CAShapeLayer 的蒙版層附加到該復合層,並向該形狀層添加圓弧路徑,並為 strokeEnd 設置動畫,如我的示例項目中所示。 我的時鍾擦除動畫顯示的圖像就像時鍾指針從圖層中心掃過一樣。 在您的情況下,您會將圓弧置於圖層底部中心,並且僅在形狀圖層中使用半圓弧。 使用這種方式的蒙版會給你的合成層帶來鋒利的裁剪。 你會失去紅色弧上的圓形端蓋。 要解決此問題,您必須將紅色弧作為自己的形狀圖層(使用strokeEnd)進行動畫處理,並分別為漸變填充的弧strokeEnd 進行動畫處理。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM