I have two UIBezierPath...
First path shows the total path from to and fro destination and the second path is a copy of the first path but that copy should be a percentage of the first path which I am unable to do.
Basically I would like the plane to stop at the part where green UIBezier path ends and not go until the past green color.
I am attaching a video in hte link that will show the animation I am trying to get. http://cl.ly/302I3O2f0S3Y
Also a similar question asked is Move CALayer via UIBezierPath
Here is the relevant code
override func viewDidLoad() {
super.viewDidLoad()
let endPoint = CGPointMake(fullFlightLine.frame.origin.x + fullFlightLine.frame.size.width, fullFlightLine.frame.origin.y + 100)
self.layer = CALayer()
self.layer.contents = UIImage(named: "Plane")?.CGImage
self.layer.frame = CGRectMake(fullFlightLine.frame.origin.x - 10, fullFlightLine.frame.origin.y + 10, 120, 120)
self.path = UIBezierPath()
self.path.moveToPoint(CGPointMake(fullFlightLine.frame.origin.x, fullFlightLine.frame.origin.y + fullFlightLine.frame.size.height/4))
self.path.addQuadCurveToPoint(endPoint, controlPoint:CGPointMake(self.view.bounds.size.width/2, -200))
self.animationPath = self.path.copy() as! UIBezierPath
let w = (viewModel?.completePercentage) as CGFloat!
// let animationRectangle = UIBezierPath(rect: CGRectMake(fullFlightLine.frame.origin.x-20, fullFlightLine.frame.origin.y-270, fullFlightLine.frame.size.width*w, fullFlightLine.frame.size.height-20))
// let currentContext = UIGraphicsGetCurrentContext()
// CGContextSaveGState(currentContext)
// self.animationPath.appendPath(animationRectangle)
// self.animationPath.addClip()
// CGContextRestoreGState(currentContext)
self.shapeLayer = CAShapeLayer()
self.shapeLayer.path = path.CGPath
self.shapeLayer.strokeColor = UIColor.redColor().CGColor
self.shapeLayer.fillColor = UIColor.clearColor().CGColor
self.shapeLayer.lineWidth = 10
self.animationLayer = CAShapeLayer()
self.animationLayer.path = animationPath.CGPath
self.animationLayer.strokeColor = UIColor.greenColor().CGColor
self.animationLayer.strokeEnd = w
self.animationLayer.fillColor = UIColor.clearColor().CGColor
self.animationLayer.lineWidth = 3
fullFlightLine.layer.addSublayer(shapeLayer)
fullFlightLine.layer.addSublayer(animationLayer)
fullFlightLine.layer.addSublayer(self.layer)
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
updateAnimationForPath(self.animationLayer)
}
func updateAnimationForPath(pathLayer : CAShapeLayer) {
let animation : CAKeyframeAnimation = CAKeyframeAnimation(keyPath: "position")
animation.path = pathLayer.path
animation.calculationMode = kCAAnimationPaced
animation.delegate = self
animation.duration = 3.0
self.layer.addAnimation(animation, forKey: "bezierPathAnimation")
}
}
extension Int {
var degreesToRadians : CGFloat {
return CGFloat(self) * CGFloat(M_PI) / 180.0
}
}
Your animation for traveling a path is exactly right. The only problem now is that you are setting the key path animation's path to the whole bezier path:
animation.path = pathLayer.path
If you want the animation to cover only part of that path, you have two choices:
Supply a shorter version of the bezier path, instead of pathLayer.path.
Alternatively, wrap the whole animation in a CAAnimationGroup with a shorter duration. For example, if your three-second animation is wrapped inside a two-second animation group, it will stop two-thirds of the way through.
Related
I created a circular button using swift, what I am trying to do is changing the border color with animation
I used the following code :
let color = CABasicAnimation(keyPath: "borderColor")
color.fromValue = UIColor.black.cgColor
color.toValue = UIColor.red.cgColor
color.duration = 1
color.repeatCount = 1
sender.layer.add(color, forKey: "color and width")
The border color is changing but it is not given the desire effect, it change the border color all at once. What I would like to have as effect is like your drawing over the old color, to keep it simple ==> like a progress bar where u see the old color like fade away and be replaced by the new color.
Is there is any way to do that?
Thanks for helping.
Updating code
var storkeLayer = CAShapeLayer()
#IBOutlet weak var Mybut: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
storkeLayer.fillColor = UIColor.clear.cgColor
storkeLayer.strokeColor = UIColor.black.cgColor
storkeLayer.lineWidth = 2
// Create a rounded rect path using button's bounds.
storkeLayer.path = CGPath.init(roundedRect: Mybut.bounds, cornerWidth: Mybut.frame.width / 2 , cornerHeight: Mybut.frame.height / 2, transform: nil)
// Add layer to the button
Mybut.layer.addSublayer(storkeLayer)
}
#IBAction func bytton(_ sender: UIButton) {
storkeLayer.fillColor = UIColor.clear.cgColor
storkeLayer.strokeColor = UIColor.red.cgColor
storkeLayer.lineWidth = 2
// Create a rounded rect path using button's bounds.
storkeLayer.path = CGPath.init(roundedRect: Mybut.bounds, cornerWidth: Mybut.frame.width / 2 , cornerHeight: Mybut.frame.height / 2, transform: nil)
// Add layer to the button
sender.layer.addSublayer(storkeLayer)
// Create animation layer and add it to the stroke layer.
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = CGFloat(0.0)
animation.toValue = CGFloat(1.0)
animation.duration = 1
animation.fillMode = kCAFillModeForwards
// animation.fillMode = kCAFillModeBackwards
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
storkeLayer.add(animation, forKey: "circleAnimation")
}
You can stroke the button's path using the following code snippet.
#IBAction func buttonTapped(_ sender: UIButton) {
let storkeLayer = CAShapeLayer()
storkeLayer.fillColor = UIColor.clear.cgColor
storkeLayer.strokeColor = UIColor.red.cgColor
storkeLayer.lineWidth = 2
// Create a rounded rect path using button's bounds.
storkeLayer.path = CGPath.init(roundedRect: sender.bounds, cornerWidth: 5, cornerHeight: 5, transform: nil) // same path like the empty one ...
// Add layer to the button
sender.layer.addSublayer(storkeLayer)
// Create animation layer and add it to the stroke layer.
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = CGFloat(0.0)
animation.toValue = CGFloat(1.0)
animation.duration = 1
animation.fillMode = kCAFillModeForwards
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
storkeLayer.add(animation, forKey: "circleAnimation")
}
This snippet creates something like this:
I must admit I have no clue how to do this in iOS -
Here's some code that makes a nice dotted line:
Now, I want that line to "run" upwards:
So, every one second it will move upwards by, itemLength * 2.0.
Of course, it would wrap around top to bottom.
So, DottedVertical should just do this completely on its own.
Really, how do you do this in iOS?
It would be great if the solution is general and will "scroll" any I suppose layer or drawn thing.
In say a game engine it's trivial, you just animate the offset of the texture. Can you perhaps offset the layer, or something, in iOS?
What's the best way?
I guess you'd want to use the GPU (layer animation right?) to avoid melting the cpu.
#IBDesignable class DottedVertical: UIView {
#IBInspectable var dotColor: UIColor = UIColor.faveColor
override func draw(_ rect: CGRect) {
// say you want 8 dots, with perfect fenceposting:
let totalCount = 8 + 8 - 1
let fullHeight = bounds.size.height
let width = bounds.size.width
let itemLength = fullHeight / CGFloat(totalCount)
let beginFromTop = !lowerHalfOnly ? 0.0 : (fullHeight * 8.0 / 15.0)
let top = CGPoint(x: width/2, y: beginFromTop)
let bottom = CGPoint(x: width/2, y: fullHeight)
let path = UIBezierPath()
path.move(to: top)
path.addLine(to: bottom)
path.lineWidth = width
let dashes: [CGFloat] = [itemLength, itemLength]
path.setLineDash(dashes, count: dashes.count, phase: 0)
dotColor.setStroke()
path.stroke()
}
(Bonus - if you had a few of these on screen, they'd have to be synced of course. There'd need to be a "sync" call that starts the running animation, so you can start them all at once with a notification or other message.)
Hate to answer my own question, here's a copy and paste solution based on the Men's suggestions above!
Good one! Superb effect...
#IBDesignable class DottedVertical: UIView {
#IBInspectable var dotColor: UIColor = sfBlack6 { didSet {setup()} }
override func layoutSubviews() { setup() }
var s:CAShapeLayer? = nil
func setup() {
// say you want 8 dots, with perfect fenceposting:
- calculate exactly as in the example in the question above -
// marching ants...
if (s == nil) {
s = CAShapeLayer()
self.layer.addSublayer(s!)
}
s!.strokeColor = dotColor.cgColor
s!.fillColor = backgroundColor?.cgColor
s!.lineWidth = width
let ns = NSNumber(value: Double(itemLength))
s!.lineDashPattern = [ns, ns]
let path = CGMutablePath()
path.addLines(between: [top, bottom])
s!.path = path
let anim = CABasicAnimation(keyPath: "lineDashPhase")
anim.fromValue = 0
anim.toValue = ns + ns
anim.duration = 1.75 // seconds
anim.repeatCount = Float.greatestFiniteMagnitude
s!.add(anim, forKey: nil)
self.layer.addSublayer(s!)
}
}
In my ios swift app I have SVG file - it's a shape of a map marker, now I want to draw it with animation. I managed to display the drawing thanks to the usage of pocketSVG https://github.com/pocketsvg/PocketSVG/ My code is as follows:
#IBOutlet weak var mainLogoView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
let url = Bundle.main.url(forResource: "mainLogo", withExtension: "svg")!
//initialise a view that parses and renders an SVG file in the bundle:
let svgImageView = SVGImageView.init(contentsOf: url)
//scale the resulting image to fit the frame of the view, but
//maintain its aspect ratio:
svgImageView.contentMode = .scaleAspectFit
//layout the view:
svgImageView.translatesAutoresizingMaskIntoConstraints = false
mainLogoView.addSubview(svgImageView)
svgImageView.topAnchor.constraint(equalTo: mainLogoView.topAnchor).isActive = true
svgImageView.leftAnchor.constraint(equalTo: mainLogoView.leftAnchor).isActive = true
svgImageView.rightAnchor.constraint(equalTo: mainLogoView.rightAnchor).isActive = true
svgImageView.bottomAnchor.constraint(equalTo: mainLogoView.bottomAnchor).isActive = true
Timer.scheduledTimer(timeInterval: TimeInterval(2), target: self, selector: "functionHere", userInfo: nil, repeats: false)
}
func functionHere(){
self.performSegue(withIdentifier: "MainSegue", sender: nil)
}
So basically in my ViewController I added on storyboard a view called mainLogoView and then I'm displaying there the SVG file. Now, since it's a shaped marker, very similar to this: http://image.flaticon.com/icons/svg/116/116395.svg I want to draw it - during the time of 2 seconds I want to draw the shape around the circle.
How can I do it?
=====EDIT:
Following Josh's advice I commented out most of the code in my viewDidLoad and did instead:
let url = Bundle.main.url(forResource: "mainLogo", withExtension: "svg")!
for path in SVGBezierPath.pathsFromSVG(at: url)
{
var layer:CAShapeLayer = CAShapeLayer()
layer.path = path.cgPath
layer.lineWidth = 4
layer.strokeColor = orangeColor.cgColor
layer.fillColor = UIColor.white.cgColor
mainLogoView.addSubview(layer)
}
but now the last line throws an error
Cannot convert value of type 'CAShapeLayer' to expected argument type 'UIView'
Also, after editing my code - could you give me a hint how could I use Josh's method here?
Instead of using the imageView method convert your SVG into a cgPath. Assign this path to a CAShapeLayer and add that shape layer to your view. You can animate the CAShapeLayer's endStroke as follows:
func animateSVG(From startProportion: CGFloat, To endProportion: CGFloat, Duration duration: CFTimeInterval = animationDuration) {
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.duration = duration
animation.fromValue = startProportion
animation.toValue = endProportion
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
svgLayer.strokeEnd = endProportion
svgLayer.strokeStart = startProportion
svgLayer.add(animation, forKey: "animateRing")
}
I did encounter the same problem, this is my solution. svgWidth, svgHeight are width and height of svg tag
let url = Bundle.main.url(forResource: "mainLogo", withExtension: "svg")!
for path in SVGBezierPath.pathsFromSVG(at: url)
{
var layer:CAShapeLayer = CAShapeLayer()
layer.transform = CATransform3DMakeScale(CGFloat(mainLogoView.frame.width/svgWidth), CGFloat(mainLogoView.frame.height/svgHeight), 1)
layer.path = path.cgPath
layer.lineWidth = 4
layer.strokeColor = orangeColor.cgColor
layer.fillColor = UIColor.white.cgColor
layer.transform = CATransform3DMakeScale(2, 2, 1)
mainLogoView.layer.addSublayer(layer)
}
I'm building a simple UIView subclass that plots some data as a line graph. I'd like to animate the data points as they come in, but as a newcomer to CAShapeLayer, I feel there's got to be a better way than what I'm doing. I've read a few articles and put together some code loosely based on this article.
Here's the code. Essentially it redraws the entire path every time new data are available, but only animates the new section (well, approximately).
let animationLayer = CALayer()
func setupDrawingLayer(){
pathLayer.frame = self.animationLayer.bounds
pathLayer.bounds = self.animationLayer.bounds
pathLayer.geometryFlipped = true
pathLayer.strokeColor = UIColor.blackColor().CGColor
pathLayer.fillColor = nil
pathLayer.lineWidth = 3.0
pathLayer.lineJoin = kCALineJoinBevel
self.animationLayer.addSublayer(pathLayer)
}
func nextPoint(){ // This is called by a timer to simulate a new data point
let path = UIBezierPath()
path.moveToPoint(self.dataPoints[0])
for i in 1...pointIndex{
path.addLineToPoint(self.dataPoints[i])
}
pathLayer.path = path.CGPath
pointIndex += 1
let pathAnimation = CABasicAnimation(keyPath:"strokeEnd")
pathAnimation.duration = 0.5
pathAnimation.fromValue = (Double(pointIndex) - 1.0) / Double(pointIndex)
pathAnimation.toValue = 1.0
self.pathLayer.addAnimation(pathAnimation, forKey:"strokeEnd")
}
Also, as my fromValue weighting is just an approximation, it doesn't lead to a smooth animation. Ideally, I would think the approach in nextPoint() should be to animate only the newest section, then "save" it to the layer… but not sure how to approach that. Any pointers would be very welcome.
UPDATE:
This approach still recreates the path with each call of nextPoint() but at least the animation is now accurate:
func nextPoint(){
let oldPath = pathLayer.path
let path = UIBezierPath()
path.moveToPoint(self.dataPoints[0])
for i in 1...pointIndex{
path.addLineToPoint(self.dataPoints[i])
}
pathLayer.path = path.CGPath
pointIndex += 1
let pathAnimation = CABasicAnimation(keyPath:"path")
pathAnimation.duration = 0.5
pathAnimation.fromValue = oldPath
pathAnimation.toValue = path.CGPath
self.pathLayer.addAnimation(pathAnimation, forKey:"path")
}
I'm currently using this code to animate the drawing of a character:
var path = UIBezierPath()
var unichars = [UniChar]("J".utf16)
var glyphs = [CGGlyph](count: unichars.count, repeatedValue: 0)
let gotGlyphs = CTFontGetGlyphsForCharacters(font, &unichars, &glyphs, unichars.count)
if gotGlyphs {
let cgpath = CTFontCreatePathForGlyph(font, glyphs[0], nil)
path = UIBezierPath(CGPath: cgpath!)
}
creates a bezierpath from a character ("J" in this case). Get path to trace out a character in an iOS UIFont
Then I create a CAShapeLayer() and add an animation to it.
let layer = CAShapeLayer()
layer.position = //CGPoint
layer.bounds = //CGRect()
view.layer.addSublayer(layer)
layer.path = path.CGPath
layer.lineWidth = 5.0
layer.strokeColor = UIColor.blackColor().CGColor
layer.fillColor = UIColor.clearColor().CGColor
layer.geometryFlipped = true
layer.strokeStart = 0.0
layer.strokeEnd = 1.0
let anim = CABasicAnimation(keyPath: "strokeEnd")
anim.duration = 8.0
anim.fromValue = 0.0
anim.toValue = 1.0
layer.addAnimation(anim, forKey: nil)
The result is my chosen character being animated correctly. However when I add another path to path with .appendPath() the appended path gets added right onto the original path as you could expect. What should I do if I want to draw a letter where all characters have appropriate spacing etc.?
Thank you for your time.
You can do this using translation on path (using CGAffineTransformMakeTranslation), since path doesn't have a "position", it is just set of points. But to make a translation every iteration of a character, we need a current width of a path - we can use CGPathGetBoundingBox() for this, which gets the box that the path will cover. So having everything we need, we can make an example. Make a path for whole word would be something along the lines:
let word = "Test"
let path = UIBezierPath()
let spacing: CGFloat = 50
var i: CGFloat = 0
for letter in word.characters {
let newPath = getPathForLetter(letter)
let actualPathRect = CGPathGetBoundingBox(path.CGPath)
let transform = CGAffineTransformMakeTranslation((CGRectGetWidth(actualPathRect) + min(i, 1)*spacing), 0)
newPath.applyTransform(transform)
path.appendPath(newPath)
i++
}
Where function getPathForLetter() is just:
func getPathForLetter(letter: Character) -> UIBezierPath {
var path = UIBezierPath()
let font = CTFontCreateWithName("HelveticaNeue", 64, nil)
var unichars = [UniChar]("\(letter)".utf16)
var glyphs = [CGGlyph](count: unichars.count, repeatedValue: 0)
let gotGlyphs = CTFontGetGlyphsForCharacters(font, &unichars, &glyphs, unichars.count)
if gotGlyphs {
let cgpath = CTFontCreatePathForGlyph(font, glyphs[0], nil)
path = UIBezierPath(CGPath: cgpath!)
}
return path
}