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:

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;
}```

### 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 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;
}```

### 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();
}
}```

### 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 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;
}```

### 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(
child: CustomPaint(
painter: _OrbitPainter(electrons),
size: Size(300, 100),
),
),
Transform.rotate(
child: CustomPaint(
painter: _OrbitPainter(electrons),
size: Size(300, 100),
),
),
Transform.rotate(
child: CustomPaint(
painter: _OrbitPainter(electrons),
size: Size(300, 100),
),
),
],
),
),
);
}```

### 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(
child: CustomPaint(painter: _OrbitPainter(orbit.electrons), size: Size(300, 100)),
),
)
.toList(),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
final randomOrbit = orbits[math.Random().nextInt(orbits.length)];
Electron(
speed: randomDoubleBetween(1, 3),
size: randomDoubleBetween(4, 8),
color: randomColor([
Colors.purpleAccent,
Colors.yellowAccent,
Colors.cyanAccent,
]),
currentPositionDegree: 0,
),
);
},
),
);
}
}```

### 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);
Electron(
speed: randomDoubleBetween(1, 3),
currentSize: size * 10,
targetSize: size,
color: randomColor([
Colors.purpleAccent,
Colors.yellowAccent,
Colors.cyanAccent,
]),
currentPositionDegree: 0,
),
);
},```

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.

Merry Christmas!

Get real time updates directly on you device, subscribe now.

1 Comment
1. Erfan says

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

This website uses cookies to improve your experience. We'll assume you're ok with this, but you can opt-out if you wish. Accept Read More