Reputation: 425
I need to render my custom object inside of a ListTile with custom painter in order to draw some custom text.
ListTile(
title: CustomPaint(
painter: RowPainter.name(
_titleFontSelected,
_titleFont,
text,
index,
MediaQuery.of(context),
currentRow,
),
),
);
Inside my RowPainter
I draw the text with the font selected.
When the row is too large, it automatically wraps and get drawn outside the given paint size.
void paint(Canvas canvas, Size size)
I like this behavior, but how can I resize the height of my paint area? Because this is a problem since this overlaps the next List row.
I know that the CustomPaint
has a property Size
settable, but I know the text dimension only inside my paint function using the TextPainter getBoxesForSelection
but it's too late.
How can I "resize" my row painter height dynamically if the text wraps?
Upvotes: 8
Views: 10150
Reputation: 1470
Use SingleChildRenderObjectWidget
and RenderBox
instead. Full simple example with dynamic resizing.
import 'dart:async';
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
home: Scaffold(
body: Center(
child: Column(
children: [
SizedBox(height: 100,),
Text('I am above'),
MyWidget(),
Text('I am below')
],
),
),
),
));
}
class MyWidget extends SingleChildRenderObjectWidget {
@override
MyRenderBox createRenderObject(BuildContext context) {
return MyRenderBox();
}
}
class MyRenderBox extends RenderBox {
double myHeight = 200;
@override
void paint(PaintingContext context, Offset offset) {
Paint paint = Paint()
..color = Colors.black..style = PaintingStyle.fill;
context.canvas.drawRect(
Rect.fromLTRB(offset.dx, offset.dy,
offset.dx + size.width, offset.dy + size.height,), paint);
}
@override
void performLayout() {
size = Size(
constraints.constrainWidth(200),
constraints.constrainHeight(myHeight),
);
}
// Timer just an example to show dynamic behavior
MyRenderBox(){
Timer.periodic(Duration(seconds: 2), handleTimeout);
}
void handleTimeout(timer) {
myHeight += 40;
markNeedsLayoutForSizedByParentChange();
layout(constraints);
}
}
CustomPainter will only size to its children's size or initial value passed to the constructor. Documentation:
Custom painters normally size themselves to their child. If they do not have a child, they attempt to size themselves to the size, which defaults to Size.zero. size must not be null.
Basics of RenderBox
Upvotes: 0
Reputation: 1137
I haven't tested it out, but this might work:
First of all, you wrap the CustomPaint
into a stateful widget (called e.g. DynamicCustomPaint
), to manipulate your widget dynamically.
You give your CustomPainter
a function onResize
, which will give you the new size of the canvas when you know it.
You call this function once you know the exact size the Canvas has to be. By using, for example, this technique where you won't have to draw the text to know what size it will be.
When the onResize
function will be called, you get the new size for the canvas and call setState
in the DynamicCustomPaint
state.
This might look like this:
class DynamicCustomPaint extends StatefulWidget {
@override
_DynamicCustomPaintState createState() => _DynamicCustomPaintState();
}
class _DynamicCustomPaintState extends State<DynamicCustomPaint> {
Size canvasSize;
@override
Widget build(BuildContext context) {
// Set inital size, maybe move this to initState function
if (canvasSize == null) {
// Decide what makes sense in your use-case as inital size
canvasSize = MediaQuery.of(context).size;
}
return CustomPaint(
size: canvasSize,
painter: RowPainter.name(_titleFontSelected, _titleFont, text, index, currentRow, onResize: (size) {
setState(() {
canvasSize = size;
});
}),
);
}
}
typedef OnResize = void Function(Size size);
class RowPainter extends CustomPainter {
RowPainter.name(
this._titleFontSelected,
this._titleFont,
this.text,
this.index,
this.currentRow,
{ this.onResize },
);
final FontStyle _titleFontSelected;
final FontStyle _titleFont;
final String text;
final int index;
final int currentRow;
final OnResize onResize;
@override
void paint(Canvas canvas, Size size) {
// TODO: implement paint
// call onResize somewhere in here
// onResize(newSize);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
Upvotes: 2
Reputation: 126594
You cannot dynamically size a custom painter, however, your problem can be solved using a CustomPaint
.
I will first elaborate on the dynamic sizing and then explain how to solve this problem using a constant size.
This is essentially, where CustomPaint
has its limits because it does not provide a way for you to size the painter based on the content.
The proper way of doing this is implementing your own RenderBox
and overriding performLayout
to size your render object based on the contents.
The RenderBox
documentation is quite detailed on this, however, you might still find it difficult to get into it as it is quite different from building widgets.
All of the above should not be needed in your case because you do not have a child
for your custom paint.
You can simply supply the size
parameter to your CustomPaint
and calculate the required height in the parent widget.
You can use a LayoutBuilder
to get the available width:
LayoutBuilder(
builder: (context, constraints) {
final maxWidth = constraints.maxWidth;
...
}
)
Now, you can simply use a TextPainter
to retrieve the required size before even entering your custom paint:
builder: (context, constraints) {
...
final textPainter = TextPainter(
text: TextSpan(
text: 'Your text',
style: yourTextStyle,
),
textDirection: TextDirection.ltr,
);
textPainter.layout(maxWidth: maxWidth); // This will make the size available.
return CustomPaint(
size: textPainter.size,
...
);
}
Now, you can even pass your textPainter
to your custom painter directly instead of passing the style arguments.
Your logic might be a bit more complicated, however, the point is that you can calculate the size before creating the CustomPaint
, which allows you to set the size.
If you need something more complicated, you will likely have to implement your own RenderBox
.
Upvotes: 11