1. 程式人生 > >微信小程式 MaterialDesign(1)---- button(漣漪)

微信小程式 MaterialDesign(1)---- button(漣漪)

本文用來介紹關於如何在微信小程式中實現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在產出的時候 並未刪除,所以它會一直增加增加增加

就像這樣
這裡寫圖片描述

所以我們可以將播放完畢的動畫從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;
            }
        }
效果

這裡寫圖片描述