天天看點

快應用中實作自定義抽屜元件

1.  什麼是抽屜元件

抽屜元件是一種特殊的彈出面闆,可以模拟手機App中推入拉出抽屜的效果。抽屜一般具有如下特點:

抽屜可顯示在左邊,也可以顯示在右邊;

抽屜寬度可定制;

抽屜有遮罩層,點選遮罩層可收起抽屜;

手勢滑動可呼出抽屜;

      抽屜(Drawer)元件結構分為控制器和抽屜内容兩部分。 一般來說,controls都是按鈕、圖示之類的可點選的元件,類似真實抽屜的把手,content是抽屜内部的東西,每個抽屜的content都是不一樣的。點選controls可以觸發content的顯示和收起。 是以,在使用抽屜元件的頁面布局可以抽象成如下結構:

<div class=“page">
   <div class=“controls">
     <image></image>
    </div>
   <stack class=“drawer_container”>
       <div class=“page_content”>
          …
       </div>
        <drawer class="drawer">
           <div class=“content”>
            …
            </div>
         </drawer >
   </stack>
</div>      

2.實作步驟

   抽屜元件屬于一種擴充能力,目前快應用已有的元件是無法滿足的,需要自定義元件實作。

2.1自定義子元件

     抽屜外觀都是通用的,但是抽屜内部格局content不一樣,在設計的時候,不能直接寫死content布局,否則一旦content部分的UI有變化,會導緻子元件也要修改,違背了代碼設計中的“開閉”原則。

    是以,我們子元件drawer.ux中,使用了slot 元件來承載父元件中定義的content,由使用drawer元件的頁面來完成content布局,如下圖所示:

快應用中實作自定義抽屜元件

2.2子元件設計

支援的屬性如下:

屬性 類型 預設值 描述
mode String left 設定抽屜的顯示位置,支援left和right
mask boolean true 抽屜展開時是否顯示遮罩層
maskClick Boolean true 點選遮罩層是否關閉抽屜
width Number 320px 抽屜寬度

支援的事件:

事件名稱 參數 描述
drawerchange {showDrawer:booleanValue} 抽屜收起、展開的回調事件

2.3抽屜展開和收起

抽屜預設是關閉不顯示的,通過“display: none;” 來隐藏。

收起、展開通過X軸的平移動畫控制,收起時,移到螢幕之外,展開時平移到可視區域。不管是展開還是收起,都是平滑的動畫效果。

抽屜顯示在左側還是右側,是通過div的flex-direction控制的,顯示在左側時,設定row,顯示在右側時,設定為row-reverse。

圖1 左抽屜打開、收起style

快應用中實作自定義抽屜元件

​​

圖2 有抽屜打開、收起style

快應用中實作自定義抽屜元件

​​

圖3 左抽屜動畫

快應用中實作自定義抽屜元件

​​

圖4  右抽屜動畫

快應用中實作自定義抽屜元件

​​

2.4遮罩層實作

遮罩層初始狀态不顯示,通過“display: none;” 來隐藏。抽屜展示時,顯示遮罩層,收起時,不顯示,遮罩層使用透明度實作。

快應用中實作自定義抽屜元件

​​

2.5父子元件通信

父元件通過parentVm.$broadcast()向子元件傳遞抽屜打開、收起的事件,子元件通過$on()監聽事件和參數。

子元件通過$watch()方法監聽抽屜顯示模式mode屬性的變化,進而修改css樣式,讓其在正确的位置顯示抽屜。

子元件通過drawerchange事件及參數通知父元件。

2.6手勢呼出抽屜

 在抽屜處手勢滑動,呼出抽屜,需要監聽touchstart和touchend事件。注意滑動範圍,隻有在抽屜邊緣處呼出抽屜,其其他位置不呼出。

3.總結

實作抽屜元件,您可以從中學會如下知識點:

熟悉快應用子元件的設計和屬性定義;

熟悉父子元件通信;

熟悉動畫樣式的實作;

學會如何實作一個遮罩層。

欲了解更多詳情,請參見:

華為官網:

<template>
    <div id="drawercontent" style="display:flex;position:absolute;width:100%;height:100%;top:0;left:0;bottom: 0; flex-direction: {{flexdirection}}" onswipe="dealDrawerSwipe">
       <div class="{{maskstyle}}" onclick="close('mask')"></div>
        <div id="unidrawercontent" class="{{unidrawerstyle}}" style="width:{{drawerWidth}}+'px'}">
            <slot></slot>
        </div>
            
    </div>
</template>

<style>
    .stack {
        flex-direction: column;
        height: 100%;
        width: 100%;
    }

    .uni-mask-open {
        display: flex;
        height: 100%;
        width: 100%;
        position: absolute;
        background-color: rgb(0, 0, 0);
        opacity: 0.4;
    }
    .uni-mask-closed {
        height: 100%;
        width: 100%;
        position: absolute;
        background-color: rgb(0, 0, 0);
        display: none;
    }

    .uni-drawer {
        display: none;
        height: 100%;
    }

    .uni-drawer-open-left {
        display: flex;
        height: 100%;
        animation-name: translateX;
        animation-timing-function: linear;
        animation-fill-mode: forwards;
        animation-duration: 300ms;
    }

    .uni-drawer-closed-left {
        display: flex;
        height: 100%;
        animation-name: translateXReverse;
        animation-timing-function: linear;
        animation-fill-mode: forwards;
        animation-duration: 600ms;
    }

    .uni-drawer-open-right {
        display: flex;
        height: 100%;
        flex-direction: row-reverse;
        animation-name: translateXRight;
        animation-timing-function: linear;
        animation-fill-mode: forwards;
        animation-duration: 300ms;
    }

    .uni-drawer-closed-right {
        display: flex;
        height: 100%;
        flex-direction: row-reverse;
        animation-name: translateXRightReverse;
        animation-timing-function: linear;
        animation-fill-mode: forwards;
        animation-duration: 600ms;
    }

    @keyframes translateX {
        from {
            transform: translateX(-110px);
        }

        to {
            transform: translateX(0px);
        }
    }

    @keyframes translateXReverse {
        from {
            transform: translateX(0px);
        }

        to {
            transform: translateX(-750px);
        }
    }

    @keyframes translateXRight {
        from {
            transform: translateX(300px);
        }

        to {
            transform: translateX(0px);
        }
    }

    @keyframes translateXRightReverse {
        from {
            transform: translateX(0px);
        }

        to {
            transform: translateX(750px);
        }
    }
</style>

<script>
    module.exports = {
        props: {
            /**
             * 顯示模式(左、右),隻在初始化生效
             */
            mode: {
                type: String,
                default: ''
            },
            /**
             * 蒙層顯示狀态
             */
            mask: {
                type: Boolean,
                default: true
            },
            /**
             * 遮罩是否可點選關閉
             */
            maskClick: {
                type: Boolean,
                default: true
            },
            /**
             * 抽屜寬度
             */
            width: {
                type: Number,
                default: 320
            }
        },
        data() {
            return {
                visibleSync: false,
                showDrawer: false,
                watchTimer: null,
                drawerWidth: 600,
                maskstyle: 'uni-mask-closed',
                unidrawerstyle: 'uni-drawer',
                flexdirection: 'row'

            }
        },
        onInit() {
            console.info("drawer oninit");
            this.$on('broaddrawerstate', this.drawerStateEvt);
            this.drawerWidth = this.width;
            if(this.mode=="left"){
                this.flexdirection="row";
            }else{
                this.flexdirection="row-reverse";
            }
            this.$watch('mode', 'onDrawerModeChange');
        },

        onDrawerModeChange: function (newValue, oldValue) {
            console.info("onDrawerModeChange newValue= " + newValue 
            + ", oldValue=" + oldValue);
            if (newValue === 'left') {
                this.flexdirection = 'row';
            } else {
                this.flexdirection = 'row-reverse';
            }
        },

        drawerStateEvt(evt) {
            this.showDrawer = evt.detail.isOpen;
            console.info("drawerStateEvt  this.showDrawer= " + this.showDrawer);
            if (this.showDrawer) {
                this.open();
            } else {
                this.close();
            }

        },
        close(type) {
            // 抽屜尚未完全關閉或遮罩禁止點選時不觸發以下邏輯
            if ((type === 'mask' && !this.maskClick) || !this.visibleSync) {
                return;
            }
            console.info("close");
            this.maskstyle = 'uni-mask-closed';
            if (this.mode == "left") {

                this.unidrawerstyle = 'uni-drawer-closed-left';
            } else {
                this.unidrawerstyle = 'uni-drawer-closed-right';
            }

            this._change('showDrawer', 'visibleSync', false)
        },
        open() {
            // 處理重複點選打開的事件
            if (this.visibleSync) {
                return;
            }
            console.info("open this.mode="+this.mode);
            this.maskstyle = 'uni-mask-open';
            if (this.mode == "left") {
                this.unidrawerstyle = 'uni-drawer-open-left';
            } else {
                this.unidrawerstyle = 'uni-drawer-open-right';
            }

            this._change('visibleSync', 'showDrawer', true)
        },
        _change(param1, param2, status) {
            this[param1] = status;
            if (this.watchTimer) {
                clearTimeout(this.watchTimer);
            }
            this.watchTimer = setTimeout(() => {
                this[param2] = status;
                this.$emit('drawerchange', {'showDrawer':status});
            }, status ? 50 : 300)
        },
        dealDrawerSwipe: function(e) {
            console.info("dealDrawerSwipe");
            let direction=e.direction;
                if (this.mode == "left") {
                     if(direction=="left"){
                        this.close();
                     }
                }else{
                   if(direction=="right"){
                         this.close();
                     }
                }
            
            },

    }
</script>

頁面hello.ux:
<import name="drawer" src="../Drawer/drawer.ux"></import>
<template>
  <!-- Only one root node is allowed in template. -->
  <div class="container">
    <div class="title">
      <div class="icon" @click="isOpen">
        <text class="icon-item" for="[1,1,1,1]"></text>
      </div>
      <text class="page-title">模拟drawer元件</text>
    </div>
    <stack style="width: 100%;height:100%;" ontouchstart="touchstart" ontouchend="touchend">
      <div class="content">
        <text style="color: #0faeff;">點選左上角按鈕滑出左側抽屜</text>
        <text class="txt" onclick="switchLocation">切換抽屜滑出位置左或右</text>
        <text style="color: #0faeff;margin-left: 10px;margin-right: 10px">手指在螢幕左側邊緣右滑亦可滑出左側抽屜,手指在螢幕右側邊緣左滑亦可滑出右側抽屜</text>
        <text style="color: #0faeff;margin-top: 20px;margin-left: 10px;margin-right: 10px">滑出抽屜的寬度預設為600px(即最大可設定的寬度,最小可設定寬度為父容器的100px), 如果輸入的值超出500則按最大可設定寬度顯示,小于最小可設定寬時則按最小可設定寬度顯示</text>
        <input id="input" class="input" type="number" placeholder="請輸入寬度值,機關為px" value="{{inputValue}}" onchange="changeValue" />
        <text style="color: #0faeff;">鍵盤收起後,即可滑動或點選呼出抽屜</text>
        <text class="txt" onclick="maxWidth">設定抽屜為最大寬度</text>
        <text class="txt" onclick="minWidth">設定抽屜為最小寬度</text>
      </div>

      <drawer id="drawer" mode="{{drawerShowMode}}" width="{{drawerWidth}}" mask-click="true" @drawerchange="change">
        <tabs class="tabs" style="width: {{drawerWidth}}px;">
          <tab-content class="tabcontent">
            <list class="list">
              <block for="listarray">
                <list-item class="list-item" type="item" onclick="chooseItem($idx)">
                  <text>第{{ $item }}章測試目錄</text>
                </list-item>
              </block>
            </list>
            <text>this is second page</text>
          </tab-content>
          <tab-bar class="tabbar">
            <text class="text">part one</text>
            <text class="text">part two</text>
          </tab-bar>
        </tabs>
      </drawer>
    </stack>
  </div>
</template>

<style>
  .container {
    flex-direction: column;
  }
  /* 自定義内容屬性 */
  .content {
    flex-direction: column;
    justify-content: center;
    align-items: center;
    width: 100%;
    height: 100%;
  }
  .txt {
    width: 80%;
    height: 80px;
    background-color: #0faeff;
    border-radius: 10px;
    text-align: center;
    margin-left: 80px;
    margin-top: 10px;
    margin-bottom: 10px;
  }
  .input {
    width: 80%;
    height: 80px;
    border: 1px solid #000000;
    margin-left: 80px;
  }
  /* 标題屬性 */
  .title {
    height: 120px;
    width: 100%;
    align-items: center;
    background-color: #0faeff;
    padding-left: 20px;
  }
  .page-title {
    font-size: 40px;
    padding-left: 150px;
  }
  .icon {
    width: 60px;
    height: 60px;
    flex-direction: column;
    justify-content: space-around;
  }
  .icon-item {
    height: 4px;
    background-color: rgb(212, 212, 212);
    width: 100%;
  }

  .tabs {
    height: 100%;
    background-color: rgb(248, 230, 230);
  }
  .tabcontent {
    width: 100%;
    height: 90%;
  }
  .tabbar {
    width: 100%;
    height: 10%;
  }
  .text {
    width: 50%;
    height: 100%;
    font-size: 50px;
    text-align: center;
  }
  .list {
    flex: 1;
    width: 100%;
  }
  .list-item {
    height: 90px;
    width: 100%;
    padding: 0px 20px;
    border-bottom: 1px solid #f0f0f0;
    align-items: center;
    justify-content: space-between;
  }
</style>

<script>
  import prompt from '@system.prompt';
  module.exports = {
    data: {
      componentData: {},
      display: false,
      listarray: '',
      drawerWidth: 360,
      inputValue: '',
      drawerShowMode: 'right',
      movestartX: 0
    },

    onInit() {
      this.listarray = this.getList(20);
    },

    isOpen() {
      this.display = !this.display;
      if (this.display) {
        this.showDrawer();
      } else {
        this.closeDrawer();
      }
    },
    // 打開抽屜
    showDrawer(e) {
      this.$broadcast('broaddrawerstate', {
        isOpen: true
      })
    },
    // 關閉抽屜
    closeDrawer(e) {
      this.$broadcast('broaddrawerstate', {
        isOpen: false
      })
    },
    // 抽屜狀态發生變化觸發
    change(e) {
      console.info("change e=" + JSON.stringify(e));
      this.display = e.detail.showDrawer;
    },

    getList(num) {
      let list = []
      for (let i = 1; i <= num; i++) {
        list.push(i)
      }
      return list
    },

    switchLocation() {
      if (this.drawerShowMode === 'left') {
        this.drawerShowMode = 'right';
      } else {
        this.drawerShowMode = 'left';
      }
    },
    changeValue(e) {
      if (e.value >= 600) {
        this.drawerWidth = 600
      } else if (e.value <= 300) {
        this.drawerWidth = 300
      } else {
        this.drawerWidth = e.value
      }
      console.log("hjj", this.drawerWidth);
      if (e.value.length === 3) {
        this.$element('input').focus({ focus: false })
      }
    },
    maxWidth() {
      this.drawerWidth = 600
    },
    minWidth() {
      this.drawerWidth = 300
    },

    chooseItem(index) {
      prompt.showToast({
        message: `該内容為簡單示例,點選了第${index + 1}條`,
      })
    },

    touchstart(e) {
      console.info("touchstart");
      this.movestartX = e.touches[0].offsetX;
    },
    touchend(e) {
      console.info("touch end e:" + JSON.stringify(e));
      let moveEndX = e.changedTouches[0].offsetX;
      if (this.drawerShowMode === "left") {
        //在螢幕左邊緣從左往右邊滑動時,呼出抽屜
        if (this.movestartX < 30) {
          let dis = moveEndX - this.movestartX;
          if (dis > 30) {
            this.showDrawer();
          }
        }

      } else {
        //在螢幕右邊緣從右往左邊滑動時,呼出抽屜
        if (this.movestartX > 720) {
          let dis = moveEndX - this.movestartX;
          if (dis < -30) {
            this.showDrawer();
          }
        }

      }
    },
  }
</script>