最近有一个需求,要实现一个通过拖拽来改变区域的宽度。
PS: 文尾有 GitHub 地址,喜欢的小伙伴可以点个赞哦😉
详细描述:
效果图如下:在两个区域中间,有一个拖拽的区域,当鼠标按下,左右拖动时出现一条线,当鼠标松开时,改变区域的宽度。
Demo
在线实例地址:https://georgeleoo.github.io/resize-layout/
思考
看到这个需求,感觉有点复杂,但是实际上很简单。我们只需把这个需求进行拆分即可简单化。
通过观察,我们可以将这个需求拆分成3个部分:
- 布局
- 拖动拖拽线
- 松开鼠标改变区域的宽度
需求1: 布局
对于这个布局来说很简单,有两个div,这两个div中间有一个拖拽区域和一根拖拽线。
图中红色为拖拽线,蓝色区域是拖拽区域,只有线是可拖拽的如果我们就直接写布局代码,很简单,随随便便的就能写好。但是这样做的话,通用性不高,我希望的是对任意的布局都有拖拽改变区域宽度的功能。
我们要把这个功能做成一个通用组件,对任意的布局都可以进行拖拽,该如何去编辑组件呢?
我们先从使用方法开始去逆推组件的写法,我们可以把这个功能抽成组件。左右区域是一个组件,每个组件有两个区域,即内容区域和拖拽区域,然后将这两个组件放到一个大布局组件中进行子组件的限定。使用方法大概如下:
<ResizeLayout>
<ResizeCol>左侧</ResizeCol>
<ResizeCol>右侧</ResizeCol>
</ResizeLayout>
由此,我们可以知道,ResizeLayout
是一个区域布局,在这个布局中,有两个子组件 ResizeCol
,分别表示是左侧内容和右侧内容。
有时候我们需要给左侧设置一个默认的宽度,右侧自动分配剩下的内容,那么就需要给 ResizeCol
添加一个 width
属性即可。
<ResizeLayout>
<ResizeCol width="220px">左侧</ResizeCol>
<ResizeCol>右侧</ResizeCol>
</ResizeLayout>
如何去自动平分剩下的宽度?
首先我们得遍历子组件ResizeCol
,然后读取其自身上的 width
属性,并将其放入一个 list
中,如果某一组件未设置宽度,也要将数据保存到 list
中,只是 width
属性为空。这样才能计算出有多少个未设置宽度的子组件的数量。ResizeCol
的数量:我们可以通过this.$slots.default
来获取,然后再查找this.$slots.default
中componentOptions.tag
是ResizeCol
类型组件。
平均宽度的计算会使用如下简单的数学公示:
其中
- AW:剩下未设置宽度的
ResizeCol
的平均宽度 - W:
ResizeLayout
的总宽度 - HW:已设置宽度的
ResizeCol
的总宽度 - NWC:剩下未设置宽度的
ResizeCol
的数量
需求2: 拖动拖拽线
我们先思考一下,要实现拖动这个功能需要哪些步骤?
要拖动拖拽线,我们必须把鼠标移动到可拖动的区域,而拖动的区域的拖拽线是定位在这个区域的右侧;接着按下鼠标左键,这个时候记录一下鼠标按下的位置startClientX
,然后移动鼠标,记录moveClientX
的值;通过观察,要改变这根拖拽线的位置,我们只需改变它的right
值即可,这个right值其实就是startClientX - moveClientX
,当right > 0
时,表示向左拖动,当right < 0
时,表示向右侧拖动。
既然要操作鼠标,那么我们就需要监听鼠标事件,分别是mousedown
和 mousemove
事件来分别监听鼠标按下和移动事件。
<div class="resize-col-line" @mousedown="handlerMouseDown" ></div>
/**
* @description 鼠标按下时
*/
handlerMouseDown (e) {
this.isMouseDown = true
this.startClientX = e.clientX
this.addEventListener()
},
/**
* @description 设置监听事件
*/
addEventListener () {
document.addEventListener('mousemove', this.handlerMouseMove)
// document.addEventListener('mouseup', this.handlerMouseUp)
},
/**
* @description 鼠标移动时
*/
handlerMouseMove (e) {
this.moveClientX = e.clientX
this.setResizeBarLineStyle()
},
/**
* @description 设置 ResizeBarLine 的样式
*/
setResizeBarLineStyle () {
// 计算偏移量:鼠标按下的点到鼠标移动的点的偏移量
let offset = this.startClientX - this.moveClientX
// 最小宽度
let minWidth = parseInt(this.minWidth)
if (minWidth < 0) {
console.warn('minWidth 不得小于0')
minWidth = 0
}
// 当 offset 大于 0 时,表示向左拖动,小于 0 表示向右拖动
if (offset >= 0) {
// 当前元素的宽度
const currentWidth = parseInt(
this.ResizeLayout.colWidthList[this.index].width
)
// 若 当前元素的宽度 - 偏移量 < 最小宽度 时,则最终的偏移量就是 当前元素的宽度 - 最小宽度
if (currentWidth - offset < minWidth) {
offset = currentWidth - minWidth
}
} else {
// 与当前元素相邻的元素的宽度
const nextWidth = parseInt(
this.ResizeLayout.colWidthList[this.index + 1].width
)
// 若 当前元素的宽度 - 偏移量 < 最小宽度 时,则最终的偏移量就是 当前元素的宽度 - 最小宽度
if (nextWidth - Math.abs(offset) < minWidth) {
offset = -(nextWidth - minWidth)
}
}
this.resizeBarLineStyle = {
right: offset + Math.ceil(this._resizeBarThick / 2) + 'px',
borderLeftColor: this.borderLeftColor
}
},
需求3: 松开鼠标改变区域的宽度
要实现拖动这个功能又需要哪些步骤?
假设我们从图中的起始位置移动到绿色区域的位置,绿色区域的右边框的左侧区域就是鼠标松开后的最终的左侧区域宽度,绿色区域的右边框的右侧区域就是鼠标松开后的最终的右侧区域宽度。
松开鼠标后,由图可知:
最终的左侧区域宽度
=
原左侧ResizeCol
的宽度 -right
+ 绿色区域的一半。
最终的右侧区域宽度=
原右侧ResizeCol
的宽度 -right
- 绿色区域的一半。
绿色区域一半的原因是拖拽线始终在拖拽区域的中间
如果右侧有多列的话,只会改变与拖拽线相邻的那列。
setWidth() {
// 当前元素的宽度
const currentWidth = this.ResizeLayout.colWidthList[this.index].width;
// 与当前元素相邻的元素的宽度
const nextWidth = this.ResizeLayout.colWidthList[this.index + 1].width;
// 当前元素最终的宽度
let finalCurrentWidth =
parseInt(currentWidth) -
parseInt(this.resizeBarLineStyle.right) +
Math.ceil(this.resizeBarWidth / 2);
// 与当前元素相邻的元素最终的宽度
let finalNextWidth =
parseInt(nextWidth) +
parseInt(this.resizeBarLineStyle.right) -
Math.ceil(this.resizeBarWidth / 2);
this.$set(
this.ResizeLayout.colWidthList[this.index],
"width",
finalCurrentWidth + "px"
);
this.$set(
this.ResizeLayout.colWidthList[this.index + 1],
"width",
finalNextWidth + "px"
);
},
这样我们就有个大概的思路了,完整代码可参考github,基本每一步都有注释,需要的小伙伴可以star
一下。
最后在看一个利用ResizeLayout
仿 VSCODE UI
的效果图
Github地址:https://github.com/GeorgeLeoo/resize-layout.git