I needed a loading widget that draws the moving sine and cosine functions into a canvas. I coded it with no problem using a CustomPaint widget and a CustomPainter, but when I profile it, i Have discovered it runs on about 49fps, and not on 60fps. The UI thread is working good, taking about 6ms for each frame, but the Raster thread is taking longer. I have tried painting less points on the canvas (doing i=i+5 instead of i++ on the for loop), but the result is quite the same.
¿Can somebody suggest me an idea on how could I improve the performance?. The widget code is below, and so is the DevTools screenshot of what the Raster thread is doing in every frame, in case it can be useful.
import 'dart:math';
import 'package:flutter/material.dart';
class LoadingChart extends StatefulWidget{
final Color color1;
final Color color2;
final double lineWidth;
final bool line;
final Size size;
const LoadingChart({
#required this.color1,
#required this.color2,
#required this.size,
#required this.lineWidth,
this.line = true,
Key key
}): super(key: key);
#override
State<StatefulWidget> createState() => _LoadingChartState();
}
class _LoadingChartState extends State<LoadingChart>
with SingleTickerProviderStateMixin{
AnimationController _controller;
double randomHeight(Random random, double max){
return random.nextDouble()*max;
}
#override
void initState() {
_controller = AnimationController(vsync: this, duration: Duration(seconds: 1));
_controller.addListener(() {setState(() {});});
_controller.repeat();
super.initState();
}
#override
void dispose(){
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return SizedBox(
height: widget.size.height,
width: widget.size.width,
child: CustomPaint(
painter: PathPainter(
color1: widget.color1,
color2: widget.color2,
value: _controller.value,
line: widget.line,
),
)
);
}
}
class PathPainter extends CustomPainter {
final Color color1;
final Color color2;
final double lineWidth;
final bool line;
final double value;
PathPainter({
#required this.value,
this.color1=Colors.red,
this.color2=Colors.green,
this.line = true,
this.lineWidth=4.0,
}): super();
#override
void paint(Canvas canvas, Size size) {
final height = size.height;
final width = size.width;
Paint paint1 = Paint()
..color = color1
..style = PaintingStyle.stroke
..strokeWidth = lineWidth;
Paint paint2 = Paint()
..color = color2
..style = PaintingStyle.stroke
..strokeWidth = lineWidth;
Path path1 = Path();
Path path2 = Path();
/* If line is true, draw sin and cos functions, otherwise, just some points */
for (double i = 0; i < width; i=i+5){
double f = i*2*pi/width + 2*pi*value;
double g = i*2*pi/width - 2*pi*value;
if (i == 0){
path1.moveTo(0, height/2 + height/6*sin(f));
path2.moveTo(0, height/2 + height/6*cos(g));
continue;
}
path1.lineTo(i, height/2 + height/6*sin(f));
path2.lineTo(i, height/2 + height/6*cos(g));
}
/* Draw both lines */
canvas.drawPath(path1, paint1);
canvas.drawPath(path2, paint2);
}
#override
bool shouldRepaint(PathPainter oldDelegate) {
return oldDelegate.value != value || oldDelegate.color1 != color1
|| oldDelegate.color2 != color2 || oldDelegate.line != line
|| oldDelegate.lineWidth != lineWidth;
}
}
PS: I'm running the app on profile mode so that shouldn't be the problem. Also I wanted to mention that it's the only widget being redrawn on the screen.
Thanks a lot!!
CustomPainter can receive a listenable so maybe you can use the animation controller there to update it with every tick
class _LoadingChartState extends State<LoadingChart>
with SingleTickerProviderStateMixin{
AnimationController _controller;
double randomHeight(Random random, double max){
return random.nextDouble()*max;
}
#override
void initState() {
_controller = AnimationController(vsync: this, duration: Duration(seconds: 1));
//_controller.addListener(() {setState(() {});}); no need to setState
_controller.repeat();
super.initState();
}
#override
void dispose(){
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return SizedBox(
height: widget.size.height,
width: widget.size.width,
child: CustomPaint(
willChange: true, //this can help (Whether the raster cache should be told that this painting is likely)
painter: PathPainter(
color1: widget.color1,
color2: widget.color2,
line: widget.line,
listenable: _controller //pass the controller as it is (An animationController extends a Listenable)
),
)
);
}
}
And in PathPainter you give to the constructor the listenable and pass it to the CustomPainter constructor that accepts a listenable called repaint
class PathPainter extends CustomPainter {
final Animation listenable;
final Color color1;
final Color color2;
final double lineWidth;
final bool line;
PathPainter({
this.listenable,
this.color1=Colors.red,
this.color2=Colors.green,
this.line = true,
this.lineWidth=4.0,
}): super(repaint: listenable); //don't forget calling the CustomPainter constructor with super
#override
void paint(Canvas canvas, Size size) {
double value = listenable.value; // get its value here
final height = size.height;
final width = size.width;
Paint paint1 = Paint()
..color = color1
..style = PaintingStyle.stroke
..strokeWidth = lineWidth;
Paint paint2 = Paint()
..color = color2
..style = PaintingStyle.stroke
..strokeWidth = lineWidth;
Path path1 = Path();
Path path2 = Path();
/* If line is true, draw sin and cos functions, otherwise, just some points */
for (double i = 0; i < width; i=i+5){
double f = i*2*pi/width + 2*pi*value;
double g = i*2*pi/width - 2*pi*value;
if (i == 0){
path1.moveTo(0, height/2 + height/6*sin(f));
path2.moveTo(0, height/2 + height/6*cos(g));
continue;
}
path1.lineTo(i, height/2 + height/6*sin(f));
path2.lineTo(i, height/2 + height/6*cos(g));
}
/* Draw both lines */
canvas.drawPath(path1, paint1);
canvas.drawPath(path2, paint2);
}
#override
bool shouldRepaint(PathPainter oldDelegate) {
//delete the oldDelegate.value, it doesn't exists anymore
return oldDelegate.color1 != color1
|| oldDelegate.color2 != color2 || oldDelegate.line != line
|| oldDelegate.lineWidth != lineWidth;
}
}
I'm in debug mode so I expect you get a better performance in profile mode
Related
I can change both ends to square or rounded using the strokeCap property but can't figure out how to apply tailored settings for each end (ie one end rounded and one square).
How can I achieve this effect?
import 'package:flutter/material.dart';
class LinePainter extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
var height = size.height;
var width = size.width;
var paint = Paint()
..color = Colors.red
..strokeWidth = 20
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
var path = Path();
path.moveTo(width * 0.25, height / 2);
path.lineTo(width * 0.75, height / 2);
canvas.drawPath(path, paint);
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}
class Example extends StatefulWidget {
const Example({Key? key}) : super(key: key);
#override
State<Example> createState() => _ExampleState();
}
class _ExampleState extends State<Example> with SingleTickerProviderStateMixin {
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomPaint(
painter: LinePainter(),
child: Container(),
),
);
}
}
There is no existing PaintingStyle or StrokeCap option for setting only one cap, currently both caps are controlled the same.
If you just want the rounded cap at one end, an alternative would be to draw the line with no caps, then draw a circle overlapping the end. This would only work for solid colors though.
#override
void paint(Canvas canvas, Size size) {
var height = size.height;
var width = size.width;
var thickness = 20;
var capRadius = thickness * 0.5 ;
//Line paint, is set to stroke
var linePaint = Paint()
..color = Colors.red
..strokeWidth = thickness
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.butt; //butt is default, no caps
//Cap paint, is set to fill by default
var capPaint = Paint()
..color = Colors.red;
//Draw line
var path = Path();
path.moveTo(width * 0.25, height / 2);
path.lineTo(width * 0.75, height / 2);
canvas.drawPath(path, linePaint);
//Draw cap
canvas.drawCircle( Offset(width * 0.75, height / 2), capRadius, capPaint );
}
This is an image I found on Stackoverflow only and I want a Slider shaped like this. I tried packages like sleek_circular_slider and other packages but I was not able to create this shape.
I can create this shape in CustomPainter and when I tried to use SliderTrackShape to customize the SLider Shape it didn't worked.
class CustomSlider extends SliderTrackShape {
#override
Rect getPreferredRect({
RenderBox? parentBox,
Offset offset = Offset.zero,
SliderThemeData? sliderTheme,
bool? isEnabled,
bool? isDiscrete,
}) {
final double thumbWidth =
sliderTheme!.thumbShape!.getPreferredSize(true, isDiscrete!).width;
final double? trackHeight = sliderTheme.trackHeight;
assert(thumbWidth >= 0);
assert(trackHeight! >= 0);
assert(parentBox!.size.width >= thumbWidth);
assert(parentBox!.size.height >= trackHeight!);
final double trackLeft = offset.dx + thumbWidth / 2;
final double trackTop =
offset.dy + (parentBox!.size.height - trackHeight!) / 2;
final double trackWidth = parentBox.size.width - thumbWidth;
return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
}
#override
void paint(
PaintingContext context,
Offset offset, {
RenderBox? parentBox,
SliderThemeData? sliderTheme,
Animation<double>? enableAnimation,
TextDirection? textDirection,
Offset? thumbCenter,
bool? isDiscrete,
bool? isEnabled,
}) {
if (sliderTheme!.trackHeight == 0) {
return;
}
final Rect trackRect = getPreferredRect(
parentBox: parentBox,
offset: offset,
sliderTheme: sliderTheme,
isEnabled: isEnabled,
isDiscrete: isDiscrete,
);
final Paint fillPaint = Paint()
..color = sliderTheme.activeTrackColor!
..style = PaintingStyle.fill;
final Paint borderPaint = Paint()
..color = Colors.black
..strokeWidth = 4.0
..style = PaintingStyle.stroke;
final pathSegment = Path()
..moveTo(0, 2500)
..quadraticBezierTo(250 / 2, 250, 250, 250 / 2);
context.canvas.drawPath(pathSegment, fillPaint);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
This code renders just the thumb shape and no slider when I use this widget inside tracakShape for SliderTheme. But when I use similar code for CustomShape I am able to create this shape successfully like this :
class CustomSlider extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
var paint = Paint();
paint.color = Color(0xff84CADE);
paint.style = PaintingStyle.stroke;
paint.strokeWidth = 4.0;
Path path = Path();
path.moveTo(0, size.height / 2);
path.quadraticBezierTo(size.width / 2, size.height, size.width, size.height / 2);
canvas.drawPath(path, paint);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
So I guess my question is how can I render this shape that I am succesfully rendering when I am extending from SliderTrackShape as I am unable to do so when trying to extend from SliderTrackShape class and create this shape.
I'm trying to draw this kind of shape with flutter:
Expected result
So far I can draw an arc using drawArc():
class CurvePainter extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
var paint = Paint();
paint.color = Colors.green;
paint.style = PaintingStyle.fill;
paint.strokeWidth = 5;
final rect = Rect.fromLTRB(50, 100, 130, 200);
final startAngle = -pi;
final sweepAngle = pi;
canvas.drawArc(rect, startAngle, sweepAngle, false, paint);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
But is that the right way to do it or should I use quadraticBezierTo and drawPath?
Help me in drawing the zigZag line/border as show in the image at bottom. I found a zigzag paint function in flutter doc https://api.flutter.dev/flutter/painting/paintZigZag.html but not sure how to use it.
You need to use CustomPaint() Widget.
Create your own CustomerPainter, say MyPainter, and place this in your widget tree:
CustomPaint(
size: MediaQuery.of(context).size,
painter: MyPainter(),
),
The class MyPainter should extend CustomPainter and also should override the paint() method which is where you specify the path to be painted on the canvas.
So you can just put the above 'paintZigZag' code there or call it like below along with appropriate parameters.
import 'package:flutter/material.dart';
import 'dart:math' as math;
class MyPainter extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
var paint = Paint();
paint.color = Colors.blue;
paint.style = PaintingStyle.fill;
paintZigZag(canvas, paint, Offset(0, 100), Offset(200, 100), 100, 5);
}
void paintZigZag(Canvas canvas, Paint paint, Offset start, Offset end,
int zigs, double width) {
assert(zigs.isFinite);
assert(zigs > 0);
canvas.save();
canvas.translate(start.dx, start.dy);
end = end - start;
canvas.rotate(math.atan2(end.dy, end.dx));
final double length = end.distance;
final double spacing = length / (zigs * 2.0);
final Path path = Path()..moveTo(0.0, 0.0);
for (int index = 0; index < zigs; index += 1) {
final double x = (index * 2.0 + 1.0) * spacing;
final double y = width * ((index % 2.0) * 2.0 - 1.0);
path.lineTo(x, y);
}
path.lineTo(length, 0.0);
canvas.drawPath(path, paint);
canvas.restore();
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
MyPainter class
class MyPainter extends CustomPainter {
Paint _paint;
**Constructor**
MyPainter(){
_paint = Paint()
..color = Colors.indigo;
}
#override
void paint(Canvas canvas, Size size){
draw one rectangle (size, color, style)
var rect1 = Rect.fromLTWH(0, 0, 20.0,20.0);
color
_paint.color = Color(0xffACCAF6);
style
_paint.style = PaintingStyle.fill;
draw rectangle
canvas.drawRect(rect1, _paint);
}
bool shouldRepaint
#override
bool shouldRepaint(CustomPainter oldDelegate)
return oldDelegate != this;
}
}
img