How to create the popular Matrix effect (digital rain) in Flutter
Tags: flutter, dart, tutorial, appdevelopment
Published: 2020-09-17 11:14:16

I recently found out it's been over twenty years since the release of The Matrix. The digital rain of letters is probably known to almost everyone even those who haven't watched the movie. Let's give it a try and mimic this effect using Flutter.

![Our goal](https://dev-to-uploads.s3.amazonaws.com/i/5ydzti3hmr19f33qxvvh.gif)

Having seen a few videos of the effect, I can summarize it as follows:

* It consists of numerous "character rains" that all start at the top of the screen with a random horizontal position
* Every now and then a new rain spawns
* The latest character is always white, the colors before are green and there is a gradient so that the first characters fade out
* Background is black
* The characters are random characters including letters, numbers and other characters
* The vertical rains differ in the length (height) of visible characters

# Implementation

Let's start with the implementation of a single vertical rain. Once we have that, we let the whole effect be a composition of multiple rains.

```dart
import 'package:flutter/material.dart';

class VerticalTextLine extends StatefulWidget {
  VerticalTextLine({
    this.speed = 12.0,
    this.maxLength = 10,
    Key key
  }) : super(key: key);

  final double speed;
  final int maxLength;

  @override
  _VerticalTextLineState createState() => _VerticalTextLineState();
}

class _VerticalTextLineState extends State<VerticalTextLine> {
  List<String> _characters = ['T', 'E', 'S', 'T'];

  @override
  Widget build(BuildContext context) {

    return Column(
      mainAxisSize: MainAxisSize.min,
      mainAxisAlignment: MainAxisAlignment.start,
      children: _getCharacters(),
    );
  }

  List<Widget> _getCharacters() {
    List<Widget> textWidgets = [];

    for (var character in _characters) {
      textWidgets.add(
          Text(character, style: TextStyle(color: Colors.white),)
      );
    }

    return textWidgets;
  }
}
```

This is a very simple start. Just a widget that displays a static vertical line of text at the top of its container.

The constructor arguments are speed and `maxLength`, which will later determine, how quickly the rain runs down the screen and what the maximum length of the visible text is. This way we have a variance in position, speed and length.

![Iteration 1](https://dev-to-uploads.s3.amazonaws.com/i/4obim29m7woq373tnv0h.png)

Now, instead of displaying a plain white text, we want the last character to be white and the text before that to have the above described gradient. 

```dart
@override
Widget build(BuildContext context) {
  List<Color> colors = [Colors.transparent, Colors.green, Colors.green, Colors.white];

  double greenStart;
  double whiteStart = (_characters.length - 1) / (_characters.length);

  if (((_characters.length - _maxLength) / _characters.length) < 0.3) {
    greenStart = 0.3;
  }
  else {
    greenStart = (_characters.length - _maxLength) / _characters.length;
  }

  List<double> stops = [0, greenStart, whiteStart, whiteStart];

  return _getShaderMask(stops, colors);
}

ShaderMask _getShaderMask(List<double> stops, List<Color> colors) {
  return ShaderMask(
    shaderCallback: (Rect bounds) {
      return LinearGradient(
        begin: Alignment.topCenter,
        end: Alignment.bottomCenter,
        stops: stops,
        colors: colors,
      ).createShader(bounds);
    },
    blendMode: BlendMode.srcIn,
    child: Column(
      mainAxisSize: MainAxisSize.min,
      mainAxisAlignment: MainAxisAlignment.start,
      children: _getCharacters(),
    )
  );
}
```

For the purpose of displaying the text as a gradient, we use something called a `ShaderMask`. This can be used to fade out the edge of a child. The `shaderCallback` parameter expects a function that receives a bounding rectangle and returns a `Shader`. Good thing the `Gradient` class provides a `createShader()` method. This enables us to control the shading behavior by using a Gradient.

We use `blendMode.srcIn`. That means: take the source (the child parameter) and use the destination (`shaderCallback` return value) as a mask. This leads to the text staying the way it is with the gradient only defining its color.

The stops argument of a Gradient is a list of doubles that represent the relative position across the gradient, at which a certain color (which is defined at the same position in the colors argument) should begin. So if the stops list contained 0 and 0.5 and the colors list contained `Colors.white` and `Colors.red`, the gradient would start with white and interpolate to red until half of its height and would be red from there to the bottom.

We want it to start with transparent color and then interpolate to green (at the character that is located `maxLength` characters before the end of the column). This color should last until the penultimate visible character where it changes to white.

If the whole character list is smaller than `maxLength`, we just take 30 % as a static point for the green color to start.

![Iteration 2](https://dev-to-uploads.s3.amazonaws.com/i/5v7b8l0ahhci72g8l3o2.png)

Next step is to turn this text with a static length into a growing column.

```dart
class _VerticalTextLineState extends State<VerticalTextLine> {
  int _maxLength;
  Duration _stepInterval;
  List<String> _characters = [];
  Timer timer;

  @override
  void initState() {
    _maxLength = widget.maxLength;
    _stepInterval = Duration(milliseconds: (1000 ~/ widget.speed));
    _startTimer();
    super.initState();
  }
  void _startTimer() {
    timer = Timer.periodic(_stepInterval, (timer) {
      final _random = new Random();
      final list = ['A', 'B', 'C'];
      String element = list[_random.nextInt(list.length)];

      setState(() {
        _characters.add(element);
      });
    });
  }

...
```

`speed` is interpreted in the way that 1.0 means 1 character per second. So it's the frequency of characters added per second.

![Iteration 3](https://dev-to-uploads.s3.amazonaws.com/i/u6odf1xrsljgvxl6yuqv.png)

Instead of having a static list of allowed characters, we can also use a random int to create a String from a UTF character code:

```dart
String element = String.fromCharCode(
  _random.nextInt(512)
);
```

Now we need to create a widget that composes multiple of our vertical text lines:

```dart
import 'dart:async';
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter_matrix_effect/vertical_text_line_current.dart';

class MatrixEffect extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _MatrixEffectState();
  }
}

class _MatrixEffectState extends State<MatrixEffect> {
  List<Widget> _verticalLines = [];
  Timer timer;

  @override
  void initState() {
    _startTimer();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: _verticalLines
    );
  }
  
  void _startTimer() {
    timer = Timer.periodic(Duration(milliseconds: 300), (timer) {
      setState(() {
        _verticalLines.add(
            _getVerticalTextLine(context)
        );
      });
    });
  }

  @override
  void dispose() {
    timer.cancel();
    super.dispose();
  }

  Widget _getVerticalTextLine(BuildContext context) {
    Key key = GlobalKey();
    return Positioned(
      key: key,
      left: Random().nextDouble() * MediaQuery.of(context).size.width,
      child: VerticalTextLine(
        speed: 1 + Random().nextDouble() * 9,
        maxLength: Random().nextInt(10) + 5
      ),
    );
  }
}

```

Every 300 milliseconds, we add a new `VerticalTextLine` to the list. 

Since we're dealing with a list of widgets here, it's mandatory to use the key parameter of the widgets we add. Otherwise, if we manipulated the list by removing one element, a reference would be missing which would result in no widget being properly removed from the list (and from the widget tree).

We place the rains by using a `Positioned` widget with a random `left` parameter. Also, we let the speed vary from 1 to 10 and the maximum length from 5 to 15.

![Iteration 4](https://dev-to-uploads.s3.amazonaws.com/i/fi57fjihkf0igk4zgvr6.png)

We still have one problem: Every vertical text line we spawn, stays in the widget tree forever. It might fade out over time because of our gradient, but it will always occupy memory. This leads to the app becoming very laggy after a short period of time. We need the rain to decide when to disappear and tell its parent.

```dart
  VerticalTextLine({
    @required this.onFinished,
    this.speed = 12.0,
    this.maxLength = 10,
    Key key
  }) : super(key: key);

  final double speed;
  final int maxLength;
  final VoidCallback onFinished;

...

  void _startTimer() {
    timer = Timer.periodic(_stepInterval, (timer) {
      final _random = new Random();
      String element = String.fromCharCode(
        _random.nextInt(512)
      );

      final box = context.findRenderObject() as RenderBox;

      if (box.size.height > MediaQuery.of(context).size.height * 2) {
        widget.onFinished();
        return;
      }

      setState(() {
        _characters.add(element);
      });
    });
  }
```

We achieve the above mentioned exit condition by using a callback that is being called from the vertical text line. The condition to disappear is when the size of the rendered widget (the column) is twice as high as the current screen height. This way, the moment of disappearing should be smooth enough not to be noticed by the viewer. The timer does not need to be cancelled manually, because we do this `onDispose` anyways.

The `onFinished` callback we provide to the `VerticalTextLine` looks as follows:

```dart
onFinished: () {
  setState(() {
    _verticalLines.removeWhere((element) {
      return element.key == key;
    });
  });
},
```

# Conclusion

Using a `ShaderMask`, a `LinearGradient`, a `Timer` and a `GlobalKey`, we were able to quickly mimic the well-known visual effect from The Matrix.

[GET FULL CODE](https://github.com/flutter-clutter/flutter-the-matrix-effect)