1. 程式人生 > >微信小程式仿照微信拖動縮放圖片和擷取頭像

微信小程式仿照微信拖動縮放圖片和擷取頭像

效果圖

在這裡插入圖片描述

整體思路:

  1. 實現圖片的縮放和拖動;
  2. 在圖片上方蓋上中間鏤空的半透明遮罩;
  3. 根據擷取方框區域的圖片。截圖時,在方框區域將擷取的圖片繪製出來,然後使用wx.canvasToTempFilePath擷取圖片。

實現過程和遇到的問題:

  1. 實現縮放和拖動最初是使用touchStarttouhMove,處理觸控事件來實現圖片的縮放和拖動,但是這樣的實現在小程式上會很卡。後來無意中發現了小程式提供的movable-areamovable-view可以很方便地實現控制元件的拖動和縮放。
  2. 使用canvas繪製圖片上方的半透明遮罩,最初我是把canvas
    放在movable-aread的上方,也就是在wxml檔案中排在movable-area後面,這樣會導致圖片監聽不了拖動事件和縮放事件。後來我將canvas前置,利用小程式中canvas在最上層的特點,實現遮罩,而又不影響圖片的拖動和縮放。
  3. 使用給canvas設定rgb(0,0,0,0.5)來實現canvas的半透明,而不是使用其opacity屬性。因為在小程式中實現截圖,需要先通過canvas將圖片繪製出來。如果使用opacity繪製出來的圖片也是半透明的,由於尺寸的誤差,canvas繪製的圖片和後面顯示的圖片會出現重影,效果不佳。
  4. canvas不對底部欄覆蓋。我這裡是使用絕對定位,canvas
    相對螢幕底部120rpx,底部欄的高度正好也是120rpx。這裡也可以使用canvasclearRect方法實現。
  5. canvas準確繪製要擷取的圖片。關鍵在於以下幾個引數:源圖片的大小尺寸,當前圖片顯示的尺寸、方框在顯示的圖片中的位置和大小。而方框在顯示的圖片中的位置又是通過方框和顯示的圖片分別相對於螢幕的位置得到。根據方框在顯示圖片中的位置和顯示圖片的尺寸求得一個比例,然後乘以源圖片的位置,就拿到了方框在源圖片中的位置和大小。
  6. 在實現原微信截圖終端"還原"的問題,在小程式上縮放和拖動不能同時進行,一步到位。具體原因在程式碼註釋中有解釋。
  7. 解決擷取圖片拿不到檔案的問題。重要的是要對canvas
    繪製圖片加上監聽,在canvas繪製圖片完成後才提取圖片檔案。

依然存在的問題

  1. 由於js計算的尺寸上的誤差或者其他未知原因,用canvas繪製的圖片和顯示的圖片會有個垂直方向上的輕微偏移。所以仔細看在點選完成時會有一個錯位移動的現象。這個誤差怎麼解決,希望有心的網友可以告訴我。
  2. 前面也提到了,點選“撤銷”(微信裡叫“還原”),只能分兩步撤銷拖動和縮放,回到原來的位置。
  3. 受限於movable-area圖片拖動能超出邊界的範圍不多,這應該可以通過放大movable-area解決,不過這樣的話後面的一系列定位演算法都要更改。網友有需要的話可以自己實現。

核心程式碼

Page({

  data: {
    // movable-view左上角相對於movable-area的座標x,y,
    // 由於movable-area左上角位於螢幕原點,因此x,y也是movable-view相對於螢幕的座標
    x: 0,
    y: 0,
    scale: 1, // 用來控制moveable-view的縮放比例,回撤重置時需要

    src: {
      // 圖片的原始檔資料
      path: '',
      orientation: 'up',
      // width和height在最初時和螢幕尺寸做比較,做一個合適的縮放
      // 在截圖的時候,計算方框在源圖片的位置也需要用到width和height
      width: 0,
      height: 0,
      ratio: 1, // 圖片的長寬比,最開始需要根據ratio判斷圖片的形狀是長圖還是寬圖,進行合適的居中放置
    },
    image: {
      // 最初圖片在螢幕上顯示的寬度和高度
      // 經過縮放後也是基於這兩個尺寸計算新的尺寸
      initialWidth: 0,
      initialHeight: 0,
      // 控制最初圖片在螢幕中的位置
      initialX: 0,
      initialY: 0,
      // 經過縮放移動後圖片在螢幕中的位置
      // 截圖時找方框在源圖片中的位置,是基於螢幕座標系,所以需要圖片的當前位置
      curX: 0,
      curY: 0,
      // 圖片當前的縮放比例,用於計算圖片當前顯示的尺寸大小
      curScale: 1
    },
    // 螢幕尺寸windowWidth和windowHeight
    windowWidth: 0,
    windowHeight: 0,
    cropBorder: {
      // 截圖的方框相對於螢幕中的位置
      x: 0,
      y: 0,
      // 截圖方框的尺寸
      size: 0
    },
  },

  /**
   * 生命週期函式--監聽頁面載入
   */
  onLoad: function(options) {
    let that = this;
    let _src = JSON.parse(options.data);
    _src.ratio = _src.height / _src.width;
    let systemInfo = wx.getSystemInfoSync();
    // 螢幕內框範圍(這裡定義左右邊界30,上下邊際50)
    // 但是因為movable-view能超出movable-area的範圍太小,為了儘量放大圖片的活動範圍,這裡取消了這個邊界
    // let borderWidth = systemInfo.windowWidth - 2 * 30
    // let borderHeight = systemInfo.windowHeight - 2 * 50
    let _image = that.data.image
    if (_src.width > systemInfo.windowWidth && _src.height > systemInfo.windowHeight) {
      // 如果圖片尺寸大於螢幕尺寸,需要縮放
      if (_src.ratio > 1) {
        // 如果是長圖,按照寬度進行縮放
        _image.initialWidth = systemInfo.windowWidth
        // >> 0用來取整
        _image.initialHeight = (_src.ratio * systemInfo.windowWidth) >> 0
      } else {
        // 如果是寬圖,按照高度進行縮放
        _image.initialWidth = systemInfo.windowHeight,
          _image.initialHeight = (systemInfo.windowHeight / _src.ratio) >> 0
      }
    } else {
      _image.initialWidth = _src.width
      _image.initialHeight = _src.height
    }
    // 控制圖片在螢幕居中
    _image.initialX = (systemInfo.windowWidth - _image.initialWidth) >> 1
    _image.initialY = (systemInfo.windowHeight - _image.initialHeight) >> 1
    console.log(JSON.stringify(_image))
    // 定義方框的位置和尺寸
    let _cropBorder = {}
    _cropBorder.size = systemInfo.windowWidth * 0.9
    _cropBorder.x = (systemInfo.windowWidth - _cropBorder.size) >> 1
    _cropBorder.y = (systemInfo.windowHeight - _cropBorder.size) >> 1
    that.setData({
      src: _src,
      image: _image,
      cropBorder: _cropBorder,
      windowWidth: systemInfo.windowWidth,
      windowHeight: systemInfo.windowHeight
    })
  },

  /**
   * 移動movable-view的回撥
   */
  onChange(e) {
    console.log(e.detail)
    let that = this;
    that.setData({
      'image.curX': e.detail.x,
      'image.curY': e.detail.y
    })
  },

  /**
   * 縮放moveable-view的回撥
   */
  onScale(e) {
    console.log(e.detail)
    let that = this;
    that.setData({
      'image.curX': e.detail.x,
      'image.curY': e.detail.y,
      'image.curScale': e.detail.scale
    })
  },

  /**
   * 生命週期函式--監聽頁面初次渲染完成
   */
  onReady: function() {
    let that = this;
    let cropBorder = that.data.cropBorder
    this.context = wx.createCanvasContext('myCanvas')
    // 設定背景黑色透明度0.5,不要使用opacity,會導致後期截出來的圖片也是半透明
    this.context.setFillStyle('rgba(0,0,0,0.5)')
    this.context.fillRect(0, 0, that.data.windowWidth, that.data.windowHeight)
    // 挖出來一個方框,這個方框區域就是全透明瞭
    this.context.clearRect(cropBorder.x, cropBorder.y, cropBorder.size, cropBorder.size)
    // 畫方框的外框
    this.context.setStrokeStyle('white')
    // 往外畫大一圈,這樣在canvas上填充圖片的時候框線就不會變細啦
    this.context.strokeRect(cropBorder.x - 1, cropBorder.y - 1, cropBorder.size + 2, cropBorder.size + 2)
    this.context.draw()
  },

  /**
   * 取消截圖
   */
  cancel(event) {
    wx.navigateBack()
  },

  /**
   * 回撤,這地方有個問題:
   * 本來是想圖片一次性移動和縮放回到最初的位置和大小,但是發現點選第一次只能實現圖片移動到初始點
   * 點選第二次才是縮放到初始大小
   * 後來發現小程式移動有一個過程,會不停地回撥onChange(),
   * 如果一定要實現,只能是當onChange中x,y歸0之後再控制縮放,
   * 但是x和y是浮點數,零判斷不精確而且影響效能,所以放棄了。
   */
  reset(event) {
    let that = this;
    that.setData({
      scale: 1,
      x: 0,
      y: 0,
    })
  },

  /**
   * 點選完成的回撥
   * 完成截圖,回傳圖片到上一頁
   */
  complete(event) {
    let that = this
    let src = that.data.src
    console.log(src)
    let cropBorder = this.data.cropBorder
    let image = that.data.image
    // 當前圖片顯示的大小
    let curImageWidth = image.initialWidth * image.curScale
    let curImageHeight = image.initialHeight * image.curScale
    // 將方框位置換算到源圖片中的位置srcX,srcY
    let srcX = (cropBorder.x - image.curX) / curImageWidth * src.width
    // canvas的height是100%,而bottom是120rpx,因此canvas的位置不是在原點,需要減去這個120rpx
    // 置於這裡為什麼要有個120,因為底部欄也是透明的,但字是亮的,我想呈現底部欄在上方不被遮罩擋住的效果
    // 經過測試,這裡-4可以解決canvas繪製的圖片向上偏移的問題,為什麼是-4我也不知道,與邊框的厚度有關?
    let srcY = (cropBorder.y - image.curY - 120 / 750 * that.data.windowWidth-4) / curImageHeight * src.height
    // 方框區域對映到源圖片中的尺寸
    let srcWidth = cropBorder.size / curImageWidth * src.width
    let srcHeight = cropBorder.size / curImageHeight * src.height
    console.log('srcX = ' + srcX + ', srcY = ' + srcY + ', srcWidth = ' + srcWidth + ', srcHeight = ' + srcHeight + ', cropX = ' + cropBorder.x + ', cropY = ' + cropBorder.y + ', cropSize = ' + cropBorder.size)
    // 繪製圖片不要透明啦,不然會看到重影
    this.context.setFillStyle('rgba(0,0,0,1)')
    // 鑑於尺寸的精確度,方框內圖片的覆蓋在y方向會有微微的偏移,
    // 但是一旦截圖就返回上一頁了,強迫症患者沒有後悔的餘地。
    this.context.drawImage(src.path, srcX, srcY, srcWidth, srcHeight, cropBorder.x, cropBorder.y, cropBorder.size, cropBorder.size)
    // 這裡繪圖一定要有回撥,不然圖片還沒繪製完成就截圖那就GG了
    this.context.draw(true, function(res) {
      wx.canvasToTempFilePath({
        canvasId: 'myCanvas',
        x: cropBorder.x,
        y: cropBorder.y,
        width: cropBorder.size,
        height: cropBorder.size,
        destWidth: cropBorder.size,
        destHeight: cropBorder.size,
        fileType: 'jpg',
        success: function(data) {
          console.log(data)
          // 將圖片回傳到上一頁
          var pages = getCurrentPages();
          if (pages.length > 1) {
            var prePage = pages[pages.length - 2];
            prePage.setData({
              newAvatar: data.tempFilePath
            })
          }
          wx.navigateBack()
        },
        fail: function(err) {
          console.log(err)
        }
      })
    })
  },
})

原始碼

https://github.com/sheaye/wx-avatar-cropper