微信小程序使用自定义组件实现图片双指缩放效果

导读

  之前需要在小程序里实现对一张图片的单指拖动双指缩放效果。试了网上很多别人的代码已经微信自己的一些控件,基本双指缩放的时候都是会以左上角为原点进行缩放,而微信自己的那些控件使用的时候又比较有局限性(具体放弃原因已经忘了),效果不太理想。后来索性自己写了个组件来实现。

大概实现原理

  单指移动很简单,主要就是通过控制 标签style中的margin-topmargin-left来实现。
  双指缩放的话,通过改变标签的宽高来改变大小,为了保证缩放的中心点为双指中心点,我们同样通过动态的控制margin-topmargin-left来实现。

效果预览

  先看一下是不是想要的效果吧
height="498" width="320" src="http://ow2hkke1k.bkt.clouddn.com/42183227ac01ef5548c2f6685dc12eda_6559509642583883858.mp4">






   如果不想看代码解释的话,可以直接拖到最下面直接下载使用。垃圾代码,没什么好解释的。





实现

初始化组件

  为了方便扩展和复用,我们使用组件的方式来实现这个需求,命名一个为zoomImgByView的组件,组件的wxml代码如下

<view 
    style="width:{{view_width}}px;height:{{view_height}}px;background:red">
    <image
      id="mapImage"
      style="width:{{imgWidth}}px;height:{{imgHeight}}px;position: relative;top:0px;bottom:0px;margin-top:{{marginTop}}px;margin-left:{{marginLeft}}px;"
      src="{{img_src}}"
      mode="aspectFill"
      catchload="_imgLoadEvent"
      catchtouchstart='_touchStartEvent'
      catchtouchmove='_touchMoveEvent'
      catchtouchend='_touchEndEvent'
      catchtouchcancel='_touchEndEvent'/>
view>

   基本就是一个view里面套了一个image。其中view_widthview_height 为图片显示区域的大小,imgWidthimgHeight为图片的实际宽高。这当中img_srcview_widthview_height三个属性时需要开放给页面调用的,所以组件的properties中我们写上这三个属性。

    //图片地址
    img_src: {
      type: String
    },
    //可视区域的大小
    view_width: {
      type: String
    },
    view_height: {
      type: String
    }

再来看标签中样式相关的属性,主要需要控制的四个属性是imgWidthimgHeightmarginTopmarginLeft,我们在组件的data中初始化这四个属性。

data: {
    imgWidth:0,
    imgHeight:0,
    marginTop:0,
    marginLeft:0
  },

最后再在组件的JS代码最前面定义一些后面会用到的变量

var lastTouchPoint = { x: 0, y: 0 };//记录上次触摸时手指的点
var newDist = 0;//本次触摸事件双指间距
var oldDist = 0;//上次触摸事件双指间距
var inTouch = false;    //是否处于触摸过程中,如果在触摸则不执行回弹动画

至此我们组件的一些相关变量定义以及完成,已经可以在页面中调用了,我们在main.json(我的页面文件名字叫main)中声明这个组件

{
  "usingComponents": {
    "zoomImgByView": "/component/zoomImgByView/zoomImgByView"
  }
}

然后再在main.wxml中引用该组件


 <zoomImgByView 
          img_src="{{imgSrc}}"
          view_width="{{viewWidth}}"
          view_height="{{viewHeight}}"/> 

在main.js中初始化这些控制变量,我直接在onload事件中写的,并且将图片显示区域设置为屏幕大小。

onLoad: function (options) {
    wx.getSystemInfo({
      success: res => {
        this.setData({
          viewHeight: res.windowHeight,
          viewWidth: res.windowWidth,
          imgSrc:"http://bizhi.sogou.com/bizhi/images/newpark/bg1_1.jpg"
        })
      }
    })
  },

到此位置,组件的初始化工作已经完成了,不过我们是看不到图片的,因为现在的宽高还是0,加下来我们来具体实现组件中的一些方法。

组件中的方法实现

图片加载

  前面我们在组件的wxml文件中写了 catchload="_imgLoadEvent",这个方法是image标签图片加载完成后调用的,具体实现代码如下:

      lastTouchPoint = { x: 0, y: 0 };
      var ratio;
      var heightRatio = event.detail.height / this.data.view_height;
      var widthRatio = event.detail.width / this.data.view_width;
      ratio = widthRatio;
      if (widthRatio > heightRatio) {
        ratio = heightRatio
      }
      this.setData({
        imgWidth: event.detail.width / ratio,
        imgHeight: event.detail.height / ratio,
        marginLeft: -(event.detail.width / ratio - this.data.view_width) / 2,
        marginTop: -(event.detail.height / ratio - this.data.view_height) / 2,
      })
      //打开定时器一直计算是否需要执行回弹动画
      setInterval(e => {
        if (!inTouch) {
          this._reboundAnimation(); 
        }
      }, 5)

   基本逻辑是先获取到图片原本的宽高,然后根据图片原本宽高和组件显示区域宽高的比例,来确定图片改如何进行第一次缩放以铺满组件显示区域。通过ratio变量来确定图片第一次缩放的比例,计算出图片超出显示区域大小来确定marginLeftmarginTop的初始值。
   接着我们打开了一个定时器,没5ms来执行一次_reboundAnimation()方法,此方法用来进行image标签的回弹效果,需要注意的是,此方法由于一直执行,消耗了大量的CPU,所以此处需要谨慎处理。

触摸开始

   当触摸动作开始的时候,我们需要进行一些赋值操作,wxml中的catchtouchstart='_touchStartEvent'为触摸开始调用的方法,具体实现如下:

    /**
     * 触摸开始事件
     */
    _touchStartEvent: function () {
      inTouch = true
      lastTouchPoint = { x: 0, y: 0 }
      oldDist = 0
    },

   分别是将触摸状态设置为true,防止在触摸事件中执行了回弹动画,将上次触摸点归零,上次的双指距离归为零。

触摸进行中

   当在触摸过程中的时候,开始对样式进行一些调整,catchtouchmove='_touchMoveEvent'事件的具体实现分为两步,分别是单指拖动和双指缩放:

单指拖动

   _touchMoveEvent中单指移动事件代码如下

_touchMoveEvent: function (e) {
      //单指移动事件
      if (e.touches.length == 1) {
        if (lastTouchPoint.x == 0 && lastTouchPoint.y == 0) {
          lastTouchPoint.x = e.touches[0].clientX
          lastTouchPoint.y = e.touches[0].clientY
        } else {
          var xOffset = e.touches[0].clientX - lastTouchPoint.x
          var yOffset = e.touches[0].clientY - lastTouchPoint.y
          this.setData({
            marginTop: this.data.marginTop + yOffset,
            marginLeft: this.data.marginLeft + xOffset,
          })
          lastTouchPoint.x = e.touches[0].clientX
          lastTouchPoint.y = e.touches[0].clientY
        }
        console.log(this.data.marginTop)
      }

  如果是本次触摸事件的第一次执行,则将本次触摸点的值赋给lastTouchPoint,否则计算出本次触摸点和上次的偏差后,修改marginTopmarginLeft的值后,再将本次触摸点的值赋给lastTouchPoint,用作下次计算。单指拖动很简单,不赘述。

双指缩放

   先在单指拖动的代码后面追加如下代码:

     //双指缩放事件
      if (e.touches.length == 2) {
        if (oldDist == 0) {
          oldDist = this._spacing(e);
        } else {
          newDist = this._spacing(e);
          if (newDist > oldDist + 1) {
            this._zoom(newDist / oldDist, e);
            oldDist = newDist;
          }
          if (newDist < oldDist - 1) {
            this._zoom(newDist / oldDist, e);
            oldDist = newDist;
          }
        }
      }

  其中oldDist为上次触摸事件时,两指的距离。显而易见,newDist 表示本次触摸事件两指距离。_spacing(e)方法负责计算出两指之间的距离,_zoom(f,e)方法负责处理图片的实际缩放,方法中的f是缩放的比例。
  _spacing(e) 方法的具体实现如下

    /**
     * 计算两指间距
     */
    _spacing: function (event) {
      var x = event.touches[0].clientX - event.touches[1].clientX;
      var y = event.touches[0].clientY - event.touches[1].clientY;
      return Math.sqrt(x * x + y * y);
    },

   而_zoom(f,e) 方法的具体实现如下:

    _zoom: function (f, event) {
      var xRatio = this._calcXRatio(event)
      var yRatio = this._calcYRatio(event)
      if (this.data.imgWidth <= this.data.view_width && f < 1) {
        var ratio = this.data.view_width / this.data.imgWidth
        this.setData({
          imgWidth: this.data.imgWidth * ratio,
          imgHeight: this.data.imgHeight * ratio
        })
        return;
      }
      if (this.data.imgHeight <= this.data.view_height && f < 1) {
        var ratio = this.data.view_height / this.data.imgHeight
        this.setData({
          imgWidth: this.data.imgWidth * ratio,
          imgHeight: this.data.imgHeight * ratio
        })
        return;
      }
      this.setData({
        //此处的ratio为双指中心点在图片的百分比
        marginLeft: this.data.marginLeft + xRatio * this.data.imgWidth * (1 - f),
        marginTop: this.data.marginTop + yRatio * this.data.imgHeight * (1 - f),
        imgWidth: this.data.imgWidth * f,
        imgHeight: this.data.imgHeight * f,
      })
    },

   zoom方法中的_calcXRatio_calcYRatio两个方法是用来计算双指中心点处于图片的什么位置,这样做是为了得出后面marginLeftmarginTop需要的改变的值占图片宽高变化的多少,使得双指的中心点不动。方法实现如下:

    /**
     * 计算x轴上的双指中心点比例
     */
    _calcXRatio: function (event) {
      var xRatio = ((event.touches[0].clientX + event.touches[1].clientX) / 2 - this.data.marginLeft) / this.data.imgWidth
      return xRatio
    },
    /**
     * 计算y轴上的双指中心点比例
     */
    _calcYRatio: function (event) {
      var yRatio = ((event.touches[0].clientY + event.touches[1].clientY) / 2 - this.data.marginTop) / this.data.imgHeight
      return yRatio
    },

   在计算出xRatioyRatio之后,后面的两个if判断的用处是在图片已经处于最小状态时,继续缩小的话直接return。而两个if后面的this.setData则是真正的缩放图片,同时修改marginLeftmarginTop的值。

触摸结束

   上面的就是触摸事件执行中的相关逻辑,接下来还有一些收尾工作。首先是catchtouchend='_touchEndEvent' catchtouchcancel='_touchEndEvent'的触摸结束事件。这个事件做的很简单,只是把inTouch给改成了false

    /**
     * 触摸事件结束
     */
    _touchEndEvent: function () {
      inTouch = false
    },

回弹动画事件

也就是一开始在图片加载完成后设置的定时器里面执行的方法_reboundAnimation() ,这个方法的原理很简单,就是不断的判断marginTopmarginLeft是否导致image超出边界了,如果是则慢慢的移动回去。不过主要是因为每5ms执行一次,很吃CPU,还是有待优化啊。方法实现如下:

    /**
     * 边界的回弹动画
     */
    _reboundAnimation: function () {
      if (this.data.marginTop > 0) {
        this.setData({
          marginTop: this.data.marginTop - 4
        })
        if (this.data.marginTop - 4 < 0) {
          this.setData({
            marginTop: 0
          })
        }
      }
      if (this.data.marginLeft > 0) {
        this.setData({
          marginLeft: this.data.marginLeft - 4
        })
        if (this.data.marginLeft < 0) {
          this.setData({
            marginLeft: 0
          })
        }
      }
      if (this.data.marginLeft < 0 && (this.data.imgWidth - Math.abs(this.data.marginLeft)) < this.data.view_width) {
        this.setData({
          marginLeft: this.data.marginLeft + 4
        })
      }
      if (this.data.marginTop < 0 && (this.data.imgHeight - Math.abs(this.data.marginTop)) < this.data.view_height) {
        this.setData({
          marginTop: this.data.marginTop + 4
        })
      }
    },

Over

源码地址

https://download.csdn.net/download/xiao_wl/10293903

你可能感兴趣的:(小程序)