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

一、背景

在移动应用开发中,实时相机功能结合水印的需求十分常见。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页面写入代码:

<template>
	<view class="live-camera" :style="{ width: windowWidth, height: windowHeight }">
		<view class="preview" :style="{ width: windowWidth, height: windowHeight}">
			<live-pusher id="livePusher" ref="livePusher" class="livePusher" mode="FHD" beauty="0" whiteness="0"
				:aspect="aspect" min-bitrate="1000" audio-quality="16KHz" device-position="back" auto-focus="false"
				muted="true" :enable-camera="true" :enable-mic="false" :zoom="false" @statechange="statechange"
				:style="{ width: windowWidth, height: windowHeight }"></live-pusher>
			<!--相机水印-->
			<cover-view class="remind">
				<text class="remind-text remind-name" style="">{{ username }}</text>
				<text class="remind-text remind-address" style="">{{ address }}</text>
				<text class="remind-text remind-time" style="">{{ time }}</text>
			</cover-view>
		</view>
		<view class="menu">
			<!-- 备注:这里是底部操作的图标,图标根据自己的项目风格,下载到static目录下后引入 -->
			<!--返回键-->
			<cover-image class="menu-back" @tap="back" src="@/static/images/back.png"></cover-image>
			<!--快门键-->
			<cover-image class="menu-snapshot" @tap="snapshot" src="@/static/images/kuaimen.png"></cover-image>
			<!--反转键-->
			<cover-image class="menu-flip" @tap="flip" src="@/static/images/fanzhuan.png"></cover-image>
		</view>
	</view>
</template>

<script>
	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-PLUS
				if (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;
			},
		}
	};
</script>

<style lang="less">
	.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;
	}
</style>

2.使用页面(takePhoto.vue)

<template>
	<view class="page">
		<!-- 水印相机页面 -->
		<navigator class="buttons" url="./watermarkCamera"><button type="primary">打开水印相机</button></navigator>
		<view>拍摄结果预览图,见下方</view>
		<view class="img-list">
			<view class="img-item" v-for="(item,index) in imgList" :key="index" @click="lookImg(index)">
				<image :src="item"></image>
			</view>
		</view>
		<canvas id="canvas-clipper" canvas-id="canvas-clipper" type="2d"
			:style="{width: canvasSiz.width+'px',height: canvasSiz.height+'px',position: 'absolute',left:'-500000px',top: '-500000px'}" />
	</view>
</template>

<script>
	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 < content.length; i++) {
						drawTxt += content[i];
						if (ctx.measureText(drawTxt).width >= 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 {
							// 内容绘制完毕,但是剩下的内容宽度不到lineMaxWidth
							if (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;
					}
				});
			}

		}
	};
</script>

<style lang="less">
	.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%;
	}
</style>

四、其它配置

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

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

五、注意事项

  • 需要真机调试

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

  • 位置信息需要GPS支持

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

六、效果

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值