How to get offset of canvas in CustomPainter's paint function? - flutter

lets say that we have a background image:
I want to render few views with shared background:
I think that I can use CustomPainter and draw my background image translated by canvas offset, but I don't know how to get that property inside paint function:
class PanelBackgroundPainter extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
var rect = Offset.zero & size;
canvas.clipRect(rect);
canvas.drawImage(image, new Offset(-canvasOffsetX, -canvasOffsetY), new Paint());
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
How can I calculate canvasOffsetX and canvasOffsetY?
I am using rows and columns to lay out my panels:
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: new Column(
children: <Widget>[
new Expanded(
child: new Container(
color: new Color.fromARGB(255, 128, 128, 128),
),
flex: 100,
),
new Container(height: 10.0),
new Container(
color: new Color.fromARGB(255, 128, 128, 128),
height: 120.0,
),
new Container(height: 10.0),
new Expanded(
child: new Container(
child: new Row(
children: <Widget>[
new Expanded(
child: new Container(
color: new Color.fromARGB(255, 128, 128, 128),
),
flex: 50,
),
new Container(width: 10.0),
new Expanded(
child: new Container(
color: new Color.fromARGB(255, 128, 128, 128),
),
flex: 50,
),
],
),
),
flex: 200,
),
],
),
);
}
}

While I don't know that it is necessarily a great idea, it is possible. The best explanation is probably just to look at this code:
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
class SharedBackgroundPainter extends CustomPainter {
final ui.Image image;
final RenderBox renderBox;
final RenderBox parentRender;
SharedBackgroundPainter({#required this.image, #required this.renderBox, #required this.parentRender});
#override
void paint(Canvas canvas, Size size) {
var rect = ui.Offset.zero & size;
var globalThisTopLeft = renderBox.localToGlobal(new ui.Offset(0.0, 0.0), ancestor: parentRender);
canvas.clipRect(rect);
canvas.drawImage(image, new Offset(-globalThisTopLeft.dx, -globalThisTopLeft.dy), new Paint());
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
class BackgroundBuilder extends StatelessWidget {
final Size size;
final ui.Image image;
final RenderBox parentRender;
const BackgroundBuilder({Key key, #required this.size, #required this.image, #required this.parentRender})
: super(key: key);
#override
Widget build(BuildContext context) {
return new LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
RenderBox box = context.findRenderObject();
assert(box != null);
return new CustomPaint(
size: new Size(constraints.maxWidth, constraints.maxHeight),
painter: new SharedBackgroundPainter(image: image, renderBox: box, parentRender: parentRender),
);
});
}
}
class SharedBackgroundBuilder extends StatelessWidget {
final ui.Image toDraw;
final Size size;
final RenderBox parentRender;
const SharedBackgroundBuilder({Key key, #required this.toDraw, #required this.size, #required this.parentRender})
: super(key: key);
#override
Widget build(BuildContext context) {
return new Column(
children: <Widget>[
new Expanded(
child: new BackgroundBuilder(
image: toDraw,
size: size,
parentRender: parentRender,
),
flex: 100,
),
new SizedBox(height: 10.0),
new SizedBox(
height: 120.0,
child: new BackgroundBuilder(
image: toDraw,
size: size,
parentRender: parentRender,
),
),
new SizedBox(height: 10.0),
new Expanded(
child: new Container(
child: new Row(
children: <Widget>[
new Expanded(
child: new BackgroundBuilder(
image: toDraw,
size: size,
parentRender: parentRender,
),
flex: 50,
),
new Container(width: 10.0),
new Expanded(
child: new BackgroundBuilder(
image: toDraw,
size: size,
parentRender: parentRender,
),
flex: 50,
),
],
),
),
flex: 200,
),
],
);
}
}
/// This is to resize an image to the size of the overall structure. Note that this will
/// only work in an environment where LayoutBuilder can find constraints.
class ImageResizingPainter extends CustomPainter {
final ui.Image image;
final BoxFit boxfit;
ImageResizingPainter(this.image, this.boxfit);
#override
void paint(ui.Canvas canvas, ui.Size size) {
final ui.Rect rect = ui.Offset.zero & size;
final Size imageSize = new Size(image.width.toDouble(), image.height.toDouble());
FittedSizes sizes = applyBoxFit(boxfit, imageSize, size);
// if you don't want it centered for some reason change this.
final Rect inputSubrect = Alignment.center.inscribe(sizes.source, Offset.zero & imageSize);
final Rect outputSubrect = Alignment.center.inscribe(sizes.destination, rect);
canvas.drawImageRect(image, inputSubrect, outputSubrect, new Paint());
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
class ImageResizer extends StatelessWidget {
final ui.Image image;
final BoxFit boxFit;
// if you want a different boxfit you can set it. See BoxFit documentation.
const ImageResizer({Key key, #required this.image, this.boxFit = BoxFit.cover}) : super(key: key);
ui.Image getResizedImage(Size size) {
var pictureRecorder = new ui.PictureRecorder();
Canvas canvas = new Canvas(pictureRecorder);
CustomPainter painter = new ImageResizingPainter(image, boxFit);
painter.paint(canvas, size);
return pictureRecorder.endRecording().toImage(size.width.floor(), size.height.floor());
}
Widget build(BuildContext context) {
return new LayoutBuilder(builder: (context, constraints) {
RenderBox box = context.findRenderObject();
var size = new Size(constraints.maxWidth, constraints.maxHeight);
// This might not be a good idea to do in the build function, but we're going to ignore that
// and do it anyways.
ui.Image resizedImage = getResizedImage(size);
return new SharedBackgroundBuilder(toDraw: resizedImage, size: size, parentRender: box);
});
}
}
class ImageDrawer extends CustomPainter {
final ui.Image image;
ImageDrawer(this.image);
#override
void paint(ui.Canvas canvas, ui.Size size) {
canvas.drawImage(image, ui.Offset.zero, new Paint());
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
/// This is just to build an image to display. I'm too lazy to load one =D
class GradientPainter extends CustomPainter {
#override
void paint(ui.Canvas canvas, ui.Size size) {
Rect rect = ui.Offset.zero & size;
var linearGradient = new LinearGradient(
colors: [Colors.red, Colors.green],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
var shader = linearGradient.createShader(rect);
canvas.drawRect(rect, new Paint()..shader = shader);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
class GradientGetter extends StatelessWidget {
// I'm too lazy to create an image so I'm just drawing a gradient
ui.Image getGradientImage(Size size) {
var pictureRecorder = new ui.PictureRecorder();
Canvas canvas = new Canvas(pictureRecorder);
GradientPainter painter = new GradientPainter();
painter.paint(canvas, size);
return pictureRecorder.endRecording().toImage(size.width.floor(), size.height.floor());
}
#override
Widget build(BuildContext context) {
return new LayoutBuilder(builder: (context, constraints) {
var size = new Size(constraints.maxWidth, constraints.maxHeight);
// remove the / 20 to get it smooth again, it's just there to test
// out the scaling.
var changedSize = size / 20.0;
ui.Image gradientImage = getGradientImage(changedSize);
assert(gradientImage != null);
return new ImageResizer(
image: gradientImage,
);
});
}
}
/// This is just so that you can copy/paste this and have it run.
void main() => runApp(new MyApp());
class MyApp extends StatefulWidget {
#override
State<StatefulWidget> createState() => MyAppState();
}
class MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: new Padding(
padding: const EdgeInsets.all(30.0),
child: new GradientGetter(),
));
}
}
As I went through it I realized that it needs to be more complex than I originally thought - you have to deal with scaling the image to the size of the container etc. This does all that now, although I haven't tested it super extensively.
Also note that this gives no consideration to performance - it's almost for sure bad performance-wise.

Related

How to paint a location icon in flutter?

I am trying to paint a icon with green color. I am using custom painter class.
Sample code:
import 'dart:ui';
import 'package:flutter/material.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
#override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
double x = 0.0;
double y = 0.0;
List<Offset> mark = [];
void updatePosition(TapDownDetails details) {
x = details.localPosition.dx;
y = details.localPosition.dy;
setState(() {
mark.add(Offset(x, y));
print("mark:$mark");
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: GestureDetector(
onTapDown: updatePosition,
child: RepaintBoundary(
child: Container(
margin: const EdgeInsets.only(left: 8, right: 8),
height: 300,
width: double.infinity,
decoration: BoxDecoration(
border: Border.all(color: Colors.blue),
image: const DecorationImage(
image: AssetImage(
'assets/dark.jpg',
),
fit: BoxFit.cover)),
child: CustomPaint(
painter: MyCustomPainter(mark),
),
)),
),
),
);
}
}
class MyCustomPainter extends CustomPainter {
final List<Offset> marks;
const MyCustomPainter(this.marks);
#override
void paint(Canvas canvas, Size size) async {
Paint paint = Paint()
..color = Colors.green
..strokeCap = StrokeCap.round
..strokeWidth = 15.0;
canvas.drawPoints(PointMode.points, marks, paint);
}
#override
bool shouldRepaint(MyCustomPainter oldDelegate) {
return true;
}
}
It's helped me to draw a points list. How can I draw a list of location icon instead of green points?

How to Make Widget Flutter Fast?

I'm trying to make my own photo codecs, I made a 512 * 512 image,
I'm just trying to build with one color and arrange in a Container in Column and Row
My Code:
SizedBox(
height: 512,
width: 512,
child: Column(
children: List.generate(512, (index) {
return Row(
children: List.generate(512, (index) {
return Container(
height: 1,
width: 1,
color: Colors.blue,
);
}),
);
}),
),
),
I tried this code, it is very slow,
So how to build flutter widget fast?
You have to use CustomPainter class to draw your own custom widget
As said by powerman23rus said, you should use a CustomPainter, here's an example of implementation based on the code you've provided:
class ImageWidget extends StatelessWidget {
final Color color;
final Size size;
const ImageWidget({
super.key,
this.color = Colors.blue,
this.size = const Size(512, 512),
});
#override
Widget build(BuildContext context) {
return CustomPaint(
painter: ImagePainter(color: color),
size: size,
);
}
}
class ImagePainter extends CustomPainter {
final Color color;
ImagePainter({required this.color});
#override
void paint(Canvas canvas, Size size) {
final paint = Paint()..color = color;
canvas.drawRect(Offset.zero & size, paint);
}
#override
bool shouldRepaint(ImagePainter oldDelegate) => false;
}
You can try the full example on DartPad to check by yourself the performance.

CustomPainter together with Paint()..blendMode gives weird blinking effect while scrolling

After launching the application, the green circle is not visible. In order for the circle to appear, you need to scroll up. Then if you scroll down, the circle starts blinking as long as the scrolloffset is within about 20 pixels. If the scrolloffset is larger than 20 pixels, the circle disappears and appears only when the scrolloffset is 0.
If wrap the widget in RepaintBoundary, then the circle appears on any scroll and then does not disappear.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class DemoPainter extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
canvas.save();
final path = Path()..addOval(Rect.fromCircle(center: Offset.zero, radius: 50));
final paint = Paint()..color = Colors.green;
paint.blendMode = BlendMode.dstOver;
canvas.drawPath(path, paint);
canvas.restore();
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
class TestWidget extends StatelessWidget {
const TestWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return SizedBox(
height: 80,
child: Center(
child: CustomPaint(
painter: DemoPainter(),
child: Text(
"Child widget",
style: TextStyle(color: Colors.blue[200], fontWeight: FontWeight.bold, fontSize: 20),
),
),
));
}
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(primarySwatch: Colors.blue),
home: Scaffold(
body: SingleChildScrollView(
child: Column(
children: [
const TestWidget(),
const TestWidget(),
const TestWidget(),
const TestWidget(),
const RepaintBoundary(child: TestWidget()),
Container(
height: 2000,
color: Colors.red,
)
],
),
),
),
);
}
}
The link to Dartpad, but i can't reproduce the effect on the web platform.
What could be the reason for such effect and how to get rid of it?

Flutter How to get size of dynamic widget

What I've done:
The black rectangle is the size of the canvas.
const double radius = 50;
class TableShape extends StatelessWidget {
final String name;
final Color color;
const TableShape({
Key? key,
required this.name,
required this.color,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap:(){debugPrint("ok");},
child: LayoutBuilder(
builder: (context, constraints) {
final maxWidth = constraints.maxWidth;
final textPainter = TextPainter(
text: TextSpan(
text: name,
style: const TextStyle(fontFamily: 'Graphik', fontSize: 30, color: Colors.white),
),
textDirection: TextDirection.ltr,
textAlign: TextAlign.center
);
textPainter.layout(maxWidth: maxWidth);
return CustomPaint(
size: Size(textPainter.width>radius*2?textPainter.width:radius*2, radius*2),
painter: MyPainter(color: color, txt: textPainter),
);
})
);
}
}
class MyPainter extends CustomPainter {
TextPainter txt;
Color color;
MyPainter({
required this.txt,
required this.color,
});
#override
void paint(Canvas canvas, Size size) {
canvas.clipRect(Rect.fromLTWH(0, 0, size.width, size.height));
var paint = Paint()..color = color;
bool txtLarger = txt.width>radius*2;
canvas.drawCircle(Offset(txtLarger?txt.width/2:radius,radius), radius, paint);
//table name:
txt.paint(canvas, Offset(txtLarger?0:radius-txt.width/2,radius-txt.height/2));
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
#override
bool hitTest(Offset position) {
return sqrt(pow(txt.width/2-position.dx,2)+pow(radius-position.dy,2)) <= radius;
}
}
I need to get the width because I place the widget on my screen according to its width. The width is dynamic: the bigger the text, the wider the canvas. Is it possible ? Or maybe you have an other approach to get this widget than the way I did ?
get widget size by global key:
final GlobalKey _widgetKey = GlobalKey();
Size _getSize(GlobalKey key){
final State state = key.currentState;
final BuildContext context = key.currentContext;
final RenderBox box = state.context.findRenderObject();
return context.size;
}
Widget build(BuildContext context) {
return GestureDetector(
key: _widgetKey,
onTap:(){_getSize(_widgetKey);},
child: LayoutBuilder(
builder: (context, constraints) {
Use GlobalKey to find RenderBox then get the size. Remember you need to make sure the widget was rendered.
Example:
import 'package:flutter/material.dart';
void main() => runApp(App());
class App extends StatelessWidget {
#override
Widget build(BuildContext context) => const MaterialApp(home: Home());
}
class Home extends StatefulWidget {
const Home({Key? key}) : super(key: key);
#override
HomeState createState() => HomeState();
}
class HomeState extends State<Home> {
var key = GlobalKey();
Size? redboxSize;
#override
void initState() {
WidgetsBinding.instance?.addPostFrameCallback((_) {
setState(() {
redboxSize = getRedBoxSize(key.currentContext!);
});
});
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Example')),
body: Column(
children: [
SizedBox(
height: 100,
child: Center(
child: Container(
key: key,
child: const Text('Hello oooooooooooooooo'),
color: Colors.redAccent,
),
),
),
if (redboxSize != null) Text('Redbox size: $redboxSize')
],
),
);
}
Size getRedBoxSize(BuildContext context) {
final box = context.findRenderObject() as RenderBox;
return box.size;
}
}

Can a child RenderObject take a parent transformation into account?

I have implemented a 'Marker' that uses a GlobalKey to find the location of a Container and draws a blue box around it. This works great as long as no transformation is taking place (e.g. FittedBox with scaleDown). But if there is, the blue box appears at an incorrect position.
Does anyone know how to account for the transformation to make the blue box appear in the correct location?
Full code example:
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(const MyApp());
}
var boxKey = GlobalKey();
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Center(
child: SizedBox(
height: 100,
width: 100,
child: FittedBox(
fit: BoxFit.scaleDown,
child: Container(
height: 200,
width: 200,
color: Colors.yellow,
child: Stack(
children: [
const Marker(),
Align(
alignment: const Alignment(-0.5, -0.5),
child: Container(
key: boxKey,
width: 30,
height: 30,
color: Colors.red,
)),
],
),
),
),
),
),
);
}
}
class Marker extends LeafRenderObjectWidget {
const Marker({Key? key}) : super(key: key);
#override
RenderMarker createRenderObject(BuildContext context) {
return RenderMarker();
}
#override
void updateRenderObject(BuildContext context, RenderMarker renderObject) {}
}
class RenderMarker extends RenderProxyBox {
#override
void performLayout() {
super.performLayout();
}
#override
void paint(PaintingContext context, Offset offset) {
super.paint(context, offset);
var renderBox = boxKey.currentContext!.findRenderObject() as RenderBox;
var center = renderBox.localToGlobal(Offset.zero) +
Offset(renderBox.size.width / 2, renderBox.size.height / 2);
context.canvas.drawRect(
Rect.fromCenter(center: center, width: 50, height: 50),
Paint()..color = Colors.blue);
}
}
I found a working solution:
#override
void paint(PaintingContext context, Offset offset) {
super.paint(context, offset);
var renderBox = boxKey.currentContext!.findRenderObject() as RenderBox;
var p = parent! as RenderObject;
var parentOffset = renderBox.localToGlobal(Offset.zero, ancestor: p);
var center = parentOffset +
Offset(renderBox.size.width / 2, renderBox.size.height / 2);
context.canvas.drawRect(
Rect.fromCenter(center: center + offset, width: 50, height: 50),
Paint()..color = Colors.blue);
}
Notice how I calculate everything in relation to the parent now.