In Flutter, is a Custom Painter and all its class variables re-constructed from scratch every time the paint method is called? -- when setState is called in the parent?? I didn't expect that, but it seems to be the case:
I ask because I have a Custom Painter that contains an object inside the paint method (a sequence of points) that is the basis of all the subsequent painting effects. But... this object takes math in the first place just to be created... and it requires the canvas dimensions as part of that creation... which is why its inside the paint method.
So... I thought... instead of calculating the same exact skeleton shape every time paint is called, I'll make it a nullable class variable and initialize it once... then just check if its null every time in paint instead of recreating it every time.
I thought this was best practice to "move calculations out of the paint method when possible."
BUT... the unexpected result is that when I check, Flutter always says my object is null (it's recreated every time anyway).
Custom Painter:
import 'package:flutter/material.dart';
import 'dart:math';
class StackPainter02 extends CustomPainter {
final List<double> brightnessValues;
Paint backgroundPaint = Paint();
Paint segmentPaint = Paint();
List<Offset>? myShape;
StackPainter02({
required this.brightnessValues,
}) {
backgroundPaint.color = Colors.black;
backgroundPaint.style = PaintingStyle.fill;
segmentPaint.style = PaintingStyle.stroke;
}
#override
void paint(Canvas canvas, Size size) {
final W = size.width;
final H = size.height;
segmentPaint.strokeWidth = W / 100;
canvas.drawPaint(backgroundPaint);
// unfortunately, we must initialize this here because we need the view dimensions
if (myShape == null) {
myShape = _myShapePoints(brightnessValues.length, 0.8, W, H, Offset(W/2,H/2));
}
for (int i = 0; i<myShape!.length; i++) {
// bug fix... problem: using "i+1" results in index out-of-range on wrap-around
int modifiedIndexPlusOne = i;
if (modifiedIndexPlusOne == myShape!.length-1) {
modifiedIndexPlusOne = 0;
} else {
modifiedIndexPlusOne++;
}
// draw from point i to point i+1
Offset segmentStart = myShape![i];
Offset segmentEnd = myShape![modifiedIndexPlusOne];
double b = brightnessValues[i];
if (b < 0) {
b = 0; // !!!- temp debug... problem: brightness array algorithm is not perfect
}
int segmentAlpha = (255*b).toInt();
segmentPaint.color = Color.fromARGB(segmentAlpha, 255, 255, 200);
canvas.drawLine(segmentStart, segmentEnd, segmentPaint);
}
}
#override
bool shouldRepaint(covariant StackPainter02 oldDelegate) {
//return (oldDelegate.brightnessValues[32] != brightnessValues[32]); // nevermind
return true;
}
}
Build Method in Parent:
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: CustomPaint(
painter: //MyCustomPainter(val1: val1, val2: val2),
StackPainter02(
brightnessValues: brightnessValues,
),
size: Size.infinite,
),
),
);
}
PS - A ticker is used in parent and the "brightnessValues" are recalculated on every Ticker tick -> setState
Related
For some reason, my CustomPainter does not draw anything to the screen. I'm trying to build a Pie-Chart but the painter only works when I set the sweepAngle to 2*pi.
The Widget where CustomPaint is called has the following structure:
class PieChart extends StatelessWidget {
const PieChart({
Key? key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return SizedBox(
height: 160,
width: 210,
child: CustomPaint(
painter: PieChartPainter(categories: dataset, width: 10),
),
);
}
}
This is my CustomPainter class:
class PieChartPainter extends CustomPainter {
PieChartPainter({
required this.categories,
required this.width,
});
final List<Category> categories;
final double width;
#override
void paint(Canvas canvas, Size size) {
Offset center = Offset(size.width / 2, size.height / 2);
double radius = min(size.width / 2, size.height / 2);
double total = 0;
// Calculate total amount from each category
for (var expense in categories) {
total += expense.amount;
}
// The angle/radian at 12 o'clock
double startRadian = -pi / 2;
for (var index = 0; index < categories.length; index++) {
final currentCategory = categories.elementAt(index);
final sweepRadian = currentCategory.amount / total * 2 * pi;
final paint = Paint()
..style = PaintingStyle.fill
..strokeWidth = width
..color = categoryColors.elementAt(index % categories.length);
final rect = Rect.fromCenter(
center: center, width: radius * 2, height: radius * 2);
canvas.drawArc(
rect,
startRadian,
2 * pi, // should really be "sweepRadian"
false,
paint,
);
startRadian += sweepRadian;
}
}
#override
bool shouldRepaint(PieChartPainter oldDelegate) {
return true;
}
}
I can almost certainly say that the problem has nothing to do with the data and colors that I provide, because whenever I log the current elements to the console, it gives the correct output.
Here, you can see my Widget structure:
And here is an image of the component itself: (Notice that only the last category-color is shown for the entire circle, because whenever I use a sweepAngle that is less than 2*pi, the entire widget doesn't show colors.)
This is the component when I set a sweepAngle that is less than 2*pi:
I really cannot figure out what might be causing this issue. Does anyone have an Idea what else is influenced by the sweepAngle parameter? I also have no idea why the colored circles to the left of the individual categories are not visible, because they're rendered in an entirely different Widget-branch...
If you have any idea on how to solve this, I would be more than happy to provide more information but as long as I don't know where to look, I don't want to spam this issue with unnecessary information.
For anyone wondering:
The problem had to do with the "--enable-software-rendering" argument. Once I ran flutter with flutter run --enable-software-rendering, it all worked as expected.
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).
Can Flutter's inbuilt Canvas drawing methods be directly used to render variable-width strokes, for example to reflect pressure applied throughout each stroke in a handwriting app?
Ideally, in a manner compatible with saving in the XML-esque format of SVG (example at bottom).
What I think I've noticed / troubles I'm having / current attempts:
canvas.drawPath, canvas.drawPoints, canvas.drawPolygon, canvas.drawLines etc all take only a single Paint object, which can in turn have a single strokeWidth (as opposed to taking lists of Paint objects or strokeWidths, such that parameters of the path besides position could change point to point and be interpolated between).
Drawing lines, polygons or points of varying strokeWidths or radii by iterating over lists of position and pressure data and using the respective Canvas method results in no interpolation / paths not looking continuously stroked.
Screenshot from OneNote showing the behaviour I'd like:
Screenshot from the app the minimal working example below produces:
(Unoptimized) minimal working example:
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(Container(
color: Colors.white,
child: Writeable(),
));
}
class Writeable extends StatefulWidget {
#override
_WriteableState createState() => _WriteableState();
}
class _WriteableState extends State<Writeable> {
List<List<double>> pressures = List<List<double>>();
List<Offset> currentLine = List<Offset>();
List<List<Offset>> lines = List<List<Offset>>();
List<double> currentLinePressures = List<double>();
double pressure;
Offset position;
Color color = Colors.black;
Painter painter;
CustomPaint paintCanvas;
#override
Widget build(BuildContext context) {
painter = Painter(
lines: lines,
currentLine: currentLine,
pressures: pressures,
currentLinePressures: currentLinePressures,
color: color);
paintCanvas = CustomPaint(
painter: painter,
);
return Listener(
onPointerMove: (details) {
setState(() {
currentLinePressures.add(details.pressure);
currentLine.add(details.localPosition);
});
},
onPointerUp: (details) {
setState(() {
lines.add(currentLine.toList());
pressures.add(currentLinePressures.toList());
currentLine.clear();
currentLinePressures.clear();
});
},
child: paintCanvas,
);
}
}
class Painter extends CustomPainter {
Painter(
{#required this.lines,
#required this.currentLine,
#required this.color,
#required this.pressures,
#required this.currentLinePressures});
final List<List<Offset>> lines;
final List<Offset> currentLine;
final Color color;
final List<List<double>> pressures;
final List<double> currentLinePressures;
double scalePressures = 10;
Paint paintStyle = Paint();
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
// Paints here using drawPoints and PointMode.lines, but have also tried
// PointMode.points, PointMode.polygon and drawPath with a path variable and
// moveTo, lineTo methods.
#override
void paint(Canvas canvas, Size size) {
// Paint line currently being drawn (points added since pointer was
// last lifted)
for (int i = 0; i < currentLine.length - 1; i++) {
paintStyle.strokeWidth = currentLinePressures[i] * scalePressures;
canvas.drawPoints(
PointMode.lines, [currentLine[i], currentLine[i + 1]], paintStyle);
}
// Paint all completed lines drawn since app start
for (int i = 0; i < lines.length; i++) {
for (int j = 0; j < lines[i].length - 1; j++) {
paintStyle.strokeWidth = pressures[i][j] * scalePressures;
canvas.drawPoints(
PointMode.lines, [lines[i][j], lines[i][j + 1]], paintStyle);
}
}
}
}
I'm about to try writing my own implementation for rendering aesthetic SVG-friendly data from PointerEvents, but so many of the existing classes feel SVG/pretty-vectors-compatible (e.g. all the lineTos, moveTos, stroke types and endings and other parameters) that I thought it worth checking if there's something I've missed, and these methods can already do this?
Example of a few lines in an SVG file saved by Xournal++, with the stroke-width parameter changing for each line segment, and all other listed parameters presumably also having potential to change. Each line contains a moveTo command (M) and a lineTo command (L), where the latter draws a line from the current position (the last moveTo-ed or lineTo-ed), reminiscent of Flutter's segments/sub-paths and current point, to the specified offset:
<path style="fill:none;stroke-width:0.288794;stroke-linecap:round;stroke-linejoin:round;stroke:rgb(0%,100%,0%);stroke-opacity:1;comp-op:src;clip-to-self:true;stroke-miterlimit:10;" d="M 242.683594 45.519531 L 242.980469 45.476562 "/>
<path style="fill:none;stroke-width:0.295785;stroke-linecap:round;stroke-linejoin:round;stroke:rgb(0%,100%,0%);stroke-opacity:1;comp-op:src;clip-to-self:true;stroke-miterlimit:10;" d="M 242.980469 45.476562 L 243.28125 45.308594 "/>
<path style="fill:none;stroke-width:0.309105;stroke-linecap:round;stroke-linejoin:round;stroke:rgb(0%,100%,0%);stroke-opacity:1;comp-op:src;clip-to-self:true;stroke-miterlimit:10;" d="M 243.28125 45.308594 L 243.601562 45.15625 "/>
The approach seems to be 'draw a very short line, change stroke-width, draw the next very short line starting from the previous position', which I've tried to emulate with the paint method above.
How to detect drag velocity in flutter ?
I want to draw on screen in flutter using custom paint, when the velocity is less the stroke width should be less, but when the drag velocity is high the stroke width should be greater.
GestureDetector(
onPanUpdate: (DragUpdateDetails details) {
setState(
() {
RenderBox object = context.findRenderObject();
Offset _localPosition =
object.globalToLocal(details.globalPosition);
_points = List.from(_points)..add(_localPosition);
},
);
},
onPanEnd: (DragEndDetails details) => {
_deletedPoints.clear(),
_points.add(null),
// _listPoints.add(_points),
// _listPoints = List.from(_listPoints)..add(_points),
},
child: CustomPaint(
painter: Draw(points: _points),
size: Size.infinite,
),
),
The custom draw widget that extends customPainter
class Draw extends CustomPainter {
List<Offset> points;
// List<List<Offset>> points;
Draw({this.points});
#override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = brushColor
..strokeCap = StrokeCap.round
..strokeWidth = brushWidth;
for (int i = 0; i < points.length - 1; i++) {
if (points[i] != null && points[i + 1] != null) {
canvas.drawLine(points[i], points[i + 1], paint);
}
}
}
#override
bool shouldRepaint(Draw oldDelegate) => oldDelegate.points != points;
}
velocity is how much the position changed in a given time.
for this purpose you have the details.delta.distance in the onPanUpdate callback, which returns a double indicating how much the pointer has moved since last update, the bigger it is, the larger the velocity.
in your case, you can change the stroke width based on the distance traveled.
I haven't used any of these myself but you could look into
DragUpdateDetails. details in onPanUpdate has an Offset object called delta. Which are coordinates which update every time onPanUpdate is called.
https://api.flutter.dev/flutter/gestures/DragUpdateDetails-class.html
There's also a class called VelocityTracker
https://api.flutter.dev/flutter/gestures/VelocityTracker-class.html
Hope this helps you a bit
[Update] a practical example
late final Ticker _velocityTicker;
final List<int> _velocityRecs = List.filled(6, 0, growable: true);
double _rec = 0;
get _deltaSum {
var sum = 0;
for (var i = 1; i < _velocityRecs.length; i++) {
sum += (_velocityRecs[i] - _velocityRecs[i - 1]).abs();
}
return sum;
}
/// in initState()
_velocityTicker = Ticker((duration) {
_velocityRecs.removeAt(0);
// add whatever values you want to track
_velocityRecs.add(_currentIndex);
// You get a 'velocity' here, do something you want
if (_deltaSum > 2) {
// _offsetYController.forward();
} else if (_deltaSum < 1) {
// _offsetYController.reverse();
}
});
/// then .start()/.stop() the ticker in your event callback
/// Since each ticker has a similar time interval,
/// for Simplicity, I did not use duration here,
/// if you need a more accurate calculation value,
/// you may need to use it
In short, You can't.
Although details.delta and VelocityTracker are mentioned above, neither of them technically has access to the user's PointerEvent velocity,
For example, if the user drags the slider and stops, but the finger does not leave the screen, the velocity should be about 0, but this state can't be captured in flutter.
velocityTracker doesn't help
If you want to implement it yourself, one idea is to create a Timer/Ticker, record the last value and put it into a list, then set it to zero, and each time you want get, the sum of the whole list will be averaged to get the velocity, you need to be careful to determine how much the length of your List is appropriate
I am testing the performance in drawing using Flutter. I am using Path to draw line between each point detected by Listener because I have read that the performance would increase using it. I am using Listener because I tried also the Apple Pencil on iPad 2017 by changing the kind property to stylus.
The problem is that I was hoping to get a response in the stroke design similar to notability, it seems much slower, acceptable but not as much as I would like.
So I'm looking for tips to increase performance in terms of speed.
At the following link they recommended using NotifyListener(), but I didn't understand how to proceed. If it really improves performance I would like even an example to be able to implement it.
If Flutter has some limitations when it comes to drawing with your fingers or with a stylus then let me know.
performance issue in drawing using flutter
import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart';
class DrawWidget extends StatefulWidget {
#override
_DrawWidgetState createState() => _DrawWidgetState();
}
class _DrawWidgetState extends State<DrawWidget> {
Color selectedColor = Colors.black;
double strokeWidth = 3.0;
List<MapEntry<Path, Object>> pathList = List();
StrokeCap strokeCap = (Platform.isAndroid) ? StrokeCap.butt : StrokeCap.round;
double opacity = 1.0;
Paint pa = Paint();
#override
Widget build(BuildContext context) {
return Listener(
child: CustomPaint(
size: Size.infinite,
painter: DrawingPainter(
pathList: this.pathList,
),
),
onPointerDown: (details) {
if (details.kind == PointerDeviceKind.touch) {
print('down');
setState(() {
Path p = Path();
p.moveTo(details.localPosition.dx, details.localPosition.dy);
pa.strokeCap = strokeCap;
pa.isAntiAlias = true;
pa.color = selectedColor.withOpacity(opacity);
pa.strokeWidth = strokeWidth;
pa.style = PaintingStyle.stroke;
var drawObj = MapEntry<Path,Paint>(p, pa);
pathList.add(drawObj);
});
}
},
onPointerMove: (details) {
if (details.kind == PointerDeviceKind.touch) {
print('move');
setState(() {
pathList.last.key.lineTo(details.localPosition.dx, details.localPosition.dy);
});
}
},
/*onPointerUp: (details) {
setState(() {
});
},*/
);
}
}
class DrawingPainter extends CustomPainter {
DrawingPainter({this.pathList});
List<MapEntry<Path, Object>> pathList;
#override
void paint(Canvas canvas, Size size) {
for(MapEntry<Path, Paint> m in pathList) {
canvas.drawPath(m.key, m.value);
}
}
#override
bool shouldRepaint(DrawingPainter oldDelegate) => true;
}
I think you should not use setState, rather use state management like Bloc or ChangeNotifier or smth.
Also, just drawing a path with this:
canvas.drawPath(m.key, m.value);
Works for only small stroke widths, it leaves a weird-looking line full of blank spaces when drawing.
I implemented this by using Bloc that updates the UI based on the gesture functions (onPanStart, onPanEnd, onPanUpdate). It holds a List of a data model that I called CanvasPath that represents one line (so from onPanStart to onPanEnd events), and it holds the resulting Path of that line, list of Offsets and Paint used to paint it.
In the end paint() method draws every single Path from this CanvasPath object and also a circle in every Offset.
` for every canvasPath do this:
canvas.drawPath(canvasPath.path, _paint);
for (int i = 0; i < canvasPath.drawPoints.length - 1; i++) {
//draw Circle on every Offset of user interaction
canvas.drawCircle(
canvasPath.drawPoints[i],
_raidus,
_paint);
}`
I made a blog about this, where it is explained in much more details:
https://ivanstajcer.medium.com/flutter-drawing-erasing-and-undo-with-custompainter-6d00fec2bbc2