Flutter中如何使用camera插件实现相机预览宽高比遮罩层
Flutter 实现Instagram风格相机宽高比遮罩(基于camera插件)
核心思路是用层叠布局把相机预览、遮罩、操作UI按顺序堆叠,重点要解决两个问题:一是相机预览不能拉伸变形,二是遮罩要精准留出对应比例的透明拍摄区,不影响下层手势操作。
实现要点
- 基础依赖:先完成camera插件的基础配置,完成相机权限申请、相机控制器初始化,确保能正常调出相机预览
- 层叠结构:用
Stack作为根布局,从下到上依次放「适配缩放的相机预览」「宽高比遮罩层」「拍摄操作UI」 - 遮罩实现:半透明遮挡层居中留出固定宽高比的透明区域,可按需给透明区加描边,还原Instagram的视觉效果
- 手势穿透:给遮罩层加
IgnorePointer,避免遮挡点击对焦、双指缩放等相机手势 - 成片对齐:拍照后按设定的宽高比裁切原始照片,保证成片和预览看到的范围完全一致
核心实现代码
import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; class InstagramCameraPage extends StatefulWidget { const InstagramCameraPage({super.key}); @override State<InstagramCameraPage> createState() => _InstagramCameraPageState(); } class _InstagramCameraPageState extends State<InstagramCameraPage> { late CameraController _cameraController; // 可切换的拍摄比例,Instagram常用1:1、4:5、9:16 double _targetAspectRatio = 4 / 5; bool _isInit = false; @override void initState() { super.initState(); _initCamera(); } Future<void> _initCamera() async { final cameras = await availableCameras(); _cameraController = CameraController( cameras.first, ResolutionPreset.high, enableAudio: false, ); await _cameraController.initialize(); if (!mounted) return; setState(() => _isInit = true); } @override void dispose() { _cameraController.dispose(); super.dispose(); } // 拍照逻辑,拍完按比例裁切即可 Future<void> _takePhoto() async { final XFile photo = await _cameraController.takePicture(); // 这里补充图片裁切、保存逻辑 } @override Widget build(BuildContext context) { if (!_isInit) { return const Scaffold( backgroundColor: Colors.black, body: Center(child: CircularProgressIndicator(color: Colors.white)), ); } final Size screenSize = MediaQuery.of(context).size; // 计算相机预览缩放值,解决预览拉伸问题 final double previewScale = 1 / (_cameraController.value.aspectRatio * screenSize.aspectRatio); return Scaffold( backgroundColor: Colors.black, body: Stack( fit: StackFit.expand, children: [ // 底层:不变形的相机预览 Transform.scale( scale: previewScale, alignment: Alignment.center, child: Center(child: CameraPreview(_cameraController)), ), // 中层:宽高比遮罩 _buildRatioMask(screenSize), // 上层:操作UI Positioned( bottom: 50, left: 0, right: 0, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ GestureDetector( onTap: _takePhoto, child: Container( width: 72, height: 72, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.white, border: Border.all(color: Colors.white30, width: 4), ), ), ) ], ), ) ], ), ); } // 构建宽高比遮罩 Widget _buildRatioMask(Size screenSize) { final double previewWidth = screenSize.width; final double previewHeight = previewWidth / _targetAspectRatio; // 计算上下遮罩的高度 final double maskHeight = (screenSize.height - previewHeight) / 2; return IgnorePointer( // 透传手势,不影响相机对焦、缩放操作 child: Column( children: [ // 上部半透明遮罩 Container( height: maskHeight, width: double.infinity, color: Colors.black.withOpacity(0.7), ), // 中间透明拍摄区,加白边还原Instagram效果 AspectRatio( aspectRatio: _targetAspectRatio, child: Container( decoration: BoxDecoration( border: Border.all(color: Colors.white, width: 1), ), ), ), // 下部半透明遮罩 Container( height: maskHeight, width: double.infinity, color: Colors.black.withOpacity(0.7), ), ], ), ); } }
效果参考

扩展提示
- 如果需要做拍摄比例切换功能,只需要把
_targetAspectRatio改成可观察的状态变量,切换时调用setState即可实时更新遮罩 - 横竖屏切换场景下,需要重新计算预览缩放值和遮罩尺寸,避免变形
- 如果需要实现镂空圆角、异形遮罩效果,可以把遮罩层换成
CustomPaint,通过路径组合绘制任意形状的镂空区
内容的提问来源于stack exchange,提问作者litt
相关产品推荐
相关产品推荐

