如何在ios中绘制多种颜色的动画路径?
How to draw an animated path with multiple colors in ios?
我需要在我的 iOS 应用程序中绘制类似下图的内容,但圆弧可能包含更多颜色:
我知道如何绘制它,但我正在寻找一种方法来为路径的绘制设置动画。
有一个类似的问题 here, except that the circle is not animated. This 是另一个问题,它解释了如何为圆圈设置动画,在我的情况下它工作正常,除了它不处理路径中的多种颜色。
如何做到这一点?
我找到了一个非常有效的通用解决方案。由于无法绘制不同颜色的独特路径,因此我只绘制了所有不同颜色的路径,没有动画,这些路径构成了我想要的路径。之后,我在相反方向绘制了一条覆盖所有这些路径的独特路径,并对这条路径应用了动画。
例如,在上面的例子中,我用下面的代码绘制了两条弧线:
class CircleView: UIView {
let borderWidth: CGFloat = 20
let startAngle = CGFloat(Double.pi)
let middleAngle = CGFloat(Double.pi + Double.pi / 2)
let endAngle = CGFloat(2 * Double.pi)
var primaryColor = UIColor.red
var secondaryColor = UIColor.blue
var currentStrokeValue = CGFloat(0)
override func draw(_ rect: CGRect) {
let center = CGPoint(x: self.frame.width / 2, y: self.frame.height / 2)
let radius = CGFloat(self.frame.width / 2 - borderWidth)
let path1 = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: middleAngle, clockwise: true)
let path2 = UIBezierPath(arcCenter: center, radius: radius, startAngle: middleAngle, endAngle: endAngle, clockwise: true)
path1.lineWidth = borderWidth
primaryColor.setStroke()
path1.stroke()
path2.lineWidth = borderWidth
secondaryColor.setStroke()
path2.stroke()
}
}
之后,我得到路径 path3
,然后我将它添加到将添加到视图的层:
var path3 = UIBezierPath(arcCenter: center, radius: radius, startAngle: endAngle, endAngle: startAngle, clockwise: true)
注意这条路径是倒序覆盖前两条路径,因为它的startAngle等于endAngle
的值,它的endAngle等于startAngle
,顺时针属性 设置为 true
。这条路径是我要制作动画的路径。
例如,如果我想显示整个(假想)路径(由不同颜色的路径组成的路径)的 40%,我将其转换为显示我的覆盖路径的 60% path3
.我们可以在问题中提供的 link 中找到动画 path3
的方式。
import UIKit
import QuartzCore
import CoreGraphics
class ViewController: UIViewController,UIGestureRecognizerDelegate {
var btnview : UIButton!
var buttonCenter = CGPoint.zero
var firstlayerpoint = CGPoint.zero
var firstLayer = CAShapeLayer()
var secondLayer = CAShapeLayer()
var thirdLayer = CAShapeLayer()
var initialPosition = CGRect()
let label = UILabel()
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var blueLabel: UILabel!
@IBOutlet weak var greenLabel: UILabel!
@IBOutlet weak var redLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
firstLayer = self.createCircleWithBounds(bounds: CGRect(x:0, y:0, width:100,height:100), Position: self.view.center, StrokeColor: UIColor.blue, LineWidth: 20.0)
firstLayer.strokeStart = 0.00
firstLayer.strokeEnd = 0.33
self.view.layer.addSublayer(firstLayer)
secondLayer = self.createCircleWithBounds(bounds: CGRect(x:0, y:0, width:100,height:100), Position: self.view.center, StrokeColor: UIColor.red, LineWidth: 20.0)
secondLayer.strokeStart = 0.33
secondLayer.strokeEnd = 0.66
self.view.layer.addSublayer(secondLayer)
thirdLayer = self.createCircleWithBounds(bounds: CGRect(x:0, y:0, width:100,height:100), Position: self.view.center, StrokeColor: UIColor.green, LineWidth: 20.0)
thirdLayer.strokeStart = 0.66
thirdLayer.strokeEnd = 1.00
self.view.layer.addSublayer(thirdLayer)
btnview = UIButton(frame: CGRect(x: self.view.center.x - 20 , y: self.view.center.y - 20 , width: 40, height: 40))
btnview.backgroundColor = UIColor.gray
btnview.isUserInteractionEnabled = true
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.panButton(panGesture:)))
// panGesture.minimumNumberOfTouches = 1
btnview.addGestureRecognizer(panGesture)
self.view.addSubview(btnview)
// Do any additional setup after loading the view, typically from a nib.
nameLabel.isHidden = true
blueLabel.isHidden = true
greenLabel.isHidden = true
redLabel.isHidden = true
}
func panButton(panGesture: UIPanGestureRecognizer) {
//let translation = panGesture.translation(in: self.btnview)
panGesture.view!.center = btnview.center
panGesture.setTranslation(CGPoint.zero, in: self.view)
// var point = CGPoint.zero
// point = firstLayer.frame.size.center
if panGesture.state == .began {
label.isHidden = false
buttonCenter = btnview.center // store old button center
}
else if panGesture.state == .ended || panGesture.state == .failed || panGesture.state == .cancelled {
print(btnview.frame.origin.x)
print(greenLabel.frame.origin.x)
if btnview.frame.origin.x > greenLabel.frame.origin.x
{
// lblflayer.isHidden = false
// lblsecondlayer.isHidden = true
// lblthirdlayer.isHidden = true
nameLabel.isHidden = false
nameLabel.text = "Blue"
nameLabel.backgroundColor = UIColor.blue
}
else if btnview.frame.origin.x > blueLabel.frame.origin.x
{
// print(btnview.frame.origin.x)
// print(lblsecondlayer.frame.origin.x)
// lblsecondlayer.isHidden = false
// lblflayer.isHidden = true
// lblthirdlayer.isHidden = true
nameLabel.isHidden = false
nameLabel.text = "Red"
nameLabel.backgroundColor = UIColor.red
}
else if btnview.frame.origin.x > redLabel.frame.origin.x
{
print(btnview.frame.origin.x)
print(redLabel.frame.origin.x)
greenLabel.isHidden = true
// lblsecondlayer.isHidden = true
// lblthirdlayer.isHidden = false
nameLabel.isHidden = false
nameLabel.text = "Green"
nameLabel.backgroundColor = UIColor.green
}
else
{
nameLabel.isHidden = true
// lblflayer.isHidden = true
// lblsecondlayer.isHidden = true
// lblthirdlayer.isHidden = true
}
btnview.center = buttonCenter // restore button center
}
else
{
let location = panGesture.location(in: view) // get pan location
btnview.center = location // set button to where finger is
}
}
func createCircleWithBounds(bounds: CGRect, Position position: CGPoint, StrokeColor color: UIColor, LineWidth lineWidth: CGFloat) -> CAShapeLayer {
//let shapelayer = CAShapeLayer.layer
let shapelayer = CAShapeLayer()
shapelayer.strokeColor = color.cgColor
shapelayer.fillColor = UIColor.clear.cgColor
shapelayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: bounds.width / 2).cgPath
shapelayer.bounds = bounds
shapelayer.position = position
shapelayer.lineCap = kCALineCapButt
shapelayer.lineWidth = lineWidth
return shapelayer
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
@Reynaldo Aguilar 的回答很有效。您可以通过创建遮罩层并为其框架设置动画来实现相同的目的。这种方法的好处是它适用于任何(和多色)背景。这种方法的缺点是,如果线路不起作用,它可能不起作用,因为在这种情况下,多个点可能同时 hidden/revealed(当你只希望其中一个是)。
为了便于使用,您可以子类化 UIView 并像这样重写它的绘制和初始化方法来添加线条。
初始化:
init(frame: CGRect, path: [UIBezierPath], strokeColor: [UIColor], fillColor: [UIColor], lineWidth: [CGFloat]) {
// Initialize the view
super.init(frame: frame)
self.paths = path
self.strokeColors = strokeColor
self.fillColors = fillColor
self.lineWidths = lineWidth
self.backgroundColor = UIColor.clear // Background will always be clear by default
}
平局:
override func draw(_ rect: CGRect) {
super.draw(rect)
guard paths.count == strokeColors.count && strokeColors.count == fillColors.count && fillColors.count == lineWidths.count else {
print("ERROR: ARRAYS DON'T MATCH") // Stronger error handling recommended
return
}
for psfl in 0..<paths.count {
// Fill path if appropriate
self.fillColors[psfl].setFill()
self.paths[psfl].fill()
self.strokeColors[psfl].setStroke()
self.paths[psfl].lineWidth = self.lineWidths[psfl]
self.paths[psfl].stroke()
}
}
然后您可以使用一个函数来为遮罩层设置动画
动画:
func animate(startingRect: CGRect, duration: Double, animationKey: String) {
// Create a path based on the starting rect
let maskPath = UIBezierPath(rect: startingRect)
// Create a path based on the final rect
let finalPath = UIBezierPath(rect: self.frame)
// Create a shapelayer for the animation block
let maskLayer = CAShapeLayer()
maskLayer.frame = startingRect
// Add the mask layer to the custom view
self.layer.mask = maskLayer
// Animation block
let animation = CABasicAnimation(keyPath: "path")
animation.delegate = self // (Optionaly) set the delegate so we can remove the mask when the animation completes
animation.fromValue = maskLayer.path
animation.toValue = finalPath.cgPath
animation.duration = duration
maskLayer.add(animation, forKey: animationKey) // Add the animation to the mask layer
// Necessary for the animation to work properly
CATransaction.begin()
CATransaction.setDisableActions(true)
maskLayer.path = maskPath.cgPath
CATransaction.commit()
}
有了子类,实现起来很容易。初始化它,将它作为子视图添加到您想要的位置,然后在您希望动画开始时调用 animate()。您还可以 fiddle 使用其他东西,例如初始化和动画期间的 alpha,如果您希望隐藏绘图以开始。
我需要在我的 iOS 应用程序中绘制类似下图的内容,但圆弧可能包含更多颜色:
我知道如何绘制它,但我正在寻找一种方法来为路径的绘制设置动画。
有一个类似的问题 here, except that the circle is not animated. This 是另一个问题,它解释了如何为圆圈设置动画,在我的情况下它工作正常,除了它不处理路径中的多种颜色。
如何做到这一点?
我找到了一个非常有效的通用解决方案。由于无法绘制不同颜色的独特路径,因此我只绘制了所有不同颜色的路径,没有动画,这些路径构成了我想要的路径。之后,我在相反方向绘制了一条覆盖所有这些路径的独特路径,并对这条路径应用了动画。
例如,在上面的例子中,我用下面的代码绘制了两条弧线:
class CircleView: UIView {
let borderWidth: CGFloat = 20
let startAngle = CGFloat(Double.pi)
let middleAngle = CGFloat(Double.pi + Double.pi / 2)
let endAngle = CGFloat(2 * Double.pi)
var primaryColor = UIColor.red
var secondaryColor = UIColor.blue
var currentStrokeValue = CGFloat(0)
override func draw(_ rect: CGRect) {
let center = CGPoint(x: self.frame.width / 2, y: self.frame.height / 2)
let radius = CGFloat(self.frame.width / 2 - borderWidth)
let path1 = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: middleAngle, clockwise: true)
let path2 = UIBezierPath(arcCenter: center, radius: radius, startAngle: middleAngle, endAngle: endAngle, clockwise: true)
path1.lineWidth = borderWidth
primaryColor.setStroke()
path1.stroke()
path2.lineWidth = borderWidth
secondaryColor.setStroke()
path2.stroke()
}
}
之后,我得到路径 path3
,然后我将它添加到将添加到视图的层:
var path3 = UIBezierPath(arcCenter: center, radius: radius, startAngle: endAngle, endAngle: startAngle, clockwise: true)
注意这条路径是倒序覆盖前两条路径,因为它的startAngle等于endAngle
的值,它的endAngle等于startAngle
,顺时针属性 设置为 true
。这条路径是我要制作动画的路径。
例如,如果我想显示整个(假想)路径(由不同颜色的路径组成的路径)的 40%,我将其转换为显示我的覆盖路径的 60% path3
.我们可以在问题中提供的 link 中找到动画 path3
的方式。
import UIKit
import QuartzCore
import CoreGraphics
class ViewController: UIViewController,UIGestureRecognizerDelegate {
var btnview : UIButton!
var buttonCenter = CGPoint.zero
var firstlayerpoint = CGPoint.zero
var firstLayer = CAShapeLayer()
var secondLayer = CAShapeLayer()
var thirdLayer = CAShapeLayer()
var initialPosition = CGRect()
let label = UILabel()
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var blueLabel: UILabel!
@IBOutlet weak var greenLabel: UILabel!
@IBOutlet weak var redLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
firstLayer = self.createCircleWithBounds(bounds: CGRect(x:0, y:0, width:100,height:100), Position: self.view.center, StrokeColor: UIColor.blue, LineWidth: 20.0)
firstLayer.strokeStart = 0.00
firstLayer.strokeEnd = 0.33
self.view.layer.addSublayer(firstLayer)
secondLayer = self.createCircleWithBounds(bounds: CGRect(x:0, y:0, width:100,height:100), Position: self.view.center, StrokeColor: UIColor.red, LineWidth: 20.0)
secondLayer.strokeStart = 0.33
secondLayer.strokeEnd = 0.66
self.view.layer.addSublayer(secondLayer)
thirdLayer = self.createCircleWithBounds(bounds: CGRect(x:0, y:0, width:100,height:100), Position: self.view.center, StrokeColor: UIColor.green, LineWidth: 20.0)
thirdLayer.strokeStart = 0.66
thirdLayer.strokeEnd = 1.00
self.view.layer.addSublayer(thirdLayer)
btnview = UIButton(frame: CGRect(x: self.view.center.x - 20 , y: self.view.center.y - 20 , width: 40, height: 40))
btnview.backgroundColor = UIColor.gray
btnview.isUserInteractionEnabled = true
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.panButton(panGesture:)))
// panGesture.minimumNumberOfTouches = 1
btnview.addGestureRecognizer(panGesture)
self.view.addSubview(btnview)
// Do any additional setup after loading the view, typically from a nib.
nameLabel.isHidden = true
blueLabel.isHidden = true
greenLabel.isHidden = true
redLabel.isHidden = true
}
func panButton(panGesture: UIPanGestureRecognizer) {
//let translation = panGesture.translation(in: self.btnview)
panGesture.view!.center = btnview.center
panGesture.setTranslation(CGPoint.zero, in: self.view)
// var point = CGPoint.zero
// point = firstLayer.frame.size.center
if panGesture.state == .began {
label.isHidden = false
buttonCenter = btnview.center // store old button center
}
else if panGesture.state == .ended || panGesture.state == .failed || panGesture.state == .cancelled {
print(btnview.frame.origin.x)
print(greenLabel.frame.origin.x)
if btnview.frame.origin.x > greenLabel.frame.origin.x
{
// lblflayer.isHidden = false
// lblsecondlayer.isHidden = true
// lblthirdlayer.isHidden = true
nameLabel.isHidden = false
nameLabel.text = "Blue"
nameLabel.backgroundColor = UIColor.blue
}
else if btnview.frame.origin.x > blueLabel.frame.origin.x
{
// print(btnview.frame.origin.x)
// print(lblsecondlayer.frame.origin.x)
// lblsecondlayer.isHidden = false
// lblflayer.isHidden = true
// lblthirdlayer.isHidden = true
nameLabel.isHidden = false
nameLabel.text = "Red"
nameLabel.backgroundColor = UIColor.red
}
else if btnview.frame.origin.x > redLabel.frame.origin.x
{
print(btnview.frame.origin.x)
print(redLabel.frame.origin.x)
greenLabel.isHidden = true
// lblsecondlayer.isHidden = true
// lblthirdlayer.isHidden = false
nameLabel.isHidden = false
nameLabel.text = "Green"
nameLabel.backgroundColor = UIColor.green
}
else
{
nameLabel.isHidden = true
// lblflayer.isHidden = true
// lblsecondlayer.isHidden = true
// lblthirdlayer.isHidden = true
}
btnview.center = buttonCenter // restore button center
}
else
{
let location = panGesture.location(in: view) // get pan location
btnview.center = location // set button to where finger is
}
}
func createCircleWithBounds(bounds: CGRect, Position position: CGPoint, StrokeColor color: UIColor, LineWidth lineWidth: CGFloat) -> CAShapeLayer {
//let shapelayer = CAShapeLayer.layer
let shapelayer = CAShapeLayer()
shapelayer.strokeColor = color.cgColor
shapelayer.fillColor = UIColor.clear.cgColor
shapelayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: bounds.width / 2).cgPath
shapelayer.bounds = bounds
shapelayer.position = position
shapelayer.lineCap = kCALineCapButt
shapelayer.lineWidth = lineWidth
return shapelayer
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
@Reynaldo Aguilar 的回答很有效。您可以通过创建遮罩层并为其框架设置动画来实现相同的目的。这种方法的好处是它适用于任何(和多色)背景。这种方法的缺点是,如果线路不起作用,它可能不起作用,因为在这种情况下,多个点可能同时 hidden/revealed(当你只希望其中一个是)。
为了便于使用,您可以子类化 UIView 并像这样重写它的绘制和初始化方法来添加线条。
初始化:
init(frame: CGRect, path: [UIBezierPath], strokeColor: [UIColor], fillColor: [UIColor], lineWidth: [CGFloat]) {
// Initialize the view
super.init(frame: frame)
self.paths = path
self.strokeColors = strokeColor
self.fillColors = fillColor
self.lineWidths = lineWidth
self.backgroundColor = UIColor.clear // Background will always be clear by default
}
平局:
override func draw(_ rect: CGRect) {
super.draw(rect)
guard paths.count == strokeColors.count && strokeColors.count == fillColors.count && fillColors.count == lineWidths.count else {
print("ERROR: ARRAYS DON'T MATCH") // Stronger error handling recommended
return
}
for psfl in 0..<paths.count {
// Fill path if appropriate
self.fillColors[psfl].setFill()
self.paths[psfl].fill()
self.strokeColors[psfl].setStroke()
self.paths[psfl].lineWidth = self.lineWidths[psfl]
self.paths[psfl].stroke()
}
}
然后您可以使用一个函数来为遮罩层设置动画
动画:
func animate(startingRect: CGRect, duration: Double, animationKey: String) {
// Create a path based on the starting rect
let maskPath = UIBezierPath(rect: startingRect)
// Create a path based on the final rect
let finalPath = UIBezierPath(rect: self.frame)
// Create a shapelayer for the animation block
let maskLayer = CAShapeLayer()
maskLayer.frame = startingRect
// Add the mask layer to the custom view
self.layer.mask = maskLayer
// Animation block
let animation = CABasicAnimation(keyPath: "path")
animation.delegate = self // (Optionaly) set the delegate so we can remove the mask when the animation completes
animation.fromValue = maskLayer.path
animation.toValue = finalPath.cgPath
animation.duration = duration
maskLayer.add(animation, forKey: animationKey) // Add the animation to the mask layer
// Necessary for the animation to work properly
CATransaction.begin()
CATransaction.setDisableActions(true)
maskLayer.path = maskPath.cgPath
CATransaction.commit()
}
有了子类,实现起来很容易。初始化它,将它作为子视图添加到您想要的位置,然后在您希望动画开始时调用 animate()。您还可以 fiddle 使用其他东西,例如初始化和动画期间的 alpha,如果您希望隐藏绘图以开始。