この記事では、WeChat アプレット 保存画像コンポーネントの開発について説明します。皆さんのお役に立てれば幸いです。
多くの WeChat ミニ プログラムでは、ユーザーがポスターを保存することでアクティビティを共有し、より多くの人にミニ プログラムについて知らせることができます。私はミニ プログラムを開発しているときにこれに遭遇したはずです。バー。 [関連する学習の推奨事項: 小さなプログラム開発チュートリアル ]
今日は、会社で作成した小さなプログラムでポスターを保存する機能を共有します。まず、以前の社内のニーズがどのようなものだったのかをまず説明させていただきます。同社のオンライン ミニ プログラムは、新規ユーザーのプロモーションを長期的な目的としており、各ユーザーは独自のポスターを作成する必要があり、個人ポスターによるプロモーションは簡単な方法です。
私も課題を受けて、まずは調べ物をしようとユニバーサルインターネットに行ったのですが、先輩が「これと似たようなことをやったことがある」と教えてくれたんですが、その時は課題をこなすだけだったので、コードがとても汚かったので、彼は他のプロジェクトから始めました。私はコードを探して探して、ついに見つけました~~~しかし、私に与えられた時間は限られており、タスクは重いので、私が作らなければなりませんでしたいくつかの調整を行って最初に送信してください。その後、オンライン記事に従い、落とし穴を段階的に追跡し、ポスターを保存するためのコンポーネントを段階的に実装しました。
まず、コンポーネントが uniapp を使用することを宣言しましょう。これは、具体的には、絵を描く、テキストを描く、ポスターをアルバムに保存するという基本的な機能を実装します。開発中に十分に使用されます。
キャンバスにポスターを描きます。 uni.canvasToTempFilePath
を使用して、描画されたキャンバスを画像に変換します。 uni.saveImageToPhotosAlbum
を使用して、携帯電話のアルバムへのローカル一時パスに写真を保存します。 私のアイデアは、使用されるすべてのメソッドをコンポーネントにカプセル化し、使用する必要があるメソッドを呼び出し、関連するパラメーターを調整するために親コンポーネントのみを使用することです。 具体的な使用方法については、サンプル コードをご覧ください。
Promise を使用してポスター コンテンツを描画する順序を決定する物体。 promise.all()
メソッドはキャンバス ペイント操作の最後のステップを実行します context.draw()
画像とアバターを描画するとき、コンポーネントは uni.getImageInfo()
を通じて画像の関連情報を取得します。このメソッドの呼び出しが成功するための前提条件は、ダウンロード ドメイン名とリクエスト ドメインであることです。名前は WeChat アプレットのバックグラウンドで設定する必要がありますが、エラーを防ぐために、uploadFile ドメイン名も一緒に設定することが最善です。ただし、公式のヒントではダウンロードドメイン名のホワイトリストを設定することになっていますが、画像情報が取得できないという大きな落とし穴があります。
関連する構成がない場合、デバッグ中または試用版、正式版などで vconsole デバッグ ツールが開きます。 uni.getImageInfo() は画像情報を取得できますが、vconsole を閉じると uni.getImageInfo() が失敗するという落とし穴もあります。
##drawText(options) キャンバス上に単一行または複数行のテキストを描きます
<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>
以上がミニ プログラムに画像コンポーネントを保存する機能を実装する手順を説明します。の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。