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

快应用中实现自定义抽屉组件_快应用_02​​

图2 有抽屉打开、收起style

快应用中实现自定义抽屉组件_快应用_03​​

图3 左抽屉动画

快应用中实现自定义抽屉组件_快应用_04​​

图4  右抽屉动画

快应用中实现自定义抽屉组件_快应用_05​​

2.4遮罩层实现

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

快应用中实现自定义抽屉组件_快应用_06​​

2.5父子组件通信

父组件通过parentVm.$broadcast()向子组件传递抽屉打开、收起的事件,子组件通过$on()监听事件和参数。

子组件通过$watch()方法监听抽屉显示模式mode属性的变化,从而修改css样式,让其在正确的位置显示抽屉。

子组件通过drawerchange事件及参数通知父组件。

2.6手势呼出抽屉

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

3.总结

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


熟悉快应用子组件的设计和属性定义;

熟悉父子组件通信;

熟悉动画样式的实现;

学会如何实现一个遮罩层。


欲了解更多详情,请参见:

华为官网:

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