Flutter 在特定路径位置将小部件绘制到画布上

ibr*_*him 3 custom-painting flutter

我的页面中有这个自定义路径

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;
}
Run Code Online (Sandbox Code Playgroud)

我如何在此路径上的某处绘制/渲染小部件(例如复选框)?

我尝试使用堆叠小部件并定位复选框,但这在每个设备上看起来都不同。

Thi*_*rry 6

在此输入图像描述

将小部件放置在屏幕上特定位置的最简单方法是使用堆栈和Positioned小部件。

不过,我们面临的问题是定位(top, right, bottom, left)指的是小部件的侧面child而不是中心。

所以,我们需要调整定位。

解决方案 1 - 使用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: (_) {},
      ),
    ),
  ),
),
Run Code Online (Sandbox Code Playgroud)

48选择它是为了确保我们大于完整尺寸(包括由materialTapTargetSize和 引起的填充)visualDensity

这引出了第二个解决方案。

解决方案 2:计算整个尺寸Checkbox

尽管复选框具有“ static const widthof” 18,但它可能会根据“ ”materialTapTargetSize和“ ”而有所不同visualDensity

如果我们看一下GitHub 上的源代码CheckBox

在此输入图像描述

我们可以定义一个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;
}
Run Code Online (Sandbox Code Playgroud)

我们的Positioned小部件可以进一步简化为:

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

完整源代码

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),
);
Run Code Online (Sandbox Code Playgroud)