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>