微信小程序 materialUi(1)---- button(涟

2018-07-28  本文已影响0人  馒头_5c99

本文用来介绍关于如何在微信小程序中实现materia风格的ui化

注意:该ui使用微信小程序原生语法,动画均使用animate以及过渡效果实现,未使用微信的api创建动画

1.准备

创建一个自定义组件 sc-button


文件目录

在sc-button.json中指明这是一个自定义组件

{
  "component":true
}

2. 封装button

2.1 初始html格式

<button class="btn-class">
    <slot></slot>
</button>

2.2 处理微信原生事件以及指令

微信小程序的button有很多内置的微信指令例如 open-type,size,plain 等以及原生的方法如getuserinfo,getphonenumber 等 所以我们封装button的时候要把这些能力进行相应的处理。
可以分为两类:一种是指令,一种是事件
指令 可以从properties里将微信原生的button的所有指令声明,然后直接赋值到内部封装的button里。
事件 我们可以根据事件的捕获冒泡以及open-type的唯一性,让其在触发后根据open-type选择事件直接冒泡到外层即可,但是需要将获取的value也传递出去
例如:

properties: {
        openType: {
            type: String
        },
        size: {
            type: String,
            value: 'default'
        },
        plain: {
            type: Boolean,
            value: false
        }
},
data: {
        // 事件的map表
        openTypeToBindEvent: {
            'getUserInfo': 'getuserinfo',
            'getphonenumber': 'getphonenumber',
            'launchApp': 'error',
            'contact': 'contact'
        }
},
methods: {
        // 绑定未冒泡的事件手动触发到上一层
        _returnEventData(e) {
            this.triggerEvent(`${this.data.openTypeToBindEvent[this.properties.openType]}`);
        }
}

然后直接赋值到button里,注意,这里需要判断一下值是否存在

<button class="btn-class"
        bind:getuserinfo="{{openType === 'getUserInfo' ? '_returnEventData' : '' }}"
        bind:getphonenumber="{{openType === 'getphonenumber' ? '_returnEventData' : '' }}"
        bind:error="{{openType === 'launchApp' ? '_returnEventData' : '' }}"
        bind:contact="{{openType === 'contact' ? '_returnEventData' : '' }}"
        open-type="{{openType || ''}}"
        size="{{size || ''}}"
        plain="{{plain || ''}}"
>
    <slot></slot>
</button>

2.3 material 的 涟漪实现

2.3.1 重置/增加button的一些样式

button { 
    display: flex;
    justify-content: center;
    align-items: center;
    position: relative;
    box-sizing: border-box;
    overflow: hidden;
    line-height: 66px;
    min-width: 88px;
    height: 36px;
    padding: 0 16px;
    margin: 0;
    font-size: 32rpx;
    border-radius: 2px;
    transition: all .2s cubic-bezier(.4,0,.2,1); // 增加过渡效果
}

2.3.2 增加涟漪

注意:微信小程序不支持js操纵dom元素 即没有appenChild一类的方法来添加元素,所以我们只能声明一个元素来进行涟漪的展示
<button class="btn-class"
        capture-bind:tap="{{ripple ? '_addRipple' : ''}}"
        capture-bind:longpress="{{ripple ? '_longPress' : ''}}"
        capture-bind:touchend="{{ripple ? '_touchend' : ''}}"
        bind:getuserinfo="{{openType === 'getUserInfo' ? '_returnEventData' : '' }}"
        bind:getphonenumber="{{openType === 'getphonenumber' ? '_returnEventData' : '' }}"
        bind:error="{{openType === 'launchApp' ? '_returnEventData' : '' }}"
        bind:contact="{{openType === 'contact' ? '_returnEventData' : '' }}"
        open-type="{{openType || ''}}"
        size="{{size || ''}}"
        plain="{{plain || ''}}"
>
    <slot></slot>
    <!-- 涟漪view -->
    <view class="ripple">
    </view>
</button>
涟漪的动画css样式
/* 涟漪的初始样式 */
.ripple {
    border-radius: 100%;
    background-color: #000000;
    left: 20px;
    top: 20px;
    opacity: 0.3;
    transform: scale(0.3);
    width: 10px;
    height: 10px;
    position: absolute;
}
/* 涟漪的点击扩散动画 */
.ripple-animation {
    animation: ripple 0.6s ease-out;
    animation-fill-mode: forwards;
}
/* 涟漪的长按扩散动画 */
.ripple-animation-hold{
    animation: ripple-hold 1s ease-out;
    animation-fill-mode: forwards;
}

@keyframes ripple {
    from {
        transform: scale(0.1);
        opacity: 0.3;
    }

    to {
        transform: scale(2.5);
        opacity: 0;
    }
}

@-webkit-keyframes ripple-hold {
    from {
        transform: scale(0.1);
        opacity: 0.3;
    }

    to {
        transform: scale(2.5);
        opacity: 0.3;
    }
}
涟漪的播放控制

两种播放控制
点击 - ripple-animation 动画
长按 - ripple-animation-hold 动画

然后我们在点击的时候播放 ripple-animation 长按播放ripple-animation-hold 即可

那么如何判断这个view的位置以及大小呢,因为每个人点击button的位置不一样,button的大小不一样,如果view过小就可能覆盖不到整个button,过大就太耗费性能。

所以,大小我们定为button长边的两倍
然后点击button的哪个位置,ripple就在哪个位置播放,因此必须设置ripple的position为absolute,我们就可以通过控制其left,以及top来控制ripple的位置。

问题就是ripple的位置,大小该如何设置

下面,我们在html里声明view的位置大小属性。

<view style="width:{{width}}px;height:{{width}}px;left:{{left}}px;top:{{top}}px"
          class="ripple-class {{click?'ripple-animation':hold?'ripple-animation-hold':''}}">
</view>
添加点击事件
methods: {
        // 短按(长按同理)
        _tap(e) {
           // 获取button的大小,位置
           this._queryMultipleNodes('btn-class').then(res => {
                    // 关于button的属性     // 关于button位置的属性
                    const button = res[0], viewPort = res[1];
                    const boxWidth = parseInt(button.width);   // button的宽度
                    const boxHeight = parseInt(button.height);  // button的长度
                    const rippleWidth = boxWidth > boxHeight ? boxWidth : boxHeight;
                    // 我们需要计算的是ripple相对于button左上角的距离
                    // 注意 e.detail.y(点击位置)是相当于文档的高度不是当前窗口的高度,因此需要减去滚动的距离以及button的top
                    const rippleX = (e.detail.x - (button.left + viewPort.scrollLeft)) - rippleWidth / 2;
                    const rippleY = (e.detail.y - (button.top + viewPort.scrollTop)) - rippleWidth / 2;
                    this.setData({
                        click:true,
                        width: rippleWidth,
                        left: rippleX ,
                        top: rippleY 
                    });
            });
        },
        // 该方法返回选择元素的大小,位置
        _queryMultipleNodes: function (e) {
            return new Promise((resolve, reject) => {
                const query = this.createSelectorQuery();
                query.select(e).boundingClientRect();
                query.selectViewport().scrollOffset();
                query.exec(function (res) {
                    resolve(res);
                });
            })
        }
}
实现效果
效果图
但是这样出现了一个bug,即点击多次,不会出现多个涟漪效果,而是会导致一个view的动画结束然后重复播放
解决办法:

采用wx-for来循环产出ripple,这样可以实现多个涟漪的效果,那么我们可以定义一个ripple数组,每次点击的时候不断往该数组push进新的ripple然后由浏览器渲染就好了,我们还需要分配 wx-key来避免渲染数组的性能问题。

我们需要为每个rippleItem分配 短按 播放动画的标识 startAnimate 以及 长按播放动画标识 holdAnimate
data: {
        rippleList: [],
        rippleId: 0
},
methods:{
    _tap(e) {
            if (!this.properties.disabled) {
                this._queryMultipleNodes('.' + this.data.btnClass).then(res => {
                    const button = res[0], viewPort = res[1];
                    const boxWidth = parseInt(button.width);   // button的宽度
                    const boxHeight = parseInt(button.height);  // button的长度
                    const rippleWidth = boxWidth > boxHeight ? boxWidth : boxHeight;
                    const rippleX = (e.detail.x - (button.left + viewPort.scrollLeft)) - rippleWidth / 2;
                    const rippleY = (e.detail.y - (button.top + viewPort.scrollTop)) - rippleWidth / 2;
                    this.data.rippleList.push({
                        rippleId: `ripple-${this.data.rippleId++}`,
                        width: rippleWidth ,
                        left: rippleX ,
                        top: rippleY ,
                        startAnimate: true,
                        holdAnimate: holdAnimate || false
                    });
                    this.setData({
                        rippleList: this.data.rippleList
                    });
              });
          }
     }
}
实现效果
效果图
到这里我们又发现了一个问题 就是 ripple在产出的时候 并未删除,所以它会一直增加增加增加

就像这样


多个ripple未删除

所以我们可以将播放完毕的动画从rippleList删除,这样可以进行一定的优化,利用小程序的animationend事件可以触发每个ripple的动画播放完毕事件,然后取得id并从rippleList找到这个id删除即可。可以找到这个id的item实在是太耗费性能了。
于是我们想到,每个动画播放完毕一定是这个list的最前面的一个item,也就是每次触发动画播放完毕事件我们只需要删除list中的第一个就好了,但是小程序需要每次都执行setData方法来对数组进行更新,这会导致我们按一百个ripple就执行一百次setData,大量耗费性能,因此需要一个防抖来控制setData的执行,

<view wx:for="{{rippleList}}"
          wx:key="rippleId"
          id="{{item.rippleId}}"
          style="width:{{item.width}}px;height:{{item.height}}px;left:{{item.left}}px;top:{{item.top}}px"
          class="ripple-class {{item.startAnimate ? item.holdAnimate ? 'ripple-animation-slow-hold' :'ripple-animation-slow' : ''}}"
          bind:animationend="{{item.holdAnimate ? null : '_scbuttonrippleAnimationend'}}">
    </view>
_buttonrippleAnimationend() {
            // 防抖
            this.data.rippleList.shift();
            if (this.data.timer) {
                clearTimeout(this.data.timer);
                this.data.timer = setTimeout(deleteRipple.bind(this), 300);
            } else {
                this.data.timer = setTimeout(deleteRipple.bind(this), 300);
            }
            
            function deleteRipple() {
                this.setData({
                    rippleList: this.data.rippleList
                });
                clearTimeout(this.data.timer);
                this.data.timer = null;
            }
        }
效果
效果图
该ui框架大部分组件已经实现 欢迎大家 star https://github.com/xbup/sc-ui
上一篇 下一篇

猜你喜欢

热点阅读