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
相关产品推荐
相关产品推荐

