1.  什么是抽屉组件

抽屉组件是一种特殊的弹出面板,可以模拟手机App中推入拉出抽屉的效果。抽屉一般具有如下特点:

  1. 抽屉可显示在左边,也可以显示在右边;
  2. 抽屉宽度可定制;
  3. 抽屉有遮罩层,点击遮罩层可收起抽屉;
  4. 手势滑动可呼出抽屉;

       抽屉(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>

android 抽屉 控件 应用抽屉设置_android 抽屉 控件

2.实现步骤

    抽屉组件属于一种扩展能力,当前快应用已有的组件是无法满足的,需要自定义组件实现。

2.1自定义子组件

      抽屉外观都是通用的,但是抽屉内部格局content不一样,在设计的时候,不能直接写死content布局,否则一旦content部分的UI有变化,会导致子组件也要修改,违背了代码设计中的“开闭”原则。

     所以,我们子组件drawer.ux中,使用了slot 组件来承载父组件中定义的content,由使用drawer组件的页面来完成content布局,如下图所示:

android 抽屉 控件 应用抽屉设置_sed_02


2.2子组件设计

支持的属性如下:

属性

类型

默认值

描述

mode

String

left

设置抽屉的显示位置,支持left和right

mask

boolean

true

抽屉展开时是否显示遮罩层

maskClick

Boolean

true

点击遮罩层是否关闭抽屉

width

Number

320px

抽屉宽度

支持的事件:

事件名称

参数

描述

drawerchange

{showDrawer:booleanValue}

抽屉收起、展开的回调事件

2.3抽屉展开和收起

  1. 抽屉默认是关闭不显示的,通过“display: none;” 来隐藏。
  2. 收起、展开通过X轴的平移动画控制,收起时,移到屏幕之外,展开时平移到可视区域。不管是展开还是收起,都是平滑的动画效果。
  3. 抽屉显示在左侧还是右侧,是通过div的flex-direction控制的,显示在左侧时,设置row,显示在右侧时,设置为row-reverse。

android 抽屉 控件 应用抽屉设置_遮罩层_03


android 抽屉 控件 应用抽屉设置_快应用_04


图1 左抽屉打开、收起style

   图2 有抽屉打开、收起style

android 抽屉 控件 应用抽屉设置_快应用_05


android 抽屉 控件 应用抽屉设置_sed_06


  图3 左抽屉动画

   图4  右抽屉动画

2.4遮罩层实现

遮罩层初始状态不显示,通过“display: none;” 来隐藏。抽屉展示时,显示遮罩层,收起时,不显示,遮罩层使用透明度实现。

android 抽屉 控件 应用抽屉设置_android 抽屉 控件_07


2.5父子组件通信

  1. 父组件通过parentVm.$broadcast()向子组件传递抽屉打开、收起的事件,子组件通过$on()监听事件和参数。
  2. 子组件通过$watch()方法监听抽屉显示模式mode属性的变化,从而修改css样式,让其在正确的位置显示抽屉。
  3. 子组件通过drawerchange事件及参数通知父组件。

2.6手势呼出抽屉

 在抽屉处手势滑动,呼出抽屉,需要监听touchstart和touchend事件。注意滑动范围,只有在抽屉边缘处呼出抽屉,其其他位置不呼出。

3.总结

实现抽屉组件,您可以从中学会如下知识点:

  1. 熟悉快应用子组件的设计和属性定义;
  2. 熟悉父子组件通信;
  3. 熟悉动画样式的实现;
  4. 学会如何实现一个遮罩层。

欲了解更多详情,请参见:
华为官网:
https://developer.huawei.com/consumer/cn/forum/topic/0202636422958390131?fid=18?ha_source=zzh

最后附上完整的实现代码:

抽屉drawer.ux

<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>

android 抽屉 控件 应用抽屉设置_android 抽屉 控件