仿微信圈子繪制帶小程序碼的海報(bào)(超詳細(xì)爬坑指南)

前言:在弄「新城名錄」這個(gè)小程序時(shí),需要將發(fā)布的信息內(nèi)容生成一張帶小程序碼的海報(bào),方便分享和轉(zhuǎn)發(fā)。海報(bào)的形式是參照“微信圈子”里面的樣式,折騰不了不少時(shí)間,也踩了很多坑,故在此記錄下來(lái)。

首先看看實(shí)現(xiàn)效果


點(diǎn)擊“生成海報(bào)”,預(yù)覽生成的海報(bào)然后保存至相冊(cè)

此小程序是基于uni-app開發(fā)的,也就是vue那套寫法,所以將海報(bào)的生成邏輯弄成了單獨(dú)的組件。
整個(gè)實(shí)現(xiàn)流程大致如下圖:


繪制過(guò)程

一、初始化基本尺寸

通過(guò)wx.getSystemInfo獲得窗口信息。
因?yàn)閏anvas上繪制文字單位是px,所以要通過(guò)像素比來(lái)計(jì)算文字大小。

let _ = this
wx.getSystemInfo ({
    success (res) {
        _.windowInfo.width = _.canvasStyle.width = res.windowWidth
        _.windowInfo.height = res.windowHeight
        _windowInfo.ratio = res.pixelRatio / 2
        // 根據(jù)像素比,計(jì)算文字的大小
        _.canvasStyle.textDf *= _windowInfo.ratio
        _.canvasStyle.textSm *= _windowInfo.ratio
    }
})

二、獲取小程序碼

2-1: 生成buffer

獲取小程序碼看文檔就行了沒(méi)什么可說(shuō)的:傳送門
我是通過(guò)云函數(shù)獲取小程序碼的Buffer

// 云函數(shù)
app.router('wxacode', async (ctx, next) => {
    try {
        let rs = await cloud.openapi.wxacode.get({
            path: event.path,
            width: 100,
            is_hyaline: true
        })
        ctx.body = rs
     } catch (err) {
        ctx.body = err
     }
}) 

wxacode.createQRCodewxacode.get 兩個(gè)接口加起來(lái)最多可生成10萬(wàn)個(gè)小程序碼。同一參數(shù)的path是一個(gè),不限請(qǐng)求次數(shù)。
wxacode.getUnlimited 是無(wú)限個(gè)小程序碼,但path有限制,好像不能帶參數(shù)還沒(méi)試過(guò)。

2-2: 調(diào)用云函數(shù)獲得buffer并轉(zhuǎn)為base64

wx.cloud.callFunction ({
     ...
}).then(rs => {
    let base64 = wx.arrayBufferToBase64(rs.result.buffer.data || rs.result.buffer)
})

在開發(fā)的過(guò)程中莫名其妙的小程序碼就繪制不出來(lái)了,最后發(fā)現(xiàn)這里rs.result.buffer對(duì)象中,一會(huì)有data字段一會(huì)沒(méi)有,別問(wèn)為什么總之遇到了,最好判斷下。

let imgSrc = 'data:image/jpeg;base64,'  + base64

這里轉(zhuǎn)換的是沒(méi)有base64前綴的,若要顯示在 <image> 標(biāo)簽上,需要加上前綴。

2-3:獲得本地臨時(shí)鏈接

用wx.getFileSystemManager().writeFile方法寫入到本地。
在繪制完成之后,通過(guò)wx.getFileSystemManager().unlink刪除臨時(shí)文件。

let filePath = `${wx.env.USER_DATA_PATH}/wxacode.jpeg`
wx.getFileSystemManager().writeFile({
    filePath,
    data,
    encoding: 'base64'
    ...
 })

三、計(jì)算畫布的高度

首先看看海報(bào)的布局



由圖可知畫布的高度=圖片區(qū)域+文字區(qū)域+小程序碼區(qū)域+邊距

3-1:計(jì)算繪制圖片所占的高度

用canvas繪制圖片,首先要將圖片下載成功后才能繪制。
在使用wx.downloadFile下載圖片時(shí),如果遇到錯(cuò)誤:downloadFile:fail url not in domain list
那么就要在小程序管理后臺(tái)中:開發(fā)>開發(fā)設(shè)置>服務(wù)器域名 去設(shè)置downloadFile合法域名
如果用到了云存儲(chǔ),合法域名就在 云開發(fā)>存儲(chǔ) 中找到文件的https的下載地址


云存儲(chǔ)的域名

圖片下載完成后,通過(guò)wx.getImageInfo來(lái)獲得圖片的尺寸,然后根據(jù)數(shù)量不同采用不同的排版方式。


圖片排版方式

在這里獲取到圖片信息時(shí),就計(jì)算好坐標(biāo)、尺寸暫存起來(lái),等繪制的時(shí)候直接使用即可。

3-2:計(jì)算繪制文字所占的高度

繪制文字主要的問(wèn)題是,canvas是沒(méi)有自動(dòng)換行的,所以要把文字一個(gè)個(gè)的取出來(lái),然后計(jì)算寬度。
小程序提供了測(cè)量文本尺寸信息的接口:CanvasContext.measureText
這個(gè)玩意呢不建議用,因?yàn)樵谡鏅C(jī)測(cè)試時(shí),這個(gè)接口的運(yùn)算速度啊慢得要死,文字一多簡(jiǎn)直不能玩了。
后面找了個(gè)現(xiàn)成的函數(shù)來(lái)獲取文本的寬度。原文鏈接
同樣在這里獲取文字寬度信息的同時(shí),將坐標(biāo)計(jì)算好暫存起來(lái),等繪制的時(shí)候直接使用即可。
最后是小程序區(qū)域的高度,是固定高度100

四、開始繪制

4-1: 獲取CanvasContext

首先創(chuàng)建 canvas 的繪圖上下文 CanvasContext 對(duì)象
注意這里要將this參數(shù)帶上
在自定義組件下,當(dāng)前組件實(shí)例的this,表示在這個(gè)自定義組件下查找擁有 canvas-id 的 canvas ,如果省略則不在任何自定義組件內(nèi)查找

let ctx = wx.createCanvasContext ('mycanvas', this)

然后就是按順序繪制圖片、文字、小程序碼區(qū)域

4-2: 導(dǎo)出圖片

繪制完成之后就需要用wx.canvasToTempFilePath,將畫布的內(nèi)容導(dǎo)出成指定大小的圖片。
官方文檔說(shuō)了:在 draw() 回調(diào)里調(diào)用該方法才能保證圖片導(dǎo)出成功。
理論上應(yīng)該是如下這樣子:

ctx.draw(true, () =>  {
    wx.canvasToTempFilePath({ ... })
})

但是,這個(gè)回調(diào)它根本不執(zhí)行呀!
后面查到的是說(shuō):繪制速度太快(what ???) 無(wú)法進(jìn)入canvas.draw的回調(diào)函數(shù),需要在外層套個(gè)setTimeout。

let _ = this
ctx.draw(true, setTimeout(() => {
    wx.canvasToTempFilePath ({
        fileType: 'jpg',
        canvasId: 'mycanvas',
        x: 0,
        y: 0,
        width: _.canvasStyle.width,
        height: _.canvasStyle.height,
        success (res) {
              _.imgSrc = res.tempFilePath
        }
    }, _)
}, 300))

在使用wx.canvasToTempFilePath記得把this傳入進(jìn)去,同時(shí)最好指定好畫布區(qū)域的寬高,不然可能存在圖片空白的情況。

4-3: 預(yù)覽圖片

繪制的<canvas>節(jié)點(diǎn)是隱藏在屏幕外的,真正用于預(yù)覽的是<image>節(jié)點(diǎn)
如圖:


為什么不直接用<canvas>來(lái)預(yù)覽?
因?yàn)轭A(yù)覽的尺寸和canvas的尺寸不一樣,所以就要做縮放,將<canvas>標(biāo)簽用css3 transform:scale(.8, .8) 在真機(jī)上是沒(méi)有作用的!?。?br> 所以只能把wx.canvasToTempFilePath導(dǎo)出的圖片路徑,放到<image>上來(lái)做顯示。
但是,
導(dǎo)出來(lái)的圖片有可能存在留白區(qū)域,像就是沒(méi)繪制完,這里就要如4-2所說(shuō),把導(dǎo)出寫到draw回調(diào)中,同時(shí)一定要把延時(shí)加上。

五、保存圖片

當(dāng)你滿懷欣喜的爬完上面的坑,以為調(diào)用一下wx.saveImageToPhotosAlbum接口把圖片保存完,就大功告成了碼?
不存在的?。?!
此接口需要用戶授權(quán),才能成功將圖片保存,而如果用戶不小心點(diǎn)了拒絕授權(quán),那么是不是要手動(dòng)調(diào)用下跳轉(zhuǎn)到授權(quán)設(shè)置頁(yè)面。

wx.getSetting({
    success(res) {
        if (!res.authSetting['scope.writePhotosAlbum']) {
            wx.authorize({
                scope: 'scope.writePhotosAlbum',
                ...
                fail () {
                    wx.openSetting(...)
                }
            })
        } 
    }
})

跳轉(zhuǎn)授權(quán)?跳轉(zhuǎn)得了屁勒!
翻看wx.openSetting的官方文檔,有那么一小撮字告訴你:用戶發(fā)生點(diǎn)擊行為后,才可以跳轉(zhuǎn)打開設(shè)置頁(yè)
所以你還得整個(gè)對(duì)話框,讓用戶點(diǎn)一下子。

wx.getSetting({
    success(res) {
        // 進(jìn)行授權(quán)檢測(cè),未授權(quán)則進(jìn)行彈層授權(quán)
        if (!res.authSetting['scope.writePhotosAlbum']) {
            wx.authorize({
                scope: 'scope.writePhotosAlbum',
                // 拒絕授權(quán)時(shí),則進(jìn)入手機(jī)設(shè)置頁(yè)面,可進(jìn)行授權(quán)設(shè)置
                fail(err) {
                    wx.showModal({
                        title: '提示',
                        content: '需要您授權(quán)才能保存到相冊(cè)',
                        success: (res) => {
                            if (res.confirm) {
                                wx.openSetting({
                                  ...
                                })
                            }
                        }
                    })
                }
            })
        } 
    }
})

最后附上完整代碼地址:https://github.com/yiPian/poster

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。