You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter技术问询:如何实现反转ClipOval制作头像裁剪蒙版

实现Flutter反转ClipOval(中心椭圆清晰,外部灰化蒙版)

嘿,作为Flutter新手能关注到这种交互细节真的很赞!你要的反转ClipOval效果,本质就是给整个区域加灰化蒙版,只让中心椭圆区域透出清晰头像,这里给你两种实用的实现方案,适配你正在做的平移/缩放头像场景:

方法一:用CustomPaint自定义蒙版(灵活可控)

这种方式适合需要精细控制蒙版形状、位置的场景,能完美跟随头像的平移和缩放变化。核心思路是先画全屏灰化矩形,再用BlendMode.clear在中间挖一个椭圆洞。

import 'package:flutter/material.dart';

class AvatarCropPreview extends StatefulWidget {
  final ImageProvider avatarImage;
  const AvatarCropPreview({super.key, required this.avatarImage});

  @override
  State<AvatarCropPreview> createState() => _AvatarCropPreviewState();
}

class _AvatarCropPreviewState extends State<AvatarCropPreview> {
  // 头像的偏移量
  Offset _offset = Offset.zero;
  // 头像的缩放比例
  double _scale = 1.0;
  // 裁剪椭圆的直径(可以根据需求调整)
  final double _cropDiameter = 200;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: GestureDetector(
        // 处理缩放手势
        onScaleUpdate: (details) {
          setState(() {
            _scale = details.scale.clamp(1.0, 3.0); // 限制缩放范围
            _offset += details.focalPointDelta;
          });
        },
        child: Stack(
          fit: StackFit.expand,
          children: [
            // 底层是可平移缩放的头像
            Transform.translate(
              offset: _offset,
              child: Transform.scale(
                scale: _scale,
                child: Image(
                  image: widget.avatarImage,
                  fit: BoxFit.cover,
                  width: MediaQuery.of(context).size.width,
                  height: MediaQuery.of(context).size.height,
                ),
              ),
            ),
            // 上层是反转ClipOval的灰化蒙版
            CustomPaint(
              painter: _InverseOvalMaskPainter(
                cropDiameter: _cropDiameter,
                offset: _offset,
                scale: _scale,
                screenSize: MediaQuery.of(context).size,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class _InverseOvalMaskPainter extends CustomPainter {
  final double cropDiameter;
  final Offset offset;
  final double scale;
  final Size screenSize;

  _InverseOvalMaskPainter({
    required this.cropDiameter,
    required this.offset,
    required this.scale,
    required this.screenSize,
  });

  @override
  void paint(Canvas canvas, Size size) {
    // 1. 画全屏灰化蒙版
    final grayPaint = Paint()
      ..color = Colors.black.withOpacity(0.6); // 灰化透明度可调整
    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), grayPaint);

    // 2. 计算椭圆的实际位置(跟随头像的平移缩放)
    final centerX = screenSize.width / 2 + offset.dx / scale;
    final centerY = screenSize.height / 2 + offset.dy / scale;
    final rect = Rect.fromCircle(
      center: Offset(centerX, centerY),
      radius: cropDiameter / 2 / scale,
    );

    // 3. 用BlendMode.clear挖椭圆洞
    final clearPaint = Paint()
      ..blendMode = BlendMode.clear;
    canvas.drawOval(rect, clearPaint);
  }

  @override
  bool shouldRepaint(covariant _InverseOvalMaskPainter oldDelegate) {
    // 当参数变化时重绘
    return oldDelegate.offset != offset ||
        oldDelegate.scale != scale ||
        oldDelegate.cropDiameter != cropDiameter;
  }
}

方法二:用Stack+BlendMode快速实现(代码简洁)

如果不需要太复杂的自定义逻辑,用Stack叠加图层结合BlendMode也能快速搞定,原理是在灰化蒙版上叠加一个透明的椭圆区域,通过混合模式让下面的头像透出来。

import 'package:flutter/material.dart';

class SimpleAvatarCrop extends StatefulWidget {
  final ImageProvider avatarImage;
  const SimpleAvatarCrop({super.key, required this.avatarImage});

  @override
  State<SimpleAvatarCrop> createState() => _SimpleAvatarCropState();
}

class _SimpleAvatarCropState extends State<SimpleAvatarCrop> {
  Offset _offset = Offset.zero;
  double _scale = 1.0;
  final double _cropSize = 200;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: GestureDetector(
        onScaleUpdate: (details) {
          setState(() {
            _scale = details.scale.clamp(1.0, 3.0);
            _offset += details.focalPointDelta;
          });
        },
        child: Stack(
          fit: StackFit.expand,
          children: [
            // 可平移缩放的头像
            Transform.translate(
              offset: _offset,
              child: Transform.scale(
                scale: _scale,
                child: Image(
                  image: widget.avatarImage,
                  fit: BoxFit.cover,
                  width: MediaQuery.of(context).size.width,
                  height: MediaQuery.of(context).size.height,
                ),
              ),
            ),
            // 灰化蒙版 + 椭圆透明洞
            Container(
              color: Colors.black.withOpacity(0.6),
              child: Center(
                child: BlendModeMask(
                  blendMode: BlendMode.clear,
                  child: Container(
                    width: _cropSize,
                    height: _cropSize,
                    decoration: const BoxDecoration(
                      shape: BoxShape.circle,
                      color: Colors.transparent,
                    ),
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

// 自定义BlendModeMask组件
class BlendModeMask extends StatelessWidget {
  final BlendMode blendMode;
  final Widget child;

  const BlendModeMask({super.key, required this.blendMode, required this.child});

  @override
  Widget build(BuildContext context) {
    return ShaderMask(
      shaderCallback: (bounds) {
        return Paint()
          ..blendMode = blendMode
          ..shader = LinearGradient(colors: [Colors.black, Colors.black])
              .createShader(bounds);
      },
      child: child,
    );
  }
}

注意事项

  • 如果你需要椭圆裁剪区域固定(不跟随头像缩放),可以去掉代码中缩放相关的计算,直接用固定的椭圆尺寸。
  • 灰化蒙版的透明度可以通过withOpacity()调整,数值越大蒙版越暗。
  • 平移缩放的范围可以根据需求修改clamp()的参数,避免头像移出屏幕过多。

内容的提问来源于stack exchange,提问作者boeledi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 11:55:03