> 技术文档 > uniapp安卓app实现水印相机(附完整代码)_uniapp水印相机

uniapp安卓app实现水印相机(附完整代码)_uniapp水印相机


一、背景

在移动应用开发中,实时相机功能结合水印的需求十分常见。uniapp提供的uni.chooseImage(OBJECT)可以直接调用相机进行拍照,但部分机型中调用 uniapp提供的chooseImage  API进行拍照后会出现应用闪退的bug(应用重启),出现该问题的原因https://juejin.cn/post/7308219746830630938,是安卓系统回收资源,结束了应用进程导致的(调用系统相机进行拍摄,应用处于后台)。

本文将通过uniapp的live-pusher组件,实现一个支持实时水印、拍照保存的安卓相机应用,并提供完整代码实现。

二、实现原理

使用live-pusher组件进行相机画面捕捉,通过canvas叠加水印层,结合uni.saveFile实现照片保存。

三、代码实现

1.水印相机组件(这里创建页面时要选择nvue,live-pusher在nvue中更友好),这里命名为:waterMark.nvue

在waterMark.nvue页面写入代码:

{{ username }}{{ address }}{{ time }}let _this = null;export default {data() {return {dotype: \'watermark\',message: \'live-camer\', //水印内容username: \'张三\',//水印中名字部分address: \'无法获取地址\',//水印中地址部分time: \'2025-04-04 10:23\',//水印中时间部分poenCarmeInterval: null, //打开相机的轮询aspect: \'2:3\', //比例windowWidth: \'\', //屏幕可用宽度windowHeight: \'\', //屏幕可用高度camerastate: false, //相机准备好了livePusher: null, //流视频对象snapshotsrc: null, //快照timer: null, //定时器};},onLoad(e) {_this = this;if (e.dotype != undefined) this.dotype = e.dotype;this.initCamera();},onReady() {this.getAddress();let date = new Date()this.time = this.dateFormat(\"YYYY-mm-dd HH:MM\", date);this.livePusher = uni.createLivePusherContext(\'livePusher\', this);this.startPreview(); //开启预览并设置摄像头this.poenCarme();},onShow() {clearInterval(this.timer)// 每隔10秒刷新地址和时间this.timer = setInterval(() => {this.getAddress();let date = new Date()this.time = this.dateFormat(\"YYYY-mm-dd HH:MM\", date);}, 10000);},onUnload() {clearInterval(this.timer)},methods: {getAddress() {uni.getLocation({type: \'gcj02\',geocode: true,isHighAccuracy: true,success: (res) => {this.address = res.address.province + res.address.city + res.address.district + res.address.street + res.address.streetNum + res.address.poiName;console.log(\'当前位置:\', this.address);console.log(\'当前位置的经度:\' + res.longitude);console.log(\'当前位置的纬度:\' + res.latitude);}});},//轮询打开poenCarme() {//#ifdef APP-PLUSif (plus.os.name == \'Android\') {this.poenCarmeInterval = setInterval(function() {console.log(_this.camerastate);if (!_this.camerastate) _this.startPreview();}, 2500);}//#endif},//初始化相机initCamera() {uni.getSystemInfo({success: function(res) {_this.windowWidth = res.windowWidth;_this.windowHeight = res.windowHeight;let zcs = _this.aliquot(_this.windowWidth, _this.windowHeight);_this.aspect = _this.windowWidth / zcs + \':\' + _this.windowHeight / zcs;console.log(\'画面比例:\' + _this.aspect);}});},//整除数计算aliquot(x, y) {if (x % y == 0) return y;return this.aliquot(y, x % y);},//开始预览startPreview() {this.livePusher.startPreview({success: a => {console.log(a);}});},//停止预览stopPreview() {this.livePusher.stopPreview({success: a => {_this.camerastate = false; //标记相机未启动}});},//状态statechange(e) {//状态改变console.log(e);if (e.detail.code == 1007) {_this.camerastate = true;} else if (e.detail.code == -1301) {_this.camerastate = false;}},//返回back() {uni.navigateBack();},//抓拍snapshot() {this.livePusher.snapshot({success: e => {_this.snapshotsrc = e.message.tempImagePath;_this.stopPreview();_this.setImage();uni.navigateBack();}});},//反转flip() {this.livePusher.switchCamera();},//设置setImage() {let pages = getCurrentPages();let prevPage = pages[pages.length - 2]; //上一个页面//直接调用上一个页面的setImage()方法,把数据存到上一个页面中去prevPage.$vm.watermark({path: _this.snapshotsrc,info: {username: this.username,address: this.address,time: this.time}});},dateFormat(fmt, date) {let ret;const opt = {\"Y+\": date.getFullYear().toString(), // 年\"m+\": (date.getMonth() + 1).toString(), // 月\"d+\": date.getDate().toString(), // 日\"H+\": date.getHours().toString(), // 时\"M+\": date.getMinutes().toString(), // 分\"S+\": date.getSeconds().toString() // 秒// 有其他格式化字符需求可以继续添加,必须转化成字符串};for (let k in opt) {ret = new RegExp(\"(\" + k + \")\").exec(fmt);if (ret) {fmt = fmt.replace(ret[1], (ret[1].length == 1) ? (opt[k]) : (opt[k].padStart(ret[1].length, \"0\")))};};return fmt;},}};.live-camera {justify-content: center;align-items: center;}.preview {justify-content: center;align-items: center;}.remind {position: absolute;top: 80rpx;left: 20rpx;z-index: 100;}.remind-text {color: #dddddd;width: 710rpx;}.remind-name {font-size: 40rpx;}.remind-address {font-size: 36rpx;}.remind-time {font-size: 30rpx;}.menu {position: absolute;left: 0;bottom: 0;width: 750rpx;height: 180rpx;z-index: 98;align-items: center;justify-content: center;}.menu-mask {position: absolute;left: 0;bottom: 0;width: 750rpx;height: 180rpx;z-index: 98;}.menu-back {position: absolute;left: 30rpx;bottom: 50rpx;width: 80rpx;height: 80rpx;z-index: 99;align-items: center;justify-content: center;}.menu-snapshot {width: 130rpx;height: 130rpx;z-index: 99;}.menu-flip {position: absolute;right: 30rpx;bottom: 50rpx;width: 80rpx;height: 80rpx;z-index: 99;align-items: center;justify-content: center;}

2.使用页面(takePhoto.vue)

拍摄结果预览图,见下方var _this;export default {data() {return {windowWidth: \'\',windowHeight: \'\',imagesrc: null,imgList: [],canvasSiz: {width: 188,height: 273}};},onLoad() {_this = this;this.init();},methods: {//添加照片水印watermark(info) {console.log(\"获取到的数据为\", info)uni.getImageInfo({src: info.path,success: function(image) {console.log(image);_this.canvasSiz.width = image.width;_this.canvasSiz.height = image.height;let maxWidth = image.width - 60;setTimeout(() => {let ctx = uni.createCanvasContext(\'canvas-clipper\', _this);ctx.drawImage(info.path,0,0,image.width,image.height);//具体位置如需和相机页面上一致还需另外做计算,此处仅做大致演示ctx.setFillStyle(\'white\');ctx.setFontSize(50);ctx.fillText(info.info.username, 20, 150);ctx.setFontSize(50);let previousRowHeight = _this.textPrewrap(ctx, info.info.address, 20, 220,70, maxWidth, 3);//再来加个时间水印ctx.setFontSize(40);ctx.fillText(info.info.time, 20, previousRowHeight + 70);ctx.draw(false, () => {uni.canvasToTempFilePath({destWidth: image.width,destHeight: image.height,canvasId: \'canvas-clipper\',fileType: \'jpg\',success: function(res) {_this.savePhoto(res.tempFilePath);}},_this);});}, 500)}});},textPrewrap(ctx, content, drawX, drawY, lineHeight, lineMaxWidth, lineNum) {var drawTxt = \'\'; // 当前绘制的内容var drawLine = 1; // 第几行开始绘制var drawIndex = 0; // 当前绘制内容的索引// 判断内容是否可以一行绘制完毕if (ctx.measureText(content).width <= lineMaxWidth) {ctx.fillText(content.substring(drawIndex, i), drawX, drawY);} else {for (var i = 0; i = lineMaxWidth) {if (drawLine >= lineNum) {ctx.fillText(content.substring(drawIndex, i) + \'..\', drawX, drawY);break;} else {ctx.fillText(content.substring(drawIndex, i + 1), drawX, drawY);drawIndex = i + 1;drawLine += 1;drawY += lineHeight;drawTxt = \'\';}} else {// 内容绘制完毕,但是剩下的内容宽度不到lineMaxWidthif (i === content.length - 1) {ctx.fillText(content.substring(drawIndex), drawX, drawY);return drawY;console.log(\"最后高度为\", drawY);}}}}},//保存图片到相册,方便核查savePhoto(path) {this.imgList.push(path)},lookImg(index) {// 预览图片uni.previewImage({current: index,urls: this.imgList,});},//初始化init() {let _this = this;uni.getSystemInfo({success: function(res) {_this.windowWidth = res.windowWidth;_this.windowHeight = res.windowHeight;}});}}};.page {width: 750rpx;justify-content: center;align-items: center;flex-direction: column;display: flex;.buttons {width: 600rpx;}}.img-list {padding: 20rpx;display: flex;align-items: center;justify-content: flex-start;flex-wrap: wrap;}.img-item {width: 100rpx;height: 100rpx;margin-right: 20rpx;margin-bottom: 20rpx;}.img-item image {width: 100%;height: 100%;}

四、其它配置

1.直播推流权限(必选)

2.定位权限(根据需要选择)

五、注意事项

  • 需要真机调试

  • 部分安卓机型需要手动开启相机权限

  • 位置信息需要GPS支持

  • 如果打卡拍照水印正常显示,背景是白屏,则是相机权限问题(1.manifest.json勾选;2.手机应用授权使用相机;3.真机调试的时候要勾选使用标准基座,不要自定义基座)

六、效果

DJ舞曲网