3

I have this custom path in my page

class TestPathPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..style = PaintingStyle.stroke
      ..strokeWidth = 2.0
      ..color = Colors.black;

    var x = size.width;
    var y = size.height;
    print(x);

    var path = Path()
      ..moveTo(x, y / 4)
      ..lineTo(x * 0.95, y / 4)
      ..lineTo(x * 0.95, y / 3)
      ..lineTo(x * 0.99, y / 3)
      ..lineTo(x * 0.99, y / 3.7)
      ..lineTo(x * 0.955, y / 3.7)
      ..lineTo(x * 0.955, y / 3.15)
      ..lineTo(x * 0.98, y / 3.15)
      ..lineTo(x * 0.98, y / 3.5)
      ..lineTo(x * 0.94, y / 3.5) //  <==== I want to display a Checkbox here
      ..lineTo(x * 0.94, y / 2)
      ..lineTo(x * 0.91, y / 2)
      ..lineTo(x * 0.91, y / 1.65)
      ..lineTo(x * 0.94, y / 1.65)
      ..lineTo(x * 0.94, y / 1.4)
      ..lineTo(x * 0.91, y / 1.4);

    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(TestPathPainter oldDelegate) => false;
}

How can i draw/render a widget ( a checkbox for example ) somewhere on this path?

I tried using a stacked widget and positioning the checkboxes but this wont look the same on every device.

Thierry
  • 7,775
  • 2
  • 15
  • 33
ibrahim
  • 573
  • 1
  • 6
  • 20

1 Answers1

6

enter image description here

The easiest way to position a widget at a specific location on the screen is to use a Stack and a Positioned widget.

Though, the problem we face is that the positioning (top, right, bottom, left) refers to the side of the child widget and not the center.

So, we need to adjust the positioning.

Solution 1 - Using SizedBox > Center

Positioned(
  left: point.dx * width - 24,
  top: point.dy * height - 24,
  child: SizedBox(
    width: 48,
    height: 48,
    child: Center(
      child: Checkbox(
        value: true,
        onChanged: (_) {},
      ),
    ),
  ),
),

48 has been chosen to be sure we are bigger than the full size (including the padding caused by the materialTapTargetSize and the visualDensity.

This leads us to a second solution.

Solution 2: Computing the full size of the Checkbox

Though the Checkbox has a static const width of 18, it may vary depending on the materialTapTargetSize and the visualDensity.

If we have a look at the source code of the CheckBox on GitHub:

enter image description here

We can define a ComputeCheckBoxSize:

double computeCheckBoxSize(BuildContext context) {
  final ThemeData themeData = Theme.of(context);
  final MaterialTapTargetSize effectiveMaterialTapTargetSize =
      themeData.checkboxTheme.materialTapTargetSize ??
          themeData.materialTapTargetSize;
  final VisualDensity effectiveVisualDensity =
      themeData.checkboxTheme.visualDensity ?? themeData.visualDensity;
  Size size;
  switch (effectiveMaterialTapTargetSize) {
    case MaterialTapTargetSize.padded:
      size = const Size(kMinInteractiveDimension, kMinInteractiveDimension);
      break;
    case MaterialTapTargetSize.shrinkWrap:
      size = const Size(
          kMinInteractiveDimension - 8.0, kMinInteractiveDimension - 8.0);
      break;
  }
  size += effectiveVisualDensity.baseSizeAdjustment;
  return size.longestSide;
}

Our Positioned widget can be further simplified to:

Positioned(
  left: point.dx * width - checkBoxSize / 2,
  top: point.dy * height - checkBoxSize / 2,
  child: Checkbox(
    value: true,
    onChanged: (_) {},
  ),
)

Full source code

import 'dart:math' show Random;
import 'package:flutter/material.dart';

void main() {
  runApp(
    MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Demo',
      home: HomePage(),
    ),
  );
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    double checkBoxSize = computeCheckBoxSize(context);
    return Scaffold(
      body: LayoutBuilder(
        builder: (context, constraints) {
          final height = constraints.biggest.height;
          final width = constraints.biggest.width;
          return Stack(
            children: [
              Container(color: Colors.amber.shade100),
              Positioned.fill(child: CustomPaint(painter: TestPathPainter())),
              ...points
                  .map(
                    (point) => Positioned(
                      left: point.dx * width - checkBoxSize / 2,
                      top: point.dy * height - checkBoxSize / 2,
                      child: Checkbox(
                        value: true,
                        onChanged: (_) {},
                      ),
                    ),
                  )
                  .toList(),
            ],
          );
        },
      ),
    );
  }
}

double computeCheckBoxSize(BuildContext context) {
  final ThemeData themeData = Theme.of(context);
  final MaterialTapTargetSize effectiveMaterialTapTargetSize =
      themeData.checkboxTheme.materialTapTargetSize ??
          themeData.materialTapTargetSize;
  final VisualDensity effectiveVisualDensity =
      themeData.checkboxTheme.visualDensity ?? themeData.visualDensity;
  Size size;
  switch (effectiveMaterialTapTargetSize) {
    case MaterialTapTargetSize.padded:
      size = const Size(kMinInteractiveDimension, kMinInteractiveDimension);
      break;
    case MaterialTapTargetSize.shrinkWrap:
      size = const Size(
          kMinInteractiveDimension - 8.0, kMinInteractiveDimension - 8.0);
      break;
  }
  size += effectiveVisualDensity.baseSizeAdjustment;
  print(size);
  return size.longestSide;
}

class TestPathPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..style = PaintingStyle.stroke
      ..strokeWidth = 2.0
      ..color = Colors.black;

    final path = Path()
      ..moveTo(
        points[0].dx * size.width,
        points[0].dy * size.height,
      );
    points.sublist(1).forEach(
          (point) => path.lineTo(
            point.dx * size.width,
            point.dy * size.height,
          ),
        );
    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(TestPathPainter oldDelegate) => false;
}

final random = Random();
final List<Offset> points = List.generate(
  10,
  (index) => Offset(.1 + random.nextDouble() * .8, .1 + index * .8 / 9),
);
Thierry
  • 7,775
  • 2
  • 15
  • 33