vue 开发波纹点击特效组件

zq0213_ 7年前
   <p>最近在使用 vue2 做一个新的 material ui 库,波纹点击效果在 material design 中被多次使用到,于是决定把它封装成一个公共的组件,使用时直接调用就好啦。</p>    <h2><strong>开发之前的思考</strong></h2>    <p>常见的波纹点击效果的实现方式是监听元素的 mousedown 事件,在元素内部创建一个 <strong>波纹元素</strong> ,并调整元素的 transform: scale(0); 到 transform: scale(1); , 通过计算点击的位置来设置 <strong>波纹元素</strong> 的大小和位置,以达到波纹扩散的效果。</p>    <p>我将组件分为两个部分, circleRipple.vue 和 TouchRipple.vue 各自实现不同的功能</p>    <ol>     <li> <p>circleRipple.vue 波纹扩散组件,完成波纹扩散的效果</p> </li>     <li> <p>TouchRipple.vue 监听 mouse 和 touch 相关事件,控制 circleRipple 的显示,位置。</p> </li>    </ol>    <h2><strong>circleRipple.vue</strong></h2>    <p>circleRipple 需要完成波纹扩展的效果,而且可以从外部控制它的大小和位置, 所以利用 vue 的 transition 动画完成效果, 提供 mergeStyle 、 color 、 opacity 参数来从外部控制它的样式。实现代码如下。</p>    <pre>  <code class="language-javascript"><template>    <transition name="mu-ripple">      <div class="mu-circle-ripple" :style="styles"></div>    </transition>  </template>    <script>  import {merge} from '../utils'  export default {    props: {      mergeStyle: {        type: Object,        default () {          return {}        }      },      color: {        type: String,        default: ''      },      opacity: {        type: Number      }    },    computed: {      styles () {        return merge({}, {color: this.color, opacity: this.opacity}, this.mergeStyle)      }    }  }  </script>    <style lang="less">  @import "../styles/import.less";  .mu-circle-ripple{    position: absolute;    width: 100%;    height: 100%;    left: 0;    top: 0;    pointer-events: none;    user-select: none;    border-radius: 50%;    background-color: currentColor;    background-clip: padding-box;    opacity: 0.1;  }    .mu-ripple-enter-active, .mu-ripple-leave-active{    transition: transform 1s @easeOutFunction, opacity 2s @easeOutFunction;  }    .mu-ripple-enter {    transform: scale(0);  }    .mu-ripple-leave-active{    opacity: 0 !important;  }  </style></code></pre>    <p>vue2 对于动画方面做了比较大的修改,除了把指令换成组件外,它还可以完成更复杂的动画效果,具体可以看这里 <a href="/misc/goto?guid=4959716212306503207" rel="nofollow,noindex">vue2 transition</a></p>    <h2><strong>TouchRipple.vue</strong></h2>    <p>TouchRipple 需要控制 circleRipple 的显示。完成以下内容:</p>    <ol>     <li> <p>监听 mouse 和 touch 相关事件, 控制 circleRipple 的显示。</p> </li>     <li> <p>通过点击事件 event 对象, 计算出 circleRipple 的大小和位置</p> </li>     <li> <p>如果频繁点击可能出现多个 circleRipple</p> </li>    </ol>    <h3><strong>首先,基本模板 + 数据模型</strong></h3>    <pre>  <code class="language-javascript"><template>    <!--最外层用div包裹-->    <div @mousedown="handleMouseDown" @mouseup="end()" @mouseleave="end()" @touchstart="handleTouchStart"  @touchend="end()" @touchcancel="end()">      <!--外层包裹防止波纹溢出-->      <div :style="style" ref="holder">        <!--多个波纹用 v-for 控制-->        <circle-ripple :key="ripple.key" :color="ripple.color" :opacity="ripple.opacity" :merge-style="ripple.style" v-for="ripple in ripples"></circle-ripple>      </div>      <!--利用slot分发实际内容-->      <slot></slot>    </div>  </template>    <script>  import circleRipple from './circleRipple'  export default {    props: {      // 是否从中间扩散,设为false会从点击处扩散      centerRipple: {        type: Boolean,        default: true      },      // 外层包裹的样式      style: {        type: Object,        default () {          return {            height: '100%',            width: '100%',            position: 'absolute',            top: '0',            left: '0',            overflow: 'hidden'          }        }      },      // 波纹颜色      color: {        type: String,        default: ''      },      // 波纹透明度      opacity: {        type: Number      }    },    data () {      return {        nextKey: 0, // 记录下一个波纹元素的key值, 相当于uuid,不设置的话会使动画失效        ripples: [] // 波纹元素参数数组      }    },    mounted () {      this.ignoreNextMouseDown = false // 防止既有 touch 又有 mouse点击的情况    },    methods: {      start (event, isRippleTouchGenerated) {        // 开始波纹效果      },      end () {        // 结束波纹效果      },      handleMouseDown (event) {        // 监听 鼠标单击      },      handleTouchStart (event) {        // 监听 touchstart 方法      }    },    components: {      'circle-ripple': circleRipple    }  }  </script></code></pre>    <h3><strong>开始和结束波纹效果</strong></h3>    <p>增加一个波纹元素只需要在 <strong>ripple</strong> 增加一个 object 即可,不同的是当需要从点击处扩展时,需要计算一下波纹元素的大小和位置。</p>    <pre>  <code class="language-javascript">{    // isRippleTouchGenerated 是否是touch 事件开始的    start (event, isRippleTouchGenerated) {      // 过滤 touchstart 和 mousedown 同时存在的情况      if (this.ignoreNextMouseDown && !isRippleTouchGenerated) {        this.ignoreNextMouseDown = false        return      }            // 添加一个 波纹元素组件      this.ripples.push({        key: this.nextKey++,         color: this.color,        opacity: this.opacity,        style: this.centerRipple ? {} : this.getRippleStyle(event) // 不是从中心扩展的需要计算波纹元素的位置      })      this.ignoreNextMouseDown = isRippleTouchGenerated   },   end () {     if (this.ripples.length === 0) return     this.ripples.splice(0, 1) // 删除一个波纹元素     this.stopListeningForScrollAbort() // 结束 touch 滚动的处理    }  }</code></pre>    <p>因为 vue2 基于 Virtual DOM 的, 所以如果没有 key 在增加一个元素又同时删除一个元素的时候,dom tree并没有发生变化,是不会产生动画效果的。</p>    <h3><strong>监听 mousedown 和 touchstart</strong></h3>    <p>mousedown 和 touchstart 处理上会有所不同,但都是用来启动波纹效果的, touch涉及到多点点击的问题,我们一般取第一个即可。</p>    <pre>  <code class="language-javascript">{      handleMouseDown (event) {        // 只监听鼠标左键的点击        if (event.button === 0) {          this.start(event, false)        }      },      handleTouchStart (event) {        event.stopPropagation() // 防止多个波纹点击组件嵌套        if (event.touches) {          this.startListeningForScrollAbort(event) // 启动 touchmove 触发滚动处理          this.startTime = Date.now()        }        this.start(event.touches[0], true)      }  }</code></pre>    <h3><strong>touchmove控制</strong></h3>    <p>当发生touchMove事件是需要判断是否,移动的距离和时间,然后结束小波纹点击小姑</p>    <pre>  <code class="language-javascript">{    // touchmove 结束波纹控制    stopListeningForScrollAbort () {      if (!this.handleMove) this.handleMove = this.handleTouchMove.bind(this)      document.body.removeEventListener('touchmove', this.handleMove, false)    },    startListeningForScrollAbort (event) {      this.firstTouchY = event.touches[0].clientY      this.firstTouchX = event.touches[0].clientX      document.body.addEventListener('touchmove', this.handleMove, false)    },    handleTouchMove (event) {      const timeSinceStart = Math.abs(Date.now() - this.startTime)      if (timeSinceStart > 300) {        this.stopListeningForScrollAbort()        return      }      const deltaY = Math.abs(event.touches[0].clientY - this.firstTouchY)      const deltaX = Math.abs(event.touches[0].clientX - this.firstTouchX)      // 滑动范围在 > 6px 结束波纹点击效果      if (deltaY > 6 || deltaX > 6) this.end()    }  }</code></pre>    <h3><strong>计算波纹的位置和大小</strong></h3>    <p>需要从点击处扩散的波纹效果,需要计算波纹元素的大小和位置</p>    <pre>  <code class="language-javascript">{    getRippleStyle (event) {      let holder = this.$refs.holder      //  这个方法返回一个矩形对象,包含四个属性:left、top、right和bottom。分别表示元素各边与页面上边和左边的距离。      let rect = holder.getBoundingClientRect()       // 获取点击点的位置      let x = event.offsetX      let y      if (x !== undefined) {        y = event.offsetY      } else {        x = event.clientX - rect.left        y = event.clientY - rect.top      }      // 获取最大边长      let max      if (rect.width === rect.height) {        max = rect.width * 1.412      } else {        max = Math.sqrt(          (rect.width * rect.width) + (rect.height * rect.height)        )      }      const dim = (max * 2) + 'px'      return {        width: dim,        height: dim,        // 通过margin控制波纹中心点和点击点一致        'margin-left': -max + x + 'px',        'margin-top': -max + y + 'px'      }    }  }</code></pre>    <h2><strong>使用</strong></h2>    <p>由于 touchRipple 内部都是 <strong>position:absolute</strong> 布局,使用时,需要在外部加上 <strong>position:relative</strong></p>    <pre>  <code class="language-javascript">// listItem.vue  <a :href="href" @mouseenter="hover = true" @mouseleave="hover = false" @touchend="hover = false"      @touchcancel="hover = false" class="mu-item-wrapper" :class="{'hover': hover}">      <touch-ripple class="mu-item" :class="{'mu-item-link': link}" :center-ripple="false">        <div class="mu-item-media">          <slot name="media"></slot>        </div>        <div class="mu-item-content">          // ...        </div>      </touch-ripple>  </a>  <style>    .mu-item-wrapper {      display: block;      color: inherit;      position: relative;  }  </style></code></pre>    <p> </p>    <p> </p>    <p>来自:https://segmentfault.com/a/1190000006931367</p>    <p> </p>