PsyKoWebMari
PsyKoWebMari

Reputation: 425

How to Dynamically Size a CustomPainter

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

Answers (3)

Roddy R
Roddy R

Reputation: 1470

Use SingleChildRenderObjectWidget and RenderBox instead. Full simple example with dynamic resizing.

DartPad

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

Ichor de Dionysos
Ichor de Dionysos

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

creativecreatorormaybenot
creativecreatorormaybenot

Reputation: 126594

TL;DR

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.

Dynamic 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.

Constant size

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

Related Questions