以下这段都是废话,请跳过
公司移动端开发平台进行了大变革,前端架构由DCloud大生态转换为VUE,所以移动端的UI组件库从MUI改为使用MintUI,然后开始大刀阔斧的把MintUI组件改成MUI组件的样子,然后发现少了几个较为常用的,其中一个就是,嗯,侧滑面板(也叫侧滑菜单,也叫抽屉面板-andriod官方是这么翻译的,很形象)。但是,它就是一个布局组件,具体里边菜单什么的,那都是浮云(嗯,就是爱用几年前的流行词汇,而且很喜欢在网上冲浪和踩别人的空间)
以上这段都是废话,感谢阅读
需求
- 开发一个侧滑面板(类似QQ、网易邮箱等app的)
- 可以在左边,也可以在右边
- 侧滑面板内容随意定制
- 侧滑面板相对的就有主面板,那么衍生出不同的体位和姿势
(1)主面板滑动,侧滑面板不动
(2)侧滑面板动,主面板不动
(3)它俩一块动,一起——– - 代码风格尽量和MintUI的其他组件风格类似(这个挺重要的)
参考
mintUI组件中同样?️滑动操作的tabContainer,为了满足需求5,我连函数名都抄了过来。
不说废话,上代码吧
The Waaaaaaaay
1. 设计组件结构
这个组件分为两部分,一部分为侧滑面板容器,另一部分为主面板容器,然后具体容器内部直接放了插槽,然后还需要一个主面板容器的遮罩,为了侧滑面板打开的时候显现出来。上代码了
<div class="mint-drawer-layout">
<!--侧滑栏-->
<div
ref="drawer"
class=" mint-drawer-warp"
@touchstart.stop="startDrag"
@touchmove.stop="onDrag"
@touchend.stop="endDrag"
:style="drawerStyle">
<slot name="drawer"></slot>
</div>
<!--主容器-->
<div
ref="content"
@touchstart.stop="startDrag"
@touchmove.stop="onDrag"
@touchend.stop="endDrag"
class=" mint-content-warp"
:style="contentStyle">
<!--主容器遮罩(侧滑打开状态下显示)-->
<div class="content-mask" v-tap="toggle" ref="contentMask"></div>
<slot name="content"></slot>
</div>
</div>
2. 配置设计
这块加了一些我们公司的一些需求,可能各位哥哥姐姐门用不到里边的一些props的设计,仅供参考
props: {
// 侧滑面板的宽度(单位px)
'drawerWidth': {
type: Number,
default: 200
},
// 是否可用
'enable': {
type: Boolean,
default: true
},
// 侧滑菜单是否在右边,默认为false,在左边
'isRight': {
type: Boolean,
default: false
},
// 侧滑菜单滑动操作类型
// ['fixDrawer'——固定侧滑面板,主面板滑动]
// ['fixContent'——固定主面板,侧滑面板滑动]
// ['noFixed'——一起滑动!]
'swipeType': {
type: String,
default: 'fixDrawer'
},
// 点击出现侧滑菜单的按钮的id ( @TODO 这里如何处理异步渲染的问题 )
'btnId': {
type: String,
default: ''
},
// 状态位,侧滑面板是否为打开状态
// (因为我们公司有这种一开始就把侧滑菜单打开的shabee场景,所以这才会有这么个东西)
//(如果这个不希望配置的话、可以放在data里边)
'isDrawerOpened': {
type: Boolean,
default: false
},
// 是否可滑动,如果不可滑动的话,就只能通过调用toogle方法打开侧滑面板
// 这个也是公司的一个使用场景,就是你甭滑,找个按钮触发一下侧滑面板打开的方法才能打开
//(如果这个不希望配置的话、也可以放在data里边)
'swipeable': {
type: Boolean,
default: true
}
}
2. 样式设计
不得不承认,我的css写的shit
对于整个的组件来说,它应该是默认充满整个父容器的,而且这个组件,我觉得,一般都是用来放在最外层的一个布局组件,所以,默认充满窗口就行了
所以组件的最外层来一个绝对布局,然后如下:
.mint-drawer-layout {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
overflow-x: hidden;
}
然后侧滑面板,只需要纵向充满就可以了,宽度是配置
.mint-drawer-warp {
position: absolute;
top: 0;
bottom: 0;
}
然后是主面板,没说的
.mint-content-warp {
position: relative;
width: 100%;
height: 100%;
}
4. 关键实现
终于来到了介绍到底该怎么实现的了
滑动处理三步走,start、drag、end
首先第一步上来就要设置一个记录滑动操作的状态变量——dragging,设置为false,方便屏蔽滑动时触发的其他操作的执行
——开始的时候记录开始滑动的位置
——滑动中,dragging状态记录为true,开始进行滑动位置和手指移动的联动
——滑动结束,dragging状态记录为false,计算当前的滑动位置,判断是划开侧滑面板还是关闭,并进行动画处理
其中几个细节小谈一哈
(1)左右滑动操作触发的判断:我这边是公司的规范,横轴移动位移大于五,竖轴位移不大于横轴的1.73倍就可以
(2)最后结束时判断侧滑面板的打开和关闭:是这样的,我这边取的是三分之一的侧滑面板的宽度,也就是从打开到关闭,那么像关闭的方向滑动侧滑面板宽度的三分之一就可以了,如果是关闭到打开,往打开的方向滑动三分之一就可以了
(3)左侧和右侧,还有三种不同的滑动方式:三种不同的滑动方式实际上就是控制到底哪个面板随着手指动,具体的动作过程和面板的偏移量实际上是一样的。左右两侧就更简单了,直接是对称的操作就可
滑动结束的操作,参考的tabcontainer,也挺巧妙的,各位请上眼~
/**
* 滑动结束的动画
*/
swipeLeaveTransition() {
let g = this, currentMovingDoms = [];
let {swipeType, drawerWidth} = g;
switch (swipeType) {
case 'fixDrawer':
currentMovingDoms.push(g.content);
break;
case 'fixContent':
currentMovingDoms.push(g.drawer);
break;
case 'noFixed':
currentMovingDoms.push(g.drawer);
currentMovingDoms.push(g.content);
break;
default:
break;
}
currentMovingDoms.forEach((val) => {
val.classList.add('swipe-transition');
});
setTimeout(() => {
if (g.isDO) {
this.swipeMove(drawerWidth);
} else {
this.swipeMove(0);
g.contentMask.style.opacity = 0;
g.contentMask.style.display = 'none';
}
g.isToggle = false;
currentMovingDoms.forEach((val) => {
once(val, 'webkitTransitionEnd', _ => {
val.classList.remove('swipe-transition');
g.swiping = false;
});
});
}, 0);
},
/**
* 滑动操作
* @param offset 滑动位置
*/
swipeMove(offset) {
let g = this;
let {swipeType, isRight} = g;
g.contentMask.style.display = 'block';
g.contentMask.style.opacity = Math.abs(offset) / g.drawerWidth * 0.4;
switch (swipeType) {
case 'fixDrawer':
g.content.style.webkitTransform = `translate3d(${(!isRight ? '' : '-') + offset}px, 0, 0)`;
g.swiping = true;
break;
case 'fixContent':
g.drawer.style.webkitTransform = `translate3d(${(!isRight ? '' : '-') + offset}px, 0, 0)`;
g.swiping = true;
break;
case 'noFixed':
g.content.style.webkitTransform = `translate3d(${(!isRight ? '' : '-') + offset}px, 0, 0)`;
g.drawer.style.webkitTransform = `translate3d(${(!isRight ? '' : '-') + offset}px, 0, 0)`;
g.swiping = true;
break;
default:
break;
}
},
// 开始滑动
startDrag(evt) {
let g = this;
if (!g.enable || !g.swipeable) return false;
evt = evt.changedTouches ? evt.changedTouches[0] : evt;
g.start.x = evt.pageX;
g.start.y = evt.pageY;
},
// 滑动中
onDrag(evt) {
let g = this, swiping;
if (!g.enable || !g.swipeable) return false;
g.dragging = true;
const e = evt.changedTouches ? evt.changedTouches[0] : evt;
const offsetTop = e.pageY - g.start.y;
const offsetLeft = e.pageX - g.start.x;
const y = Math.abs(offsetTop);
const x = Math.abs(offsetLeft);
swiping = !(x < 5 || (x >= 5 && y >= x * 1.73));
if (!swiping) return;
evt.preventDefault();
let offset;
if (g.isDO) {
offset = g.isRight ? (g.drawerWidth - offsetLeft) : (g.drawerWidth - (-offsetLeft));
} else {
offset = g.isRight ? -offsetLeft : offsetLeft;
}
if (offset < 0 || offset > g.drawerWidth) {
g.swiping = false;
return;
}
g.offset = offset;
g.swipeMove(offset);
},
// 结束滑动
endDrag() {
let g = this;
if (!g.enable || g.isToggle || !g.dragging) {
return false;
}
const tempWidth = g.drawerWidth / 3;
if (g.isDO && g.offset < tempWidth * 2) {
g.isDO = false;
} else if (!g.isDO && g.offset > tempWidth) {
g.isDO = true;
}
g.dragging = false;
g.swipeLeaveTransition();
}
好啦,到这应该就差不多了。。。
里边涉及到的v-tap指令是自定义的指令,为了处理移动端的点击操作,我还整理了一片陋文:https://segmentfault.com/a/11… (移动点击长按滑动vue指令)
然后这个组件的源码我放在了我fork出来的mintUI项目上
https://github.com/LylaYuKako…
谢谢各位品尝,