flutter 效果实现 —— 构建镂空区域

效果:

代码:

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.transparent,
      body: Container(
        color: Colors.red,
        child: CustomBox(
          200,
          200,
          child: Text("hello"),
        ),
      ),
    );
  }
}

class CustomBox extends SingleChildRenderObjectWidget {
  const CustomBox(this.cutWidth, this.cutHeight, {super.child, super.key});

  final double cutWidth;
  final double cutHeight;

  @override
  RenderObject createRenderObject(BuildContext context) {
    return _RenderCustomBox(cutWidth: cutWidth, cutHeight: cutHeight);
  }

  @override
  void updateRenderObject(BuildContext context, RenderObject renderObject) {
    (renderObject as _RenderCustomBox)
      ..cutWidth = cutWidth
      ..cutHeight = cutHeight;
  }
}

class _RenderCustomBox extends RenderProxyBoxWithHitTestBehavior {
  _RenderCustomBox({required double cutWidth, required double cutHeight})
      : _cutWidth = cutWidth,
        _cutHeight = cutHeight,
        super(behavior: HitTestBehavior.opaque);

  double get cutWidth => _cutWidth;
  double _cutWidth;

  set cutWidth(double value) {
    assert(value != null);
    if (value == _cutWidth) {
      return;
    }
    _cutWidth = value;
    markNeedsPaint();
  }

  double get cutHeight => _cutHeight;
  double _cutHeight;

  set cutHeight(double value) {
    assert(value != null);
    if (value == _cutHeight) {
      return;
    }
    _cutHeight = value;
    markNeedsPaint();
  }

  @override
  void performLayout() {
    super.performLayout();
    //size 为全屏
    size = constraints.biggest;
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    final inner = Size(_cutWidth, _cutHeight);
    final innerOffset = Offset((size.width - _cutWidth) / 2, (size.height - _cutHeight) / 2);

    // 通过图层暂存 + 图形裁剪构建镂空区域
    // 传递 null 等价于 offset & size,暂存当前整个屏幕,在 restore 后对图形进程合成
    context.canvas.saveLayer(null, Paint());
    // 遮罩整个屏幕
    context.canvas.drawRect(offset & size, Paint()..color = Colors.green);
    // 构造镂空区域
    // blendMode: 之前绘制的为目标图像,当前绘制的为原图像。
    // dstOut: 作用是只展示目标图像,不渲染源图像,源图像仅用作蒙板(忽略颜色,只关注透明度, 透明度越接近于1则下方图层越透明)
    // dstIn: 只渲染目标图像与源图像重合的部分,不渲染源图,源图仅用作蒙板
    // dstOver: 源图放在目标图下面
    // dstATop: 将目标图像覆盖到源图像重合的部分,其余部分渲染源图
    // xor: 源图与目标图重合部分变透明,如果只有一方不透明,则显示那一方。
    context.canvas.drawRect(
        innerOffset & inner,
        Paint()
        ..color = Colors.yellow.withOpacity(1)
          ..style = PaintingStyle.fill
          ..blendMode = BlendMode.dstOut);
    // restore 时还原整个屏幕,restore 的层级位置与执行 saveLayer 时的层级一致。
    context.canvas.restore();

// 注:save 与 saveLayer 的区别:前者用于在 save 与 restore 之间可以对旧画布执行某些操作(缩放、转换、旋转等),而在 restore 之后的画布不受影响。(举例:在 save 后将当前画布进行缩放后画一个更大的图,再 restore 后重新对原画布旋转90度。)
// 后者用于在 saveLayer 之后创建新图层,之前的旧图层不受影响,且在 restore 之后再进行合成(新旧图层共同构建了一个合成图层,且新图层的层级比旧图层要高)。
// 总结:saveLayer 存在图层合成操作,而 save 没有。

    // 通过路径填充类型构建镂空区域
    // final path = Path()
    //   ..addRect(innerOffset & inner)
    //   ..addRect(offset & size)
    //   //路径填充类型
    //   ..fillType = PathFillType.evenOdd;
    // context.canvas.drawPath(path, Paint()..color = Colors.green);

    if (child != null) {
      context.paintChild(child!, innerOffset);
    }
  }
}

posted on 2023-01-18 16:49  Lemo_wd  阅读(554)  评论(0编辑  收藏  举报

导航