專案需求寫完有一段時間了,但是還是想回過來總結一下,一是對專案的回顧優化等,二是對坑的地方做個記錄,避免以後遇到類似的問題。
需求
利用微信強大的社交能力通過小程式達到裂變的目的,拉取新使用者。
生成的海報如下
需求分析
1、利用小程式官方提供的api可以直接分享轉發到微信群開啟小程式
2、利用小程式生成海報儲存圖片到相簿分享到朋友圈,使用者長按識別二維碼關注公眾號或者開啟小程式來達到裂變的目的
實現方案
一、分析如何實現
相信大家應該都會有類似的迷惑,就是如何按照產品設計的那樣繪製成海報,其實當時我也是不知道如何下手,認真想了下得通過canvas繪製成圖片,這樣使用者儲存這個圖片到相簿,就可以分享到朋友圈了。但是要繪製的圖片上面不僅有文字還有數字、圖片、二維碼等且都是活的,這個要怎麼動態生成呢。認真想了下,需要一點一點的將文字和數字,背景圖繪製到畫布上去,這樣通過api最終合成一個圖片匯出到手機相簿中。
二、需要解決的問題
1、二維碼的動態獲取和繪製(包括如何生成小程式二維碼、公眾號二維碼、開啟網頁二維碼)
2、背景圖如何繪製,獲取圖片資訊
3、將繪製完成的圖片儲存到本地相簿
4、處理使用者是否取消授權儲存到相簿
三、實現步驟
這裡我具體寫下圍繞上面所提出的問題,描述大概實現的過程
①首先建立canvas畫布,我把畫布定位設成負的,是為了不讓它顯示在頁面上,是因為我嘗試把canvas通過判斷條件動態的顯示和隱藏,在繪製的時候會出現問題,所以採用了這種方法,這裡還有一定要設定畫布的大小。
<canvas canvas-id="myCanvas" style="width: 690px;height:1085px;position: fixed;top: -10000px;"></canvas>
②建立好畫布之後,先繪製背景圖,因為背景圖我是放在本地,所以獲取 <canvas> 元件 canvas-id 屬性,通過createCanvasContext建立canvas的繪圖上下文 CanvasContext 物件。使用drawImage繪製影像到畫布,第一個引數是圖片的本地地址,後面兩個引數是影像相對畫布左上角位置的x軸和y軸,最後兩個引數是設定影像的寬高。
const ctx = wx.createCanvasContext('myCanvas')
ctx.drawImage('/img/study/shareimg.png', 0, 0, 690, 1085)
③建立好背景圖後,在背景圖上繪製頭像,文字和數字。通過getImageInfo獲取頭像的資訊,這裡需要注意下在獲取的網路圖片要先配置download域名才能生效,具體在小程式後臺設定裡配置。
獲取頭像地址,首先量取頭像在畫布中的大小,和x軸Y軸的座標,這裡的result[0]是我用promise封裝返回的一個圖片地址
let headImg = new Promise(function (resolve) {
wx.getImageInfo({
src: `${app.globalData.baseUrl2}${that.data.currentChildren.headImg}`,
success: function (res) {
resolve(res.path)
},
fail: function (err) {
console.log(err)
wx.showToast({
title: '網路錯誤請重試',
icon: 'loading'
})
}
})
})
let avatarurl_width = 60, //繪製的頭像寬度
avatarurl_heigth = 60, //繪製的頭像高度
avatarurl_x = 28, //繪製的頭像在畫布上的位置
avatarurl_y = 36; //繪製的頭像在畫布上的位置
ctx.save(); // 先儲存狀態 已便於畫完圓再用
ctx.beginPath(); //開始繪製
//先畫個圓 前兩個引數確定了圓心 (x,y) 座標 第三個引數是圓的半徑 四引數是繪圖方向 預設是false,即順時針
ctx.arc(avatarurl_width / 2 + avatarurl_x, avatarurl_heigth / 2 + avatarurl_y, avatarurl_width / 2, 0, Math.PI * 2, false);
ctx.clip(); //畫了圓 再剪下 原始畫布中剪下任意形狀和尺寸。一旦剪下了某個區域,則所有之後的繪圖都會被限制在被剪下的區域內
ctx.drawImage(result[0], avatarurl_x, avatarurl_y, avatarurl_width, avatarurl_heigth); // 推進去圖片
這裡舉個例子說下如何繪製文字,比如我要繪製如下這個“字”,需要動態獲取前面字數的總寬度,這樣才能設定“字”的x軸座標,這裡我本來是想通過measureText來測量字型的寬度,但是在iOS端第一次獲取的寬度值不對,關於這個問題,我還在微信開發者社群提了bug,所以我想用另一個方法來實現,就是先獲取正常情況下一個字的寬度值,然後乘以總字數就獲得了總寬度,親試是可以的。
let allReading = 97 / 6 / app.globalData.ratio * wordNumber.toString().length + 325;
ctx.font = 'normal normal 30px sans-serif';
ctx.setFillStyle('#ffffff')
ctx.fillText('字', allReading, 150);
④繪製公眾號二維碼,和獲取頭像是一樣的,也是先通過介面返回圖片網路地址,然後再通過getImageInfo獲取公眾號二維碼圖片資訊
⑤如何繪製小程式碼,具體官網文件也給出生成無限小程式碼介面,通過生成的小程式可以開啟任意一個小程式頁面,並且二維碼永久有效,具體呼叫哪個小程式二維碼介面有不同的應用場景,具體可以看下官方文件怎麼說的,也就是說前端通過傳遞引數調取後端介面返回的小程式碼,然後繪製在畫布上(和上面寫的繪製頭像和公眾號二維碼一樣的)
ctx.drawImage('小程式碼的本地地址', x軸, Y軸, 寬, 高)
⑥最終繪製完把canvas畫布轉成圖片並返回圖片地址
wx.canvasToTempFilePath({
canvasId: 'myCanvas',
success: function (res) {
canvasToTempFilePath = res.tempFilePath // 返回的圖片地址儲存到一個全域性變數裡
that.setData({
showShareImg: true
})
wx.showToast({
title: '繪製成功',
})
},
fail: function () {
wx.showToast({
title: '繪製失敗',
})
},
complete: function () {
wx.hideLoading()
wx.hideToast()
}
})
⑦儲存到系統相簿;先判斷使用者是否開啟使用者授權相簿,處理不同情況下的結果。比如使用者如果按照正常邏輯授權是沒問題的,但是有的使用者如果點選了取消授權該如何處理,如果不處理會出現一定的問題。所以當使用者點選取消授權之後,來個彈框提示,當它再次點選的時候,主動跳到設定引導使用者去開啟授權,從而達到儲存到相簿分享朋友圈的目的。
// 獲取使用者是否開啟使用者授權相簿
if (!openStatus) {
wx.openSetting({
success: (result) => {
if (result) {
if (result.authSetting["scope.writePhotosAlbum"] === true) {
openStatus = true;
wx.saveImageToPhotosAlbum({
filePath: canvasToTempFilePath,
success() {
that.setData({
showShareImg: false
})
wx.showToast({
title: '圖片儲存成功,快去分享到朋友圈吧~',
icon: 'none',
duration: 2000
})
},
fail() {
wx.showToast({
title: '儲存失敗',
icon: 'none'
})
}
})
}
}
},
fail: () => { },
complete: () => { }
});
} else {
wx.getSetting({
success(res) {
// 如果沒有則獲取授權
if (!res.authSetting['scope.writePhotosAlbum']) {
wx.authorize({
scope: 'scope.writePhotosAlbum',
success() {
openStatus = true
wx.saveImageToPhotosAlbum({
filePath: canvasToTempFilePath,
success() {
that.setData({
showShareImg: false
})
wx.showToast({
title: '圖片儲存成功,快去分享到朋友圈吧~',
icon: 'none',
duration: 2000
})
},
fail() {
wx.showToast({
title: '儲存失敗',
icon: 'none'
})
}
})
},
fail() {
// 如果使用者拒絕過或沒有授權,則再次開啟授權視窗
openStatus = false
console.log('請設定允許訪問相簿')
wx.showToast({
title: '請設定允許訪問相簿',
icon: 'none'
})
}
})
} else {
// 有則直接儲存
openStatus = true
wx.saveImageToPhotosAlbum({
filePath: canvasToTempFilePath,
success() {
that.setData({
showShareImg: false
})
wx.showToast({
title: '圖片儲存成功,快去分享到朋友圈吧~',
icon: 'none',
duration: 2000
})
},
fail() {
wx.showToast({
title: '儲存失敗',
icon: 'none'
})
}
})
}
},
fail(err) {
console.log(err)
}
})
}
總結
至此所有的步驟都已實現,在繪製的時候會遇到一些非同步請求後臺返回的資料,所以我用promise和async和await進行了封裝,確保匯出的圖片資訊是完整的。在繪製的過程確實遇到一些坑的地方。比如初開始匯出的圖片比例大小不對,還有用measureText測量文字寬度不對,多次繪製(可能受網路原因)有時匯出的圖片上的文字顏色會有誤差等。如果你也遇到一些比較坑的地方可以一起探討下做個記錄,下面附下完整的程式碼
import regeneratorRuntime from '../../utils/runtime.js' // 引入模組
const app = getApp(),
api = require('../../service/http.js');
var ctx = null, // 建立canvas物件
canvasToTempFilePath = null, // 儲存最終生成的匯出的圖片地址
openStatus = true; // 宣告一個全域性變數判斷是否授權儲存到相簿
// 獲取微信公眾號二維碼
getCode: function () {
return new Promise(function (resolve, reject) {
api.fetch('/wechat/open/getQRCodeNormal', 'GET').then(res => {
console.log(res, '獲取微信公眾號二維碼')
if (res.code == 200) {
console.log(res.content, 'codeUrl')
resolve(res.content)
}
}).catch(err => {
console.log(err)
})
})
},
// 生成海報
async createCanvasImage() {
let that = this;
// 點選生成海報資料埋點
that.setData({
generateId: '點選生成海報'
})
if (!ctx) {
let codeUrl = await that.getCode()
wx.showLoading({
title: '繪製中...'
})
let code = new Promise(function (resolve) {
wx.getImageInfo({
src: codeUrl,
success: function (res) {
resolve(res.path)
},
fail: function (err) {
console.log(err)
wx.showToast({
title: '網路錯誤請重試',
icon: 'loading'
})
}
})
})
let headImg = new Promise(function (resolve) {
wx.getImageInfo({
src: `${app.globalData.baseUrl2}${that.data.currentChildren.headImg}`,
success: function (res) {
resolve(res.path)
},
fail: function (err) {
console.log(err)
wx.showToast({
title: '網路錯誤請重試',
icon: 'loading'
})
}
})
})
Promise.all([headImg, code]).then(function (result) {
const ctx = wx.createCanvasContext('myCanvas')
console.log(ctx, app.globalData.ratio, 'ctx')
let canvasWidthPx = 690 * app.globalData.ratio,
canvasHeightPx = 1085 * app.globalData.ratio,
avatarurl_width = 60, //繪製的頭像寬度
avatarurl_heigth = 60, //繪製的頭像高度
avatarurl_x = 28, //繪製的頭像在畫布上的位置
avatarurl_y = 36, //繪製的頭像在畫布上的位置
codeurl_width = 80, //繪製的二維碼寬度
codeurl_heigth = 80, //繪製的二維碼高度
codeurl_x = 588, //繪製的二維碼在畫布上的位置
codeurl_y = 984, //繪製的二維碼在畫布上的位置
wordNumber = that.data.wordNumber, // 獲取總閱讀字數
// nameWidth = ctx.measureText(that.data.wordNumber).width, // 獲取總閱讀字數的寬度
// allReading = ((nameWidth + 375) - 325) * 2 + 380;
// allReading = nameWidth / app.globalData.ratio + 325;
allReading = 97 / 6 / app.globalData.ratio * wordNumber.toString().length + 325;
console.log(wordNumber, wordNumber.toString().length, allReading, '獲取總閱讀字數的寬度')
ctx.drawImage('/img/study/shareimg.png', 0, 0, 690, 1085)
ctx.save(); // 先儲存狀態 已便於畫完圓再用
ctx.beginPath(); //開始繪製
//先畫個圓 前兩個引數確定了圓心 (x,y) 座標 第三個引數是圓的半徑 四引數是繪圖方向 預設是false,即順時針
ctx.arc(avatarurl_width / 2 + avatarurl_x, avatarurl_heigth / 2 + avatarurl_y, avatarurl_width / 2, 0, Math.PI * 2, false);
ctx.clip(); //畫了圓 再剪下 原始畫布中剪下任意形狀和尺寸。一旦剪下了某個區域,則所有之後的繪圖都會被限制在被剪下的區域內
ctx.drawImage(result[0], avatarurl_x, avatarurl_y, avatarurl_width, avatarurl_heigth); // 推進去圖片
ctx.restore(); //恢復之前儲存的繪圖上下文狀態 可以繼續繪製
ctx.setFillStyle('#ffffff'); // 文字顏色
ctx.setFontSize(28); // 文字字號
ctx.fillText(that.data.currentChildren.name, 103, 78); // 繪製文字
ctx.font = 'normal bold 44px sans-serif';
ctx.setFillStyle('#ffffff'); // 文字顏色
ctx.fillText(wordNumber, 325, 153); // 繪製文字
ctx.font = 'normal normal 30px sans-serif';
ctx.setFillStyle('#ffffff')
ctx.fillText('字', allReading, 150);
ctx.font = 'normal normal 24px sans-serif';
ctx.setFillStyle('#ffffff'); // 文字顏色
ctx.fillText('打敗了全國', 26, 190); // 繪製文字
ctx.font = 'normal normal 24px sans-serif';
ctx.setFillStyle('#faed15'); // 文字顏色
ctx.fillText(that.data.percent, 154, 190); // 繪製孩子百分比
ctx.font = 'normal normal 24px sans-serif';
ctx.setFillStyle('#ffffff'); // 文字顏色
ctx.fillText('的小朋友', 205, 190); // 繪製孩子百分比
ctx.font = 'normal bold 32px sans-serif';
ctx.setFillStyle('#333333'); // 文字顏色
ctx.fillText(that.data.singIn, 50, 290); // 簽到天數
ctx.fillText(that.data.reading, 280, 290); // 閱讀時長
ctx.fillText(that.data.reading, 508, 290); // 聽書時長
// 書籍閱讀結構
ctx.font = 'normal normal 28px sans-serif';
ctx.setFillStyle('#ffffff'); // 文字顏色
ctx.fillText(that.data.bookInfo[0].count, 260, 510);
ctx.fillText(that.data.bookInfo[1].count, 420, 532);
ctx.fillText(that.data.bookInfo[2].count, 520, 594);
ctx.fillText(that.data.bookInfo[3].count, 515, 710);
ctx.fillText(that.data.bookInfo[4].count, 492, 828);
ctx.fillText(that.data.bookInfo[5].count, 348, 858);
ctx.fillText(that.data.bookInfo[6].count, 212, 828);
ctx.fillText(that.data.bookInfo[7].count, 148, 726);
ctx.fillText(that.data.bookInfo[8].count, 158, 600);
ctx.font = 'normal normal 18px sans-serif';
ctx.setFillStyle('#ffffff'); // 文字顏色
ctx.fillText(that.data.bookInfo[0].name, 232, 530);
ctx.fillText(that.data.bookInfo[1].name, 394, 552);
ctx.fillText(that.data.bookInfo[2].name, 496, 614);
ctx.fillText(that.data.bookInfo[3].name, 490, 730);
ctx.fillText(that.data.bookInfo[4].name, 466, 850);
ctx.fillText(that.data.bookInfo[5].name, 323, 878);
ctx.fillText(that.data.bookInfo[6].name, 184, 850);
ctx.fillText(that.data.bookInfo[7].name, 117, 746);
ctx.fillText(that.data.bookInfo[8].name, 130, 621);
ctx.drawImage(result[1], codeurl_x, codeurl_y, codeurl_width, codeurl_heigth); // 繪製頭像
ctx.draw(false, function () {
// canvas畫布轉成圖片並返回圖片地址
wx.canvasToTempFilePath({
canvasId: 'myCanvas',
success: function (res) {
canvasToTempFilePath = res.tempFilePath
that.setData({
showShareImg: true
})
console.log(res.tempFilePath, 'canvasToTempFilePath')
wx.showToast({
title: '繪製成功',
})
},
fail: function () {
wx.showToast({
title: '繪製失敗',
})
},
complete: function () {
wx.hideLoading()
wx.hideToast()
}
})
})
})
}
},
// 儲存到系統相簿
saveShareImg: function () {
let that = this;
// 資料埋點點選儲存學情海報
that.setData({
saveId: '儲存學情海報'
})
// 獲取使用者是否開啟使用者授權相簿
if (!openStatus) {
wx.openSetting({
success: (result) => {
if (result) {
if (result.authSetting["scope.writePhotosAlbum"] === true) {
openStatus = true;
wx.saveImageToPhotosAlbum({
filePath: canvasToTempFilePath,
success() {
that.setData({
showShareImg: false
})
wx.showToast({
title: '圖片儲存成功,快去分享到朋友圈吧~',
icon: 'none',
duration: 2000
})
},
fail() {
wx.showToast({
title: '儲存失敗',
icon: 'none'
})
}
})
}
}
},
fail: () => { },
complete: () => { }
});
} else {
wx.getSetting({
success(res) {
// 如果沒有則獲取授權
if (!res.authSetting['scope.writePhotosAlbum']) {
wx.authorize({
scope: 'scope.writePhotosAlbum',
success() {
openStatus = true
wx.saveImageToPhotosAlbum({
filePath: canvasToTempFilePath,
success() {
that.setData({
showShareImg: false
})
wx.showToast({
title: '圖片儲存成功,快去分享到朋友圈吧~',
icon: 'none',
duration: 2000
})
},
fail() {
wx.showToast({
title: '儲存失敗',
icon: 'none'
})
}
})
},
fail() {
// 如果使用者拒絕過或沒有授權,則再次開啟授權視窗
openStatus = false
console.log('請設定允許訪問相簿')
wx.showToast({
title: '請設定允許訪問相簿',
icon: 'none'
})
}
})
} else {
// 有則直接儲存
openStatus = true
wx.saveImageToPhotosAlbum({
filePath: canvasToTempFilePath,
success() {
that.setData({
showShareImg: false
})
wx.showToast({
title: '圖片儲存成功,快去分享到朋友圈吧~',
icon: 'none',
duration: 2000
})
},
fail() {
wx.showToast({
title: '儲存失敗',
icon: 'none'
})
}
})
}
},
fail(err) {
console.log(err)
}
})
}
},