对于熟悉android开发的来说知道水印相机的实现方式大概有两种,一种是拍完照生成图片(jpg,png)后给图片加上水印,另一种是在相机预览view加上水印view,然后将view转bitmap的方式生成水印照片。而对于flutter开发来说,实现的方式可以怎样呢? 首先我们知道flutter相机相关的第三方库一般使用camera,现在我们使用camera: ^0.10.3来实现一个自定义水印相机,水印信息主要包括店名,时间和地址等,当然这些可以自定义,先看效果图:
拍照前
拍照完成
布局主要由预览视图CameraPreview,顶部和底部操作按钮组成。
相机预览区域的实现:
/// 相机预览区域
Widget _buildCameraArea() {
Widget area;
if (_takeStatus == TakeStatus.confirm) {
area = Image.file(File(_curFile.path), fit: BoxFit.fitWidth,);
} else if (widget.cameraController.value.isInitialized) {
final double screenWidth = MediaQuery.of(context).size.width;
area = CameraPreview(widget.cameraController)
.intoSizedBox(
width: screenWidth,
height: screenWidth * widget.cameraController.value.aspectRatio,
).intoFittedBox(
fit: BoxFit.fitWidth,
).intoOverflowBox(
alignment: Alignment.center,
).intoClipRect();
} else {
area = Container(color: Colors.black,);
}
return area.intoAspectRatio(
aspectRatio: widget.aspectRatio,
).addNeighbor(
Image.asset("images/icon_company.png", width: 20.sp,height: 20.sp,)
.addNeighbor(
Text(_companyName, style: TextStyle(fontSize: 14.sp, color: Colors.white,
shadows: const [
Shadow(color: Colors.grey, blurRadius: 1, offset: Offset(1, 1))
]),)
).intoRow()
.addNeighbor(
Image.asset("images/icon_time.png", width: 20.sp,height: 20.sp,)
.addNeighbor(
Text(_time, style: TextStyle(fontSize: 14.sp, color: Colors.white,
shadows: const [
Shadow(color: Colors.grey, blurRadius: 1, offset: Offset(1, 1))
]),)
).intoRow()
.intoPadding(padding: const EdgeInsets.only(top: 8).r)
).addNeighbor(
Image.asset("images/icon_address.png", width: 20.sp,height: 20.sp,)
.addNeighbor(
Text(_address, style: TextStyle(fontSize: 14.sp, color: Colors.white,
shadows: const [
Shadow(color: Colors.grey, blurRadius: 1, offset: Offset(1, 1))
]),)
).intoRow()
.intoPadding(padding: const EdgeInsets.only(top: 8).r)
).intoColumn(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
).intoRotatedBox(quarterTurns: quarterTurns)
.intoPositioned(
left: 16.r,
bottom: 24.r,
)
).intoStack()
.intoRepaintBoundary(
key: _cameraKey,
).intoCenter();
}
其中水印布局为:
Image.asset("images/icon_company.png", width: 20.sp,height: 20.sp,)
.addNeighbor(
Text(_companyName, style: TextStyle(fontSize: 14.sp, color: Colors.white,
shadows: const [
Shadow(color: Colors.grey, blurRadius: 1, offset: Offset(1, 1))
]),)
).intoRow()
.addNeighbor(
Image.asset("images/icon_time.png", width: 20.sp,height: 20.sp,)
.addNeighbor(
Text(_time, style: TextStyle(fontSize: 14.sp, color: Colors.white,
shadows: const [
Shadow(color: Colors.grey, blurRadius: 1, offset: Offset(1, 1))
]),)
).intoRow()
.intoPadding(padding: const EdgeInsets.only(top: 8).r)
).addNeighbor(
Image.asset("images/icon_address.png", width: 20.sp,height: 20.sp,)
.addNeighbor(
Text(_address, style: TextStyle(fontSize: 14.sp, color: Colors.white,
shadows: const [
Shadow(color: Colors.grey, blurRadius: 1, offset: Offset(1, 1))
]),)
).intoRow()
.intoPadding(padding: const EdgeInsets.only(top: 8).r)
).intoColumn(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
)
顶部包括一个返回和一个闪光灯图标,布局代码:
/// 顶部操作按钮
Widget _buildTopBar() {
IconData flashIcon = IconsUtils.flashOff;
if (widget.cameraController.value.isInitialized) {
if(widget.cameraController.value.flashMode == FlashMode.off) {
flashIcon = IconsUtils.flashOff;
} else {
flashIcon = IconsUtils.flashOn;
}
}
if (_takeStatus == TakeStatus.confirm) {
return Container();
}
return Image.asset("images/icon_back.png",
width: 30.sp,height: 30.sp,
).intoGestureDetector(
onTap: () {
if (CommonUtils.debounce()) {
Get.back();
}
}
).addNeighbor(
IconButton(
color: Colors.white,
icon: Icon(flashIcon, color: Colors.white, size: 32.sp,),
onPressed: _toggleFlash
)
).intoRow(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
).intoPositioned(
top: MediaQuery.of(context).padding.top + 10,
left: 10,
right: 10,
);
}
底部操作按钮布局,包括拍照前的拍照按钮和拍照完的确定和取消按钮:
/// 拍照后取消确定按钮
Widget _buildAction() {
Widget child = _takeStatus == TakeStatus.confirm ?
OutlinedButton(
onPressed: _cancel,
child: Icon(IconsUtils.cancel, size: 48.sp,color: ColorUtil.hexStringColor("#F34D57"),)
).addNeighbor(
OutlinedButton(
onPressed: (){
if (CommonUtils.debounce()) {
_confirm();
}
},
child: Icon(IconsUtils.ok, size: 48.sp,color: ColorUtil.hexStringColor("#34C866"),)
)
).intoRow(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
) : OutlinedButton(
onPressed: _takePicture,
child: Icon(IconsUtils.take, color: Colors.white, size: 64.sp,)
);
return child.intoPositioned(bottom: 50, left: 50, right: 50);
}
初始化相机:
/// 初始化相机
void _initCamera() async {
try {
setState(() {
_takeStatus = TakeStatus.preparing;
});
widget.cameraController.addListener(() {
if (mounted) setState(() {});
});
await widget.cameraController.initialize();
if (mounted) {
setState(() {
_takeStatus = TakeStatus.taking;
});
}
widget.cameraController.setFlashMode(FlashMode.off);
} on CameraException catch (e) {
LogUtil.d("CameraException ====$e");
}
}
切换补光灯的实现:
/// 切换闪光灯
void _toggleFlash() {
if(widget.cameraController.value.flashMode == FlashMode.off) {
widget.cameraController.setFlashMode(FlashMode.torch);
Toast.show("闪光灯,开启", Get.context!, gravity: 2, textStyle: TextStyle(fontSize: 10.sp), backgroundRadius:5.r, backgroundColor: Colors.white,padding: const EdgeInsets.only(left: 8, top: 5, right: 8, bottom: 5).r);
} else {
Toast.show("闪光灯,关闭", Get.context!, gravity: 2, textStyle: TextStyle(fontSize: 10.sp),backgroundRadius:5.r,backgroundColor: Colors.white,padding: const EdgeInsets.only(left: 8, top: 5, right: 8, bottom: 5).r);
widget.cameraController.setFlashMode(FlashMode.off);
}
}
拍照:
/// 拍照
void _takePicture() async {
if (widget.cameraController.value.isTakingPicture) return;
XFile file = await widget.cameraController.takePicture();
setState(() {
_curFile = file;
_takeStatus = TakeStatus.confirm;
});
}
拍照后处理:
/// 确认, 返回图片路径
void _confirm() async {
if (_isCapturing) return;
_isCapturing = true;
try {
RenderRepaintBoundary boundary = _cameraKey.currentContext?.findRenderObject() as RenderRepaintBoundary;
ui.Image image = await boundary.toImage(pixelRatio: ui.window.devicePixelRatio);
ByteData? byteData = await image.toByteData(format: ui.ImageByteFormat.png);
Uint8List? imgBytes = byteData?.buffer.asUint8List();
String? basePath = await findSavePath(savePath);
File file = File('$basePath/${DateTime.now().millisecondsSinceEpoch}.jpg');
file.writeAsBytesSync(imgBytes!);
LogUtil.d("path=====${file.path}");
// 带图片路径返回
Get.back(result: {"imagePath": file.path});
} catch (e) {
LogUtil.d("e=====$e");
}
_isCapturing = false;
}
取消,重新拍照:
/// 取消,重新拍照
void _cancel() {
setState(() {
_takeStatus = TakeStatus.preparing;
});
_initCamera();
}
获取文件存储路径:
/// 获取文件存储路径
Future<String?> findSavePath(String basePath) async {
final directory = Platform.isAndroid ? await getExternalStorageDirectory() : await getApplicationDocumentsDirectory();
String saveDir = "${directory?.path ?? ""}/$basePath";
Directory root = Directory(saveDir);
if (!root.existsSync()) {
await root.create();
}
return saveDir;
}
页面初始化:
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_time = DateTimeUtils.getDateFromStamp(DateTimeUtils.getTimeStamp(), DateTimeUtils.yyyy_MM_dd_HH_mm);
_address = '未知位置';
_initCamera();
listenerOrientation();
getLocation();
_companyName = PreferencesUtils.getInstance().get("storeName") ?? "";
}
页面可见且获取焦点状态,类似于 Android onResume():
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if(!widget.cameraController.value.isInitialized) {
return;
}
if (state == AppLifecycleState.resumed) {
_initCamera();
}
}
这就是实现水印相机的全过程了~~~