UI Challenge 4 – Electrons Counter App

Hello everyone. I have been busy and that’s why I couldn’t publish any posts.

There is an ongoing challenge in the community. It is #FlutterCounterChallenge2020.
The idea is simple: Turning the default counter app of Flutter into something fancy and exciting. I recommend you to follow the hashtag to see the amazing and stunning implementations from the community.

I decided to join the community to create something with that default boring counter app.

I came up with an idea about making something like an Atom, that Electrons revolves around. Then we can increase electrons by pressing the plus button.

BTW this is the final result. I’m going to explain how we can implement it:

Try the final result in the DartPad

As you remember, we have to split our challenge into small problems. Then we can implement them one by one.

1. Orbit

The first step is to create the orbit line. It merely is a canvas.drawOval().
Let’s try to draw it on the canvas:

class _OrbitPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final center = Offset(size.width / 2, size.height / 2);
    final rect = Rect.fromCenter(center: center, width: size.width, height: size.height);
    canvas.drawOval(
      rect,
      Paint()
        ..color = Colors.white
        ..style = PaintingStyle.stroke
        ..strokeWidth = 1,
    );
  }
  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
Try it in DartPad

2. Electron

Now we are going to place an Electron around the Orbit. 
We can calculate the (x, y) position based on a degree using cos() and sin() functions.

class _OrbitPainter extends CustomPainter {
  final double electronDegree;
  _OrbitPainter(this.electronDegree);
  @override
  void paint(Canvas canvas, Size size) {
    final center = Offset(size.width / 2, size.height / 2);
    final rect = Rect.fromCenter(center: center, width: size.width, height: size.height);
    canvas.drawOval(
      rect,
      Paint()
        ..color = Colors.white
        ..style = PaintingStyle.stroke
        ..strokeWidth = 1,
    );
    final degree = degreesToRads(electronDegree);
    final electronPos = center +
        Offset(
          math.cos(degree) * (size.width / 2),
          math.sin(degree) * (size.height / 2),
        );
    canvas.drawCircle(electronPos, 8, Paint()..color = Colors.yellowAccent);
  }
  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
Try it in DartPad

3. Move Electron around the Orbit

Now we can increase the degree repeatedly to revolve the Electron around the Orbit. I used Timer.periodic() to call a function to calculate the new position of the Electron every 16ms (To have 60 frames/second). After that, we call the setState() method to reflect the new position.
(There might be other solutions to implement the animation)

class _HomePageState extends State<HomePage> {
  double degree = 0.0;
  Timer timer;
  @override
  void initState() {
    super.initState();
    timer = new Timer.periodic(
      Duration(milliseconds: 16),
      (t) {
        setState(() {
          degree += 1;
        });
      },
    );
  }
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Color(0xFF191636),
      body: Center(
        child: CustomPaint(
          painter: _OrbitPainter(degree),
          size: Size(300, 100),
        ),
      ),
    );
  }
  @override
  void dispose() {
    timer.cancel();
    super.dispose();
  }
}
Try it in DartPad

4. Assign speed to an Electron

Now it’s time to make a model for our Electron that holds the size, color, speed, and currentPosition (in degrees) of the Electron.
Then every frame, we can increase the currentPosition by the speed property.
In this way, we can assign different speeds for our Electrons.

We should iterate over all electrons every frame, increase their speed, and iterate over all of them for drawing them in our painter class.

class Electron {
  double speed;
  double size;
  Color color;
  double currentPositionDegree;
  Electron({
    @required this.speed,
    @required this.size,
    @required this.color,
    @required this.currentPositionDegree,
  });
}
class _HomePageState extends State<HomePage> {
  List<Electron> electrons = [
    Electron(speed: 1, size: 10, color: Colors.yellowAccent, currentPositionDegree: 0),
    Electron(speed: 3, size: 6, color: Colors.purpleAccent, currentPositionDegree: 180),
  ];
  Timer timer;
  @override
  void initState() {
    super.initState();
    timer = new Timer.periodic(
      Duration(milliseconds: 16),
      (t) {
        setState(() {
          electrons.forEach((electron) {
            electron.currentPositionDegree += electron.speed;
          });
        });
      },
    );
  }
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Color(0xFF191636),
      body: Center(
        child: CustomPaint(
          painter: _OrbitPainter(electrons),
          size: Size(300, 100),
        ),
      ),
    );
  }
}
class _OrbitPainter extends CustomPainter {
  final List<Electron> electrons;
  _OrbitPainter(this.electrons);
  @override
  void paint(Canvas canvas, Size size) {
    final center = Offset(size.width / 2, size.height / 2);
    final rect = Rect.fromCenter(center: center, width: size.width, height: size.height);
    canvas.drawOval(
      rect,
      Paint()
        ..color = Colors.white
        ..style = PaintingStyle.stroke
        ..strokeWidth = 1,
    );
    electrons.forEach((electron) {
      final degree = degreesToRads(electron.currentPositionDegree);
      final electronPos = center +
          Offset(
            math.cos(degree) * (size.width / 2),
            math.sin(degree) * (size.height / 2),
          );
      canvas.drawCircle(electronPos, electron.size, Paint()..color = electron.color);
    });
  }
  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
Try it in DartPad


5. Increase Orbits

We have an implemented Orbit, and our Electrons can revolve around it.
Now we can replicate two other Orbits with different angles using the Stack widget.

Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Color(0xFF191636),
      body: Center(
        child: Stack(
          children: [
            Transform.rotate(
              angle: degreesToRads(0),
              child: CustomPaint(
                painter: _OrbitPainter(electrons),
                size: Size(300, 100),
              ),
            ),
            Transform.rotate(
              angle: degreesToRads(60),
              child: CustomPaint(
                painter: _OrbitPainter(electrons),
                size: Size(300, 100),
              ),
            ),
            Transform.rotate(
              angle: degreesToRads(120),
              child: CustomPaint(
                painter: _OrbitPainter(electrons),
                size: Size(300, 100),
              ),
            ),
          ],
        ),
      ),
    );
  }
Try it in DartPad

6. Add Electrons using the plus button

First, let’s define a model class for Orbits to hold the electrons list.
Then we can add an Electron each time we press the plus button in a random Orbit with randomized size and color.


class Orbit {
  final List<Electron> electrons;
  final angle;
  Orbit({
    @required this.electrons,
    @required this.angle,
  });
}
class _HomePageState extends State<HomePage> {
  List<Orbit> orbits = [
    Orbit(electrons: [], angle: 0),
    Orbit(electrons: [], angle: 60),
    Orbit(electrons: [], angle: 120),
  ];
  Timer timer;
  @override
  void initState() {
    super.initState();
    timer = new Timer.periodic(
      Duration(milliseconds: 16),
      (t) {
        setState(() {
          orbits.forEach((orbit) {
            orbit.electrons.forEach((electron) {
              electron.currentPositionDegree += electron.speed;
            });
          });
        });
      },
    );
  }
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Color(0xFF191636),
      body: Center(
        child: Stack(
          children: orbits
              .map(
                (orbit) => Transform.rotate(
                  angle: degreesToRads(orbit.angle),
                  child: CustomPaint(painter: _OrbitPainter(orbit.electrons), size: Size(300, 100)),
                ),
              )
              .toList(),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          final randomOrbit = orbits[math.Random().nextInt(orbits.length)];
          randomOrbit.electrons.add(
            Electron(
              speed: randomDoubleBetween(1, 3),
              size: randomDoubleBetween(4, 8),
              color: randomColor([
                Colors.purpleAccent,
                Colors.yellowAccent,
                Colors.cyanAccent,
              ]),
              currentPositionDegree: 0,
            ),
          );
        },
        child: Icon(Icons.add),
      ),
    );
  }
}
Try it in DartPad

7. Initial Animation

When we add an Electron (while they are a lot of existing electrons), we don’t understand where we placed it.
To solve this issue, let’s increase the Electron’s size at the beginning. Then we can decrease the size to the actual size gradually.
To do that, we rename the size variable to currentSize, And we define targetSize variable in our Electron model.

class Electron {
  double speed;
  double currentSize;
  double targetSize;
  Color color;
  double currentPositionDegree;
  Electron({
    @required this.speed,
    @required this.currentSize,
    @required this.targetSize,
    @required this.color,
    @required this.currentPositionDegree,
  });
}

In the beginning, we set targetSize with a random number (this is our actual size), and for currentSize we multiply the actual size by 10.
In each frame, we update the currentSize to something closer to the targetSize using lerpDouble(current, target, 0.07) function.
0.07 determines the speed of updating color.

@override
void initState() {
  super.initState();
  timer = new Timer.periodic(
    Duration(milliseconds: 16),
    (t) {
      setState(() {
        orbits.forEach((orbit) {
          orbit.electrons.forEach((electron) {
            electron.currentPositionDegree += electron.speed;
            electron.currentSize = lerpDouble(electron.currentSize, electron.targetSize, 0.07);
          });
        });
      });
    },
  );
}
onPressed: () {
  final randomOrbit = orbits[math.Random().nextInt(orbits.length)];
  final size = randomDoubleBetween(4, 8);
  randomOrbit.electrons.add(
    Electron(
      speed: randomDoubleBetween(1, 3),
      currentSize: size * 10,
      targetSize: size,
      color: randomColor([
        Colors.purpleAccent,
        Colors.yellowAccent,
        Colors.cyanAccent,
      ]),
      currentPositionDegree: 0,
    ),
  );
},
Try it in DartPad

I also did the same trick for the color of Electrons. In the beginning, they are transparent, I increase the opacity gradually using the Color.lerp() function.
I also tweaked some properties to have a better visual result.
You can find the source code here on Github, and also you can try the final result in the DartPad.

Update: You can check the LIVE DEMO right now on your browser!

Merry Christmas!

Comments (2)
Add Comment
  • Erfan

    Great job. That was nice and clean. I enjoyed and learned a lot from it. Thanks

  • f

    wow