Progress Indicator v1

Idea behind this post is described in UIBezierPath, CAAnimation and Swift article.

Requirements

  1. Indicator should be a subclass of UIView
  2. Shapes in the indicator have to be like drops
  3. Movement direction has to be adjustable
  4. Timing function has to be adjustable

Shapes

In order to create a drop shape, CAShapeLayer is used in a little unusual way.
From the start size of the stroke of the CAShapeLayer to the smallest size there is N-amount of CAShapeLayer instances drawn along a circular UIBezierPath path.

It looks like this when random colours are used for the stroke:

Screen Shot 2018-01-03 at 16.58.41
Visual representation of inner circles within drops

And when filled with the same solid color, it looks like this:

Screen Shot 2018-01-03 at 17.14.08
Solid color for the stroke

Cut to the Chase

Firstly, shapes need to be placed along UIBezierPath. In order to do this, we divide UIBezierPath’s strokeStart and strokeEnd into chunks according to shape length and shape count:

func setupStartPoints() {
    for i in 1...self.numberOfShapes {
        let fraction = (100 / self.numberOfShapes)
        let pointStart = Float(fraction * i) / 100
        self.startPoints.append(pointStart - self.shapeLength * 0.5)
    }
}

It should be noted that when setting a specific self.numberOfShapes, one needs to keep in mind that positioning depends on the count of shapes (i.e. self.numberOfShapes has to be a factor (natural divisor) of 100).

func generateShape(previousShape: CAShapeLayer?, originStart: Float, originEnd: Float) -> CAShapeLayer {
    let shape = CAShapeLayer()
    shape.path = self.shapePath.cgPath

    self.calculateStroke(shape: shape, previousShape: previousShape, originStart: originStart, originEnd: originEnd)

    shape.lineCap = kCALineCapRound
    shape.fillColor = UIColor.clear.cgColor
    shape.strokeColor = self.strokeColor.cgColor
    return shape
}

It is important to set kCALineCapRound in order to achieve smooth curves for the shapes.

Actual calculation is performed in the following method:

    func calculateStroke(shape: CAShapeLayer, previousShape: CAShapeLayer?, originStart: Float, originEnd: Float) {
        let shapeWidthVariance : CGFloat = round((self.shapeOriginWidth * CGFloat(self.shapeLength)) * 10) / 10
        var lineWidth: CGFloat = 0
        var strokeStart: CGFloat = 0
        var strokeEnd: CGFloat = 0
        if let previousShape = previousShape {
            strokeStart = previousShape.strokeStart - CGFloat(self.shapeElementLength)
            strokeEnd = previousShape.strokeEnd - CGFloat(self.shapeElementLength)
            lineWidth = previousShape.lineWidth - shapeWidthVariance
        } else {
            strokeStart = CGFloat(originStart)
            strokeEnd = CGFloat(originEnd)
            lineWidth = self.shapeOriginWidth
        }

        shape.lineWidth = lineWidth
        shape.strokeStart = strokeStart.normalizedShapePoint
        shape.strokeEnd = strokeEnd.normalizedShapePoint
    }

So whether we are drawing the first shape, or any other, strokeStart and strokeEnd are based on the self.shapeElementLength, which is 0.01. So in every 0.1 there are 10 shapes with different width values.

As a result, these are progress indicators possible with the provided engine:

Jan-06-2018 00-32-36
Timing is set to EaseInEaseOut
linear
Timing is set to linear
easeout
Timing is set to easeOut

Outcome

As a result, we have more-or-less flexible engine with a small customisation options.

Sources can be found here on GitHub.
See you in next animation magic article!

Advertisements

One thought on “Progress Indicator v1

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s