JH Jeong
Posted on April 7, 2022
As you can see from my previous posts, I have recently been making various animations on Canvas on Flutter. This time, I implemented an animation in which the ball actually falls freely. In the process of making this, I found some parts that I didn't know and misunderstood about Flutter's animation, and I'm going to write several trials and errors.
Skeleton Code
First, the Skeleton code for implementing Bounce Ball is "Drag and Drop" of the previous article.
Drag and Drop in Flutter Canvas
It's because it's fun to have an interaction even if it's.
Widgets
child: AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Container(
width: 300,
height: 300,
color: Colors.white70,
child: CustomPaint(
painter: _paint(ballPath: ball.draw),
),
);
}
)
class _paint extends CustomPainter {
final Path ballPath;
_paint({
required this.ballPath,
});
@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = Colors.brown
..style = PaintingStyle.stroke
..strokeWidth = 2;
Path path = Path();
path.addPath(ballPath, Offset.zero);
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
Let's wrap the child of the Gesture Detector with the Animated Builder. And I created a _paint class that inherits CustomPainter. At this time, unlike before, the path was handed over as a factor, which is to view the ball as an object. (I'll go into more detail below.)
To draw the ballPath, add it to the path as an addPath function.
Ball Object
The ball will be controlled by turning it into an object. This is because the movement of the ball needs to be manipulated in more detail, so it becomes very complicated when the variables are created separately.
class myBall{
late double xPos;
late double yPos;
late double xVel;
late double yVel;
late double ballRad;
late Path draw;
double baseTime = 0.002;
myBall.origin(){
xPos=100;
yPos=100;
xVel=0;
yVel=0;
ballRad=20;
draw=Path();
for(double i=0; i<ballRad-1; i++){
draw.addOval(Rect.fromCircle(
center: Offset(
xPos, yPos
),
radius: i
));
}
}
void stop(){
xVel=0;
yVel=0;
}
void outVel(){
if(yVel.abs()<10){
yVel=0;
}
if(xVel.abs()<10){
xVel=0;
}
}
void setPosition(double x, double y){
xPos=x;
yPos=y;
}
bool isBallRegion(double checkX, double checkY){
if((pow(xPos-checkX, 2)+pow(yPos-checkY, 2))<=pow(ballRad, 2)){
return true;
}
return false;
}
void updateDraw(){
draw=Path();
for(double i=0; i<ballRad-1; i++){
draw.addOval(Rect.fromCircle(
center: Offset(
xPos,
yPos,
),
radius: i
));
}
}
void updateAnimation(double animationValue){
draw=Path();
for(double i=0; i<ballRad-1; i++){
draw.addOval(Rect.fromCircle(
center: Offset(
xPos + animationValue*xVel*baseTime,
yPos + animationValue*yVel*baseTime,
),
radius: i
));
}
}
}
As instance variables, the position of the ball, the speed value of the ball, and the shape of the ball, Path, were declared. And I added some instance methods to control these variables.
- stop() sets all the ball speeds to zero.
- outVel() is to stop the ball when the speed of the ball is less than a specific value. This is because the ball cannot be repeated indefinitely in the process of free fall and bounce back.
- setPosition() specifies the position of the ball
- The isBallRegion() determines whether the coordinates of the factor are included in the ball.
- updateDraw() and updateAnimation() modify the path of the ball to make the ball move.
There are some methods that can be operated, which I hope you can check at the Github link at the end of this article.
Free Fall's Variable
Now let's declare some variables for free fall.
bool isClick = false;
bool isClickAfter = true;
var ball = myBall.origin();
late AnimationController _animationController;
double baseTime = 0.016;
double accel = 1000;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: Duration(milliseconds: 10)
);
_animationController.repeat();
}
@override
void dispose(){
_animationController.dispose();
super.dispose();
}
- The isClick is necessary to prevent movement while the ball is clicked.
- The isClickAfter is needed to make isClick run as true in the last refresh when isClick becomes false in true.
- BaseTime is a variable that tells how many seconds to calculate the movement of the ball. I set the flutter to 60 fps and refresh to 16 ms.
- The acceleration of accel is set to 1000, which is similar to the actual 980 cm/s^2.
Free Fall Calculation
builder: (context, child) {
if (!isClick) {
if (ball.yVel!=0 || isClickAfter) {
ball.addYvel(baseTime * accel);
ball.subYpos(0.5 * accel * pow(baseTime, 2) - ball.yVel * baseTime);
ball.updateAnimation(_animationController.value);
isClickAfter=false;
if ((ball.yVel>0)&&(ball.yVel.abs()* _animationController.value*baseTime + ball.yPos + ball.ballRad >= 300)) {
ball.mulYvel(-0.7);
ball.outVel();
}
}
}
return Container(
width: 300,
height: 300,
color: Colors.white70,
child: CustomPaint(
painter: _paint(ballPath: ball.draw),
),
);
}
The following operations were put into the builder of the Animated Builder. Each time the builder plays, the speed of the ball increases by base time and the ball moves. If the velocity of the ball is positive (direction toward the ground) and the position of the ball exceeds the floor, it is determined that it has hit the floor and multiplied by -0.7 to the y-pull velocity. The reason for the negative number is that the direction of the y-axis velocity is changed and multiplied by 0.7, and it is assumed that there is an energy loss.
There was quite a bit of trial and error to make this part. It was due to the variability of the _animationController.value. This is because the _animationController.value went from 0 to 1 as a non-continuous value each time the same Duration was set. For this reason, it was detected several times in a row that the ball speed became zero at once (0.7 X 0.7 X ... = 0) To deal with it, I was able to create several additional restrictions on the if statement to make it work.
Conclusion
I initially intended to implement this using addStatusListener in the same way as this. And it was actually implemented and it looked like it worked well. (First Trial)
However, the more I did it, the more I felt that it was lagging, and I tried to solve this problem. So looking at the code, I wondered why addStatusListener wanted to control the variable. This is because there is no point in using Animated Builder.
I didn't know how Animated Builder was refreshed until now. Whenever _animationController.value changes in the builder in the Animated Builder, refresh together. In other words, I wasn't using Animated Builder, I was just rebuilding the entire screen every specific cycle and creating animations...! So there's gonna be a lot of lags.
But I'm glad I realized it now.
Now I'm going to try to develop it a little bit more and challenge the physical engine.
Full Code
Posted on April 7, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.