Animating thermometer using CoreAnimation

Animating thermometer using CoreAnimation

Build and animate thermometer using Core Animation

What are we going to build?

Let’s get started!

We will first start by creating a file ThermometerView which will be a subclass of UIView . All our code related to building and animating a thermometer will go in this file. This will be a reusable view which can be used in any iOS application.

Let’s create the bottom round shape of the thermometer.

let bodyLayer = CAShapeLayer()

    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    private func setup() {
        setupBodyLayer()
    }

    private func setupBodyLayer() {

        let lineWidth = bounds.width/3
        let width = bounds.width - lineWidth
        let height = bounds.height - lineWidth/2

        layer.addSublayer(bodyLayer)

        let bodyPath = UIBezierPath(ovalIn: CGRect(x: lineWidth/2, y: height - width, width: width, height: width))
        bodyLayer.path = bodyPath.cgPath
        bodyLayer.strokeColor = UIColor.white.cgColor
        bodyLayer.lineWidth = width/3
    }

Output will look like this:

We have create a layer bodyLayer of type CAShapeLayer which will be responsible for building the outer layer of the thermometer. setup() method is responsible for setting all the things related to layers. Inside the setupBodyLayer() method we have created 3 different properties lineWidth , width and height .
We are adding the bodyLayer to our views sublayer using addSublayer method.

UIBezierPath has an initialiser which takes a CGRect and draws the biggest possible oval in that rectangle. We have used it to draw our round shape of thermometer. We have used path , strokeColor and lineWidth property of the layer to style it better.

Our next step is to create the upper body of the thermometer. We can do this by moving the path to the top of the circle and then drawing a vertical line with some width. Let’s modify the path that we have created to make the changes.

private func setupBodyLayer() {

        let lineWidth = bounds.width/3
        let width = bounds.width - lineWidth
        let height = bounds.height - lineWidth/2

        layer.addSublayer(bodyLayer)

        let bodyPath = UIBezierPath(ovalIn: CGRect(x: lineWidth/2, y: height - width, width: width, height: width))
        bodyPath.move(to: CGPoint(x: width/2 + lineWidth/2, y: height - width))
        bodyPath.addLine(to: CGPoint(x: width/2 + lineWidth/2, y: 20))
        bodyLayer.path = bodyPath.cgPath
        bodyLayer.strokeColor = UIColor.white.cgColor
        bodyLayer.lineWidth = width/3
        bodyLayer.lineCap = .round
    }

Output will look like this:

Here we have added 3 lines, first we have moved our path cursor to top mid of circle using moveTo method and then we have created a line using addLine method. In the end we have used lineCap = .rounded which simply gives rounded shape to the top of the line that we have drawn.

Next we will create a levelLayer which we can move using a pan gesture and it will act as a layer which shows temperature in a thermometer.

let levelLayer = CAShapeLayer()

    var level: CGFloat = 0.3 {
        didSet {
            levelLayer.strokeEnd = level
        }
    }

    private func setupLevelLayer() {

        layer.addSublayer(levelLayer)

        let levelPath = UIBezierPath()
        levelPath.move(to: CGPoint(x: bounds.midX, y: bounds.height))
        levelPath.addLine(to: CGPoint(x: bounds.midX, y: 0))
        levelLayer.path = levelPath.cgPath
        levelLayer.strokeColor = UIColor.red.cgColor
        levelLayer.lineWidth = bounds.width
        levelLayer.strokeEnd = level
    }

We have created a new layer levelLayer and simply drawn a big fat line from the bottom of the view to the top of the view. We have used lineWidth property to make the line fatter and equal to the width of the parent view.

Output will look like this:

You will notice that the levelLayer not expanding up to the full height of the parent view, this is because we have used a property called strokeEnd

strokeEnd is a CGFloat and accepts value between 0 and 1. We have used 0.3 as the value so it is filling up 30% of the parent view. Next we will use pan gesture and change the strokeEnd value on the basis of our movement on the screen.

private func setupLevelLayer() {

        layer.addSublayer(levelLayer)

        let levelPath = UIBezierPath()
        levelPath.move(to: CGPoint(x: bounds.midX, y: bounds.height))
        levelPath.addLine(to: CGPoint(x: bounds.midX, y: 0))
        levelLayer.path = levelPath.cgPath
        levelLayer.strokeColor = UIColor.red.cgColor
        levelLayer.lineWidth = bounds.width
        levelLayer.strokeEnd = level

        let pan = UIPanGestureRecognizer(target: self, action: #selector(didPan))
        addGestureRecognizer(pan)
    }

    @objc private func didPan(recognizer: UIPanGestureRecognizer) {

        let translation = recognizer.translation(in: self)
        let percent = translation.y/bounds.height
        level = max(0, min(1, levelLayer.strokeEnd - percent))
        levelLayer.strokeEnd = level
        recognizer.setTranslation(.zero, in: self)
    }

We have added a UIPanGestureRecognizer inside setupLevelLayer method.

didPan method will be called whenever we move our thumb in the view. We are simply checking the translation inside the view using translation method. After that we are calculating the percent movement with respect to the height of the parent view. If you build and run at this time you will see that the red level layer will move up and down as you move your thumb over the screen.

Masking!

Next we will create a mask layer which will be similar to the body layer. This mask will be used with the level layer to create the drawing that we need.

let maskLayer = CAShapeLayer()

    private func setupMaskLayer() {

        maskLayer.path = bodyLayer.path
        maskLayer.strokeColor = bodyLayer.strokeColor
        maskLayer.lineWidth = bodyLayer.lineWidth - 4
        maskLayer.lineCap = bodyLayer.lineCap
        maskLayer.fillColor = nil

        levelLayer.mask = maskLayer
    }

Output will look like this:

We have created a new layer maskLayer which takes all the property of the bodyLayer with one additional property fillColor = nil

We then add it as a mask to the levelLayer which will give us the desired result. Notice we have used lineWidth = bodyLayer.lineWidth — 4 , this is done so that we can see nice little white colour on the outer side of the thermometer. If we make the lineWidth equal we would not be able to see the white border of our bodyLayer

Wrapping Up

This was a simple thermometer and animation that we created using CAShapeLayer . If you have anything to discuss or some suggestions feel free to drop a comment.

You can find the full source code on Github.

Did you find this article valuable?

Support Samarth Paboowal by becoming a sponsor. Any amount is appreciated!