I'd like to achieve the path animation effect as seen over here :
This animation (I couldn't include it because the gif is too big)
I only want to achieve the path on the map animation, I know I need to use a stacked, place my map, then use a Painter to paint such path, but how can I animate it ?
I know this question has an accepted answer, but I'd like to show an alternate solution to this problem.
First of all, creating a custom path from individual points is not optimal for the following:
calculating the length of each segment is not trivial
animating the steps evenly at small increments is difficult and resource-heavy
does not work with quadratic / bezier segments
Just like in the good old Android there is this path tracing method, so does a very similar PathMetrics exist in Flutter.
Building upon the accepted answer of this question, here is a much more generic way of animating any path.
So given a path and an animation percent, we need to extract a path from the start until that percent:
Path createAnimatedPath(
Path originalPath,
double animationPercent,
) {
// ComputeMetrics can only be iterated once!
final totalLength = originalPath
.computeMetrics()
.fold(0.0, (double prev, PathMetric metric) => prev + metric.length);
final currentLength = totalLength * animationPercent;
return extractPathUntilLength(originalPath, currentLength);
}
So now I only need to extract a path until a given length (not the percent). We need to combine all existing paths until a certain distance. Then add to this existing path some part of the last path segment.
Doing that is pretty straightforward.
Path extractPathUntilLength(
Path originalPath,
double length,
) {
var currentLength = 0.0;
final path = new Path();
var metricsIterator = originalPath.computeMetrics().iterator;
while (metricsIterator.moveNext()) {
var metric = metricsIterator.current;
var nextLength = currentLength + metric.length;
final isLastSegment = nextLength > length;
if (isLastSegment) {
final remainingLength = length - currentLength;
final pathSegment = metric.extractPath(0.0, remainingLength);
path.addPath(pathSegment, Offset.zero);
break;
} else {
// There might be a more efficient way of extracting an entire path
final pathSegment = metric.extractPath(0.0, metric.length);
path.addPath(pathSegment, Offset.zero);
}
currentLength = nextLength;
}
return path;
}
The rest of the code required to an entire example:
void main() => runApp(
new MaterialApp(
home: new AnimatedPathDemo(),
),
);
class AnimatedPathPainter extends CustomPainter {
final Animation<double> _animation;
AnimatedPathPainter(this._animation) : super(repaint: _animation);
Path _createAnyPath(Size size) {
return Path()
..moveTo(size.height / 4, size.height / 4)
..lineTo(size.height, size.width / 2)
..lineTo(size.height / 2, size.width)
..quadraticBezierTo(size.height / 2, 100, size.width, size.height);
}
#override
void paint(Canvas canvas, Size size) {
final animationPercent = this._animation.value;
print("Painting + ${animationPercent} - ${size}");
final path = createAnimatedPath(_createAnyPath(size), animationPercent);
final Paint paint = Paint();
paint.color = Colors.amberAccent;
paint.style = PaintingStyle.stroke;
paint.strokeWidth = 10.0;
canvas.drawPath(path, paint);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
class AnimatedPathDemo extends StatefulWidget {
#override
_AnimatedPathDemoState createState() => _AnimatedPathDemoState();
}
class _AnimatedPathDemoState extends State<AnimatedPathDemo>
with SingleTickerProviderStateMixin {
AnimationController _controller;
void _startAnimation() {
_controller.stop();
_controller.reset();
_controller.repeat(
period: Duration(seconds: 5),
);
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(title: const Text('Animated Paint')),
body: SizedBox(
height: 300,
width: 300,
child: new CustomPaint(
painter: new AnimatedPathPainter(_controller),
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _startAnimation,
child: new Icon(Icons.play_arrow),
),
);
}
#override
void initState() {
super.initState();
_controller = new AnimationController(
vsync: this,
);
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
}
I created a library for this: drawing_animation
You just have to provide the Path objects to the widget:
Resulting in this image output: imgur
import 'package:drawing_animation/drawing_animation.dart';
//...
List<Paths> dottedPathArray = ...;
bool run = true;
//...
AnimatedDrawing.paths(
this.dottedPathArray,
run: this.run,
animationOrder: PathOrders.original,
duration: new Duration(seconds: 2),
lineAnimation: LineAnimation.oneByOne,
animationCurve: Curves.linear,
onFinish: () => setState(() {
this.run = false;
}),
)),
You don't actually need a Stack; you could use a foregroundPainter over the map image. To animate a CustomPainter pass the AnimationController into its constructor and also to the super constructor. In paint use the value of the animation to decide how much of the path the draw. For example, if value is 0.25, draw just the first 25% of the path.
class AnimatedPainter extends CustomPainter {
final Animation<double> _animation;
AnimatedPainter(this._animation) : super(repaint: _animation);
#override
void paint(Canvas canvas, Size size) {
// _animation.value has a value between 0.0 and 1.0
// use this to draw the first X% of the path
}
#override
bool shouldRepaint(AnimatedPainter oldDelegate) {
return true;
}
}
class PainterDemo extends StatefulWidget {
#override
PainterDemoState createState() => new PainterDemoState();
}
class PainterDemoState extends State<PainterDemo>
with SingleTickerProviderStateMixin {
AnimationController _controller;
#override
void initState() {
super.initState();
_controller = new AnimationController(
vsync: this,
);
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
void _startAnimation() {
_controller.stop();
_controller.reset();
_controller.repeat(
period: Duration(seconds: 5),
);
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(title: const Text('Animated Paint')),
body: new CustomPaint(
foregroundPainter: new AnimatedPainter(_controller),
child: new SizedBox(
// doesn't have to be a SizedBox - could be the Map image
width: 200.0,
height: 200.0,
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _startAnimation,
child: new Icon(Icons.play_arrow),
),
);
}
}
void main() {
runApp(
new MaterialApp(
home: new PainterDemo(),
),
);
}
Presumably you will have a list of coordinates that define the path. Assuming some list of points you'd draw the complete path with something like:
if (points.isEmpty) return;
Path path = Path();
Offset origin = points[0];
path.moveTo(origin.dx, origin.dy);
for (Offset o in points) {
path.lineTo(o.dx, o.dy);
}
canvas.drawPath(
path,
Paint()
..color = Colors.orange
..style = PaintingStyle.stroke
..strokeWidth = 4.0,
);
When value is less than 1.0 you need to devise a way to draw less than 100% of the path. For example, when value is 0.25, you might only add the first quarter of the points to the path. If your path consisted of relatively few points, you'd probably get the smoothest animation if you calculated the total length of the path and drew just the first segments of the path that added up to a quarter of the total length.
Related
The animation begins like this, with both circles in the center, but ends up where one circle reaches the end faster for some reason. How could I make it so that they reach the ends at the same time? I don't understand why this code isn't working. Any help would be much appreciated, thank you!
#override
void paint(Canvas canvas, Size size) {
var percentFinished = _animation.value;
var middleY = size.height / 2;
var middleX = size.width / 2;
double radius = 40;
var angles = [180, 0];
for (var angle in angles) {
var radians = (angle * pi) / 180;
var endingX = middleX + cos(radians) * radius;
var endingY = middleY - sin(radians) * radius;
var addToY = negativeOrPositiveOrZero(sin(radians)) * percentFinished;
var addToX = negativeOrPositiveOrZero(cos(radians)) * percentFinished;
canvas.drawCircle(Offset(endingX * addToX + middleX, endingY * addToY + middleY), 10, Paint());
}
}
int negativeOrPositiveOrZero(double a) {
int num = a.toInt();
if (num > 0){
print("1" + ": " + num.toString());
return 1;
}
else if (num < 0) {
return -1;
}
else {
return 0;
}
}
Below is just some screenshots of what I'm talking about.
The animation starts like this, with two balls in the center
But it ends in this state where one circle reaches the end before the other. The desired behavior is to have them reach their side of the screen at the same time.
I think your problem is how you compute your endingX.
var endingX = middleX + cos(radians) * radius;
It seems that your endingX should be the distance between the side of the Canvas and the perimeter of the Circles in their initial position. It's, therefore, the same for both directions:
var endingX = middleX - radius;
Then, a few simplifications on your code:
negativeOrPositiveOrZero
You have a getter for that in dart:math: sign
Trigonometry
I suppose the sample you posted is much simpler than your real code and the simplifications hereafter are probably not meaningful.
However, pay attention that near zero calculus on computers is quite messy!
import 'dart:math';
void main() {
print(' sin(0).sign: ${sin(0).sign}');
print(' sin(0).sign: ${sin(0).sign}');
print('------------------------------------------');
print(' sin(180*pi/180): ${sin(180*pi/180)}');
print('!! sin(180*pi/180).sign: ${sin(180*pi/180).sign}');
}
sin(0).sign: 0
sin(0).sign: 0
------------------------------------------
sin(180*pi/180): 1.2246467991473532e-16
!! sin(180*pi/180).sign: 1
Full example after correction and simplification
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Rolling balls',
home: Scaffold(
body: Center(
child: Container(
width: 300,
height: 200,
color: Colors.amber.shade300,
child: const MyCustomPaint(),
),
),
),
);
}
}
class MyCustomPaint extends StatefulWidget {
const MyCustomPaint({Key? key}) : super(key: key);
#override
_MyCustomPaintState createState() => _MyCustomPaintState();
}
class _MyCustomPaintState extends State<MyCustomPaint>
with SingleTickerProviderStateMixin {
late Animation<double> _animation;
late AnimationController _controller;
#override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 4),
);
Tween<double> _animationTween = Tween(begin: 0.0, end: 1.0);
_animation = _animationTween.animate(_controller)
..addListener(() => setState(() {}))
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reverse();
} else if (status == AnimationStatus.dismissed) {
_controller.forward();
}
});
_controller.forward();
}
#override
Widget build(BuildContext context) {
return CustomPaint(
painter: MyCustomPainter(_animation.value),
);
}
}
class MyCustomPainter extends CustomPainter {
final double percentFinished;
MyCustomPainter(this.percentFinished);
#override
void paint(Canvas canvas, Size size) {
double middleY = size.height / 2;
double middleX = size.width / 2;
double radius = size.width / 20;
Paint paint = Paint()..color = Colors.black;
for (int direction in [1, -1]) {
var endingX = middleX - radius;
var addToX = direction * percentFinished;
canvas.drawCircle(
Offset(endingX * addToX + middleX, middleY),
radius,
paint,
);
}
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
I'm learning Flutter and am tasked with creating some 3D type effects for transitions and other "wow" factor type things. I have been using flutter's transforms to achieve this but have run into a problem. When I rotate about the Y axis the plane containing whatever tab was showing disappears around the 80 degree mark and shows back up around the 100 degree mark. This does not happen when rotated at -90 degrees.
I've seen other answers posted such as transform: rotateY() making element disappear but none seem to cover this issue.
The question is; is there something I should be setting to avoid this since it is pretty close to off screen when I do it. If not is this a quirk or known bug in flutter's transition?
I am unable to put the full code and I'm not really familiar enough with flutter to rip out a bit and rough up an example quickly. Below is a code snippet and I'll come back with a better code snippet once I have more time. My apologies for that.
#override
Container build (BuildContext context) {
return Container(
child: Transform(
transform: Matrix4.identity()
..setEntry(3, 2, 0.004)
..translate(position.x, position.y, position.z)
..rotateX(rotation.x)
..rotateY(rotation.y)
..rotateZ(rotation.z),
child: CustomPaint(
painter: MyPainter(),
),
),
);
}
class MyPainter extends CustomPainter {
Size rectSize = Size(100, 100);
#override
Future<void> paint(Canvas canvas, Size size) async {
final paint = Paint()
..style = PaintingStyle.fill
..color = Colors.green;
Rect rect = Offset(-rectSize.width / 2.0, -rectSize.height / 2.0) & this.rectSize;
canvas.drawRect(rect, paint);
}
}
EDIT 1: I have tried to get a minimal example working but the rotation actually works as expected. I used the above code with no changes into a statefulwidget but the plane only disappears at 90 and 270 degrees as expected. I'll update the question if I find out anything else. Thanks to all who have looked at this.
After quite a number of hours since yesterday I have discovered the cause of the disappearance. Both fragments of code are held in a custom Container class which uses alignment: FractionalOffset.center to align them into the center of the screen. When this is removed the widgets return to the upper left hand corner and rotates as expected.
I'm not sure why this happens and would love to know if anybody has any ideas. However, below is a minimal replication of the issue. Uncomment the aligment: FractionalOffset.center line in runApp() to see the error in action.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:vector_math/vector_math.dart' as Vector;
import 'dart:math';
void main() {
runApp(
MaterialApp(
theme: ThemeData.dark(),
home: Stack(
// alignment: FractionalOffset.center, // Remove the comment slashes to see issue
children: [
MyWidget(),
]
)
),
);
//runApp(MyApp());
}
class MyWidget extends StatefulWidget {
#override
State<StatefulWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
Vector.Vector3 position = Vector.Vector3.zero();
Vector.Vector3 rotation = Vector.Vector3.zero();
int degrees = 0;
#override
initState() {
super.initState();
new Timer.periodic(Duration(milliseconds: 100), (timer) => {timerCall()});
}
void timerCall() {
setState(() {
});
}
#override
Container build (BuildContext context) {
degrees += 1;
if (degrees >= 360) degrees = 0;
rotation.y = degrees * (pi / 180);
return Container(
child: Transform(
transform: Matrix4.identity()
..setEntry(3, 2, 0.004)
..translate(position.x, position.y, position.z)
..rotateX(rotation.x)
..rotateY(rotation.y)
..rotateZ(rotation.z),
child: CustomPaint(
painter: MyPainter(),
),
),
);
}
}
class MyPainter extends CustomPainter {
Size rectSize = Size(200, 200);
#override
Future<void> paint(Canvas canvas, Size size) async {
final paint = Paint()
..style = PaintingStyle.fill
..color = Colors.green;
Rect rect = Offset(-rectSize.width / 2.0, -rectSize.height / 2.0) & this.rectSize;
canvas.drawRect(rect, paint);
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
// TODO: implement shouldRepaint
return true;
}
}
I have used Flutter's CustomPainter class to create a generative still image using Paths (see code below). I'd like to be able to animate such images indefinitely. What is the most straightforward approach to doing this?
import 'package:flutter/material.dart';
void main() => runApp(
MaterialApp(
home: PathExample(),
),
);
class PathExample extends StatelessWidget {
#override
Widget build(BuildContext context) {
return CustomPaint(
painter: PathPainter(),
);
}
}
class PathPainter extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = Colors.grey[200]
..style = PaintingStyle.fill
..strokeWidth = 0.0;
canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), paint);
Path path2 = Path();
for (double i = 0; i < 200; i++) {
Random r = new Random();
path2.moveTo(sin(i / 2.14) * 45 + 200, i * 12);
path2.lineTo(sin(i / 2.14) * 50 + 100, i * 10);
paint.style = PaintingStyle.stroke;
paint.color = Colors.red;
canvas.drawPath(path2, paint);
}
Path path = Path();
paint.color = Colors.blue;
paint.style = PaintingStyle.stroke;
for (double i = 0; i < 30; i++) {
path.moveTo(100, 50);
// xC, yC, xC, yC, xEnd, yEnd
path.cubicTo(
-220, 300, 500, 600 - i * 20, size.width / 2 + 50, size.height - 50);
canvas.drawPath(path, paint);
}
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
To do this, you're going to want to do a lot of what the TickerProviderStateMixin does - essentially, you need to create and manage your own Ticker.
I've done this in a simple builder widget below. It simply schedules a build every time there has been a tick, and then builds with the given value during that ticket. I've added a totalElapsed parameter as well as a sinceLastDraw parameter for convenience but you could easily choose one or the other depending on what's the most convenient for what you're doing.
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
void main() => runApp(
MaterialApp(
home: PathExample(),
),
);
class PathExample extends StatelessWidget {
#override
Widget build(BuildContext context) {
return TickerBuilder(builder: (context, sinceLast, total) {
return CustomPaint(
painter: PathPainter(total.inMilliseconds / 1000.0),
);
});
}
}
class TickerBuilder extends StatefulWidget {
// this builder function is used to create the widget which does
// whatever it needs to based on the time which has elapsed or the
// time since the last build. The former is useful for position-based
// animations while the latter could be used for velocity-based
// animations (i.e. oldPosition + (time * velocity) = newPosition).
final Widget Function(BuildContext context, Duration sinceLastDraw, Duration totalElapsed) builder;
const TickerBuilder({Key? key, required this.builder}) : super(key: key);
#override
_TickerBuilderState createState() => _TickerBuilderState();
}
class _TickerBuilderState extends State<TickerBuilder> {
// creates a ticker which ensures that the onTick function is called every frame
late final Ticker _ticker = Ticker(onTick);
// the total is the time that has elapsed since the widget was created.
// It is initially set to zero as no time has elasped when it is first created.
Duration total = Duration.zero;
// this last draw time is saved during each draw cycle; this is so that
// a time between draws can be calculated
Duration lastDraw = Duration.zero;
void onTick(Duration elapsed) {
// by calling setState every time this function is called, we're
// triggering this widget to be rebuilt on every frame.
// This is where the indefinite animation part comes in!
setState(() {
total = elapsed;
});
}
#override
void initState() {
super.initState();
_ticker.start();
}
#override
void didChangeDependencies() {
_ticker.muted = !TickerMode.of(context);
super.didChangeDependencies();
}
#override
Widget build(BuildContext context) {
final result = widget.builder(context, total - lastDraw , total);
lastDraw = total;
return result;
}
#override
void dispose() {
_ticker.stop();
super.dispose();
}
}
class PathPainter extends CustomPainter {
final double pos;
PathPainter(this.pos);
#override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = Colors.grey
..style = PaintingStyle.fill
..strokeWidth = 0.0;
canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), paint);
Path path2 = Path();
for (double i = 0; i < 200; i++) {
Random r = new Random();
path2.moveTo(sin(i / 2.14 + pos) * 45 + 200, (i * 12));
path2.lineTo(sin(i / 2.14 + pos) * 50 + 100, (i * 10));
paint.style = PaintingStyle.stroke;
paint.color = Colors.red;
canvas.drawPath(path2, paint);
}
Path path = Path();
paint.color = Colors.blue;
paint.style = PaintingStyle.stroke;
for (double i = 0; i < 30; i++) {
path.moveTo(100, 50);
// xC, yC, xC, yC, xEnd, yEnd
path.cubicTo(
-220,
300,
500,
600 - i * 20,
size.width / 2 + 50,
size.height - 50,
);
canvas.drawPath(path, paint);
}
}
// in this particular case, this is rather redundant as
// the animation is happening every single frame. However,
// in the case where it doesn't need to animate every frame, you
// should implement it such that it only returns true if it actually
// needs to redraw, as that way the flutter engine can optimize
// its drawing and use less processing power & battery.
#override
bool shouldRepaint(PathPainter old) => old.pos != pos;
}
A couple things to note - first off, the drawing is hugely sub-optimal in this case. Rather than re-drawing the background every frame, that could be made into a static background using a Container or DecoratedBox. Secondly, the paint objects are being re-created and used every single frame - if these are constant, they could be instantiated once and re-used over and over again.
Also, since WidgetBuilder is going to be running a lot, you're going to want to make sure that you do as little as possible in its build function - you're not going to want to build up a whole widget tree there but rather move it as low as possible in the tree so it only builds things that are actually animating (as I've done in this case).
I am developing an app using Flutter, where I simply want a rectangle to move on screen when I scroll with my fingers (on Android/iOS) or when I scroll with a mouse wheel or trackpad (on web).
This is what my code looks like:
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
/////////////////
void main()
{
runApp(MyCustomScroller());
}
/////////////////
class MyCustomScroller extends StatefulWidget
{
#override
_MyCustomScrollerState createState() => _MyCustomScrollerState();
}
class _MyCustomScrollerState extends State<MyCustomScroller> with TickerProviderStateMixin
{
AnimationController animController;
double scrollPos = 500;
#override
void initState()
{
super.initState();
animController = AnimationController.unbounded(
vsync: this,
value: 0,
);
animController.addListener(()
{
setState(()
{
scrollPos = animController.value;
});
});
}
#override
void dispose()
{
animController.dispose();
super.dispose();
}
void _onScrollUpdate(final double velPpf)
{
final double velPps = velPpf*60;
Simulation sim = ClampingScrollSimulation(
position: scrollPos,
velocity: velPps,
friction: 0.01,
);
animController.animateWith(sim);
scrollPos += velPpf;
}
#override
Widget build(BuildContext context)
{
return MaterialApp(
title: 'Scrolling Test',
home: Listener(
onPointerMove: (PointerMoveEvent moveEvent) // mobile, web click or web finger (on mobile)
{
_onScrollUpdate(moveEvent.delta.dy);
},
onPointerSignal: (PointerSignalEvent signalEvent) // web scrolling wheel/trackpad
{
if (signalEvent is PointerScrollEvent)
_onScrollUpdate(-signalEvent.scrollDelta.dy);
},
child: Container(
color: Colors.white,
child: CustomPaint(
painter: MyPainter(
pos: scrollPos,
),
child: Container(),
)
)
)
);
}
}
/////////////////
class MyPainter extends CustomPainter
{
final double pos;
MyPainter({
#required this.pos
});
#override
void paint(Canvas canvas, Size size)
{
Paint paint = Paint()
..color = Colors.black
..strokeWidth = 12
..style = PaintingStyle.stroke;
final double x = size.width/2;
final double y = (pos % size.height*1.5) - size.height*0.25;
final Offset offset0 = Offset(x, y);
final Offset offset1 = Offset(x, y+size.height*0.25);
canvas.drawLine(offset0, offset1, paint);
}
#override
bool shouldRepaint(CustomPainter oldDelegate)
{
return true;
}
}
Everything seems to work fine, I can see a rectangle that moves up and down as I scroll. When I stop scrolling, the AnimationController continues moving the rectangle. I know similar results can be achieved using a GestureDetector instead of a Listener, but a GestureDetector does not work on mouse scrolling on web.
The problem comes when I run my app in profiling mode: I turn on the Performance Overlay to see if the UI and Raster threads are executing fine and that I don't have any jank.
Most of the time the performance of the raster thread looks fine, as you can see in the following picture:
However sometimes, for some reason, the time taken to render a frame on the Raster thread goes up to around 16-17 ms, as you can see in the following image, and then stays around those values for the upcoming frames, until the animation finishes:
Anybody has any idea why this happens?
I have a CustomPainter that I want to render some items every few milliseconds. But I only want to render the items that have changed since the last draw. I plan on manually clearing the area that will be changing and redrawing just in the area. The problem is that the canvas in Flutter seems to be completely new every time paint() is called. I understand that I can keep track of the entire state and redraw everything every time, but for performance reasons and the specific use case that is not preferable. Below is sample code that could represent the issue:
I understand that everything will need to be redrawn when the canvas size changes.
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
class CanvasWidget extends StatefulWidget {
CanvasWidget({Key key}) : super(key: key);
#override
_CanvasWidgetState createState() => _CanvasWidgetState();
}
class _CanvasWidgetState extends State<CanvasWidget> {
final _repaint = ValueNotifier<int>(0);
TestingPainter _wavePainter;
#override
void initState() {
_wavePainter = TestingPainter(repaint: _repaint);
Timer.periodic( Duration(milliseconds: 50), (Timer timer) {
_repaint.value++;
});
super.initState();
}
#override
Widget build(BuildContext context) {
return CustomPaint(
painter: _wavePainter,
);
}
}
class TestingPainter extends CustomPainter {
static const double _numberPixelsToDraw = 3;
final _rng = Random();
double _currentX = 0;
double _currentY = 0;
TestingPainter({Listenable repaint}): super(repaint: repaint);
#override
void paint(Canvas canvas, Size size) {
var paint = Paint();
paint.color = Colors.transparent;
if(_currentX + _numberPixelsToDraw > size.width)
{
_currentX = 0;
}
// Clear previously drawn points
var clearArea = Rect.fromLTWH(_currentX, 0, _numberPixelsToDraw, size.height);
canvas.drawRect(clearArea, paint);
Path path = Path();
path.moveTo(_currentX, _currentY);
for(int i = 0; i < _numberPixelsToDraw; i++)
{
_currentX++;
_currentY = _rng.nextInt(size.height.toInt()).toDouble();
path.lineTo(_currentX, _currentY);
}
// Draw new points in red
paint.color = Colors.red;
canvas.drawPath(path, paint);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
Redrawing the whole canvas, even on every frame, is completely efficient. Trying to reuse the previous frame will often not be more efficient.
Looking at the code you posted, there are certain areas with rooms for improvement, but trying to preserve parts of the canvas should not be one of them.
The real performance issue you are having, is from repeatedly changing a ValueNotifier from a Timer.periodic event, every 50 ms. A much better way to handle redrawing on every frame, is to use AnimatedBuilder with a vsync, so the paint method of the CustomPainter will be called on every frame. This is similar to Window.requestAnimationFrame in the web browser world, if you are familiar with that. Here vsync stands for "vertical sync", if you are familiar with how computer graphics work. Essentially, your paint method will be called 60 times per second, on a device with 60 Hz screen, and it'll paint 120 times per second on a 120 Hz screen. This is the correct and scalable way to achieve buttery smooth animation across different kind of devices.
There are other areas worth optimizing, before thinking about preserving parts of the canvas. For example, just briefly looking at your code, you have this line:
_currentY = _rng.nextInt(size.height.toInt()).toDouble();
Here I assume you want to have a random decimal between 0 and size.height, if so, you can simply write _rng.nextDouble() * size.height, instead of casting a double to int and back again, and (probably unintentionally) rounding it during that process. But the performance gain from stuff like these is negligible.
Think about it, if a 3D video game can run smoothly on a phone, with each frame being dramatically different from the previous one, your animation should run smoothly, without having to worry about manually clearing parts of the canvas. Trying to manually optimize the canvas will probably lead to performance loss instead.
So, what you really should be focusing, is to use AnimatedBuilder instead of Timer to trigger the canvas redraw in your project, as a starting point.
For example, here's a small demo I made using AnimatedBuilder and CustomPaint:
Full source code:
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage>
with SingleTickerProviderStateMixin {
List<SnowFlake> snowflakes = List.generate(100, (index) => SnowFlake());
AnimationController _controller;
#override
void initState() {
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 1),
)..repeat();
super.initState();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.blue, Colors.lightBlue, Colors.white],
stops: [0, 0.7, 0.95],
),
),
child: AnimatedBuilder(
animation: _controller,
builder: (_, __) {
snowflakes.forEach((snow) => snow.fall());
return CustomPaint(
painter: MyPainter(snowflakes),
);
},
),
),
);
}
}
class MyPainter extends CustomPainter {
final List<SnowFlake> snowflakes;
MyPainter(this.snowflakes);
#override
void paint(Canvas canvas, Size size) {
final w = size.width;
final h = size.height;
final c = size.center(Offset.zero);
final whitePaint = Paint()..color = Colors.white;
canvas.drawCircle(c - Offset(0, -h * 0.165), w / 6, whitePaint);
canvas.drawOval(
Rect.fromCenter(
center: c - Offset(0, -h * 0.35),
width: w * 0.5,
height: w * 0.6,
),
whitePaint);
snowflakes.forEach((snow) =>
canvas.drawCircle(Offset(snow.x, snow.y), snow.radius, whitePaint));
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
class SnowFlake {
double x = Random().nextDouble() * 400;
double y = Random().nextDouble() * 800;
double radius = Random().nextDouble() * 2 + 2;
double velocity = Random().nextDouble() * 4 + 2;
SnowFlake();
fall() {
y += velocity;
if (y > 800) {
x = Random().nextDouble() * 400;
y = 10;
radius = Random().nextDouble() * 2 + 2;
velocity = Random().nextDouble() * 4 + 2;
}
}
}
Here I'm generating 100 snowflakes, redrawing the whole screen every frame. You can easily change the number of snowflakes to 1000 or higher, and it would still run very smoothly. Here I'm also not using the device screen size as much as I should be, as you can see, there are some hardcoded values like 400 or 800. Anyway, hopefully this demo would give you some faith in Flutter's graphics engine. :)
Here is another (smaller) example, showing you everything you need to get going with Canvas and Animations in Flutter. It might be easier to follow:
import 'package:flutter/material.dart';
void main() {
runApp(DemoWidget());
}
class DemoWidget extends StatefulWidget {
#override
_DemoWidgetState createState() => _DemoWidgetState();
}
class _DemoWidgetState extends State<DemoWidget>
with SingleTickerProviderStateMixin {
AnimationController _controller;
#override
void initState() {
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 1),
)..repeat(reverse: true);
super.initState();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (_, __) => CustomPaint(
painter: MyPainter(_controller.value),
),
);
}
}
class MyPainter extends CustomPainter {
final double value;
MyPainter(this.value);
#override
void paint(Canvas canvas, Size size) {
canvas.drawCircle(
Offset(size.width / 2, size.height / 2),
value * size.shortestSide,
Paint()..color = Colors.blue,
);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
The only solution currently available is to capture the progress as image and then draw the image instead of executing the whole canvas code.
for drawing the image you can use canvas.drawImage as mentioned by pskink in the above comment.
but the solution i would recommend is to wrap the CustomPaint with RenderRepaint to convert that widget to image. for detials refer to
Creating raw image from Widget or Canvas and (https://medium.com/flutter-community/export-your-widget-to-image-with-flutter-dc7ecfa6bafb for brief implementation), and have a condition to check if you are building for the first time or not.
class _CanvasWidgetState extends State<CanvasWidget> {
/// Just to track if its the first frame or not.
var _flag = false;
/// Will be used for generating png image.
final _globalKey = new GlobalKey();
/// Stores the image bytes
Uint8List _imageBytes;
/// No need for this actually;
/// final _repaint = ValueNotifier<int>(0);
TestingPainter _wavePainter;
Future<Uint8List> _capturePng() async {
try {
final boundary = _globalKey
.currentContext.findRenderObject();
ui.Image image = await boundary.toImage();
ByteData byteData =
await image.toByteData(format: ui.ImageByteFormat.png);
var pngBytes = byteData.buffer.asUint8List();
var bs64 = base64Encode(pngBytes);
print(pngBytes);
print(bs64);
setState(() {});
return pngBytes;
} catch (e) {
print(e);
}
}
#override
void initState() {
_wavePainter = TestingPainter();
Timer.periodic( Duration(milliseconds: 50), (Timer timer) {
if (!flag) flag = true;
/// Save your image before each redraw.
_imageBytes = _capturePng();
/// You don't need a listener if you are using a stful widget.
/// It will do just fine.
setState(() {});
});
super.initState();
}
#override
Widget build(BuildContext context) {
return RepaintBoundary(
key: _globalkey,
child: Container(
/// Use this if this is not the first frame.
decoration: _flag ? BoxDecoration(
image: DecorationImage(
image: MemoryImage(_imageBytes)
)
) : null,
child: CustomPainter(
painter: _wavePainter
)
)
);
}
}
This way the image will not be a part of your custom painter and let me tell you, i tried drawing image using canvas but it was not that efficient, the MemoryImage provided by flutter renders image in a much better way.