Home > Software design >  Animate a line between two points - swift
Animate a line between two points - swift

Time:02-19

I have a class called CategoryCell which us UICollectionViewCell.

On the CellForItemAt function:

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

drawfunction.draw_Footing(withView: cell.view)

the drawing will be in the cell.view.

draw_Footing functions is a function to draw some lines, and it is located in drawfunction class which it NSObject. in the same class, I have the function call animateShape which can animate a single line between two points (CGpoint).

    func animateShape(view: UIView, p1: CGpoint, p2: CGPoint) {
        
        let shapeLayer = CAShapeLayer()
        shapeLayer.removeFromSuperlayer()
                
            // create whatever path you want
        shapeLayer.fillColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 0).cgColor
        shapeLayer.strokeColor = color.cgColor
        shapeLayer.lineWidth = linewidth//CGFloat(1.5)
        shapeLayer.path = path.cgPath
                
            // animate it
        view.layer.addSublayer(shapeLayer)
        let animation = CABasicAnimation(keyPath: "strokeEnd")
        animation.fromValue = 0
        animation.duration = duration//0.5
        shapeLayer.add(animation, forKey: "MyAnimation")

}

I have 4 points G1, G2, G3, G4. I need to animate a line between these 4 points. So, if I do:

animateShape(view, p1: G1, p2: G2)
animateShape(view, p1: G2, p2: G3)
animateShape(view, p1: G3, p2: G4)

All the line will be animated in the same time. I need to animate first the line between G1 and G2, and after completion, need to animate the line between G2 and G3 and not in the same time. I tried to include dispatchQueue, but I am not sure and I don't know how.

Any advise?

CodePudding user response:

The things is, I do not see how the path was actually created using your points p1 and p2

Anyways, I am assuming your end goal is to do a drawing line path animation in a UICollectionViewCell and that is what I tried to achieve based on the given the description in your question.

First the drawing class:

class DrawFunction: NSObject
{
    weak var shapeLayer: CAShapeLayer?
    
    // Change as you wish
    let duration = 2.0
    
    // Responsible for drawing the lines from any number of points
    func drawFooting(points: [CGPoint])
    {
        guard !points.isEmpty else { return }
        
        // Remove old drawings
        shapeLayer?.removeFromSuperlayer()
        
        let path = UIBezierPath()
        
        // This is actual drawing path using the points
        // I don't see this in your code
        for (index, point) in points.enumerated()
        {
            // first pair of points
            if index == 0
            {
                // Move to the starting point
                path.move(to: point)
                continue
            }
            
            // Draw a line from the previous point to the current
            path.addLine(to: point)
        }
        
        // Create a shape layer to visualize the path
        let shapeLayer = CAShapeLayer()
        shapeLayer.fillColor = UIColor.clear.cgColor
        shapeLayer.strokeColor = randomColor().cgColor
        shapeLayer.lineWidth = 5
        shapeLayer.path = path.cgPath
        
        self.shapeLayer = shapeLayer
    }
    
    // Animate function to be called after shape has been drawn
    // by specifying the view to show this animation in
    func animateShape(in view: UIView)
    {
        if let shapeLayer = shapeLayer
        {
            view.layer.addSublayer(shapeLayer)
            let animation = CABasicAnimation(keyPath: "strokeEnd")
            animation.fromValue = 0
            animation.duration = duration
            shapeLayer.add(animation, forKey: "MyAnimation")
        }
    }
    
    // You can ignore this function, just for convenience
    private func randomColor() -> UIColor
    {
        let red = CGFloat(arc4random_uniform(256)) / 255.0
        let blue = CGFloat(arc4random_uniform(256)) / 255.0
        let green = CGFloat(arc4random_uniform(256)) / 255.0
        
        return UIColor(red: red, green: green, blue: blue, alpha: 1.0)
    }
}

Then basic custom cell set up, nothing fancy, just added for completeness

// Basic Cell, nothing unique here
class CategoryCell: UICollectionViewCell
{
    static let identifier = "cell"
    
    override init(frame: CGRect)
    {
        super.init(frame: frame)
        configure()
    }
    
    required init?(coder: NSCoder)
    {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func configure()
    {
        contentView.backgroundColor = .lightGray
        contentView.layer.cornerRadius = 5.0
        contentView.clipsToBounds = true
    }
}

The view controller set up where the most interesting parts are in the willDisplay cell function

class LineAnimateVC: UICollectionViewController
{
    // Random points to draw lines
    let points = [[CGPoint(x: 0.0, y: 10),
                   CGPoint(x: UIScreen.main.bounds.midX, y: 10),
                   CGPoint(x: UIScreen.main.bounds.midX, y: 50)],
                  
                  [CGPoint(x: 0.0, y: 10),
                   CGPoint(x: UIScreen.main.bounds.midX, y: 10),
                   CGPoint(x: UIScreen.main.bounds.midX, y: 50),
                   CGPoint(x: UIScreen.main.bounds.maxX, y: 50)],
                  
                  [CGPoint(x: 0.0, y: 10),
                   CGPoint(x: UIScreen.main.bounds.midX, y: 10),
                   CGPoint(x: UIScreen.main.bounds.midX, y: 50),
                   CGPoint(x: UIScreen.main.bounds.midX   40, y: 50),
                   CGPoint(x: UIScreen.main.bounds.midX   40, y: UIScreen.main.bounds.maxY),
                   CGPoint(x: UIScreen.main.bounds.maxX, y: UIScreen.main.bounds.maxY)],
                  
                  [CGPoint(x: 0.0, y: 10),
                   CGPoint(x: UIScreen.main.bounds.midX, y: 10),
                   CGPoint(x: UIScreen.main.bounds.midX, y: 50)],
                  
                  [CGPoint(x: 0.0, y: 10),
                   CGPoint(x: UIScreen.main.bounds.midX, y: 10),
                   CGPoint(x: UIScreen.main.bounds.midX, y: 50),
                   CGPoint(x: UIScreen.main.bounds.midX   40, y: 50),
                   CGPoint(x: UIScreen.main.bounds.midX   40, y: UIScreen.main.bounds.maxY),
                   CGPoint(x: UIScreen.main.bounds.maxX, y: UIScreen.main.bounds.maxY)]
    ]
    
    override func viewDidLoad()
    {
        super.viewDidLoad()
        
        view.backgroundColor = .white
        title = "Line animate"
        
        collectionView.register(CategoryCell.self,
                                forCellWithReuseIdentifier: CategoryCell.identifier)
        
        collectionView.backgroundColor = .white
    }
    
    // Number of cells equals to points we have
    override func collectionView(_ collectionView: UICollectionView,
                                 numberOfItemsInSection section: Int) -> Int
    {
        return points.count
    }
    
    override func collectionView(_ collectionView: UICollectionView,
                                 cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
    {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CategoryCell.identifier,
                                                      for: indexPath) as! CategoryCell
        
        return cell
    }
    
    // Add animation when cell is about to be displayed
    override func collectionView(_ collectionView: UICollectionView,
                                 willDisplay cell: UICollectionViewCell,
                                 forItemAt indexPath: IndexPath)
    {
        let cell = cell as! CategoryCell
        
        // Draw the path and perform the animation
        let drawingFunction = DrawFunction()
        drawingFunction.drawFooting(points: points[indexPath.row])
        drawingFunction.animateShape(in: cell.contentView)
    }
}

Just for completeness, my flow layout set up

// Basic set up stuff
extension LineAnimateVC: UICollectionViewDelegateFlowLayout
{
    func collectionView(_ collectionView: UICollectionView,
                        layout collectionViewLayout: UICollectionViewLayout,
                        sizeForItemAt indexPath: IndexPath) -> CGSize
    {
        return CGSize(width: collectionView.frame.width, height: 300)
    }
    
    func collectionView(_ collectionView: UICollectionView,
                        layout collectionViewLayout: UICollectionViewLayout,
                        minimumLineSpacingForSectionAt section: Int) -> CGFloat
    {
        return 20
    }
}

This gives me an animated path in the collectionview cell

Draw Path Line Animation UICollectionView UICollectionViewCell CAShapeLayer UIBezierPath CABasicAnimation Animate line between two points CGPoint

Hope this gives you some ideas to achieve your task

Update

Based on OP, Xin Lok's comment:

However still did not get what I want, lets say I have path1 = [p1,p2,p3,p4,p5] and path2 = [m1,m2,m3], if I run drawFooting(points: path1) and drawFooting(path2), both of the 2 paths will be animated in the same time , and this what I don't want, I need to complete animation for Path1, and then after finish to proceed with animation of Path2. I tried to insert sleep, but it did not work

Based on that comment, One way I can think of achieving that is to I think the key is to reuse and persist with the shape layer and the path.

Here are some updates I made based on that conclusion

First I just made a simple struct so we can create lines easily

struct Line
{
    var points: [CGPoint] = []
    
    init(_ points: [CGPoint])
    {
        self.points = points
    }
}

Then I create some random lines and grouped them in an array

// Random lines
// Random lines
let line1 = Line([CGPoint(x: 0, y: 10),
                  CGPoint(x: UIScreen.main.bounds.midX, y: 10)])

let line2 = Line([CGPoint(x: 0, y: 70),
                  CGPoint(x: UIScreen.main.bounds.midX, y: 70),
                  CGPoint(x: UIScreen.main.bounds.midX, y: 100)])

let line3 = Line([CGPoint(x: 0, y: 150),
                 CGPoint(x: UIScreen.main.bounds.midX, y: 110),
                 CGPoint(x: UIScreen.main.bounds.maxX, y: 190)])

let line4 = Line([CGPoint(x: 0, y: 210),
                  CGPoint(x: UIScreen.main.bounds.maxX / 4, y: 235),
                  CGPoint(x: UIScreen.main.bounds.maxX * 0.75, y: 220),
                  CGPoint(x: UIScreen.main.bounds.maxX,
                          y: UIScreen.main.bounds.maxY)])

var lineGroups: [[Line]] = []

private func setLines()
{
    // First cell, it should draw lines in the order 3 -> 1 -> 2
    // Second cell, in the order 4 -> 3 -> 2 -> 1
    lineGroups = [[line3, line1, line2],
                  [line4, line3, line2, line1]]
}

Importantly note the line order in each array, because this is the order they will be drawn

In the drawing class, I made some changes to persist the CAShapeLayer and path

A special mention to jrturton in the comments for suggesting CGMutablePath and simplifying the path creation.

class DrawFunction: NSObject
{
    weak var shapeLayer: CAShapeLayer?
    var path: CGMutablePath?
    
    // Change as you wish
    let duration = 5.0
    
    // Responsible for drawing the lines from any number of points
    func drawFooting(line: Line)
    {
        var shapeLayer = CAShapeLayer()
        
        if self.shapeLayer != nil
        {
            shapeLayer = self.shapeLayer!
        }
        
        if path == nil
        {
            path = CGMutablePath()
        }
        
        // Thank you @jrturton for this
        path?.addLines(between: line.points)
        
        shapeLayer.fillColor = UIColor.clear.cgColor
        shapeLayer.strokeColor = randomColor().cgColor
        shapeLayer.lineWidth = 5
        shapeLayer.path = path
        
        self.shapeLayer = shapeLayer
    }
    
    // Animate function to be called after shape has been drawn
    // by specifying the view to show this animation in
    func animateShape(in view: UIView)
    {
        if let shapeLayer = shapeLayer
        {
            view.layer.addSublayer(shapeLayer)
            let animation = CABasicAnimation(keyPath: "strokeEnd")
            animation.fromValue = 0
            animation.duration = duration
            shapeLayer.add(animation, forKey: "MyAnimation")
        }
    }
    
    // You can ignore this function, just for convenience
    private func randomColor() -> UIColor
    {
        let red = CGFloat(arc4random_uniform(256)) / 255.0
        let blue = CGFloat(arc4random_uniform(256)) / 255.0
        let green = CGFloat(arc4random_uniform(256)) / 255.0
        
        return UIColor(red: red, green: green, blue: blue, alpha: 1.0)
    }
}

Then some minor changes in the collectionview cell configuration

// Number of cells equals to lines we have
override func collectionView(_ collectionView: UICollectionView,
                             numberOfItemsInSection section: Int) -> Int
{
    return lineGroups.count
}

override func collectionView(_ collectionView: UICollectionView,
                             cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
    let cell
        = collectionView.dequeueReusableCell(withReuseIdentifier: CategoryCell.identifier,
                                                  for: indexPath) as! CategoryCell
    
    return cell
}

// Add animation when cell is about to be displayed
override func collectionView(_ collectionView: UICollectionView,
                             willDisplay cell: UICollectionViewCell,
                             forItemAt indexPath: IndexPath)
{
    let cell = cell as! CategoryCell
    
    let lines = lineGroups[indexPath.item]
    
    // Draw the path and perform the animation
    let drawingFunction = DrawFunction()
    
    for line in lines
    {
        drawingFunction.drawFooting(line: line)
    }
    
    drawingFunction.animateShape(in: cell.contentView)
}

Now again, for convenience, remember the order in which they should be drawn:

First cell, it should draw lines in the order 3 -> 1 -> 2
Second cell, in the order 4 -> 3 -> 2 -> 1

The end result:

Animate a line between two CGPoints swift iOS CAAnimation chaining CAShapeLayer CGMutablePath

  • Related