[英]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,請告訴我。
這是我創建的示例時鍾擦除動畫:
請注意,您可以使用此效果來遮罩任何圖層,而不僅僅是圖像視圖。
您可以直接通過https://github.com/DuncanMC/ClockWipeSwift 訪問新的存儲庫。
對於您的應用程序,我會將您需要動畫化的儀表部分設置為圖層的組合。 然后將基於 CAShapeLayer 的蒙版層附加到該復合層,並向該形狀層添加圓弧路徑,並為 strokeEnd 設置動畫,如我的示例項目中所示。 我的時鍾擦除動畫顯示的圖像就像時鍾指針從圖層中心掃過一樣。 在您的情況下,您會將圓弧置於圖層底部中心,並且僅在形狀圖層中使用半圓弧。 使用這種方式的蒙版會給你的合成層帶來鋒利的裁剪。 你會失去紅色弧上的圓形端蓋。 要解決此問題,您必須將紅色弧作為自己的形狀圖層(使用strokeEnd)進行動畫處理,並分別為漸變填充的弧strokeEnd 進行動畫處理。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.