forked from ascoders/weekly
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
363 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,361 @@ | ||
磁贴布局三部曲:功能分析、实现分析、性能优化的第一部 - 功能分析。 | ||
|
||
因为需要做自由布局与磁贴布局混排,以及磁贴布局嵌套,所以要实现一套磁贴分析功能,所以本系列不是简单的介绍使用 [react-grid-layout](https://github.com/react-grid-layout/react-grid-layout) 这个库就行了,而是深入分析磁贴布局的特性,以及重头实现一遍。 | ||
|
||
对磁贴布局不熟悉的话,[react-grid-layout](https://github.com/react-grid-layout/react-grid-layout) 也是个很好的 Demo 体验页,大家可以先体验一下再阅读文章。 | ||
|
||
## 精读 | ||
|
||
### 简单碰撞 | ||
|
||
磁贴布局最重要的就是碰撞了,用过 Demo 就会发现,**磁贴左右不会碰撞,只有上下会产生碰撞**,这是因为网页天然是从上而下阅读的,因此垂直碰撞比水平碰撞更自然。 | ||
|
||
<img width=400 src="https://user-images.githubusercontent.com/7970947/205471135-68b8054f-222a-495a-a74c-0d3421946a17.gif"> | ||
|
||
那么垂直的碰撞方向是什么样的呢?实际上**只有自上而下的碰撞,没有自下而上的碰撞** | ||
|
||
为了讲清楚这个原理,先看下面的例子: | ||
|
||
```text | ||
[-----] [-----] | ||
| A | → | B | | ||
[-----] [-----] | ||
``` | ||
|
||
如上所示,将方块 A 移动到方块 B 的位置,如果此时 A 的 Y 轴位置小于等于 B,则会将 B 挤下去。结果如下所示: | ||
|
||
```text | ||
[-----] | ||
| A | | ||
[-----] | ||
[-----] | ||
| B | | ||
[-----] | ||
``` | ||
|
||
如果 A 的 Y 轴位置比 B 大,则碰撞结果是 A 跑到了 B 的下面: | ||
|
||
```text | ||
[-----] | ||
[-----] | B | | ||
| A | → [-----] | ||
[-----] | ||
``` | ||
|
||
结果如下所示: | ||
|
||
```text | ||
[-----] | ||
| B | | ||
[-----] | ||
[-----] | ||
| A | | ||
[-----] | ||
``` | ||
|
||
如果 A 挤到 B 和 C 中间会如何呢?见下图: | ||
|
||
```text | ||
[-----] | ||
[-----] | B | | ||
| A | → [-----] | ||
[-----] [-----] | ||
[ C ] | ||
[-----] | ||
``` | ||
|
||
很容易想到,A 会落到 B 与 C 的中间位置。那问题来了,实现的时候,当时 A 放到 B 的下方,还是认为 A 放到 C 的上方? | ||
|
||
乍一看会觉得,这不一样吗?对这个例子来说是的,但对其他例子就不同了。实际上**应该始终认为是 A 放到了 B 的下方**。原因的话,我们举一个反例就行,假设认为 A 放到了 C 的上方,那么看下面的例子: | ||
|
||
```text | ||
[-----] | ||
[-----] | B | | ||
| A | → [-----] | ||
[-----] [ X ] | ||
[-----] | ||
[ C ] | ||
[-----] | ||
``` | ||
|
||
如上图所示,B 和 C 中间夹了一个狭长的 X,此时 A 拖入 B 和 C 的中间,并未与 X 产生碰撞,那结果一定是 A 落在了 B 的下方。如果落在 C 的上方,A 就悬空了。 | ||
|
||
所以磁贴布局模式下,组件始终只能落在另一个组件下面,除了 Y 轴为 0 的情况下,可以定到组件上方。 | ||
|
||
### 连续碰撞 | ||
|
||
连续碰撞是指当磁贴布局产生碰撞而导致位置变化后,需要重新调整整体位置,或者继续与其他组件位置产生碰撞的情况,首先看下面这个简单例子: | ||
|
||
```text | ||
[-----] | ||
| A | | ||
[-----] | ||
↓ | ||
[-----] | ||
| B | | ||
[-----] | ||
[-----] | ||
| C | | ||
[-----] | ||
``` | ||
|
||
如果把 A 拖动到 B 位置,遵循简单碰撞原理,必须 Y 轴高于 B 的 Y 轴才会置于 B 下方,此时会把 C 顶上去。但仅做到这一步,A 原来的位置会产生空缺,需要重新吸附到顶部,这就是连续碰撞: | ||
|
||
```text | ||
[ ] [-----] | ||
|Empty| | B | | ||
[ ] [-----] | ||
[-----] [-----] | ||
| B | [ A ] | ||
[-----] → Remove Empty [-----] | ||
[-----] [-----] | ||
| A | [ C ] | ||
[-----] [-----] | ||
[-----] | ||
| C | | ||
[-----] | ||
``` | ||
|
||
这时候你可能会想,结果不就是 B 和 A 交换了位置嘛,实际上用 [react-grid-layout](https://github.com/react-grid-layout/react-grid-layout) 看起来效果也是如此,那么代码实现时是不是不用这么麻烦?直接判断 A 与 B 是否产生位置交换,如果交换了,按照交换的方式处理不就行了吗? | ||
|
||
听上去很美好,因为按照 A 与 B 交换的思路处理效果一致,而且性能更优,因为不用重新计算 C 组件被挤走,然后 A、B、C 再重新挤上去。但实际上交换方案是不可行的,我们看下面的例子: | ||
|
||
```text | ||
[-----] | ||
| A | | ||
[-----] | ||
↓ | ||
[-------------] | ||
| B | | ||
[-------------] | ||
[-----] | ||
| C | | ||
[-----] | ||
``` | ||
|
||
如果把 A 和 B 位置交换,会发现 C 悬空了。之所以上面的例子可以用交换思路,是因为 A 与 B 交换后,A 还可以 “挡住” C 的上移。但这个例子因为 B 很长,但 A 很短,A、B 交换后,A 就挡不住 C 的上移了: | ||
|
||
```text | ||
[-------------] | ||
| B | | ||
[-------------] | ||
[-----] [-----] | ||
| C | | A | | ||
[-----] [-----] | ||
``` | ||
|
||
所以为了保证任何时候位移都不会产生 BUG,需要老老实实的分两步来判断:1. 判断 A 移到 B 的底部。2. 新的 A 把下面组件挤走,同时如果上面还有空位置,需要整体向上位移。 | ||
|
||
看起来还是比较消耗性能的,但通过一些优化手段是可以极大减少计算量的,我们到系列的 “性能优化” 部分再说。 | ||
|
||
### 碰撞边界 case | ||
|
||
我们再考虑两个极端情况,第一种是要碰撞的组件过于矮的时候,第二种是要碰撞的组件过高的时候。 | ||
|
||
首先是过矮的情况,我们看下面 5 种情况: | ||
|
||
```text | ||
[-----] | ||
| | ← [ A ] | ||
| B | | ||
| | | ||
[-----] | ||
[-----] | ||
| | | ||
| C | | ||
| | | ||
[-----] | ||
``` | ||
|
||
上面的情况插入到 B 的上方(假设 B 上方没有元素了,如果有的话,假设 B 上方为 X,那么应该认为 A 插入到 X 的底部)。 | ||
|
||
```text | ||
[-----] | ||
| | | ||
| B | | ||
| | ← [ A ] | ||
[-----] | ||
[-----] | ||
| | | ||
| C | | ||
| | | ||
[-----] | ||
``` | ||
|
||
上面的情况插入到 B 的下方。 | ||
|
||
```text | ||
[-----] | ||
| | | ||
| B | | ||
| | | ||
[-----] | ||
[-----] | ||
| | ← [ A ] | ||
| C | | ||
| | | ||
[-----] | ||
``` | ||
|
||
上面的情况插入到 B 的下方。 | ||
|
||
```text | ||
[-----] | ||
| | | ||
| B | | ||
| | | ||
[-----] | ||
[-----] | ||
| | | ||
| C | | ||
| | ← [ A ] | ||
[-----] | ||
``` | ||
|
||
上面的情况插入到 C 的下方。 | ||
|
||
```text | ||
[-----] | ||
| | | ||
| B | | ||
| | [---] | ||
[-----] ← [ A ] | ||
[-----] [---] | ||
| | | ||
| C | | ||
| | | ||
[-----] | ||
``` | ||
|
||
上面的情况和简单碰撞里提到的例子一样,碰撞位置在 B 与 C 之间,还是会认为插入到 B 的下方。 | ||
|
||
总结一下,过矮的情况下很多时候拖动组件只会与一个组件产生碰撞,当拖拽中心点在碰撞组件中心点上方时,插入到碰撞组件上方的组件下面(如果上方没有组件则插入到顶部)。当然插入到上方组件下面也不是真的找到上方组件是什么,具体如何做我们等到【实现分析】篇再讲。反之,如果中心点相对在下方,就插入到碰撞组件的下方。如果同时碰撞了多个组件,则忽略中心点偏移量靠上的碰撞,仅考虑中心点偏移量靠下的碰撞。 | ||
|
||
关于中心点上方其实也可以进一步优化,比如当目标碰撞组件太长的时候,可能比较难移到下方,此时在还没有拖拽到中心点下方时就要做下方碰撞判定了,此时判断依据可以优化为:碰撞时,拖拽组件的 Y 只要比目标组件的 Y 大(或者再加一个常数阈值,该阈值由拖拽组件高度决定,比如是高度的 1/3),那么就认为拖入到目标组件底部,比如: | ||
|
||
```text | ||
[-----] | ||
| | [---] | ||
| | ← [ A ] | ||
| B | [---] | ||
| | | ||
| | | ||
[-----] | ||
``` | ||
|
||
如上图所示,虽然 A 的中心点在 B 中心点上方,但因为 `A.y - B.y > A.height / 3`,所以判定插入到 B 的下方。当然这也会导致拖入超高组件上方很困难,所以要不要这么设定看用户喜好。 | ||
|
||
再看组件过高的情况: | ||
|
||
```text | ||
[---] | ||
[-----] [ ] | ||
| B | ← [ A ] | ||
[-----] [ ] | ||
[-----] [---] | ||
| C | | ||
[-----] | ||
``` | ||
|
||
上面的情况插入 B 的上方(如果 B 上面还有组件 X,则判定为插入该 X 下方)。 | ||
|
||
```text | ||
[-----] [---] | ||
| B | [ ] | ||
[-----] ← [ A ] | ||
[-----] [ ] | ||
| C | [---] | ||
[-----] | ||
``` | ||
|
||
上面的情况插入 B 的下方。 | ||
|
||
```text | ||
[-----] | ||
| B | | ||
[-----] | ||
[-----] [---] | ||
| C | [ ] | ||
[-----] ← [ A ] | ||
[ ] | ||
[---] | ||
``` | ||
|
||
上面的情况插入 C 的下方。 | ||
|
||
总结一下,当拖拽组件过高时,还是维持中心点判断规则,但更可能同时碰撞到多个组件,此时沿用 “中心点偏移量靠上的碰撞” 的原则就行了。但这里有一个较大的区别,拖拽组件矮的时候最多同时和两个组件碰撞,但拖拽组件高的时候,可能同时和 N 个组件碰撞,如下图所示: | ||
|
||
```text | ||
[-----] [---] | ||
| B | [ ] | ||
[-----] [ ] | ||
[-----] [ ] | ||
| C | [ ] | ||
[-----] ← [ A ] | ||
[-----] [ ] | ||
| D | [ ] | ||
[-----] [ ] | ||
[-----] [ ] | ||
| E | [---] | ||
[-----] | ||
``` | ||
|
||
此时就要看和哪个组件碰撞的优先级最高了。我们单从 B、C、D、E 的角度看,A 分别应该放在 B 下方、C 下方、D 上方、E 上方,其中 B 下方与 C 上方是同一个位置,但与 D 上方、E 上方都不是同一个位置,此时就要看拖拽到哪个位置产生的位移最小了,因为最小的位移是最不突兀的,最符合用户的预期。 | ||
|
||
另一个边界情况就是拖拽组件过高时,如果中心点还未移动到下方,但高度却超出了下面组件下方,也要视为拖拽到下方: | ||
|
||
```text | ||
[-----] | ||
| | | ||
| | | ||
| | | ||
| A | | ||
| | | ||
| | | ||
| | | ||
[-----] | ||
↓ | ||
[-----] | ||
| B | | ||
[-----] | ||
``` | ||
|
||
如上图所示,A 非常高,B 很矮,**当 A 往下移动时,可能 A 的底部都超出 B 底部了(可以优化为 B 的中间),但 A 的中心点仍然在 B 中心点上方**,此时在用户已经认为可以交换位置了,所以判断是否移动到下方多了一个**优先**判断条件:拖拽组件底部超出目标组件底部。同理拖拽到上方也类似。 | ||
|
||
要注意的是,这个例子与下面的例子表现并不一致,下面的例子 A 向左移时,应该放置 B 的上方,而上面的例子却放置 B 的下方: | ||
|
||
```text | ||
[-----] | ||
| | | ||
| | | ||
| | | ||
← | A | | ||
[-----] | | | ||
| B | | | | ||
[-----] | | | ||
[-----] | ||
``` | ||
|
||
发现了吗?单从垂直位置来看,都是 A 的底部超过了 B 底部,但有时候和 B 互换,有时候却不互换。区分方法就是该碰撞发生时,这两个区块是否已经发生过碰撞。如果未发生过碰撞则严格根据中心点偏移量判断,偏移量靠上则放在上方,反之下方;已经处于碰撞状态则根据顶部或底部判断,顶部超出目标中心点则放上方,底部超出目标中心点则放下方。 | ||
|
||
### 碰撞边界与静态区块 | ||
|
||
如果没有静态组件,碰撞边界就只有容器顶部。加上静态组件后,产生位移时要判断加上一段位移是否会把静态组件挤走,如果会挤走,则该拖拽位置无效。 | ||
|
||
### 固定步长 | ||
|
||
磁贴布局为了方便对齐,往往会把父容器切割为 12 或者 6 等分,此时拖拽位置就不会完全跟手,当拖拽没有超过临界点的时候,实际拖拽位置不会跟随移动。 | ||
|
||
## 总结 | ||
|
||
磁贴布局的功能主要聚焦在组件间碰撞逻辑上,目标是让用户能够自然的布局,所以组件间碰撞逻辑也要尽可能自然,符合直觉。 | ||
|
||
> 讨论地址是:[精读《磁贴布局 - 功能分析》· Issue #458 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/458) | ||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。** | ||
|
||
> 关注 **前端精读微信公众号** | ||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg"> | ||
|
||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)) |