How to animate line draw in custom painter in Flutter? - flutter

I want to animate the line drawing in custom painter canvas. So far what I can do is create two circles at two points and then create a line between those two points. But I don't know how to animate the line as if it is going from one point to the other. I have tried something but I can't make it work. Please check the code and suggest me if you have any idea.
import 'package:flutter/material.dart';
import 'dart:math' as math;
class ProgressMonitorAnimation extends StatefulWidget {
#override
State<StatefulWidget> createState() => _ProgressMonitorAnimationState();
}
class _ProgressMonitorAnimationState extends State<ProgressMonitorAnimation> with TickerProviderStateMixin {
double _progress = 0.0;
Animation<double> animation;
#override
void initState() {
var controller = AnimationController(duration: Duration(milliseconds: 3000), vsync: this);
animation = Tween(begin: 1.0, end: 0.0).animate(controller)..addListener(() {
setState(() {
_progress = animation.value;
});
});
controller.forward();
super.initState();
}
#override
Widget build(BuildContext context) {
return Transform(
alignment: Alignment.center,
transform: Matrix4.rotationX(math.pi),
child: CustomPaint(
foregroundPainter: ProgressPainter(_progress),
),
);
}
}
class ProgressPainter extends CustomPainter {
double _progress;
ProgressPainter(this._progress);
#override
void paint(Canvas canvas, Size size) {
final Paint circlePainter = Paint()..color = Colors.green;
final Paint linePainter = Paint()..color = Colors.black..strokeWidth = 4..strokeCap = StrokeCap.round;
canvas.drawCircle(Offset(0.0, 30.0 * 3), 10.0, circlePainter);
canvas.drawCircle(Offset(15.0 * 2, 80.0 * 3), 10.0, circlePainter);
canvas.drawLine(Offset(0.0 / (_progress * 10), 30.0 * 3), Offset((30.0 * 3) + (15.0) / (_progress * 15) * 2, (80.0 * 3) / (_progress * 15)), linePainter);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}

You can do as follows using flutter Custom Painter.
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
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.black;
paint.style = PaintingStyle.stroke;
paint.strokeWidth = 4.0;
canvas.drawPath(path, paint);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
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);
}
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;
}
class AnimatedPathDemo extends StatefulWidget {
#override
_AnimatedPathDemoState createState() => _AnimatedPathDemoState();
}
class _AnimatedPathDemoState extends State<AnimatedPathDemo>
with SingleTickerProviderStateMixin {
AnimationController _controller;
Completer<GoogleMapController> _controllerMap = Completer();
static final CameraPosition _initialPosition = CameraPosition(
// target: LatLng(12.947437, 77.685345),
target: LatLng(7.8731, 80.7718),
zoom: 8,
);
void _startAnimation() {
_controller.stop();
_controller.reset();
_controller.repeat(
period: Duration(seconds: 2),
);
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(title: const Text('Animated Paint')),
body: Stack(
children: <Widget>[
GoogleMap(
// rotateGesturesEnabled: false,
mapType: MapType.normal,
compassEnabled: false,
initialCameraPosition: _initialPosition,
// polylines: _polylines,
// markers: _markers,
onMapCreated: (GoogleMapController controller) {
// controller.setMapStyle(Utils.mapStyles);
_controllerMap.complete(controller);
},
),
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();
}
}

Related

How to animate a line with multiple offset points using CustomPainter in Flutter?

I want to create a line that animates to multiple offset points until the full line is painted out, using CustomPainter in Flutter.
I have almost achieved this effect, by using an animation object to tween to each new point, and an index to track the line progress.
Then, in CustomPainter, I paint 2 lines. One to line animates to the new position, and a second which draws the existing path based off the index.
However, there is a small UI error as the GIF shows, where the corners 'fill out' after a new point is added.
Note, a I tried using a TweenSequence borrowing the idea mentioned in this recent video but couldn't get it to work. FlutterForward, Youtube Video - around 14:40
import 'package:flutter/material.dart';
class LinePainterAnimation extends StatefulWidget {
const LinePainterAnimation({Key? key}) : super(key: key);
#override
State<LinePainterAnimation> createState() => _LinePainterAnimationState();
}
class _LinePainterAnimationState extends State<LinePainterAnimation>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
final List<Offset> _offsets = [
const Offset(50, 300),
const Offset(150, 100),
const Offset(300, 300),
const Offset(200, 300),
];
int _index = 0;
Offset _begin = const Offset(0, 0);
Offset _end = const Offset(0, 0);
#override
void initState() {
_begin = _offsets[0];
_end = _offsets[1];
_controller = AnimationController(
duration: const Duration(seconds: 1), vsync: this)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
_index++;
if (_index < _offsets.length - 1) {
_begin = _offsets[_index];
_end = _offsets[_index + 1];
_controller.reset();
_controller.forward();
setState(() {});
}
}
});
super.initState();
}
#override
Widget build(BuildContext context) {
Animation<Offset> animation =
Tween<Offset>(begin: _begin, end: _end).animate(_controller);
return Scaffold(
body: AnimatedBuilder(
animation: _controller,
builder: (context, child) => CustomPaint(
painter: LinePainter(
startOffset: _begin,
endOffset: animation.value,
offsets: _offsets,
index: _index,
),
child: Container(),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
_controller.reset();
_controller.forward();
_begin = _offsets[0];
_end = _offsets[1];
_index = 0;
setState(() {});
},
child: const Text('Play'),
),
);
}
}
class LinePainter extends CustomPainter {
final Offset startOffset;
final Offset endOffset;
final List<Offset> offsets;
final int index;
LinePainter({
required this.startOffset,
required this.endOffset,
required this.offsets,
required this.index,
});
#override
void paint(Canvas canvas, Size size) {
var paint = Paint()
..color = Colors.red
..strokeWidth = 20
..strokeCap = StrokeCap.butt
..style = PaintingStyle.stroke;
var pathExisting = Path();
pathExisting.moveTo(offsets[0].dx, offsets[0].dy);
for (int i = 0; i < index + 1; i++) {
pathExisting.lineTo(offsets[i].dx, offsets[i].dy);
}
var pathNew = Path();
pathNew.moveTo(startOffset.dx, startOffset.dy);
pathNew.lineTo(endOffset.dx, endOffset.dy);
canvas.drawPath(pathNew, paint);
canvas.drawPath(pathExisting, paint);
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
OK - so I finally came up with a solution after a bit more research, which is perfect for this use case and animating complex paths - [PathMetrics][1]
So to get this working the basic steps are 1) define any path 2) calculate & extract this path using PathMetrics 3) then animate this path over any duration, based on the 0.0 to 1.0 value produced by the animation controller, and voilĂ  it works like magic!
Note, the references I found to get this working: [Moving along a curved path in flutter][2] & [Medium article][3]
Updated code pasted below if this is helpful to anyone.
[![Solution][4]]
import 'dart:ui';
import 'package:flutter/material.dart';
class LinePainterAnimation extends StatefulWidget {
const LinePainterAnimation({Key? key}) : super(key: key);
#override
State<LinePainterAnimation> createState() => _LinePainterAnimationState();
}
class _LinePainterAnimationState extends State<LinePainterAnimation>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
#override
void initState() {
super.initState();
_controller =
AnimationController(duration: const Duration(seconds: 1), vsync: this);
_controller.forward();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: AnimatedBuilder(
animation: _controller,
builder: (context, child) => CustomPaint(
painter: LinePainter(_controller.value),
child: Container(),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
_controller.reset();
_controller.forward();
},
child: const Text('Play'),
),
);
}
}
class LinePainter extends CustomPainter {
final double percent;
LinePainter(this.percent);
#override
void paint(Canvas canvas, Size size) {
var paint = Paint()
..color = Colors.red
..strokeWidth = 20
..strokeCap = StrokeCap.butt
..style = PaintingStyle.stroke;
var path = getPath();
PathMetrics pathMetrics = path.computeMetrics();
PathMetric pathMetric = pathMetrics.elementAt(0);
Path extracted = pathMetric.extractPath(0.0, pathMetric.length * percent);
canvas.drawPath(extracted, paint);
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
Path getPath() {
return Path()
..lineTo(50, 300)
..lineTo(150, 100)
..lineTo(300, 300)
..lineTo(200, 300);
}
[1]: https://api.flutter.dev/flutter/dart-ui/PathMetrics-class.html
[2]: https://stackoverflow.com/questions/60203515/moving-along-a-curved-path-in-flutter
[3]: https://medium.com/flutter-community/playing-with-paths-in-flutter-97198ba046c8
[4]: https://i.stack.imgur.com/ayoHn.gif

Heart Beat line for splash screen

i want to add heart beat horizontal line for splash screen, it will start animating from one side and go all the way to other if the initial data is still not loaded it will continue animation
i tried below code but its only pulse kinda animation
import 'dart:math';
import 'package:flutter/material.dart';
class SpritePainter extends CustomPainter {
final Animation<double> _animation;
SpritePainter(this._animation) : super(repaint: _animation);
void circle(Canvas canvas, Rect rect, double value) {
double opacity = (1.0 - (value / 4.0)).clamp(0.0, 1.0);
Color color = Color.fromRGBO(0, 117, 194, opacity);
double size = rect.width / 2;
double area = size * size;
double radius = sqrt(area * value / 4);
final Paint paint = Paint()..color = color;
canvas.drawCircle(rect.center, radius, paint);
}
#override
void paint(Canvas canvas, Size size) {
Rect rect = Rect.fromLTRB(0.0, 0.0, size.width, size.height);
for (int wave = 3; wave >= 0; wave--) {
circle(canvas, rect, wave + _animation.value);
}
}
#override
bool shouldRepaint(SpritePainter oldDelegate) {
return true;
}
}
class SpriteDemo extends StatefulWidget {
#override
SpriteDemoState createState() => SpriteDemoState();
}
class SpriteDemoState extends State<SpriteDemo>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
#override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
);
//_startAnimation();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
void _startAnimation() {
_controller
..stop()
..reset()
..repeat(period: const Duration(seconds: 1));
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Pulse')),
body: CustomPaint(
painter: SpritePainter(_controller),
child: SizedBox(
width: 200.0,
height: 200.0,
),
),
floatingActionButton: FloatingActionButton(
onPressed: _startAnimation,
child: new Icon(Icons.play_arrow),
),
);
}
}
void main() {
runApp(
MaterialApp(
home: SpriteDemo(),
),
);
}
I want it to start(animate) from center left and go all the way to right just like in image

Flutter resize circle_wave widget

Here we have a very awesome implemented widget to have this circular wave
now in this code i can't resize width and height of this widget. before posting problem i try to resolve that, but i can't
GitHub Link: demo_circle_wave
import 'dart:math' as math;
import 'dart:ui';
import 'package:flutter/material.dart';
import 'common.dart';
class DemoCircleWave extends StatefulWidget {
const DemoCircleWave({
Key key,
}) : super(key: key);
#override
_DemoCircleWaveState createState() => _DemoCircleWaveState();
}
class _DemoCircleWaveState extends State<DemoCircleWave>
with TickerProviderStateMixin {
static const colors = <Color>[
Color(0xFFFF2964),
Color(0xFF32FF3A),
Color(0xFF4255FF)
];
AnimationController controller;
AnimationController addPointController;
Animation<double> addPointAnimation;
int _counter = 0;
#override
void initState() {
super.initState();
controller = AnimationController(
vsync: this,
upperBound: 2,
duration: const Duration(seconds: 10),
)..repeat();
addPointController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
);
addPointAnimation =
addPointController.drive(CurveTween(curve: Curves.ease));
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
void _incrementCounter() {
setState(() {
_counter++;
addPointController.forward(from: 0);
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Circle wave')),
backgroundColor: Colors.black,
body: Stack(
children: [
for (int i = 0; i < 3; i++)
Positioned.fill(
child: TweenAnimationBuilder(
tween: Tween<double>(begin: 0, end: 1),
duration: const Duration(milliseconds: 500),
curve: Curves.easeIn,
builder: (_, double opacity, __) {
return CustomPaint(
painter: CircleWavePainter(
controller,
addPointAnimation,
i,
colors[i].withOpacity(opacity),
_counter,
),
);
},
),
),
CounterText(counter: _counter),
],
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
class CircleWavePainter extends CustomPainter {
CircleWavePainter(
this.animation,
this.addAnimation,
this.index,
this.color,
this.count,
) : super(repaint: animation);
final Animation<double> animation;
final Animation<double> addAnimation;
final int index;
final Color color;
final int count;
static const halfPi = math.pi / 2;
static const twoPi = math.pi * 2;
final n = 7;
#override
void paint(Canvas canvas, Size size) {
final t = animation.value;
final halfWidth = size.width / 2;
final halfHeight = size.height / 2;
final q = index * halfPi;
List<Offset> computeOffsets(int length) {
final offsets = <Offset>[];
for (var i = 0; i < length; i++) {
final th = i * twoPi / length;
double os = map(math.cos(th - twoPi * t), -1, 1, 0, 1);
os = 0.125 * math.pow(os, 2.75);
final r = 165 * (1 + os * math.cos(n * th + 1.5 * twoPi * t + q));
offsets.add(Offset(
r * math.sin(th) + halfWidth, -r * math.cos(th) + halfHeight));
}
return offsets;
}
final offsets = computeOffsets(count);
if (count > 1 && addAnimation.value < 1) {
final t = addAnimation.value;
final oldOffsets = computeOffsets(count - 1);
for (var i = 0; i < count - 1; i++) {
offsets[i] = Offset.lerp(oldOffsets[i], offsets[i], t);
}
offsets[count - 1] = Offset.lerp(
oldOffsets[count - 2],
offsets[count - 1],
t,
);
}
final path = Path()..addPolygon(offsets, true);
canvas.drawPath(
path,
Paint()
..blendMode = BlendMode.lighten
..color = color
..strokeWidth = 8
..style = PaintingStyle.stroke,
);
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}

Weird performance issue in drawing path using flutter

I'm trying to draw some pointing triangles on CustomPaint and I'm facing performance issues when I change the shape of the triangle. If you execute the current code the refresh rate is fast.
If you change the part Offset(x,y+8), with Offset(x,y+4), the triangle shape changes slightly but the performance drops and the refresh rate is poor.
Does anyone knows why that happens and how can I resolve this issue?
import 'dart:math' as Math;
import 'package:flutter/material.dart';
void main() =>
runApp(
MaterialApp(
title: 'Demo',
home: Scaffold(
body: Shapes()
),
)
);
class Shapes extends StatefulWidget {
#override
_ShapesState createState() => new _ShapesState();
}
class _ShapesState extends State<Shapes> with SingleTickerProviderStateMixin{
Animation<double> animation;
AnimationController animationController;
#override
void initState() {
super.initState();
animationController = AnimationController(
vsync: this,
duration: Duration(seconds: 5),
)..addListener(() => setState(() {}));
animation = Tween<double>(
begin: 50.0,
end: 120.0,
).animate(animationController);
animationController.forward();
animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
animationController.repeat();
}
});
}
#override
Widget build(BuildContext context) {
return CustomPaint(painter: ShapesPainter());
}
}
class ShapesPainter extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
final paint = Paint();
paint.strokeWidth = 2;
for (var i = 0; i < 1000; i++) {
Math.Random rnd = new Math.Random();
double x = 5 + rnd.nextInt(300 - 5).toDouble();
double y = 5 + rnd.nextInt(1200 - 5).toDouble();
final Path path = Path();
path.addPolygon([
Offset(x+6,y+8),
Offset(x,y+-8),
Offset(x-6,y+8),
Offset(x,y+8),
], true);
paint.color = Color(0xFF3f6cbf);
paint.style = PaintingStyle.stroke;
canvas.drawPath(path, paint);
paint.color = Color.fromARGB(255, 255, 0, 0);
paint.style = PaintingStyle.fill;
canvas.drawPath(path, paint);
}
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}

Flutter Custom Painter Draw Loop [duplicate]

i am trying to achieved some thing like this in flutter
One way is with CustomPainter and an animation. Also look at SpriteWidget.
import 'dart:math';
import 'package:flutter/material.dart';
class SpritePainter extends CustomPainter {
final Animation<double> _animation;
SpritePainter(this._animation) : super(repaint: _animation);
void circle(Canvas canvas, Rect rect, double value) {
double opacity = (1.0 - (value / 4.0)).clamp(0.0, 1.0);
Color color = Color.fromRGBO(0, 117, 194, opacity);
double size = rect.width / 2;
double area = size * size;
double radius = sqrt(area * value / 4);
final Paint paint = Paint()..color = color;
canvas.drawCircle(rect.center, radius, paint);
}
#override
void paint(Canvas canvas, Size size) {
Rect rect = Rect.fromLTRB(0.0, 0.0, size.width, size.height);
for (int wave = 3; wave >= 0; wave--) {
circle(canvas, rect, wave + _animation.value);
}
}
#override
bool shouldRepaint(SpritePainter oldDelegate) {
return true;
}
}
class SpriteDemo extends StatefulWidget {
#override
SpriteDemoState createState() => SpriteDemoState();
}
class SpriteDemoState extends State<SpriteDemo>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
#override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
);
//_startAnimation();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
void _startAnimation() {
_controller
..stop()
..reset()
..repeat(period: const Duration(seconds: 1));
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Pulse')),
body: CustomPaint(
painter: SpritePainter(_controller),
child: SizedBox(
width: 200.0,
height: 200.0,
),
),
floatingActionButton: FloatingActionButton(
onPressed: _startAnimation,
child: new Icon(Icons.play_arrow),
),
);
}
}
void main() {
runApp(
MaterialApp(
home: SpriteDemo(),
),
);
}
You can also use Flare with: flare_flutter. It's more simple.
For those looking to create an animation which is not circular, but rectangular with possible rounded borders, you can replace the SpritePainter from the top answer with:
class SpritePainter extends CustomPainter {
final Animation<double> _animation;
SpritePainter(this._animation) : super(repaint: _animation);
void roundedRect(Canvas canvas, Rect rect, double animValue, int waveAmount) {
double opacity = (1.0 - (animValue / waveAmount)).clamp(0.0, 1.0);
Color color = new Color.fromRGBO(0, 117, 194, opacity);
final pixelMiltiplier = 20;
final newWidth = rect.width + animValue*pixelMiltiplier;
final newHeight = rect.height + animValue*pixelMiltiplier;
final widthIncrease = newWidth/rect.width;
final heightIncrease = newHeight/rect.height;
final widthOffset = (widthIncrease - 1) / 2;
final heightOffet = (heightIncrease - 1) / 2;
final Paint paint = new Paint()..color = color;
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(-rect.width * widthOffset, -rect.height * heightOffet,
rect.width * widthIncrease, rect.height * heightIncrease),
Radius.circular(15.0)),
paint);
}
#override
void paint(Canvas canvas, Size size) {
Rect rect = new Rect.fromLTRB(0.0, 0.0, size.width, size.height);
final waveAmount = 1;
if (!_animation.isDismissed) {
for (int wave = waveAmount-1; wave >= 0; wave--) {
roundedRect(canvas, rect, wave + _animation.value, waveAmount);
}
}
}
#override
bool shouldRepaint(SpritePainter oldDelegate) {
return true;
}
}