Flutter实现水印相机

1,514 阅读2分钟

对于熟悉android开发的来说知道水印相机的实现方式大概有两种,一种是拍完照生成图片(jpg,png)后给图片加上水印,另一种是在相机预览view加上水印view,然后将view转bitmap的方式生成水印照片。而对于flutter开发来说,实现的方式可以怎样呢? 首先我们知道flutter相机相关的第三方库一般使用camera,现在我们使用camera: ^0.10.3来实现一个自定义水印相机,水印信息主要包括店名,时间和地址等,当然这些可以自定义,先看效果图:

image.png

                                        拍照前
                                          
                                          

image.png

                                        拍照完成
                                        

布局主要由预览视图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();
  }
}

这就是实现水印相机的全过程了~~~