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

Jetpack Compose:如何实现带镂空卡片形状的遮罩?

实现相机预览上层带圆角卡片镂空的半透明遮罩

以下是不同移动端平台的具体实现方案:

Android 实现

方式1:自定义View绘制遮罩

创建继承自View的自定义遮罩类,通过路径运算实现镂空效果:

public class CameraMaskView extends View {
    private Paint paint;
    private Path maskPath;
    private Path holePath;
    private RectF holeRect;
    private float cornerRadius = 20f; // 圆角大小,可按需调整

    public CameraMaskView(Context context) {
        super(context);
        init();
    }

    public CameraMaskView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setColor(Color.parseColor("#80000000")); // 半透明黑色遮罩
        maskPath = new Path();
        holePath = new Path();
        holeRect = new RectF();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        maskPath.reset();
        holePath.reset();
        
        // 添加全屏遮罩路径
        maskPath.addRect(0, 0, w, h, Path.Direction.CW);
        // 计算镂空区域位置(左右各100dp,上下各200dp)
        int left = dp2px(100);
        int top = dp2px(200);
        int right = w - dp2px(100);
        int bottom = h - dp2px(200);
        holeRect.set(left, top, right, bottom);
        // 添加圆角矩形镂空路径
        holePath.addRoundRect(holeRect, cornerRadius, cornerRadius, Path.Direction.CW);
        // 用遮罩路径减去镂空路径,得到最终形状
        maskPath.op(holePath, Path.Op.DIFFERENCE);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawPath(maskPath, paint);
    }

    private int dp2px(float dpValue) {
        float scale = getContext().getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }
}

在布局中将自定义View放在相机预览控件上层:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- 相机预览控件 -->
    <TextureView
        android:id="@+id/cameraPreview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <!-- 自定义遮罩View -->
    <com.yourpackage.CameraMaskView
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</FrameLayout>

方式2:使用LayerDrawable

无需自定义View,通过Drawable组合实现:

<!-- drawable/camera_mask.xml -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 半透明背景层 -->
    <item>
        <shape android:shape="rectangle">
            <solid android:color="#80000000"/>
        </shape>
    </item>
    <!-- 透明圆角矩形镂空区域 -->
    <item
        android:top="200dp"
        android:bottom="200dp"
        android:left="100dp"
        android:right="100dp">
        <shape android:shape="rectangle">
            <solid android:color="@android:color/transparent"/>
            <corners android:radius="20dp"/>
            <!-- 可选:添加镂空区域边框 -->
            <stroke android:width="2dp" android:color="#FFFFFF"/>
        </shape>
    </item>
</layer-list>

布局中添加全屏View并设置该Drawable为背景:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextureView
        android:id="@+id/cameraPreview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <View
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@drawable/camera_mask"/>

</FrameLayout>

iOS 实现

方式1:自定义UIView绘制遮罩

创建继承自UIView的遮罩类,通过贝塞尔路径实现镂空:

class CameraMaskView: UIView {
    private let cornerRadius: CGFloat = 20.0
    private let maskColor = UIColor.black.withAlphaComponent(0.5)

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.isOpaque = false
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        self.isOpaque = false
    }

    override func draw(_ rect: CGRect) {
        super.draw(rect)
        maskColor.setFill()
        
        let fullPath = UIBezierPath(rect: rect)
        // 计算镂空区域位置
        let holeRect = CGRect(
            x: 80,
            y: 160,
            width: rect.width - 160,
            height: rect.height - 320
        )
        let holePath = UIBezierPath(roundedRect: holeRect, cornerRadius: cornerRadius)
        
        fullPath.append(holePath)
        fullPath.usesEvenOddFillRule = true
        fullPath.fill()
    }
}

在ViewController中添加遮罩到相机预览上层:

// 假设cameraPreview是相机预览视图
let maskView = CameraMaskView(frame: cameraPreview.bounds)
cameraPreview.addSubview(maskView)
maskView.autoresizingMask = [.flexibleWidth, .flexibleHeight]

方式2:使用CALayer遮罩

通过图层遮罩实现效果:

let maskView = UIView(frame: cameraPreview.bounds)
maskView.backgroundColor = UIColor.black.withAlphaComponent(0.5)

let maskLayer = CAShapeLayer()
let fullRect = maskView.bounds
let holeRect = CGRect(x: 80, y: 160, width: fullRect.width - 160, height: fullRect.height - 320)
let path = UIBezierPath(rect: fullRect)
let holePath = UIBezierPath(roundedRect: holeRect, cornerRadius: 20)

path.append(holePath)
path.usesEvenOddFillRule = true
maskLayer.path = path.cgPath
maskLayer.fillRule = .evenOdd
maskView.layer.mask = maskLayer

cameraPreview.addSubview(maskView)
maskView.autoresizingMask = [.flexibleWidth, .flexibleHeight]

Flutter 实现

通过Stack叠加相机预览和CustomPaint绘制的遮罩:

class CameraMask extends StatelessWidget {
  final double cornerRadius;
  final double horizontalPadding;
  final double verticalPadding;
  final Color maskColor;

  const CameraMask({
    super.key,
    this.cornerRadius = 20,
    this.horizontalPadding = 40,
    this.verticalPadding = 80,
    this.maskColor = const Color.fromARGB(128, 0, 0, 0),
  });

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        return CustomPaint(
          size: constraints.biggest,
          painter: _MaskPainter(
            cornerRadius: cornerRadius,
            horizontalPadding: horizontalPadding,
            verticalPadding: verticalPadding,
            maskColor: maskColor,
          ),
        );
      },
    );
  }
}

class _MaskPainter extends CustomPainter {
  final double cornerRadius;
  final double horizontalPadding;
  final double verticalPadding;
  final Color maskColor;

  _MaskPainter({
    required this.cornerRadius,
    required this.horizontalPadding,
    required this.verticalPadding,
    required this.maskColor,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = maskColor
      ..style = PaintingStyle.fill;

    final fullPath = Path()..addRect(Rect.fromLTWH(0, 0, size.width, size.height));
    final holeRect = Rect.fromLTWH(
      horizontalPadding,
      verticalPadding,
      size.width - horizontalPadding * 2,
      size.height - verticalPadding * 2,
    );
    final holePath = Path()..addRRect(RRect.fromRectAndRadius(holeRect, Radius.circular(cornerRadius)));
    
    fullPath.addPath(holePath, Offset.zero);
    fullPath.fillType = PathFillType.evenOdd;
    canvas.drawPath(fullPath, paint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false;
  }
}

布局中使用:

Stack(
  children: [
    // 相机预览组件(例如camera包的CameraPreview)
    CameraPreview(controller),
    // 遮罩组件
    CameraMask(),
  ],
)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 21:05:26