文档管理中心
指南快应用专题框架自定义组件抽屉组件

抽屉组件

简介

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

  • 抽屉可显示在左边,也可显示在右边
  • 抽屉宽度可定制
  • 抽屉有遮罩层,点击遮罩层可收起抽屉
  • 手势滑动可呼出抽屉

抽屉(drawer)组件结构分为控制器(controls)和抽屉内容(content)两部分。一般来说,controls都是按钮、图标之类的可点击的组件,类似真实抽屉的把手;content是抽屉内部的东西,每个抽屉的content都是不一样的。点击controls可以触发content的展开和收起。布局结构如下图:

布局代码如下:

收起
自动换行
深色代码主题
复制
  1. <div class="page">
  2. <div class="controls">
  3. <image></image>
  4. </div>
  5. <stack class="drawer_container">
  6. <div class="page_content">
  7. </div>
  8. <drawer class="drawer">
  9. <div class="content">
  10. </div>
  11. </drawer >
  12. </stack>
  13. </div>

开发指引

自定义子组件

  1. 定义布局样式。

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

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

    收起
    自动换行
    深色代码主题
    复制
    1. <template>
    2. <div id="drawercontent" style="display:flex;position:absolute;width:100%;height:100%;top:0;left:0;bottom: 0; flex-direction: {{flexdirection}}" >
    3. <div class="{{maskstyle}}" onclick="close('mask')"></div>
    4. <div id="unidrawercontent" class="{{unidrawerstyle}}" style="width:{{drawerWidth}}+'px'}">
    5. <slot></slot>
    6. </div>
    7. </div>
    8. </template>
  2. 规划属性和支持的事件。

    支持的属性:

    展开

    属性

    类型

    默认值

    描述

    mode

    String

    left

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

    width

    Number

    320px

    抽屉的宽度。

    mask

    Boolean

    true

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

    maskClick

    Boolean

    true

    点击遮罩层是否关闭抽屉。

    支持的事件:

    展开

    事件名称

    参数

    描述

    drawerchange

    {showDrawer:booleanValue}

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

实现抽屉展开和收起

  1. 抽屉默认是关闭不显示的,通过“display: none;”来隐藏。
  2. 通过X轴的平移动画控制展开、收起。

    展开时,移到可视区域;收起时,移到屏幕之外。不管是展开还是收起,都是平滑的动画效果,代码如下。

    左抽屉展开、收起动画:

    收起
    自动换行
    深色代码主题
    复制
    1. @keyframes translateX {
    2. from {
    3. transform: translateX(-110px);
    4. }
    5. to {
    6. transform: translateX(0px);
    7. }
    8. }
    9. @keyframes translateXReverse {
    10. from {
    11. transform: translateX(0px);
    12. }
    13. to {
    14. transform: translateX(-750px);
    15. }
    16. }

    右抽屉展开、收起动画:

    收起
    自动换行
    深色代码主题
    复制
    1. @keyframes translateXRight {
    2. from {
    3. transform: translateX(300px);
    4. }
    5. to {
    6. transform: translateX(0px);
    7. }
    8. }
    9. @keyframes translateXRightReverse {
    10. from {
    11. transform: translateX(0px);
    12. }
    13. to {
    14. transform: translateX(750px);
    15. }
    16. }
  3. 通过div的flex-direction属性控制抽屉显示在左侧还是右侧。显示在左侧时,设置为“row”。显示在右侧时,设置为“row-reverse”。
    收起
    自动换行
    深色代码主题
    复制
    1. onInit() {
    2. console.info("drawer oninit");
    3. this.$on('broaddrawerstate', this.drawerStateEvt);
    4. this.drawerWidth = this.width;
    5. if(this.mode=="left"){
    6. this.flexdirection="row";
    7. }else{
    8. this.flexdirection="row-reverse";
    9. }
    10. this.$watch('mode', 'onDrawerModeChange');
    11. },
    本文默认抽屉显示在左侧,左抽屉展开、收起的style代码如下:
    收起
    自动换行
    深色代码主题
    复制
    1. .uni-drawer-open-left {
    2. display: flex;
    3. height: 100%;
    4. animation-name: translateX;
    5. animation-timing-function: linear;
    6. animation-fill-mode: forwards;
    7. animation-duration: 300ms;
    8. }
    9. .uni-drawer-closed-left {
    10. display: flex;
    11. height: 100%;
    12. animation-name: translateXReverse;
    13. animation-timing-function: linear;
    14. animation-fill-mode: forwards;
    15. animation-duration: 600ms;
    16. }

    当设置flex-direction: row-reverse时,抽屉显示在右侧。右抽屉展开、收起的style代码如下:

    收起
    自动换行
    深色代码主题
    复制
    1. .uni-drawer-open-right {
    2. display: flex;
    3. height: 100%;
    4. flex-direction: row-reverse;
    5. animation-name: translateXRight;
    6. animation-timing-function: linear;
    7. animation-fill-mode: forwards;
    8. animation-duration: 300ms;
    9. }
    10. .uni-drawer-closed-right {
    11. display: flex;
    12. height: 100%;
    13. flex-direction: row-reverse;
    14. animation-name: translateXRightReverse;
    15. animation-timing-function: linear;
    16. animation-fill-mode: forwards;
    17. animation-duration: 600ms;
    18. }

实现遮罩层

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

收起
自动换行
深色代码主题
复制
  1. .uni-mask-open {
  2. display: flex;
  3. height: 100%;
  4. width: 100%;
  5. position: absolute;
  6. background-color: rgb(0, 0, 0);
  7. opacity: 0.4;
  8. }
  9. .uni-mask-closed {
  10. height: 100%;
  11. width: 100%;
  12. position: absolute;
  13. background-color: rgb(0, 0, 0);
  14. display: none;
  15. }

父子组件通信

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

手势滑动呼出抽屉

通过监听touchstart和touchend事件,可以实现在屏幕边缘处手势滑动呼出抽屉。

示例代码

抽屉drawer.ux代码:

收起
自动换行
深色代码主题
复制
  1. <template>
  2. <div id="drawercontent" style="display:flex;position:absolute;width:100%;height:100%;top:0;left:0;bottom: 0; flex-direction: {{flexdirection}}" onswipe="dealDrawerSwipe">
  3. <div class="{{maskstyle}}" onclick="close('mask')"></div>
  4. <div id="unidrawercontent" class="{{unidrawerstyle}}" style="width:{{drawerWidth}}+'px'}">
  5. <slot></slot>
  6. </div>
  7. </div>
  8. </template>
  9. <style>
  10. .stack {
  11. flex-direction: column;
  12. height: 100%;
  13. width: 100%;
  14. }
  15. .uni-mask-open {
  16. display: flex;
  17. height: 100%;
  18. width: 100%;
  19. position: absolute;
  20. background-color: rgb(0, 0, 0);
  21. opacity: 0.4;
  22. }
  23. .uni-mask-closed {
  24. height: 100%;
  25. width: 100%;
  26. position: absolute;
  27. background-color: rgb(0, 0, 0);
  28. display: none;
  29. }
  30. .uni-drawer {
  31. display: none;
  32. height: 100%;
  33. }
  34. .uni-drawer-open-left {
  35. display: flex;
  36. height: 100%;
  37. animation-name: translateX;
  38. animation-timing-function: linear;
  39. animation-fill-mode: forwards;
  40. animation-duration: 300ms;
  41. }
  42. .uni-drawer-closed-left {
  43. display: flex;
  44. height: 100%;
  45. animation-name: translateXReverse;
  46. animation-timing-function: linear;
  47. animation-fill-mode: forwards;
  48. animation-duration: 600ms;
  49. }
  50. .uni-drawer-open-right {
  51. display: flex;
  52. height: 100%;
  53. flex-direction: row-reverse;
  54. animation-name: translateXRight;
  55. animation-timing-function: linear;
  56. animation-fill-mode: forwards;
  57. animation-duration: 300ms;
  58. }
  59. .uni-drawer-closed-right {
  60. display: flex;
  61. height: 100%;
  62. flex-direction: row-reverse;
  63. animation-name: translateXRightReverse;
  64. animation-timing-function: linear;
  65. animation-fill-mode: forwards;
  66. animation-duration: 600ms;
  67. }
  68. @keyframes translateX {
  69. from {
  70. transform: translateX(-110px);
  71. }
  72. to {
  73. transform: translateX(0px);
  74. }
  75. }
  76. @keyframes translateXReverse {
  77. from {
  78. transform: translateX(0px);
  79. }
  80. to {
  81. transform: translateX(-750px);
  82. }
  83. }
  84. @keyframes translateXRight {
  85. from {
  86. transform: translateX(300px);
  87. }
  88. to {
  89. transform: translateX(0px);
  90. }
  91. }
  92. @keyframes translateXRightReverse {
  93. from {
  94. transform: translateX(0px);
  95. }
  96. to {
  97. transform: translateX(750px);
  98. }
  99. }
  100. </style>
  101. <script>
  102. module.exports = {
  103. props: {
  104. /**
  105. * 显示模式(左、右),只在初始化生效
  106. */
  107. mode: {
  108. type: String,
  109. default: ''
  110. },
  111. /**
  112. * 蒙层显示状态
  113. */
  114. mask: {
  115. type: Boolean,
  116. default: true
  117. },
  118. /**
  119. * 遮罩是否可点击关闭
  120. */
  121. maskClick: {
  122. type: Boolean,
  123. default: true
  124. },
  125. /**
  126. * 抽屉宽度
  127. */
  128. width: {
  129. type: Number,
  130. default: 320
  131. }
  132. },
  133. data() {
  134. return {
  135. visibleSync: false,
  136. showDrawer: false,
  137. watchTimer: null,
  138. drawerWidth: 600,
  139. maskstyle: 'uni-mask-closed',
  140. unidrawerstyle: 'uni-drawer',
  141. flexdirection: 'row'
  142. }
  143. },
  144. onInit() {
  145. console.info("drawer oninit");
  146. this.$on('broaddrawerstate', this.drawerStateEvt);
  147. this.drawerWidth = this.width;
  148. if(this.mode=="left"){
  149. this.flexdirection="row";
  150. }else{
  151. this.flexdirection="row-reverse";
  152. }
  153. this.$watch('mode', 'onDrawerModeChange');
  154. },
  155. onDrawerModeChange: function (newValue, oldValue) {
  156. console.info("onDrawerModeChange newValue= " + newValue
  157. + ", oldValue=" + oldValue);
  158. if (newValue == 'left') {
  159. this.flexdirection = 'row';
  160. } else {
  161. this.flexdirection = 'row-reverse';
  162. }
  163. },
  164. drawerStateEvt(evt) {
  165. this.showDrawer = evt.detail.isOpen;
  166. console.info("drawerStateEvt this.showDrawer= " + this.showDrawer);
  167. if (this.showDrawer) {
  168. this.open();
  169. } else {
  170. this.close();
  171. }
  172. },
  173. close(type) {
  174. // 抽屉尚未完全关闭或遮罩禁止点击时不触发以下逻辑
  175. if ((type == 'mask' && !this.maskClick) || !this.visibleSync) {
  176. return;
  177. }
  178. console.info("close");
  179. this.maskstyle = 'uni-mask-closed';
  180. if (this.mode == "left") {
  181. this.unidrawerstyle = 'uni-drawer-closed-left';
  182. } else {
  183. this.unidrawerstyle = 'uni-drawer-closed-right';
  184. }
  185. this._change('showDrawer', 'visibleSync', false)
  186. },
  187. open() {
  188. // 处理重复点击展开的事件
  189. if (this.visibleSync) {
  190. return;
  191. }
  192. console.info("open this.mode="+this.mode);
  193. this.maskstyle = 'uni-mask-open';
  194. if (this.mode == "left") {
  195. this.unidrawerstyle = 'uni-drawer-open-left';
  196. } else {
  197. this.unidrawerstyle = 'uni-drawer-open-right';
  198. }
  199. this._change('visibleSync', 'showDrawer', true)
  200. },
  201. _change(param1, param2, status) {
  202. this[param1] = status;
  203. if (this.watchTimer) {
  204. clearTimeout(this.watchTimer);
  205. }
  206. this.watchTimer = setTimeout(() => {
  207. this[param2] = status;
  208. this.$emit('drawerchange', {'showDrawer':status});
  209. }, status ? 50 : 300)
  210. },
  211. dealDrawerSwipe: function(e) {
  212. console.info("dealDrawerSwipe");
  213. let direction=e.direction;
  214. if (this.mode == "left") {
  215. if(direction=="left"){
  216. this.close();
  217. }
  218. }else{
  219. if(direction=="right"){
  220. this.close();
  221. }
  222. }
  223. },
  224. }
  225. </script>

页面hello.ux代码:

收起
自动换行
深色代码主题
复制
  1. <import name="drawer" src="../Drawer/drawer.ux"></import>
  2. <template>
  3. <!-- Only one root node is allowed in template. -->
  4. <div class="container">
  5. <div class="title">
  6. <div class="icon" @click="isOpen">
  7. <text class="icon-item" for="[1,1,1,1]"></text>
  8. </div>
  9. <text class="page-title">drawer组件</text>
  10. </div>
  11. <stack style="width: 100%;height:100%;" ontouchstart="touchstart" ontouchend="touchend">
  12. <div class="content">
  13. <text style="color: #0faeff;">点击左上角按钮滑出左侧抽屉</text>
  14. <text class="txt" onclick="switchLocation">切换抽屉滑出位置左或右</text>
  15. <text style="color: #0faeff;margin-left: 10px;margin-right: 10px">手指在屏幕左侧边缘右滑亦可滑出左侧抽屉,手指在屏幕右侧边缘左滑亦可滑出右侧抽屉</text>
  16. <text style="color: #0faeff;margin-top: 20px;margin-left: 10px;margin-right: 10px">滑出抽屉的宽度默认为320px,如果输入的值超出600则按最大可设置宽度600px显示,小于300则按最小可设置宽度300px显示</text>
  17. <input id="input" class="input" type="number" placeholder="请输入宽度值,单位为px" value="{{inputValue}}" onchange="changeValue" />
  18. <text style="color: #0faeff;">键盘收起后,即可滑动或点击呼出抽屉</text>
  19. <text class="txt" onclick="maxWidth">设置抽屉为最大宽度</text>
  20. <text class="txt" onclick="minWidth">设置抽屉为最小宽度</text>
  21. </div>
  22. <drawer id="drawer" mode="{{drawerShowMode}}" width="{{drawerWidth}}" mask-click="true" @drawerchange="change">
  23. <tabs class="tabs" style="width: {{drawerWidth}}px;">
  24. <tab-content class="tabcontent">
  25. <list class="list">
  26. <block for="listarray">
  27. <list-item class="list-item" type="item" onclick="chooseItem($idx)">
  28. <text>第{{ $item }}章测试目录</text>
  29. </list-item>
  30. </block>
  31. </list>
  32. <text>this is second page</text>
  33. </tab-content>
  34. <tab-bar class="tabbar">
  35. <text class="text">part one</text>
  36. <text class="text">part two</text>
  37. </tab-bar>
  38. </tabs>
  39. </drawer>
  40. </stack>
  41. </div>
  42. </template>
  43. <style>
  44. .container {
  45. flex-direction: column;
  46. }
  47. /* 自定义内容属性 */
  48. .content {
  49. flex-direction: column;
  50. justify-content: center;
  51. align-items: center;
  52. width: 100%;
  53. height: 100%;
  54. }
  55. .txt {
  56. width: 80%;
  57. height: 80px;
  58. background-color: #0faeff;
  59. border-radius: 10px;
  60. text-align: center;
  61. margin-left: 80px;
  62. margin-top: 10px;
  63. margin-bottom: 10px;
  64. }
  65. .input {
  66. width: 80%;
  67. height: 80px;
  68. border: 1px solid #000000;
  69. margin-left: 80px;
  70. }
  71. /* 标题属性 */
  72. .title {
  73. height: 120px;
  74. width: 100%;
  75. align-items: center;
  76. background-color: #0faeff;
  77. padding-left: 20px;
  78. }
  79. .page-title {
  80. font-size: 40px;
  81. padding-left: 150px;
  82. }
  83. .icon {
  84. width: 60px;
  85. height: 60px;
  86. flex-direction: column;
  87. justify-content: space-around;
  88. }
  89. .icon-item {
  90. height: 4px;
  91. background-color: rgb(212, 212, 212);
  92. width: 100%;
  93. }
  94. .tabs {
  95. height: 100%;
  96. background-color: rgb(248, 230, 230);
  97. }
  98. .tabcontent {
  99. width: 100%;
  100. height: 90%;
  101. }
  102. .tabbar {
  103. width: 100%;
  104. height: 10%;
  105. }
  106. .text {
  107. width: 50%;
  108. height: 100%;
  109. font-size: 50px;
  110. text-align: center;
  111. }
  112. .list {
  113. flex: 1;
  114. width: 100%;
  115. }
  116. .list-item {
  117. height: 90px;
  118. width: 100%;
  119. padding: 0px 20px;
  120. border-bottom: 1px solid #f0f0f0;
  121. align-items: center;
  122. justify-content: space-between;
  123. }
  124. </style>
  125. <script>
  126. import prompt from '@system.prompt';
  127. module.exports = {
  128. data: {
  129. componentData: {},
  130. display: false,
  131. listarray: '',
  132. drawerWidth: 360,
  133. inputValue: '',
  134. drawerShowMode: 'right',
  135. movestartX: 0
  136. },
  137. onInit() {
  138. this.listarray = this.getList(20);
  139. },
  140. isOpen() {
  141. this.display = !this.display;
  142. if (this.display) {
  143. this.showDrawer();
  144. } else {
  145. this.closeDrawer();
  146. }
  147. },
  148. // 打开抽屉
  149. showDrawer(e) {
  150. this.$broadcast('broaddrawerstate', {
  151. isOpen: true
  152. })
  153. },
  154. // 关闭抽屉
  155. closeDrawer(e) {
  156. this.$broadcast('broaddrawerstate', {
  157. isOpen: false
  158. })
  159. },
  160. // 抽屉状态发生变化触发
  161. change(e) {
  162. console.info("change e=" + JSON.stringify(e));
  163. this.display = e.detail.showDrawer;
  164. },
  165. getList(num) {
  166. let list = []
  167. for (let i = 1; i <= num; i++) {
  168. list.push(i)
  169. }
  170. return list
  171. },
  172. switchLocation() {
  173. if (this.drawerShowMode == 'left') {
  174. this.drawerShowMode = 'right';
  175. } else {
  176. this.drawerShowMode = 'left';
  177. }
  178. },
  179. changeValue(e) {
  180. if (e.value >= 600) {
  181. this.drawerWidth = 600
  182. } else if (e.value <= 300) {
  183. this.drawerWidth = 300
  184. } else {
  185. this.drawerWidth = e.value
  186. }
  187. console.log("record", this.drawerWidth);
  188. if (e.value.length === 3) {
  189. this.$element('input').focus({ focus: false })
  190. }
  191. },
  192. maxWidth() {
  193. this.drawerWidth = 600
  194. },
  195. minWidth() {
  196. this.drawerWidth = 300
  197. },
  198. chooseItem(index) {
  199. prompt.showToast({
  200. message: `该内容为简单示例,点击了第${index + 1}条`,
  201. })
  202. },
  203. touchstart(e) {
  204. console.info("touchstart");
  205. this.movestartX = e.touches[0].offsetX;
  206. },
  207. touchend(e) {
  208. console.info("touch end e:" + JSON.stringify(e));
  209. let moveEndX = e.changedTouches[0].offsetX;
  210. if (this.drawerShowMode == "left") {
  211. //在屏幕左边缘从左往右边滑动时,呼出抽屉
  212. if (this.movestartX < 30) {
  213. let dis = moveEndX - this.movestartX;
  214. if (dis > 30) {
  215. this.showDrawer();
  216. }
  217. }
  218. } else {
  219. //在屏幕右边缘从右往左边滑动时,呼出抽屉
  220. if (this.movestartX > 720) {
  221. let dis = moveEndX - this.movestartX;
  222. if (dis < -30) {
  223. this.showDrawer();
  224. }
  225. }
  226. }
  227. },
  228. }
  229. </script>
在 指南 中进行搜索
请输入您想要搜索的关键词