美文网首页
微信小程序 materialUi(1)---- button(涟

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

作者: 馒头_5c99 | 来源:发表于2018-07-28 16:20 被阅读0次

    本文用来介绍关于如何在微信小程序中实现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

    相关文章

      网友评论

          本文标题:微信小程序 materialUi(1)---- button(涟

          本文链接:https://www.haomeiwen.com/subject/edbomftx.html