一、需求

weex 容器环境下,需要一个 mask 组件,也就是弹层,最终的效果大概如下。

dotwe.org 上预览(支持 weex 容器环境):

TIM截图20180801215148.jpg

weex 容器环境下有几点需要注意的事情:

  • 容器不支持 display:none
  • 弹层的显示和隐藏通过 v-if 控制,而不是 v-show,因为 weex 不支持 v-show
  • 因为基于 v-if,因此使用 css 实现 transition 是没有效果的
  • 使用 animation.transition 实现过渡效果

二、关键问题:v-if 的过渡动画

1、问题阐述及解决思想

mask 弹层其实实现出来很简单,无非就是 position:fixed 实现掉

关键点在于过渡动画的实现,如果是传统的网页 APP,那么使用 display:none(vue 框架使用 v-show 即可,基于 display) 以及 transition:all .5s 即可

但是由于 weex 原生是不支持 display:none 的,因此对于 mask 弹层组件,只能通过 v-if 销毁或者显示组件内容,这就导致,css 上写的 transition:all .5s 这种形式的过渡是不起作用的,会直接到达最终状态,不会存在过渡效果。

一开始的时候,我也没有解决这个关键性的问题,不过我之后去看了一下 weex-ui,发现了他的 mask 弹层动画是没问题的,便去找了相关包的源代码看了一下他的解决方法。

weex-ui 的弹层组件实际上是两个组件构成的,一个是 wxc-overlay,一个是wxc-mask,在 mask 组件中引入了 overlay 组件。

即使是使用 animation.transition 解决动画问题,同样会遇到 DOM 没有加载出来就进行动画,或者进行动画和DOM加载同步(这里的同步的意思是,实际上动画执行了,但是人为感知不到,只是瞬间就没了动画的过程)

要解决这个问题,核心思想就是在 DOM 渲染出来的时候,再执行动画

2、hack 方法解决问题

weex-ui 的 mask 组件使用了一种 hack 方法,这种方法使用的很巧妙。

下面是简单的实现代码示例,而不是完整的组件代码:

<template>
<div class="overlay" v-if="show" :hack="shouldShow" ref="overlay" :style="style">
<div>
<template>
<script>
export default {
    props:{
        show:{type:Boolean,default:false},
        hashAnimation:{type:Boolean,default:true}
    },
    computed:{
        style(){
            return {
                opacity:this.show ? 1 : 0
            }       
        },
        shouldShow(){
            this.hasAnimation && this.appearOverlay(true);
            return true;
        }
    },
    methods:{
        appearOverlay(bool){
           const el = this.$refs.overlay;
            animation.transition(el,{
                styles:{
                      opacity:show ? 1 : 0
                },
                duration:500
            },()=>{});
        }
    }
}
</script>
<style scoped>
.overlay{
    position:fixed;
    width:750;
    top:0;
    left:0;
    bottom:0;
    right:0;
    background-color:rgba(0,0,0,0.6);
}
</style>

上面代码中关键的部分在于 computedshouldShow() 以及在组件的 template 中的 :hack="shouldShow"

div.overlay 是通过 v-if 进行条件渲染,show 是由父组件传递的 prop。

:hack 是一个没什么作用的属性,但是却能够保证 DOM 渲染出来之后,进行 shouldShow 的计算,从而调用了 this.appearOverlay 方法,至于 hasAnimation 则是通过 prop 控制是否需要过渡动画的, 如果不需要动画,则 this.apperOverlay 是没意义了,因为他的意义就是过渡动画。

一个 :hack 加上 setTimeout()setTimeout(()=>{},50) 对用户是无感知的),能够保证只有DOM渲染出来之后,才会进行动画效果。

三、完整的 mask 组件代码

完整的 mask 需要考虑的方面很多,比如背景色、点击蒙层是否可关闭、mask组件关闭的回调方法、内部文字的样式等等,

不过这都是样式或者是简单的问题,解决了过渡动画的触发问题,一切都好说。

给一个简单的 mask 组件的代码(vue-weex),这个组价还有很多东西没有考虑到,只是从一个实际的特例需求去做的,实现需求之后没有考虑从很多的扩展等。

下面的代码有一个问题没解决,就是只是设置了 overlay 的动画,而 overlay 内部没有设置动画。

按照 weex-ui 的设计思想,其实 div.overlaydiv.mask 应该是分开的,而不是将 div.mask 作为 div.overlay 的自足家,然后分别设置两个组件的样式和动画过渡,这也是为什么在 weex-ui 中是两个 package。

在线的体验可以在 playground 上查看:

Mask.vue:

<template>
  <div 
    class="overlay" 
    v-if="show" 
    :hack="shouldShow" 
    @click="clickHandle" 
    ref="overlay"
    :style="overlayStyle"
  >
  <div class="mask-body" v-if="show">
    <text class="mask-content" :style="contentStyle" @click="maskBodyClickHandle"> 
      <slot></slot>
    </text>
    <image :src="closeIcon" class="close-btn " @click="closeBtnClickHandle" />
  </div>
  </div> 
</template>
<script>
const animation = weex.requireModule('animation');
export default {
  props: {
    contentStyle: {
      type: Object,
      default: () => {
        return {
          height: '500px',
          width: '700px',
          backgroundColor: '#ffffff',
          paddingLeft: '20px',
          paddingRight: '20px',
          paddingTop: '20px',
          paddingBttom: '20px'
        };
      }
    },
    show: {
      type: Boolean,
      default: false
    },
    hasAnimation: {
      type: Boolean,
      default: true
    },
    opacity: {
      type: Number,
      default: 0.6
    },
    duration: {
      type: [Number, String],
      default: 300
    },
    timingFunction: {
      type: Array,
      default: () => ['ease-in', 'ease-out']
    },
    autoClickClose: {
      type: Boolean,
      default: true
    },
    closeHandle: {
      type: Function,
      default: () => {}
    }
  },
  data() {
    return {
      closeIcon:
        'https://gw.alicdn.com/tfs/TB1qDJUpwMPMeJjy1XdXXasrXXa-64-64.png'
    };
  },
  computed: {
    overlayStyle() {
      return {
        opacity: this.hasAnimation ? 0 : 1,
        backgroundColor: `rgba(0, 0, 0, ${this.opacity})`
      };
    },
    shouldShow() {
      const { show, hasAnimation } = this;
      hasAnimation &&
        setTimeout(() => {
          this.appearOverlay(show);
        }, 50);
      return show;
    },
    maskStyle() {}
  },
  methods: {
    appearOverlay(bool) {
      const { duration, timingFunction, hasAnimation } = this;
      const el = this.$refs.overlay;
      if (hasAnimation && el) {
        animation.transition(
          el,
          {
            styles: {
              opacity: bool ? 1 : 0
            },
            duration,
            delay: 0,
            timingFunction: timingFunction[bool ? 0 : 1]
          },
          () => {
            !bool && this.closeHandle();
          }
        );
      }
    },
    clickHandle() {
      this.autoClickClose && this.appearOverlay(false);
    },
    maskBodyClickHandle(e) {
      e.stopPropagation();
    },
    closeBtnClickHandle(e) {
      e.stopPropagation();
      this.appearOverlay(false);
    }
  }
};
</script>

<style scoped>
.overlay {
  width: 750px;
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
}
.mask-body {
  position: fixed;
  top: 300px;
  width: 750px;
  align-items: center;
}
.mask-content {
  width: 702px;
  background-color: #ffffff;
  padding-left: 30px;
  padding-right: 30px;
  padding-bottom: 30px;
  padding-top: 30px;
}
.close-btn {
  margin-top: 100px;
  width: 50px;
  height: 50px;
}
</style>