CAShapeLayerパスをスムーズにアニメーション化
次の画像をできるだけ模倣するようにゲージ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をアニメーション化することは可能ですか?どんな助けでも大歓迎です!
回答
ベジェパスaddArc()
関数を使用して円弧をアニメーション化し、円弧の距離を変更することはできません。
問題はコントロールポイントです。アニメーションがスムーズに機能するためには、開始シェイプと終了シェイプのコントロールポイントの数とタイプが同じである必要があります。裏で、UIBezierPath(およびCGPath)オブジェクトは、ベジェ曲線を組み合わせて円を近似する円弧を作成します(2次または3次ベジェ曲線を使用するかどうかは覚えていません)。円全体は、複数の接続されたベジェ曲線( "ベジェ「ベジェパスを含むことができる形状を作成するUIKit関数であるUIBeizerPathではなく、数学的なスプライン関数。)円のベジェ近似は4つのリンクされた3次ベジェ曲線で構成されていることを覚えているようです。(興味がある場合は、このSOの回答を参照してください。)
これがどのように機能するかについての私の理解です。(詳細が間違っている可能性がありますが、いずれにせよ問題を示しています。)完全な円の<= 1/4から> 1/4に移動すると、円弧関数は最初の1立方ベジェを使用します。セクション、次に2。<= 1/2の円から> 1/2の円への遷移では、3つのベジェ曲線にシフトし、<= 3/4の円から> 3への遷移ではシフトします。円の/ 4、それは4ベジェ曲線に切り替わります。
ソリューション:
あなたはstrokeEndを使用して正しい軌道に乗っています。常にあなたの円の一部を与える1未満のものにあなたの完全な円として形状、およびセットstrokeEndを作成するのではなく、方法であなたはそれをすることができますスムーズにアニメーション化します。(strokeStartをアニメートすることもできます。)
CAShapeLayerとstrokeEndを使用して説明したように、円をアニメーション化しました(数年前だったので、Objective-Cでした)。UIImageViewでマスクをアニメーション化するアプローチの使用に関するOSに関する記事をここに書きました。「クロックワイプ」アニメーションを作成します。完全に影付きの円の画像がある場合は、ここでその正確なアプローチを使用できます。(UIViewのコンテンツレイヤーまたは他のレイヤーにマスクレイヤーを追加し、時計のワイプデモのようにそのレイヤーをアニメーション化できるはずです。Objective-Cの解読についてサポートが必要な場合はお知らせください。
これが私が作成したサンプルの時計ワイプアニメーションです:

この効果を使用して、画像ビューだけでなく、任意のレイヤーをマスクできることに注意してください。
編集:プロジェクトのSwiftバージョンで時計ワイプアニメーションの質問と回答の更新を投稿しました。
新しいリポジトリには、次のURLから直接アクセスできます。 https://github.com/DuncanMC/ClockWipeSwift。
あなたのアプリケーションのために、私はあなたがレイヤーの複合体としてアニメートする必要があるあなたのゲージの部分をセットアップします。次に、CAShapeLayerベースのマスクレイヤーをその複合レイヤーにアタッチし、そのシェイプレイヤーに円弧パスを追加して、サンプルプロジェクトに示すようにstrokeEndをアニメーション化します。私の時計ワイプアニメーションは、レイヤーの中心から時計の針をスイープするような画像を表示します。この場合、円弧をレイヤーの下部中央に配置し、シェイプレイヤーでは半円弧のみを使用します。そのようにマスクを使用すると、合成レイヤーに鋭いエッジのトリミングが得られます。赤い弧の丸いエンドキャップを失います。これを修正するには、赤い円弧を独自のシェイプレイヤーとしてアニメーション化し(strokeEndを使用)、グラデーション塗りつぶしの円弧strokeEndを個別にアニメーション化する必要があります。