Birdman and Text Animations

I went to see Birdman the other day after it was recommended by a friend. Really excellent film. It won't be in the cinema for long now so go and see it while you can.

This is not a post about Birdman though. During the opening and closing credits the film used a distinctive animation to fade in the text by fading in all of a particular letter at once and then the next and so on until it was all visible.

You can see a short example of the sort of thing I'm referring to in the trailer (although it is slightly different in the actual film).

So when I got home I decided to see if I could recreate it fairly easily as an iOS app/UI element.

After a couple of hours I had a nice little animation that did what I wanted and looked a lot like the effect in the film that I was going for. I also made it customisable (colour, timing, font) and all "embedded" inside a UIView subclass so it could be reused in any project.

Here's a quick video of the animation effect.

My goal was to create the effect in the film and I took the easiest approach to creating that effect. I initially thought about using Text Kit or Core Graphics to render all the letter bit by bit but in the end I decided to use two labels and NSAttributedStrings. It made the project really easy to get working and it works really quickly.

I decided to use Swift too for no other reason that I thought I'd give it a try. I know for a fact that there are other ways that I could have exploited the new features available in Swift but for now it's written like an Objective-C app but in Swift. If you can improve it then I'd love to see what you are able to do. The GitHub is available to fork (link at bottom).

This is the "skeleton" of the class I created called OJFTextRevealView. As you can see it uses two private UILabels and a few properties for customising it. I was also experimenting with @IBDesignable and @IBInspectable for the first time here. It's quite useful but not as powerful/complete as I had assumed. For instance you can't use UIFont as an @IBInspectable property. Also, although I set up default values of the color, fadeDuration, and fadeDelay, Interface Builder didn't seem to recognise these defaults and created the fields as blank.

@IBDesignable class OJFTextRevealView: UIView {

    private let revealLabel1 = UILabel()
    private let revealLabel2 = UILabel()
    
    private var currentLabel:UILabel!
    
    var font: UIFont = .systemFontOfSize(21)
    @IBInspectable var color: UIColor = .blackColor()
    @IBInspectable var fadeDuration: Double = 0.4
    @IBInspectable var fadeDelay: Double = 0.4

    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        
        setupView()
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        setupView()
    }
    
    private func setupView() -> Void {
        self.backgroundColor = .clearColor()
        
        revealLabel1.textAlignment = .Center
        revealLabel2.textAlignment = .Center
        
        revealLabel1.numberOfLines = 0
        revealLabel2.numberOfLines = 0
        
        addSubview(revealLabel1)
        addSubview(revealLabel2)
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        
        revealLabel1.frame = bounds
        revealLabel2.frame = bounds
    }

    override func prepareForInterfaceBuilder() {
        revealLabel1.text = "Hello, world!"
        revealLabel1.textColor = color
        revealLabel1.font = .systemFontOfSize(21)
        revealLabel1.textAlignment = .Center
        revealLabel1.numberOfLines = 0
    }
}

The main (and only public) function in the class is the displayText function. This takes in the new string and starts the processing on it.

func displayText(newText: String) -> Void {
    revealLabel1.attributedText = NSMutableAttributedString(string: newText, attributes: [NSForegroundColorAttributeName: UIColor.clearColor(), NSFontAttributeName: font])
    currentLabel = revealLabel1
        
    let letterRanges = self.letterRangeDictionaryFromText(self.revealLabel1.attributedText.string as NSString)
        
    let rangesArray: [[NSRange]] = self.shuffle([[NSRange]](letterRanges.values))
        
    let attributedTextArray = self.attributedTextArrayFromRanges(rangesArray, baseAttributedString: self.revealLabel1.attributedText)
        
    self.animateTextAtIndex(0, fromArray: attributedTextArray, duration: self.fadeDuration, delay: self.fadeDelay)
}

You should be able to see how I created the effect from here. Initially I create an NSAttributedString with UIColor.clearColor() as the foreground color.

The next thing I do is get a dictionary with Strings as the keys and Arrays of NSRange as the value... [String : [NSRange]] from the letterRangeDictionaryFromText function.

private func letterRangeDictionaryFromText(text: NSString) -> [String : [NSRange]] {
    var letterRanges: [String : [NSRange]] = [:]
    let whiteSpaces: NSCharacterSet = NSCharacterSet.whitespaceAndNewlineCharacterSet()
    
    text.enumerateSubstringsInRange(NSMakeRange(0, text.length),
        options: NSStringEnumerationOptions.ByComposedCharacterSequences) {
            (substring, substringRange, enclosingRange, stop) -> () in
            
            if let range = substring.rangeOfCharacterFromSet(whiteSpaces, options: nil, range: nil) {
                return
            }
            
            var rangeArray: [NSRange]
            
            if (letterRanges[substring] == nil) {
                rangeArray = []
            } else {
                rangeArray = letterRanges[substring]!
            }
            
            rangeArray.append(substringRange)
            
            letterRanges[substring] = rangeArray
    }
    
    return letterRanges
}

This uses the enumerateSubstringsInRange to enumerate all the letters in the string that is passed in. It then creates a dictionary building up arrays of ranges for each letter (or symbol) in the string.

Next I use a Fisher Yates shuffle function that I found on Stack Overflow to randomise the array of arrays that I get from the dictionary. I found that without doing this the order was always the same H, V, C, etc...

I then take this array and use it to create an array of NSAttributedStrings. Each string in the array will have an additional letter "coloured in" with attributes. I was cautious about using this as I thought it would created too many fragmented attributes in the string. However, after a bit of investigation I found that when you add attributes it concatenates like, adjacent attributes so I wasn't too worried.

private func attributedTextArrayFromRanges(rangesArray: [[NSRange]], baseAttributedString: NSAttributedString) -> [NSAttributedString] {
    var attributedTextArray: [NSAttributedString] = []
    var attributedText = NSMutableAttributedString(attributedString: baseAttributedString)
    
    for ranges in rangesArray {
        attributedText = NSMutableAttributedString(attributedString: attributedText)
        
        for range in ranges {
            attributedText.addAttribute(NSForegroundColorAttributeName, value: self.color, range: range)
        }
        
        attributedTextArray.append(attributedText)
    }
    
    return attributedTextArray
}

Once I've got this array of NSAttributedStrings I just need to animate them into the view one by one. To do this I use the two labels and alternate between them setting their alpha to 0.0 and then animating it to 1.0.

private func animateTextAtIndex(index: Int, fromArray array: [NSAttributedString], duration: Double, delay: Double) -> Void {
    if index >= array.count {
        return
    }
    
    var nextLabel: UILabel!
    
    if (currentLabel == revealLabel1) {
        nextLabel = revealLabel2
    } else {
        nextLabel = revealLabel1
    }
    
    nextLabel.attributedText = array[index]
    
    UIView.animateWithDuration(
        duration,
        delay: delay,
        options: UIViewAnimationOptions.CurveLinear,
        animations: {
            () -> Void in
            nextLabel.alpha = 1.0
        })
        {
            (finished) -> Void in
            self.currentLabel.alpha = 0.0
            self.currentLabel = nextLabel
            self.animateTextAtIndex(index + 1, fromArray: array, duration: duration * 0.9, delay: delay * 0.9)
    }
}

This run recursively until it reaches the end of the array. I pass the duration into the function that way I can make the timing get shorter and the animation speed up as it goes along.

That's pretty much it. The rest of the project is just creating this view (in a storyboard here but it also works with code) and then I grabbed a load of quotes from Einstein, put them into an array and randomly change the text when the button is tapped.

There is one major flaw which I need to fix (if I ever want to use it properly) which is that if you run the displayText function while another bit of text is animating it messes up the animation because they crash into each other. Thinking about it I think the easiest way to do this is probably to use a NSOperationQueue on the main thread and queue the animations instead of running them recursively. That way, when the displayText function is run the first thing I can do is cancel the current queue and create a new one.

You can get hold of the project on GitHub - TextReveal. Have a mess around. If you find a better way of animating the text or processing the string in the first place I'd love to see it. I'll update the animation at some point to use the queue and push it to GitHub if it works.

One Comment

  1. jaime trope

    Whatsup man, just came across your youtube video and its really cool
    I was wondering if this birdman app is a real app I could download for a university project I'm doing?

    Regards, Jaime

Leave a Reply

Your email address will not be published. Required fields are marked *