Flutter 自定义 Painter 不重新绘制

问题描述 投票:0回答:1

我试图熟悉 Flutter 中的

CustomPainter
,并尝试使用自定义画家在屏幕上的特定小部件周围绘制带有切口的屏障,以表现得像教程一样。

我有一个带有自定义画家的堆栈,并且我正在使用覆盖层在主页上覆盖教程障碍。我的想法是有一个要突出显示的小部件列表,“教程”会在用户点击时逐个小部件地引导用户,画家会重新绘制以突出显示下一个目标小部件或关闭最后一个目标小部件。

不幸的是,定制画家只画了第一个变体,然后就消失了*。我可以通过

debugPrint
语句确认画家正在接收更新的边界,但它只是没有正确重新绘制(无论是否覆盖
==
hashCode
defs 以及
shouldRepaint
是否始终都是如此)设置为
true
或其中包含其他逻辑)。

GIF displaying expected behavior with help from breakpoints. Clicking on tooltip causes painter to be dismissed and next painter rendered as expected]

*在 dartpad.dev 上,画家根本不渲染,甚至是第一个。

我还尝试过为

Stack
Positioned
CustomPaint
小部件提供密钥 - 没有变化。

在调试模式下,如果我在

HolePainter
的绘制函数中设置断点,则在每次重新绘制时点击播放并返回到应用程序后,它的行为将按预期进行。

GIF Displaying undesired behavior. Clicking on tooltip causes painter to be dismissed and next painter not rendered.

我正在寻求一些帮助来理解为什么画家不重新渲染......

我在文档中遗漏了什么?是否有已知的技巧来提示预期的行为?

提前谢谢您。

我的代码片段:

import 'dart:async';

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: DebugPageTutorial(),
    );
  }
}

class DebugPageTutorial extends StatefulWidget {
  const DebugPageTutorial({super.key});

  @override
  State<DebugPageTutorial> createState() => _DebugPageTutorialState();
}

class _DebugPageTutorialState extends State<DebugPageTutorial> {
  final GlobalKey _overlayKey1 = GlobalKey();
  final GlobalKey _overlayKey2 = GlobalKey();
  final GlobalKey _overlayKey3 = GlobalKey();

  OverlayEntry? currentOverlayEntry;

  @override
  void initState() {
    super.initState();
    currentOverlayEntry = null;
  }

  @override
  void dispose() {
    _onTutorialFinished();
    super.dispose();
  }

  void _onTutorialFinished() {
    if (currentOverlayEntry?.mounted ?? false) currentOverlayEntry?.remove();
    currentOverlayEntry = null;
  }

  @override
  Widget build(BuildContext context) => Scaffold(
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text(
                key: _overlayKey1,
                'This is a tutorial page',
                style: const TextStyle(
                  fontSize: 24,
                  fontWeight: FontWeight.bold,
                ),
              ),
              Text(
                key: _overlayKey2,
                'This is a tutorial page',
              ),
              Padding(
                padding: const EdgeInsets.all(32.0),
                child: Container(
                  key: _overlayKey3,
                  decoration: const BoxDecoration(
                    shape: BoxShape.circle,
                    color: Colors.cyan,
                  ),
                  height: 50.0,
                  width: 50.0,
                ),
              ),
              MaterialButton(
                onPressed: () async {
                  currentOverlayEntry = TutorialOverlay.createOverlayEntry(
                    context,
                    overlayKeys: [
                      TutorialOverlayTooltip(
                        overlayKey: _overlayKey1,
                        overlayTooltip: const Text('Hello'),
                      ),
                      TutorialOverlayTooltip(
                        overlayKey: _overlayKey2,
                        overlayTooltip: const Text('World'),
                        padding: const EdgeInsets.all(16.0),
                        shapeBorder: const RoundedRectangleBorder(
                          borderRadius: BorderRadius.all(
                            Radius.circular(
                              8.0,
                            ),
                          ),
                        ),
                        color: Colors.green,
                      ),
                      TutorialOverlayTooltip(
                          overlayKey: _overlayKey3,
                          overlayTooltip: const Text('Beep'),
                          shapeBorder: CircleBorder(),
                          padding: const EdgeInsets.all(16.0),
                          color: Colors.red),
                      TutorialOverlayTooltip(
                        overlayKey: _overlayKey1,
                        overlayTooltip: const Text('Bop'),
                      ),
                    ],
                    onTutorialFinished: _onTutorialFinished,
                  );
                  Overlay.of(context).insert(currentOverlayEntry!);
                },
                child: const Text('Show Tutorial'),
              ),
            ],
          ),
        ),
      );
}

class TutorialOverlay extends StatefulWidget {
  /// A list of keys and overlay tooltips to display when the overlay is
  /// displayed. The overlay will be displayed in the order of the list.
  final List<TutorialOverlayTooltip> overlayKeys;

  /// A global key to use as the ancestor for the overlay entry, ensuring that
  /// the overlay entry is not shifted improperly when the overlay is only being
  /// painted on a portion of the screen. If null, the overlay will be painted
  /// based on the heuristics of the entire screen.
  final GlobalKey? ancestorKey;

  final FutureOr<void> Function()? onTutorialFinished;

  const TutorialOverlay({
    super.key,
    required this.overlayKeys,
    this.ancestorKey,
    this.onTutorialFinished,
  });

  static OverlayEntry createOverlayEntry(
    BuildContext context, {
    required List<TutorialOverlayTooltip> overlayKeys,
    GlobalKey? ancestorKey,
    FutureOr<void> Function()? onTutorialFinished,
  }) =>
      OverlayEntry(
        builder: (BuildContext context) => TutorialOverlay(
          overlayKeys: overlayKeys,
          ancestorKey: ancestorKey,
          onTutorialFinished: onTutorialFinished,
        ),
      );

  @override
  State<TutorialOverlay> createState() => _TutorialOverlayState();
}

class _TutorialOverlayState extends State<TutorialOverlay> {
  late List<TutorialOverlayTooltip> _overlayKeys;
  int _currentIndex = 0;
  TutorialOverlayTooltip? get _currentTooltip =>
      _currentIndex < _overlayKeys.length ? _overlayKeys[_currentIndex] : null;
  final ValueNotifier<int> _repaintKey = ValueNotifier<int>(0);

  @override
  void initState() {
    super.initState();
    _overlayKeys = widget.overlayKeys;
  }

  Rect? _getNextRenderBox(GlobalKey? key) {
    final RenderBox? renderBox =
        key?.currentContext?.findRenderObject() as RenderBox?;
    if (renderBox != null && renderBox.hasSize) {
      final Offset offset = renderBox.localToGlobal(
        Offset.zero,
        ancestor: widget.ancestorKey?.currentContext?.findRenderObject(),
      );
      return Rect.fromLTWH(
        offset.dx,
        offset.dy,
        renderBox.size.width,
        renderBox.size.height,
      );
    }
    return null;
  }

  @override
  Widget build(BuildContext context) {
    final Rect? nextRenderBox = _getNextRenderBox(_currentTooltip?.overlayKey);
    if (nextRenderBox == null) return const SizedBox();

    // Determine position for the tutorial text
    double textTop = nextRenderBox.top > 100
        ? nextRenderBox.top - 40
        : nextRenderBox.bottom + 20;

    debugPrint('build: ${nextRenderBox.toString()}');

    return Stack(
      children: [
        // Paint the background
        Positioned.fill(
          key: ValueKey('tutorial_paint:$_currentIndex'),
          left: 0.0,
          right: 0.0,
          top: 0.0,
          bottom: 0.0,
          child: CustomPaint(
            painter: HolePainter(
              repaint: _repaintKey,
              targetRect: nextRenderBox,
              shapeBorder: _currentTooltip?.shapeBorder ??
                  const RoundedRectangleBorder(),
              color: _currentTooltip?.color ?? const Color(0x90000000),
              direction: _currentTooltip?.direction ?? TextDirection.ltr,
              padding: _currentTooltip?.padding ?? EdgeInsets.zero,
            ),
          ),
        ),
        // Tutorial text box
        Positioned(
          top: textTop,
          left: nextRenderBox.left,
          child: Material(
            color: Colors.transparent,
            child: Container(
              padding: const EdgeInsets.all(8),
              color: Colors.white,
              child: GestureDetector(
                behavior: HitTestBehavior.translucent,
                onTap: () {
                  if (_currentIndex + 1 < _overlayKeys.length) {
                    return setState(() {
                      _currentIndex = _currentIndex + 1;
                      _repaintKey.value = _currentIndex;
                    });
                  }
                  widget.onTutorialFinished?.call();
                },
                child: _currentTooltip?.overlayTooltip ?? const SizedBox(),
              ),
            ),
          ),
        ),
      ],
    );
  }
}

/// Contains information for drawing a tutorial overlay over a given widget
/// based on the provided global key.
class TutorialOverlayTooltip {
  /// The key of the widget to highlight in the cutout of the tutorial overlay
  final GlobalKey overlayKey;

  /// The widget to render by the cutout of the totorial overlay
  final Widget overlayTooltip;

  /// The padding around the widget to render by the cutout of the totorial
  /// overlay. Default is EdgeInsets.zero
  final EdgeInsets padding;

  /// The shape of the cutout of the totorial overlay. Default is a rounded
  /// rectangle with no border radius
  final ShapeBorder shapeBorder;

  /// The color of the barrier of the totorial overlay. Default is
  /// Black with 50% opacity
  final Color color;

  /// The direction of the cutout of the totorial overlay. Default is
  /// [TextDirection.ltr]
  final TextDirection direction;

  const TutorialOverlayTooltip({
    required this.overlayKey,
    required this.overlayTooltip,
    this.padding = EdgeInsets.zero,
    this.shapeBorder = const RoundedRectangleBorder(),
    this.color = const Color(0x90000000), // Black with 50% opacity
    this.direction = TextDirection.ltr,
  });
}

/// A painter that covers the area with a shaped hole around a target box
class HolePainter extends CustomPainter {
  /// The key of the widget to highlight in the cutout of the tutorial overlay
  final ValueNotifier? repaint;

  /// The target rect to paint a hole around
  final Rect targetRect;

  /// The padding around the target rect in the hole
  final EdgeInsets padding;

  /// The shape of the hole to paint around the target rect
  final ShapeBorder shapeBorder;

  /// The color of the barrier that the hole is cut from.
  final Color color;

  /// The direction of the hole. Default is [TextDirection.ltr]
  final TextDirection direction;

  const HolePainter({
    this.repaint,
    required this.targetRect,
    this.shapeBorder =
        const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
    this.color = const Color(0x90000000), // Black with 50% opacity
    this.padding = EdgeInsets.zero,
    this.direction = TextDirection.ltr,
  }): super(repaint: repaint);

  @override
  void paint(Canvas canvas, Size size) {
    final Paint paint = Paint()
      ..color = color
      ..blendMode = BlendMode.dstOver;

    // Create a padded rectangle from the targetRect using padding
    final Rect paddedRect = Rect.fromLTRB(
      targetRect.left - padding.left,
      targetRect.top - padding.top,
      targetRect.right + padding.right,
      targetRect.bottom + padding.bottom,
    );

    // Create the background path covering the entire canvas
    Path backgroundPath = Path()
      ..addRect(
        Rect.fromLTWH(
          0,
          0,
          size.width,
          size.height,
        ),
      );

    // Create the hole path depending on the shapeBorder
    Path holePath = Path();
    if (shapeBorder is RoundedRectangleBorder) {
      BorderRadiusGeometry borderRadiusGeometry =
          (shapeBorder as RoundedRectangleBorder).borderRadius;
      BorderRadius borderRadius = borderRadiusGeometry.resolve(direction);
      holePath.addRRect(
        RRect.fromRectAndCorners(
          paddedRect,
          topLeft: borderRadius.topLeft,
          topRight: borderRadius.topRight,
          bottomLeft: borderRadius.bottomLeft,
          bottomRight: borderRadius.bottomRight,
        ),
      );
    } else if (shapeBorder is CircleBorder) {
      // Use the smaller side to ensure it fits within the padded rect
      double radius = paddedRect.width < paddedRect.height
          ? paddedRect.width / 2
          : paddedRect.height / 2;
      holePath.addOval(
        Rect.fromCircle(
          center: paddedRect.center,
          radius: radius,
        ),
      );
    } else {
      // Only support RoundedRectangleBorder and CircleBorder for now
      throw Exception('Unsupported shape border type');
    }

    // Combine the paths to create a cut-out effect
    Path combinedPath = Path.combine(
      PathOperation.difference,
      backgroundPath,
      holePath,
    );
    // Draw to the canvas
    canvas.drawPath(
      combinedPath,
      paint,
    );
    debugPrint('paint: ${targetRect.toString()}');
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    if (oldDelegate is HolePainter)
      debugPrint(
        'repaint: ${oldDelegate.targetRect.toString()} => ${targetRect.toString()}',
      );
    return true;
  }
}

flutter dart overlay flutter-custompainter custom-painter
1个回答
0
投票

基于@pskink 在该问题下的线程上的评论:

通过更改画家方法(不需要重绘可监听)和设置状态(这将导致自定义画家的重新绘制),可以更好地实现自定义画家的预期行为。此外,通过使用 ImplicitlyAnimatedWidget,我们能够平滑不同绘画之间的卡顿。

代码如下:

import 'dart:async';

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: DebugPageTutorial(),
    );
  }
}

class DebugPageTutorial extends StatefulWidget {
  const DebugPageTutorial({super.key});

  @override
  State<DebugPageTutorial> createState() => _DebugPageTutorialState();
}

class _DebugPageTutorialState extends State<DebugPageTutorial> {
  final GlobalKey _overlayKey1 = GlobalKey();
  final GlobalKey _overlayKey2 = GlobalKey();
  final GlobalKey _overlayKey3 = GlobalKey();

  OverlayEntry? currentOverlayEntry;

  @override
  void initState() {
    super.initState();
    currentOverlayEntry = null;
  }

  @override
  void dispose() {
    _onTutorialFinished();
    super.dispose();
  }

  void _onTutorialFinished() {
    if (currentOverlayEntry?.mounted ?? false) currentOverlayEntry?.remove();
    currentOverlayEntry = null;
  }

  @override
  Widget build(BuildContext context) => Scaffold(
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text(
                key: _overlayKey1,
                'This is a tutorial page',
                style: const TextStyle(
                  fontSize: 24,
                  fontWeight: FontWeight.bold,
                ),
              ),
              Text(
                key: _overlayKey2,
                'This is a tutorial page',
              ),
              Padding(
                padding: const EdgeInsets.all(32.0),
                child: Container(
                  key: _overlayKey3,
                  decoration: const BoxDecoration(
                    shape: BoxShape.circle,
                    color: Colors.cyan,
                  ),
                  height: 50.0,
                  width: 50.0,
                ),
              ),
              MaterialButton(
                onPressed: () async {
                  currentOverlayEntry = TutorialOverlay.createOverlayEntry(
                    context,
                    overlayKeys: [
                      TutorialOverlayTooltip(
                        overlayKey: _overlayKey1,
                        overlayTooltip: ConstrainedBox(
                          constraints: const BoxConstraints(maxWidth: 150),
                          child: const Text('Excepteur irure exercitation consequat esse aute occaecat voluptate nulla minim.'),
                        ),
                        color: Colors.indigo.shade900.withOpacity(0.9),
                      ),
                      TutorialOverlayTooltip(
                        overlayKey: _overlayKey2,
                        overlayTooltip: ConstrainedBox(
                          constraints: const BoxConstraints(maxWidth: 125),
                          child: const Text('Proident qui proident dolore dolor minim voluptate mollit dolore eiusmod nostrud nulla.'),
                        ),
                        padding: const EdgeInsets.all(16.0),
                        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
                        color: Colors.orange,
                      ),
                      TutorialOverlayTooltip(
                        overlayKey: _overlayKey3,
                        overlayTooltip: ConstrainedBox(
                          constraints: const BoxConstraints(maxWidth: 150),
                          child: const Text('Sint elit officia non Lorem magna id.'),
                        ),
                        shape: const CircleBorder(),
                        padding: const EdgeInsets.all(24.0),
                        color: Colors.green.shade900.withOpacity(0.9),
                      ),
                    ],
                    onTutorialFinished: _onTutorialFinished,
                  );
                  Overlay.of(context).insert(currentOverlayEntry!);
                },
                child: const Text('Show Tutorial'),
              ),
            ],
          ),
        ),
      );
}

class TutorialOverlay extends StatefulWidget {
  /// A list of keys and overlay tooltips to display when the overlay is
  /// displayed. The overlay will be displayed in the order of the list.
  final List<TutorialOverlayTooltip> overlayKeys;

  /// A global key to use as the ancestor for the overlay entry, ensuring that
  /// the overlay entry is not shifted improperly when the overlay is only being
  /// painted on a portion of the screen. If null, the overlay will be painted
  /// based on the heuristics of the entire screen.
  final GlobalKey? ancestorKey;

  final FutureOr<void> Function()? onTutorialFinished;

  const TutorialOverlay({
    super.key,
    required this.overlayKeys,
    this.ancestorKey,
    this.onTutorialFinished,
  });

  static OverlayEntry createOverlayEntry(
    BuildContext context, {
    required List<TutorialOverlayTooltip> overlayKeys,
    GlobalKey? ancestorKey,
    FutureOr<void> Function()? onTutorialFinished,
  }) =>
      OverlayEntry(
        builder: (BuildContext context) => TutorialOverlay(
          overlayKeys: overlayKeys,
          ancestorKey: ancestorKey,
          onTutorialFinished: onTutorialFinished,
        ),
      );

  @override
  State<TutorialOverlay> createState() => _TutorialOverlayState();
}

class _TutorialOverlayState extends State<TutorialOverlay> {
  late List<TutorialOverlayTooltip> _overlayKeys;
  int _currentIndex = 0;
  TutorialOverlayTooltip? get _currentTooltip =>
      _currentIndex < _overlayKeys.length ? _overlayKeys[_currentIndex] : null;

  @override
  void initState() {
    super.initState();
    _overlayKeys = widget.overlayKeys;
  }

  Rect? _getNextRenderBox(GlobalKey? key) {
    final renderBox = key?.currentContext?.findRenderObject() as RenderBox?;
    if (renderBox != null && renderBox.hasSize) {
      final Offset offset = renderBox.localToGlobal(
        Offset.zero,
        ancestor: widget.ancestorKey?.currentContext?.findRenderObject(),
      );
      return offset & renderBox.size;
    }
    return null;
  }
  @override
  Widget build(BuildContext context) {
    final Rect? nextRenderBox = _getNextRenderBox(_currentTooltip?.overlayKey);
    if (nextRenderBox == null) return const SizedBox();

    // debugPrint('build: ${nextRenderBox.toString()}');

    final tooltipColor = HSLColor.fromColor(_currentTooltip?.color ?? const Color(0x90000000));
    return AnimatedTutorial(
      duration: Durations.long2,
      targetRect: nextRenderBox,
      shape: _currentTooltip?.shape ?? const RoundedRectangleBorder(),
      color: _currentTooltip?.color ?? const Color(0x90000000),
      padding: _currentTooltip?.padding ?? EdgeInsets.zero,
      curve: Curves.ease,
      child: Material(
        color: tooltipColor.withAlpha(1).withLightness(0.75).toColor(),
        borderRadius: BorderRadius.circular(6),
        elevation: 3,
        clipBehavior: Clip.antiAlias,
        child: InkWell(
          splashColor: Colors.white24,
          highlightColor: Colors.transparent,
          onTap: () {
            int newIndex = _currentIndex + 1;
            if(newIndex >= _overlayKeys.length) {
              widget.onTutorialFinished?.call();
              return;
            }
            setState(() => _currentIndex = newIndex);
          },
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: AnimatedSize(
              duration: Durations.medium2,
              curve: Curves.ease,
              child: AnimatedSwitcher(
                duration: Durations.medium2,
                child: KeyedSubtree(
                  key: UniqueKey(),
                  child: _currentTooltip?.overlayTooltip ?? const SizedBox(),
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

/// Contains information for drawing a tutorial overlay over a given widget
/// based on the provided global key.
class TutorialOverlayTooltip {
  /// The key of the widget to highlight in the cutout of the tutorial overlay
  final GlobalKey overlayKey;

  /// The widget to render by the cutout of the totorial overlay
  final Widget overlayTooltip;

  /// The padding around the widget to render by the cutout of the totorial
  /// overlay. Default is EdgeInsets.zero
  final EdgeInsets padding;

  /// The shape of the cutout of the totorial overlay. Default is a rounded
  /// rectangle with no border radius
  final ShapeBorder shape;

  /// The color of the barrier of the totorial overlay. Default is
  /// Black with 50% opacity
  final Color color;

  const TutorialOverlayTooltip({
    required this.overlayKey,
    required this.overlayTooltip,
    this.padding = EdgeInsets.zero,
    this.shape = const RoundedRectangleBorder(),
    this.color = const Color(0x90000000), // Black with 50% opacity
  });
}

class AnimatedTutorial extends ImplicitlyAnimatedWidget {
  AnimatedTutorial({
    super.key,
    required super.duration,
    required this.targetRect,
    required this.padding,
    required ShapeBorder shape,
    required Color color,
    required this.child,
    super.curve,
  }) : decoration = ShapeDecoration(shape: shape, color: color);

  final Rect targetRect;
  final EdgeInsets padding;
  final Decoration decoration;
  final Widget child;

  @override
  ImplicitlyAnimatedWidgetState<ImplicitlyAnimatedWidget> createState() {
    return _AnimatedTutorialState();
  }
}

class _AnimatedTutorialState extends AnimatedWidgetBaseState<AnimatedTutorial> {
  RectTween? _targetRect;
  EdgeInsetsGeometryTween? _padding;
  DecorationTween? _decoration;

  @override
  Widget build(BuildContext context) {
    // timeDilation = 5; // sloooow motion for testing
    return CustomPaint(
      painter: HolePainter(
        targetRect: _targetRect?.evaluate(animation) as Rect,
        decoration: _decoration?.evaluate(animation) as ShapeDecoration,
        direction: Directionality.of(context),
        padding: _padding?.evaluate(animation) as EdgeInsetsGeometry,
      ),
      child: CustomSingleChildLayout(
        delegate: TooltipDelegate(_targetRect?.evaluate(animation) as Rect),
        child: widget.child,
      ),
    );
  }

  @override
  void forEachTween(TweenVisitor<dynamic> visitor) {
    _targetRect = visitor(_targetRect, widget.targetRect, (dynamic value) => RectTween(begin: value as Rect)) as RectTween?;
    _padding = visitor(_padding, widget.padding, (dynamic value) => EdgeInsetsGeometryTween(begin: value as EdgeInsetsGeometry)) as EdgeInsetsGeometryTween?;
    _decoration = visitor(_decoration, widget.decoration, (dynamic value) => DecorationTween(begin: value as Decoration)) as DecorationTween?;
  }
}

class TooltipDelegate extends SingleChildLayoutDelegate {
  TooltipDelegate(this.rect);

  final Rect rect;
  final padding = const Offset(0, 6);

  @override
  Offset getPositionForChild(Size size, Size childSize) {
    assert(size.width - childSize.width >= 0);
    assert(size.height - childSize.height >= 0);
    final position = rect.topLeft - childSize.bottomLeft(padding);
    return _clamp(position.dy >= 0? position : rect.bottomLeft, size, childSize);
  }

  Offset _clamp(Offset position, Size size, Size childSize) {
    return Offset(
      position.dx.clamp(0, size.width - childSize.width),
      position.dy.clamp(0, size.height - childSize.height),
    );
  }

  @override
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) => constraints.loosen();

  @override
  bool shouldRelayout(covariant SingleChildLayoutDelegate oldDelegate) => true;
}

/// A painter that covers the area with a shaped hole around a target box
class HolePainter extends CustomPainter {
  const HolePainter({
    required this.targetRect,
    required this.decoration,
    required this.padding,
    this.direction = TextDirection.ltr,
  });

  /// The target rect to paint a hole around
  final Rect targetRect;

  /// The padding around the target rect in the hole
  final EdgeInsetsGeometry padding;

  /// The shape decoration of the hole to paint around the target rect
  final ShapeDecoration decoration;

  /// The direction of the hole. Default is [TextDirection.ltr]
  final TextDirection direction;

  @override
  void paint(Canvas canvas, Size size) {
    final Paint paint = Paint()
      ..color = decoration.color ?? Colors.transparent;

    final Rect paddedRect = padding.resolve(direction).inflateRect(targetRect);
    Path path = Path()
      ..fillType = PathFillType.evenOdd
      ..addRect(Offset.zero & size)
      ..addPath(decoration.getClipPath(paddedRect, direction), Offset.zero);
    canvas.drawPath(path, paint);
    // debugPrint('paint: ${targetRect.toString()}');
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

上述代码适用于所有平台,并且没有表现出问题中突出显示的任何问题。它也可以在以下链接的要点中找到:

pskink 的 Github 要点

© www.soinside.com 2019 - 2024. All rights reserved.