首頁 > 微信小程式 > 小程式開發 > 手把手帶你在小程式中實現保存圖片組件功能

手把手帶你在小程式中實現保存圖片組件功能

青灯夜游
發布: 2021-10-27 10:46:40
轉載
2457 人瀏覽過

這篇文章帶大家聊聊微信小程式儲存圖片元件開發,希望對大家有幫助!

手把手帶你在小程式中實現保存圖片組件功能

許多微信小程式透過保存海報讓用戶去分享活動讓更多的人知道自己的小程序,想必在平時開發小程序的時候應該有遇見過吧。 【相關學習推薦:小程式開發教學

今天我就來分享下之前在公司做的一個小程式保存海報的功能。首先我先描述下之前在公司做的需求是什麼樣的。公司上線的小程式會有一個長期的活動目的就是去推廣新用戶,每個用戶都要有一張屬於自己的海報,透過個人海報去推廣則只是單純的一種方式。

接到任務後,我也先去萬能互聯網做了調查但是我的​​師兄和我說這個做過類似的但是當時只是單純為了完成任務所以代碼很亂,然後他就從其他項目的程式碼找呀找,然後找到了給我~~~ 而當時給到我的時間緊任務重呀只好先用著調整一些並且交差了。之後呢我就根據網路上的文章然後一步一步踩坑,一步一步走實現了一個保存海報的組件。

想法

首先聲明下元件採用的是uniapp,具體實現了可以繪製圖片、繪製文字以及保存海報至相簿的基本功能,在開發中這些也完全夠用了。

透過canvas繪製海報。透過uni.canvasToTempFilePath 將繪製好的 canvas轉為圖片。透過uni.saveImageToPhotosAlbum 將本地臨時路徑的圖片儲存至手機相簿。 而我的想法是將所有採用的方法全部封裝到元件中,只透過父元件去呼叫需要使用的方法和調整相關的參數。 具體使用可以查看範例程式碼

透過canvas繪製海報內容的順序先後問題

透過使用promise物件決定繪製海報內容的順序先後。 promise.all()方法進行canvas最後一步的繪畫操作 context.draw()

注意uni.getImageInfo()

  • #在繪製圖片和頭像時,元件透過uni.getImageInfo() 去取得圖片的相關信息,呼叫該方法成功的前提是需要在微信小程式後台配置download域名和request域名當然最好把uploadFile域名也一起配置,防止出差錯。但是官方給出的提示是配置download域名白名單即可,但是獲取不到圖片信息,這算是一個大坑了。

  • 如果沒有進行相關配置,在偵錯時 或 體驗版 正式版等 開啟了vconsole偵錯工具。 uni.getImageInfo() 是可以取得到圖片資訊的,一旦關閉了vconsole uni.getImageInfo() 將會fail, 也是個坑。

本元件方法,變數介紹

props

  • #canvasInfo Object (必要)

    • canvasWidth 畫布寬度

    • canvasHeight 畫布高度

    • canvasId 畫布識別

  • #isFullScreen Boolean

  • 為ture時表示畫布為手機螢幕全屏,canvasInfo設定的寬高將失效。

  • 預設為false

  • #methods

canvasInit(callback) canvas初始化,所有有關畫布canvas操作需在其回呼函數操作。

drawCanvasImage(context, src, _imageWidth, _imageHeight, dx, dy) 在canvas繪製圖片

手把手帶你在小程式中實現保存圖片組件功能

_circularX, _circularY, _circularR) 在canvas繪製一張圓形圖片

###drawText(options) 在canvas繪製單行、多行文字#################### startDrawToImage(context, promiseArr, callback) 將canvas操作draw()進行繪製############posterToPhotosAlbum(filePath) 儲存至手機相簿#############範例程式碼###
<template>
	<view>
		<view class="savePosterItem">
			<image v-show="tempFilePath" :src="tempFilePath"></image>
			<save-poster-com v-show="!tempFilePath" ref="savePoster" :canvasInfo="canvasInfo"></save-poster-com>
		</view>
		
		
		<button class="savePosterBtn" type="primary" @click="saveBtnFun">保存海报</button>
	</view>
</template>

<script>
	import SavePosterCom from &#39;@/components/SavePosterCom/SavePosterCom.vue&#39;
	export default {
		components: {
			SavePosterCom
		},
		data() {
			return {
				canvasInfo: {
					canvasWidth: 620,
					canvasHeight: 950,
					canvasId: &#39;save-poster&#39;
				},
				tempFilePath: &#39;&#39;,
				canvasBgUrl: &#39;https://images.pexels.com/photos/4065617/pexels-photo-4065617.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500&#39;,
				avatarUrl: &#39;https://p9-passport.byteacctimg.com/img/user-avatar/4dbf31fa6dec9c65b78a70d28d843c04~300x300.image&#39;
			}
		},
		onLoad() {
			let {
				drawCanvasImage,
				drawCircularAvatar,
				drawText
			} = this.$refs.savePoster.$options.methods
			this.$refs.savePoster.canvasInit(({
				context,
				comThis
			}) => {
				// 获取画布宽高
				let canvasWH = comThis.canvasWH
				// 绘制海报背景图
				let promise_1 = drawCanvasImage(context, this.canvasBgUrl, canvasWH.canvasWidth, canvasWH.canvasHeight)
				// 必须先绘制玩海报背景图 再去操作其他绘制内容
				promise_1.then(res => {
					let promise_2 = drawCircularAvatar(context, this.avatarUrl, canvasWH.canvasWidth / 2, canvasWH.canvasHeight /
						7, 70)
					
					let promise_3 = drawText({
						context: context,
						text: &#39;皮皮虾仁&#39;,
						dx: (canvasWH.canvasWidth / 2) + 60,
						dy: canvasWH.canvasHeight / 4,
						fontSize: 30,
						fontColor: &#39;#5D4037&#39;
					})
					
					let promise_4 = drawCanvasImage(context, this.avatarUrl, 150, 150, (canvasWH.canvasWidth / 2) + 85, (canvasWH.canvasHeight -
						165))
					 
					this.$refs.savePoster.startDrawToImage(context, [promise_1,promise_2,promise_4], (tempFilePath) => {
						this.tempFilePath = tempFilePath
					})
				})
			})
		},
		methods: {
			saveBtnFun() {
				uni.showModal({
					title: &#39;保存海报&#39;,
					content: &#39;海报将被保存至相册中&#39;,
					confirmText: &#39;保存&#39;,
					success: (res) => {
						if(res.confirm) {
							this.$refs.savePoster.posterToPhotosAlbum(this.tempFilePath)
						}
					}
				})
			}
		}
	}
</script>

<style>
	.savePosterItem {
		text-align: center;
	}
	.savePosterItem > image {
		width: 620rpx;
		height: 950rpx;
	}
	
	.savePosterBtn {
		margin-top: 40rpx;
		width: 80%;
	}
</style>
登入後複製
###元件原始碼###
<template>
	<view>
		<canvas :canvas-id="canvasInfo.canvasId" :style="{width: canvasWH.canvasWidth + &#39;px&#39;, height: canvasWH.canvasHeight + &#39;px&#39;}"></canvas>
	</view>
</template>

<script>
	export default {
		name: &#39;savePosterCom&#39;,
		data() {
			return {
				userPhoneWHInfo: {},
				canvasWH: {
					canvasWidth: 0,
					canvasHeight: 0
				}
			}
		},
		props: {
			// 决定保存下来的图片的宽高
			canvasInfo: {
				type: Object,
				default: () => {
					return {
						canvasWidth: 0,
						canvasHeight: 0,
						canvasId: &#39;canvasId&#39;
					}
				}
			},
			// canvas画布是不是全屏,默认是false。 false时使用必须传 canvasInfo
			isFullScreen: Boolean
		},
		created() {
			this.userPhoneWHInfo = this.getPhoneSystemInfo()
			if (this.isFullScreen) { // 画布全屏
				this.canvasWH.canvasWidth = this.userPhoneWHInfo.windowWidth
				this.canvasWH.canvasHeight = this.userPhoneWHInfo.windowHeight
			} else { // 指定宽高
				this.canvasWH.canvasWidth = this.canvasInfo.canvasWidth
				this.canvasWH.canvasHeight = this.canvasInfo.canvasHeight
			}
		},
		mounted() {},
		methods: {
			/**
			* 获取用户手机屏幕信息
			*/
			getPhoneSystemInfo() {
				const res = uni.getSystemInfoSync();
				return {
					windowWidth: res.windowWidth,
					windowHeight: res.windowHeight
				}
			},
			/** 获取 CanvasContext实例
			* @param {String} canvasId 
			*/
			getCanvasContextInit(canvasId) {
				return uni.createCanvasContext(canvasId, this)
			},
			/** 保存海报组件初始化
			* @param {Function} callback(context) 回调函数
			*/
			canvasInit(callback) {
				let context = this.getCanvasContextInit(this.canvasInfo.canvasId)
				if (context) {
					callback({
						context: context,
						comThis: this
					})
				}
			},
			/** 将上诉的绘制画到画布中 并且 将画布导出为图片
			*  @param context 画布
			*  @param {Promise[]} 存放Promise的数组 
			*  @param {Function} callback 保存图片后执行的回调函数(本地图片临时路径)
			*/
			startDrawToImage(context, promiseArr, callback) {
				// 将之前在绘图上下文中的描述(路径、变形、样式)画到 canvas 中
				let canvasId = this.canvasInfo.canvasId
				let tempFilePath = &#39;&#39;
				Promise.all(promiseArr).then(res => {
					context.draw(false, async () => {
						callback(await this.canvasToImage(canvasId))
					})
				})
			},
			/**
			* 在canvas绘制一张图片
			* @param context 画布
			* @param src 图片资源
			* @param _imageWidth 图片宽度
			* @param _imageHeight 图片高度 
			*/
			drawCanvasImage(context, src, _imageWidth, _imageHeight, dx, dy) {
				return new Promise((resolve, reject) => {
					uni.getImageInfo({
						src: src,
						success: res => {
							context.drawImage(res.path, (dx - _imageWidth), (dy - _imageHeight), _imageWidth, _imageHeight)
							resolve(context)
						},
					})
				})
			},
			/** 绘制一个圆形头像
			* @param  context 画布 
			* @param  url     图片地址
			* @param  _circularX  圆心X坐标
			* @param  _circularY  圆心Y坐标
			* @param  _circularR  圆半径
			*/
			drawCircularAvatar(context, url, _circularX, _circularY, _circularR) {
				let dx = _circularX - _circularR;
				let dy = _circularY - _circularR;
				let dwidth = _circularR * 2;
				let dheight = _circularR * 2
				return new Promise((resolve, reject) => {
					uni.downloadFile({
						url: url,
						success: res => {
							context.save()
							context.beginPath()
							// _circularX圆的x坐标  _circularY圆的y坐标  _circularR圆的半径
							context.arc(_circularX, _circularY, _circularR, 0, 2 * Math.PI)
							context.clip()
							// dx: 图像的左上角在目标canvas上 X 轴的位置
							// dy: 图像的左上角在目标canvas上 Y 轴的位置
							// dwidth: 在目标画布上绘制图像的宽度,允许对绘制的图像进行缩放
							// dheight: 在目标画布上绘制图像的高度,允许对绘制的图像进行缩放
							context.drawImage(res.tempFilePath, dx, dy, dwidth, dheight)
							context.restore()
							// context.draw()
							resolve(context)
						}
					})
				})
			},
			/** 绘制多行文本 注:, 和 空格都算一个字
			* @param context 画布
			* @param text 需要被绘制的文本
			* @param dx 左上角x坐标
			* @param dy 右上角y坐标
			* @param rowStrnum 每行多少个字 (默认为text字体个数->单行)
			* @param fontSize 文字大小 (默认16)
			* @param fontColor 文字颜色 (默认black)
			* @param lineHeight 单行文本行高 (默认0)
			*/
			drawText(options) {
				let {
					context,
					text,
					dx,
					dy,
					rowStrnum = text.length,
					lineHeight = 0,
					fontSize = 16,
					fontColor = &#39;black&#39;
				} = options
				return new Promise((resolve, reject) => {
					context.setFontSize(fontSize)
					context.setFillStyle(fontColor)
					context.setTextBaseline(&#39;middle&#39;)
					// 获取需要绘制的文本宽度
					let textWidth = Number(context.measureText(text).width)
					// console.log(&#39;textWidth&#39;,textWidth)
					// 获取文本的字数 
					let textNum = text.length
					// 获取行数 向上取整
					let lineNum = Math.ceil(textNum / rowStrnum)
					// console.log(&#39;textNum&#39;,textNum)
					// console.log(&#39;lineNum&#39;,lineNum)
					for (let i = 0; i < lineNum; i++) {
						let sliceText = text.slice(i * rowStrnum, (i + 1) * rowStrnum)
						// fillText 的 dx = 文字最左边的距离到屏幕政策的距离
						context.fillText(sliceText, dx - textWidth, dy + i * lineHeight);
					}
					resolve(context)
				})
			},
			/** 将画布导出为图片
			* @param canvasId 画布标识
			*/
			canvasToImage(canvasId) {
				return new Promise((resolve, reject) => {
					uni.canvasToTempFilePath({
						canvasId: canvasId, // 画布标识
						success: res => {
							// 在H5平台下,tempFilePath 为 base64
							resolve(res.tempFilePath)
						},
						fail: err => {
							console.log(&#39;err&#39;, err)
							reject(err)
						}
					}, this)
				})
			},
			/** 保存生成的图片到本地相册中
			*  @param {String} filePath 图片临时路劲
			*/
			posterToPhotosAlbum(filePath) {
				console.log(&#39;filePath&#39;,filePath)
				uni.showLoading({
					title: &#39;保存中...&#39;
				})
				uni.saveImageToPhotosAlbum({
					filePath: filePath,
					success: (res) => {
						uni.showToast({
							title: &#39;保存成功,请前往手机相册中查看&#39;,
							mask: true,
							icon: &#39;none&#39;,
							duration: 2000
						})
					},
					fail: (err) => {
						console.log(&#39;err&#39;,err)
						if (err.errMsg.includes(&#39;deny&#39;)||err.errMsg.includes(&#39;denied&#39;)) { // 用户选择拒绝 
							this.openSetting()
						} else if (err.errMsg.includes(&#39;fail cancel&#39;)) { // 用户在保存图片时 取消了
							uni.showToast({
								title: &#39;已取消保存,无法保存至相册&#39;,
								mask: true,
								icon: &#39;none&#39;,
								duration: 2000
							})
							return
						}
					},
					complete: () => {
						uni.hideLoading()
					}
				})
			},
			/**
			* 打开摄像头设置权限页面
			*/
			openSetting() {
				uni.showModal({
					title: &#39;温馨提示&#39;,
					content: &#39;保存图片至相册中,需要您同意添加访问相册权限&#39;,
					cancelText: &#39;拒绝&#39;,
					confirmText: &#39;同意&#39;,
					success: res => {
						if (res.confirm) {
							uni.openSetting({
								success: settingdata => {
									if (settingdata.authSetting[&#39;scope.writePhotosAlbum&#39;]) {
										console.log(&#39;获取权限成功,给出再次点击图片保存到相册的提示。&#39;)
										uni.showToast({
											title: &#39;授权成功,请再次点击保存&#39;,
											icon: &#39;none&#39;,
											duration: 2000,
										})
									} else {
										console.log(&#39;获取权限失败,给出不给权限就无法正常使用的提示&#39;)
										uni.showToast({
											title: &#39;需要访问相册权限&#39;,
											icon: &#39;none&#39;,
											duration: 2000,
										})
									}
								},
								fail: (res) => {
									console.log(&#39;err&#39;, err)
								}
							})
						} else {
							uni.showToast({
								title: &#39;已拒绝授权,无法保存至相册&#39;,
								mask: true,
								icon: &#39;none&#39;,
								duration: 2000
							})
							return
						}
					}
				})
			}
		}
	}
</script>

<style>
</style>
登入後複製
###效果################更多程式設計相關知識,請造訪:###程式設計入門###! ! ###

以上是手把手帶你在小程式中實現保存圖片組件功能的詳細內容。更多資訊請關注PHP中文網其他相關文章!

來源:juejin.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
最新問題
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板