在小程序中如何保存自定義二維碼圖片或海報(此文示例demo代碼基於Taro框架)


前言:這一個筆記主要是寫在小程序生成宣傳海報之類的,然后用戶點擊保存按鈕就可以保存到手機的功能。主要使用的還是canvas元素。小程序也有提供對應的一些API幫助我們實現這個需求。下面上一些代碼和自我思路當作一個記錄吧(P.S:Taro是一個用於多端高速開發的框架,基於react和TS。可以自行百度或谷歌一下查詢相關資料,標簽元素和API同樣適用原生小程序寫法,標簽換一下即可) 轉載請注明出處謝謝(尊重下我花時間在做筆記唄哈哈哈)

 

思路:一開始只知道得用canvas標簽才能做到產品需求所要的效果,畢竟是自定義化的。但是怎么顯示和怎么保存就查了相關資料。加上自己實現需求后的一些想法,大概如下:

  1.建議canvas標簽在頁面onload時候就進行繪畫渲染,這是為避免用戶首次進入后點擊生成保存時canvas標簽還沒完成導致空白圖片

  2.既然自定義化就避免不了一些UI圖片,這一點就比較麻煩,圖片多了可能超出小程序上傳代碼包大小限制,而且對於一些API也只支持網絡圖片。所以就直接放在網絡服務器上,通過wx.getImageInfo獲取對應圖片的信息,例如寬高和微信臨時鏈接,這里要注意這個API支持https格式的網絡圖片,別踩坑了~

  3.現在canvas畫好了,那怎么保存成圖呢?想了想去在文檔看過,又回去翻了翻,有個wx.canvasToTempFilePath專為這功能而生,就是把制定的canvasId整個轉換成圖片格式保存生成臨時模板文件鏈接。那就簡單啦,直接調用把參數填好就可以啦

  4.獲取到圖片臨時鏈接也有了,怎么讓整張保存到手機呢?首先肯定是要用戶授權啦,所以就用到wx.getSetting和wx.authorize這兩個API了,第一個是用來檢測授權設置,第二個是彈出用戶是否授權允許小程序訪問手機相冊/圖像功能的。如果不走這個授權那肯定保存不了圖片的,而且authorize這個授權有點坑,如果用戶拒絕授權了,那馬上點擊不一定會在出現授權框,因為微信官方為了用戶體驗,若用戶拒絕會有一定時間性才會再讓用戶確認是否授權。。。而且scope參數值在這里必須為'scope.writePhotosAlbum'
 
  5.最后一步,把小程序保存圖片的API直接調用wx.saveImageToPhotosAlbum,它就會把我們傳入的鏈接保存到用戶手機里面。注意:這個API是不支持網絡圖片路徑的,所以也就是上面為什么要生成臨時鏈接的原因
 
  End:到這里基本就完成了,其實也不難,主要是用到的API邏輯走通就可以了。至於canvas嘛,靠各自畫工能力了哈哈(下面直接上代碼)
  

HTML部分:

<View>
   <Canvas className='poster' canvasId='poster' style='width:355px;height:635px;'></Canvas>
</View>

 

CSS部分就不貼上來了,主要是外層容器給個寬高度和定位。根據需求做浮動或者其他即可。至於style的值是要確定好直接賦值的,這樣畫布大小和后續內容才能定位

 

JS部分:(data變量就不放上來了,下面的js中要做demo把一些變量隨便換成文字和數值就可以了)

// 繪畫Canvas-分享圖標題
  drawTitle(datas: any) {
    const cvsCtx = Taro.createCanvasContext('poster', this) // 獲取到指定的canvas標簽
    const grd = cvsCtx.createLinearGradient(0, 0, 0, 635); // 進行繪制背景
    grd.addColorStop(0, '#FDFCF9'); // 繪制漸變背景色
    grd.addColorStop(1, '#FCF4D4'); // 繪制漸變背景色
    cvsCtx.fillStyle = grd; // 賦值給到canvas
    cvsCtx.fillRect(0, 0, 355, 635); // 繪制canvas大小的舉行
    let text = datas.desc;  // 這是要繪制的文本
    const chr = text.split(''); // 這個方法是將一個字符串分割成字符串數組
    let temp: any = ''; //  截取文字賦值
    let row: any = []; // 把截取后的段落匯合成數組
    cvsCtx.font = 'normal lighter 15px sans-serif' // 設置繪制文字樣式
    cvsCtx.setFillStyle('#941D11') // 設置繪制文字顏色
    for (let a = 0; a < chr.length; a++) { // 進行循環遍歷看是否超出canvas文字所在寬度
      if (cvsCtx.measureText(temp).width < 350) {
        temp += chr[a];
      }
      else {
        a--; //這里添加了a-- 是為了防止字符丟失
        row.push(temp);
        temp = '';
      }
    }
    row.push(temp);
    if (row.length > 4) { // 如果文字段落大於4行
      const rowCut = row.slice(0, 5); // 截取最后一行進行省略號處理
      const rowPart = rowCut[4]; 
      let test: any = '';
      let empty: any = [];
      for (var a = 0; a < rowPart.length; a++) {
        if (cvsCtx.measureText(test).width < 320) {
          test += rowPart[a];
        }
        else {
          break;
        }
      }
      empty.push(test);

      const group = empty[0] + "..." // 這里只顯示5行,超出的用...表示
      rowCut.splice(4, 1, group);
      row = rowCut;
    }
    let i = 0  // 此處計算文本最終所占高度方便后面元素定位
    for (let b = 0; b < row.length; b++) {
      cvsCtx.fillText(row[b], 11, 80 + b * 25, 333);
      i = 80 + b * 25
    }
    this.state.canvasTextHeight = i
    cvsCtx.setFontSize(21) // 文字樣式
    cvsCtx.setFillStyle('#FB4949') // 文字樣式
    cvsCtx.fillText(`${datas.rise_info.rise_percent}%`, 160, 41, 70) // 文字樣式
    this.ImageInfo('https://static.jingzhuan.cn/WeChat/longtou/details-title.png').then(res => { // 繪制網絡圖片
      // 獲取畫布
      const cvsCtx = Taro.createCanvasContext('poster', this) // 重新定位canvas對象,雙重保險
      // 繪制背景底圖
      cvsCtx.drawImage(res.path, 0, 15, 149.5, 36)
      let title = datas.name
      cvsCtx.setFontSize(18)
      cvsCtx.setFillStyle('#FFFFFF')
      cvsCtx.fillText(title, 8, 39, 214)
      cvsCtx.draw(true) // 進行繪畫
    })
    let redNum = Number(datas.rise_info.rise_num)
    let greenNum = Number(datas.rise_info.drop_num)
    let total = redNum + greenNum
    cvsCtx.setFillStyle('red')
    cvsCtx.fillRect(57, i + 25, (redNum / total) * 237, 16)
    cvsCtx.setFillStyle('#00CC66')
    cvsCtx.fillRect(57 + (redNum / total) * 237, i + 25, (greenNum / total) * 237, 16)
    cvsCtx.setFontSize(17)
    cvsCtx.setFillStyle('#FB4949')
    cvsCtx.fillText(`漲 ${redNum}只`, 55, i + 68, 100)
    cvsCtx.setFontSize(17)
    cvsCtx.setFillStyle('#00CC66')
    cvsCtx.fillText(`跌 ${greenNum}只`, 240, i + 68, 100)
    cvsCtx.font = 'normal lighter 12px sans-serif'
    cvsCtx.setFillStyle('#941D11')
    cvsCtx.fillText('主力凈流入', 12, i + 136, 120)
    cvsCtx.fillText('3日漲幅', 106, i + 136, 120)
    cvsCtx.fillText('5日漲幅', 201, i + 136, 120)
    cvsCtx.setTextAlign('left')
    cvsCtx.fillText('10日漲幅', 292, i + 136, 49)
    this.ImageInfo('https://static.jingzhuan.cn/WeChat/longtou/list-line.png').then(res => {
      // 獲取畫布
      const cvsCtx = Taro.createCanvasContext('poster', this)
      // 繪制背景底圖
      cvsCtx.drawImage(res.path, 65, i + 168, 227, 4)
      cvsCtx.draw(true)
    })
    cvsCtx.font = 'normal bold 15px sans-serif';
    if (datas.rise_info.main_net_purchase > 0) {
      cvsCtx.setFillStyle('#FB4949')
    } else {
      cvsCtx.setFillStyle('#00CC66')
    }
    cvsCtx.fillText(`${datas.rise_info.main_net_purchase > 9999 || datas.rise_info.main_net_purchase < -9999 ? `${(datas.rise_info.main_net_purchase / 10000).toFixed(2)}億` : `${datas.rise_info.main_net_purchase}萬`}`, 12, i + 114, 120)
    if (datas.rise_info.rise_percent_of_3_day > 0) {
      cvsCtx.setFillStyle('#FB4949')
    } else {
      cvsCtx.setFillStyle('#00CC66')
    }
    cvsCtx.fillText(`${datas.rise_info.rise_percent_of_3_day}%`, 104, i + 114, 120)
    if (datas.rise_info.rise_percent_of_5_day > 0) {
      cvsCtx.setFillStyle('#FB4949')
    } else {
      cvsCtx.setFillStyle('#00CC66')
    }
    cvsCtx.fillText(`${datas.rise_info.rise_percent_of_5_day}%`, 199, i + 114, 120)
    if (datas.rise_info.rise_percent_of_10_day > 0) {
      cvsCtx.setFillStyle('#FB4949')
    } else {
      cvsCtx.setFillStyle('#00CC66')
    }
    cvsCtx.setTextAlign('left')
    cvsCtx.fillText(`${datas.rise_info.rise_percent_of_10_day}%`, 290, i + 114, 49)

    cvsCtx.draw(true)
    this.state.drawTitleData = datas
  }

  // 繪畫Canvas-分享圖列表
  drawList(datas: any) {
    let i = this.state.canvasTextHeight
    const cvsCtx = Taro.createCanvasContext('poster', this)
    cvsCtx.font = 'normal lighter 12px sans-serif'
    cvsCtx.setFillStyle('#941D11')
    cvsCtx.fillText('成份股', 13, i + 208, 120)
    cvsCtx.fillText('現價', 90, i + 208, 120)
    cvsCtx.fillText('漲跌幅', 139, i + 208, 120)
    cvsCtx.fillText('主力凈流入', 203, i + 208, 120)
    cvsCtx.fillText('主力凈買%', 281, i + 208, 120)

    for (let b = 0; b < 3; b++) {
      cvsCtx.font = 'normal bold 15px sans-serif';
      cvsCtx.setFillStyle('#FB4949')
      cvsCtx.fillText(datas[b].name, 13, i + 242 + b * 38, 120)
      if (datas[b].new_price > 0) {
        cvsCtx.setFillStyle('#FB4949')
      } else {
        cvsCtx.setFillStyle('#00CC66')
      }
      cvsCtx.fillText(datas[b].new_price, 91, i + 242 + b * 38, 120)
      if (datas[b].rise_percent > 0) {
        cvsCtx.setFillStyle('#FB4949')
      } else {
        cvsCtx.setFillStyle('#00CC66')
      }
      cvsCtx.fillText(`${datas[b].rise_percent}%`, 139, i + 242 + b * 38, 120)
      if (datas[b].main_net_purchase > 0) {
        cvsCtx.setFillStyle('#FB4949')
      } else {
        cvsCtx.setFillStyle('#00CC66')
      }
      cvsCtx.fillText(`${datas[b].main_net_purchase > 9999 || datas[b].main_net_purchase < -9999 ? `${(datas[b].main_net_purchase / 10000).toFixed(2)}億` : `${datas[b].main_net_purchase}萬`}`, 208, i + 242 + b * 38, 120)
      if (datas[b].main_net_purchase_strength > 0) {
        cvsCtx.setFillStyle('#FB4949')
      } else {
        cvsCtx.setFillStyle('#00CC66')
      }
      cvsCtx.fillText(`${datas[b].main_net_purchase_strength}%`, 285, i + 242 + b * 38, 120)
    }
    let cps = 0
    this.ImageInfo(`https://小程序二維碼網絡鏈接`).then(data => {
      cvsCtx.drawImage(data.path, 10, i + 360, 60, 60)
      cvsCtx.draw(true)
    })
    cvsCtx.font = 'normal lighter 12px sans-serif'
    cvsCtx.setFillStyle('#941D11')
    cvsCtx.fillText('長按識別小程序碼', 80, i + 385, 120)
    cvsCtx.fillText('領取更多龍頭股!', 80, i + 403, 120)
  }

 

上面是畫canvas的兩個function。看懂一個即可,下面是進行保存和生成的JS

  // 獲得canvas圖片信息
  ImageInfo(path: any) {
    return new Promise((resolve, reject) => { // 采用異步Promise保證先獲取到圖片信息才進行渲染避免報錯
      Taro.getImageInfo(
        {
          src: path,
          success: function (res) {
            resolve(res)
          },
          fail: function (res) {
            reject(res)
          }
        }
      )
    })
  }

  // 保存圖片
  saveImage(imgSrc: any) {
    Taro.getSetting({
      success() {
        Taro.authorize({
          scope: 'scope.writePhotosAlbum', // 保存圖片固定寫法
          success() {
            // 圖片保存到本地
            Taro.saveImageToPhotosAlbum({
              filePath: imgSrc, // 放入canvas生成的臨時鏈接
              success() {
                Taro.showToast({
                  title: '保存成功',
                  icon: 'success',
                  duration: 2000
                })
              }
            })
          },
          fail() {
            Taro.showToast({
              title: '您點擊了拒絕微信保存圖片,再次保存圖片需要您進行截屏哦',
              icon: 'none',
              duration: 3000
            })
          }
        })
      }
    })
  }

  // 點擊保存圖片生成微信臨時模板文件path
  save() {
    const that = this
    setTimeout(() => {
      Taro.canvasToTempFilePath({ // 調用小程序API對canvas轉換成圖
        x: 0, // 開始截取的X軸
        y: 0, // 開始截取的Y軸
        width: 355, // 開始截取寬度
        height: 635,  // 開始截取高度
        destWidth: 1065,  // 截取后圖片的寬度(避免圖片過於模糊,建議2倍於截取寬度)
        destHeight: 1905, // 截取后圖片的高度(避免圖片過於模糊,建議2倍於截取寬度)
        canvasId: 'poster', // 截取的canvas對象
        success: function (res) { // 轉換成功生成臨時鏈接並調用保存方法
          that.saveImage(res.tempFilePath)
        },
        fail: function (res) {
          console.log('繪制臨時路徑失敗')
        }
      })
    }, 100) // 延時100做為點擊緩沖,可以不用
  }

 

到這里也就結束了。在保存按鈕調用save方法即可。因為我做的需求是根據數據生成不同的圖而且要展示給用戶所能看到的。可能代碼就多了點。其實原理大同小異,如果是簡單的海報直接讓UI把圖跟二維碼分兩張然后直接push在canvas上就好了,那是最快的。如果不想顯示出來給用戶看點擊即保存的猿兄們,請參照我上一個記錄隨機生成canvas分享圖的部分思路。最后上幾個我實現的效果圖(轉載請注明出處謝謝)

第一張圖是小程序效果圖,第二張圖是保存下來后的圖片(分別是1倍。2倍和3倍保存)

最簡單的:兩張圖生成的海報

 

 加了點文字效果的:

 

最后是比較多內容的:

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM