浅谈vi-motion小程序运动组件的内部原理

vi-motion 组件介绍

vi-motion组件的内部运动是css3动画,使用的是animate开源动画库

vi-motion的出现是为了解决小程序开发时的复杂运动会话框。让开发不必把精力放在复杂的动画运动上,使其更加关注业务逻辑

组件文档:vi-motion

github仓库:VisionUI, 欢迎 start 或 issues

明确组件需求及使用场景

  • 复杂的运动会话弹窗
  • 多适用于授权弹窗、儿童类小程序项目、抽奖类小程序项目等
  • 只定制运动动画,不提供任何UI界面

DOM结构设计

在上一步我们清楚的知道了组件的使用场景以及一些需要对外提供的接口

那么DOM结构(wxml)应该怎么设计呢?

既然这是一个运动类的会话弹窗,那么DOM结构与普通的会话弹窗是差不多的。一个根元素下面包含两个子元素。其中一个子元素为弹窗的mask遮罩层,一个元素是用来装载与用户的对话内容。

但是,该组件只是提供弹窗的运动方式,而不会提供弹窗的基本交互UI。所以,需要定义一个 slot 插槽用来接收开发者提供的对话UI界面

组件的内部DOM结构(wxml)

isShow 是用来控制组件的显示与隐藏

maskIsHide 用来控制遮罩层的显示与隐藏

maskColor 用来定义遮罩层的颜色

enterAnimateName 用来控制组件出现时候的动画

outAnimateName 用来控制组件隐藏时候的动画

animationDuration 用来控制动画运动的时间

<view wx:if="{{isShow}}" class="vi-dialog">
  <view 
      class="vi-dialog-mask" 
      wx:if="{{!maskIsHide}}" 
      data-sign="mask" 
      capture-catch:tap="triggerToHide" 
      style="background-color: {{maskColor}}">
  </view>
  <view 
      class="animated {{ishide ? enterAnimateName : outAnimateName}} container-calss" 
      style="animation-duration: {{animationDuration}}s">
           <slot></slot>
  </view>
</view>

基本的DOM结构出来之后,下面是给这些DOM结构编写style

我们会在样式文件(wxss)中引入
animate开源动画库

@charset "UTF-8";
@import './animate.min.wxss';
.vi-dialog {
  top: 0;
  left: 0;
  position: fixed;
  height: 100%;
  width: 100%;
  box-sizing: border-box;
  z-index: 10000;
}

.vi-motion__animated {
  transition-property: transform;
  transition-delay: 0.5s;
}

.vi-dialog-mask {
  background-color: rgba(0, 0, 0, 0.3);
  position: fixed;
  top: 0;
  bottom: 0;
  right: 0;
  left: 0;
  z-index: 9999;
}

组件的行为开发

在组件的DOM结构上,我们基本定义了一些对外的接口用来控制组件的行为表现

这些接口分别为:ishide maskIsHide maskColor enterAnimateName outAnimateName animationDuration

在清楚了一些基本接口的设计之后,我们就可以在 Component构造器中定义接口了

Component({
  properties: {
    ishide: {
      type: Boolean,
      value: false,
      observer(newValue) {
      },
    enterAnimateName: {
      type: String,
      value: 'bounce'
    },
    maskIsHide: {
      type: Boolean,
      value: false
    },
    outAnimateName: {
      type: String,
      value: ''
    },
    maskColor: {
      type: String,
      value: 'rgba(0,0,0, .3)'
    },
    animationDuration: {
      type: Number,
      value: 1
    }
  },
})

接口定义完成已经基本可以控制弹窗的现实、隐藏。但是我们在wxml结构中用 wx:if="{{isShow}}" 语句来控制组件的显示与隐藏的。为什么在接口定义的部分没有看到呢?其实是有原因的,说清楚原因我们再继续之后的开发

没有定义isShow接口的原因有:

  • wx:if语句是直接控制元素的显示隐藏,在组件进入页面的时候会有动画,但是如果组件隐藏的时候也需要动画呢?
  • 组件在隐藏的时候如果有动画,那么就必须要等动画运动结束才可以让组件隐藏

基于上面的这两个原因,我们把isShow字段放在data对象上

Component({
  properties: {
    ishide: {
      type: Boolean,
      value: false,
      observer(newValue) {
            if (newValue) {
             this.setData({
              isShow: newValue
            });
          } else {
            setTimeout(() => {
              this.setData({
                isShow: newValue
              });
            }, this.data.animationDuration * 1000)
          }
      }
    },
    ...
  },
  data: {
    isShow: false
  }
})

那既然解决了组件隐藏时动画的展示。但是,如果在组件隐藏的时候就是不想要动画呢?基于这样的需求,于是我们再定义一个接口,用来控制组件隐藏的时候是否出现动画

Component({
  properties: {
    ishide: {
      type: Boolean,
      value: false,
      observer(newValue) {
        if (this.data.outHasAnimate) {
          if (newValue) {
            this.setData({
              isShow: newValue
            });
          } else {
            setTimeout(() => {
              this.setData({
                isShow: newValue
              });
            }, this.data.animationDuration * 1000)
          }
        } else {
          this.setData({
            isShow: newValue
          });
        }
      }
    },
    outHasAnimate: {
      type: Boolean,
      value: true
    },
    ...
  },
  data: {
    isShow: false
  }
})

现阶段,我们定义了组件的行为接口。那如果是这样的业务场景:用户点击 mask 遮罩层,需要隐藏组件。那么我们就需要监听mask 遮罩层的点击事件

Component({
  methods: {
    triggerToHide(e) {
      this.triggerEvent('hide', this.data.ishide)
    }
  },
})

上面是mask遮罩层的点击方法,然后用 this.triggerEvent()自定义一个事件。之后在页面中调用 hide自定义事件就可以达到监听组件的mask元素的点击事件。

但是,如果产品提出了这样的需求:点击遮罩层不能隐藏组件。我们大可以不必在页面中调用组件的自定义事件,我们也可以再定义一个接口,用于表示是否触发组件的自定义事件

Component({
  properties: {
    clickMaskHide: {
      type: Boolean,
      value: false
    },
    ...
  },
  methods: {
    triggerToHide(e) {
      if (this.data.clickMaskHide) return false;
      this.triggerEvent('hide', this.data.ishide)
    }
  }
})

写到这,副本基本算是通关了,可以收功了。但我们好像还遗漏了一个页面布局相关的“小怪“。因为该组件的使命是提供多种运动方式,所以不会定义用于装载内容盒子的样式。所以我们需要暴露一个样式扩展的接口。

由于小程序框架本身的限制,在页面中是无法更改组件的内部元素的样式,所以小程序提供了一个接口:externalClasses

Component({
  properties: {
      ...
  },
  externalClasses: ['container-class'],
  methods: {
      ...
  },
  data: {
      ...
  }
})

在页面的元素中用 class 属性来接收组件外部的自定义样式

<view 
      class="container-calss ... " 
    style="animation-duration: {{animationDuration}}s">
    <slot></slot>
</view>

写在最后的提示:由于组件的运动方式是使用的
animate开源动画库,所以
container-class接口定义的样式不能使用css3的
transform

组件效果预览与微信公众号

《浅谈vi-motion小程序运动组件的内部原理》《浅谈vi-motion小程序运动组件的内部原理》

    原文作者:AsherSun
    原文地址: https://segmentfault.com/a/1190000017163553
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞