手把手带你在小程序中实现保存图片组件功能

本篇文章带大家聊聊微信小程序保存图片组件开发,希望对大家有所帮助!

许多微信小程序通过保存海报让用户去分享活动让更多的人知道自己的小程序,想必在平时开发小程序的时候应该有遇见过吧。【相关学习推荐:小程序开发教程

今天我就来分享下之前在公司做的一个小程序保存海报的功能。首先我先描述下之前在公司做的需求是什么样的。公司上线的小程序会有一个长期的活动目的就是去推广新用户,每个用户都要有一张属于自己的海报,通过个人海报去推广则只是单纯的一种方式。

接到任务后,我也先去万能互联网做了调查但是我的师兄和我说这个做过类似的但是当时只是单纯为了完成任务所以代码很乱,然后他就从其他项目的代码找呀找,然后找到了给我~~~ 而当时给到我的时间紧任务重呀只好先用着调整一些并且交差了。之后呢我就根据网上的文章然后一步一步踩坑,一步一步走实现了一个保存海报的组件。

思路

首先声明下组件采用的是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绘制一张图片

  • drawCircularAvatar(context, url, _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 '@/components/SavePosterCom/SavePosterCom.vue'
	export default {
		components: {
			SavePosterCom
		},
		data() {
			return {
				canvasInfo: {
					canvasWidth: 620,
					canvasHeight: 950,
					canvasId: 'save-poster'
				},
				tempFilePath: '',
				canvasBgUrl: 'https://images.pexels.com/photos/4065617/pexels-photo-4065617.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500',
				avatarUrl: 'https://p9-passport.byteacctimg.com/img/user-avatar/4dbf31fa6dec9c65b78a70d28d843c04~300x300.image'
			}
		},
		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: '皮皮虾仁',
						dx: (canvasWH.canvasWidth / 2) + 60,
						dy: canvasWH.canvasHeight / 4,
						fontSize: 30,
						fontColor: '#5D4037'
					})
					
					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: '保存海报',
					content: '海报将被保存至相册中',
					confirmText: '保存',
					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 + 'px', height: canvasWH.canvasHeight + 'px'}></canvas>
	</view>
</template>

<script>
	export default {
		name: 'savePosterCom',
		data() {
			return {
				userPhoneWHInfo: {},
				canvasWH: {
					canvasWidth: 0,
					canvasHeight: 0
				}
			}
		},
		props: {
			// 决定保存下来的图片的宽高
			canvasInfo: {
				type: Object,
				default: () => {
					return {
						canvasWidth: 0,
						canvasHeight: 0,
						canvasId: 'canvasId'
					}
				}
			},
			// 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 = ''
				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 = 'black'
				} = options
				return new Promise((resolve, reject) => {
					context.setFontSize(fontSize)
					context.setFillStyle(fontColor)
					context.setTextBaseline('middle')
					// 获取需要绘制的文本宽度
					let textWidth = Number(context.measureText(text).width)
					// console.log('textWidth',textWidth)
					// 获取文本的字数 
					let textNum = text.length
					// 获取行数 向上取整
					let lineNum = Math.ceil(textNum / rowStrnum)
					// console.log('textNum',textNum)
					// console.log('lineNum',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('err', err)
							reject(err)
						}
					}, this)
				})
			},
			/** 保存生成的图片到本地相册中
			*  @param {String} filePath 图片临时路劲
			*/
			posterToPhotosAlbum(filePath) {
				console.log('filePath',filePath)
				uni.showLoading({
					title: '保存中...'
				})
				uni.saveImageToPhotosAlbum({
					filePath: filePath,
					success: (res) => {
						uni.showToast({
							title: '保存成功,请前往手机相册中查看',
							mask: true,
							icon: 'none',
							duration: 2000
						})
					},
					fail: (err) => {
						console.log('err',err)
						if (err.errMsg.includes('deny')||err.errMsg.includes('denied')) { // 用户选择拒绝 
							this.openSetting()
						} else if (err.errMsg.includes('fail cancel')) { // 用户在保存图片时 取消了
							uni.showToast({
								title: '已取消保存,无法保存至相册',
								mask: true,
								icon: 'none',
								duration: 2000
							})
							return
						}
					},
					complete: () => {
						uni.hideLoading()
					}
				})
			},
			/**
			* 打开摄像头设置权限页面
			*/
			openSetting() {
				uni.showModal({
					title: '温馨提示',
					content: '保存图片至相册中,需要您同意添加访问相册权限',
					cancelText: '拒绝',
					confirmText: '同意',
					success: res => {
						if (res.confirm) {
							uni.openSetting({
								success: settingdata => {
									if (settingdata.authSetting['scope.writePhotosAlbum']) {
										console.log('获取权限成功,给出再次点击图片保存到相册的提示。')
										uni.showToast({
											title: '授权成功,请再次点击保存',
											icon: 'none',
											duration: 2000,
										})
									} else {
										console.log('获取权限失败,给出不给权限就无法正常使用的提示')
										uni.showToast({
											title: '需要访问相册权限',
											icon: 'none',
											duration: 2000,
										})
									}
								},
								fail: (res) => {
									console.log('err', err)
								}
							})
						} else {
							uni.showToast({
								title: '已拒绝授权,无法保存至相册',
								mask: true,
								icon: 'none',
								duration: 2000
							})
							return
						}
					}
				})
			}
		}
	}
</script>

<style>
</style>

效果

jgedx-x12pb.gif

更多编程相关知识,请访问:编程入门!!

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


小程序用什么css框架
概述 消息能力是小程序能力中的重要组成,我们为开发者提供了订阅消息能力,以便实现服务的闭环和更优的体验。 订阅消息推送位置:服务通知 订阅消息下发条件:用户自主订阅 订阅消息卡片跳转能力:点击查看详情可提爱转至该小程序的页面 消息类型 一次性订阅消息 一次性订阅消息用于解决用户使用小程序后,后续服务
判断H5页面环境在微信中还是小程序中 用小程序提供的wx.miniProgram.getEnv可以获取环境参数 &lt;script type=&quot;text/javascript&quot; src=&quot;https://res.wx.qq.com/open/js/jweixin-1.
wx.reLaunch和wx.navigateTo,wx.navigateTo的区别 2019-03-23 11:18:05 wx.navigateTo 用于保留当前页面、跳转到应用内的某个页面,使用 wx.navigateBack可以返回到原页面。对于页面不是特别多的小程序,通常推荐使用 wx.n
微信小程序如何从数组里取值_微信小程序 传值取值的几种方法总结 小程序里常见的取值有以下几种,一个完整的项目写下来,用到的概率几乎是100%。 列表index下标取值 页面传值 form表单取值 1. 列表index下标取值 实现方式是:data-index=&quot;{{index}}&quot
H5项目接入微信授权登录,通过 UA 区分微信还是普通浏览器: let&#160;ua&#160;=&#160;navigator.userAgent.toLowerCase(); let&#160;isWeixin&#160;=&#160;ua.indexOf(&#39;micromessenge
微信小程序获取data-xx=&quot;&quot;属性的值,自定义属性设置和获取(data-) 微信小程序&lt;view class=&quot;details-btn&quot; data-taskId=&quot;111&quot; bindtap=&#39;taskdetails&#39
小程序报错:TypeError: Cannot read property ‘addEventListener‘ of undefined 解决办法 将调试基础库由2.16.0(或者当前的) -&gt; 2.14.1 解决问题
H5跳转微信小程序-成功案例(VUE)(踩坑无数) TuoMei 已于 2022-07-29 09:52:22 修改 准备工作 根据官方提供的资料需准备以下几点: 1、已认证的服务号 2、绑定JS接口安全域名 (在微信公众平台设置) 3、IP白名单 (在微信公众平台设置) 4、将小程序和H5公众号进
微信小程序 页面跳转和数据传递实例详解 这篇文章主要介绍了微信小程序 页面跳转和数据传递实例详解的相关资料,这里附有实例代码帮助到家学习理解,需要的朋友可以参考下 微信小程序 页面跳转和数据传递 1.先导 在Android中,我们Activity和Fragment都有栈的概念在里面,微信小程序页面也
情景1.拉取公司代码演示: 因为github有墙,这里我们以gitee(码云)为例作为演示 (其实就是国产github,也非常好用~) 步骤一:打开Git界面 先在一个空文件夹右击Git Bash Here,打开git界面 步骤二:输入克隆远程仓库指令 别人复制的链接在这里获取 拿到别人赋值的链接自
如何开发微信小程序? 作为一名10多年一直从事互联网平台开发的从业者,我来回答下这个问题吧。 微信小程序开发流程总体可以归纳为4个步骤, 老张带您捋一捋整个环节,小白用户可以收藏了。 好了废话不多说,开始! 一、开发前小程序需要准备的资料 我们在开发微信小程序前,需要准备下相关资料。这个资料主要是后
原生小程序开发优化方案 为了更好的制定优化方案,我们 有必要先了解下小程序的底层架构、以及与普通网页开发的差异 小程序最终渲染载体与当下一些热门的技术 Flutter、React Native等不同,依然是浏览器内核,而不是原生客户端。 而对于传统的网页来说,UI 渲染和 JS 脚本是在同一个线程中
1,不要下两倍尺寸的图片, 小程序本身自己就会对元素缩小两倍,设计图片的一杯就已经很清晰了。 2,图片压缩,(主要是压缩静态资源,ps 可以压缩,然后有一些在线压缩工具,保持600-800kb 的静态) 3,通用的代码组件化 4,是在工程量太大可以分包,分包现在最大支持20m(一般都不会去分包的)
文章浏览阅读189次。人工智能研究实验室OpenAI在2022年11月30日发布了自然语言生成模型ChatGPT,上线两个月就已经超过一亿用户,成为了人工智能界当之无愧的超级大网红。ChatGPT凭借着自身强大的拟人化及时应答能力迅速破圈,引起了各行各业的热烈讨论。简单来说ChatGPT就是可以基于用户文本输入自动生成回答的人工智能聊天机器人。那肯定会有人说这不就是Siri嘛,虽然都是交互机器人但是两者的差别可老大了。那么ChatGPT在人机交互时为什么会有这么出色的表现?它到底会不会取代搜索引擎?90%的人真的会因为ChatG
文章浏览阅读193次。8. 导航和路由管理:掌握小程序的导航方式,如使用wx.navigateTo跳转页面、使用wx.redirect重定向页面等,学会实现页面之间的跳转和传参。1. 小程序的基本概念和架构:了解小程序的定义、特点以及与传统APP的区别,掌握小程序的运行环境、组件和API等基本概念。10. 支付功能:学习小程序的支付方式,如微信支付、支付宝支付等,了解支付流程和注意事项,学会实现小程序的支付功能。9. 用户授权和登录:了解小程序的用户授权机制,如获取用户信息、调用微信API等,学会实现用户的登录和注册功能。_微信小程序开发知识点总结
文章浏览阅读4.8k次,点赞7次,收藏18次。一、准备工作1. 安装微信开发者工具,并登录微信小程序账号;2. 准备斗地主游戏的图片资源;3. 准备斗地主游戏的音效资源;二、创建小程序1. 打开微信开发者工具,点击“新建小程序”,输入小程序名称,选择小程序的项目目录,点击“创建”;2. 在小程序的项目目录中,新建文件夹“images”,将准备好的斗地主游戏的图片资源放入“images”文件夹中;3. 在小程序的项目目录中,新建文件夹“sounds”,将准备好的斗地主游戏的音效资源放入“sounds”文件夹中;三、编写代码1. 在小程_扑克牌微信小程序代码
文章浏览阅读3.9k次,点赞3次,收藏7次。一、准备工作:1. 安装微信开发者工具,创建小程序项目;2. 准备游戏角色图片;3. 准备游戏背景音乐;二、实现步骤:1. 创建游戏页面,添加游戏角色图片,添加游戏背景音乐;2. 创建游戏角色类,定义游戏角色属性,如角色名称、角色图片、角色能力等;3. 创建游戏类,定义游戏属性,如游戏人数、游戏角色、游戏规则等;4. 创建游戏控制类,定义游戏流程,如游戏开始、游戏结束、游戏角色分配等;5. 创建游戏界面,实现游戏流程,如游戏开始、游戏结束、游戏角色分配等;6. 创建游戏结果页面,显示游戏_微信小程序游戏代码
文章浏览阅读1.7k次。1. 创建小程序项目:使用微信开发者工具创建一个小程序项目,并在项目中添加一个页面,用于模拟聊天。 2. 定义数据结构:定义一个数据结构,用于存储聊天记录,包括发送者、接收者、消息内容等信息。 3. 实现聊天功能:实现聊天功能,包括发送消息、接收消息、显示消息等功能。 4. 实现界面:使用微信小程序的界面框架,实现聊天界面,包括聊天记录列表、输入框等。代码示例:// 定义数据结构var chatData = { sender: '', receiver: '', message: '' };_制作聊天对话小程序代码