精读《磁贴布局 - 功能分析》
磁貼布局三部曲:功能分析、實現分析、性能優化的第一部 - 功能分析。
因為需要做自由布局與磁貼布局混排,以及磁貼布局嵌套,所以要實現一套磁貼分析功能,所以本系列不是簡單的介紹使用 react-grid-layout 這個庫就行了,而是深入分析磁貼布局的特性,以及重頭實現一遍。
對磁貼布局不熟悉的話,react-grid-layout 也是個很好的 Demo 體驗頁,大家可以先體驗一下再閱讀文章。
精讀
簡單碰撞
磁貼布局最重要的就是碰撞了,用過 Demo 就會發現,磁貼左右不會碰撞,只有上下會產生碰撞,這是因為網頁天然是從上而下閱讀的,因此垂直碰撞比水平碰撞更自然。
那么垂直的碰撞方向是什么樣的呢?實際上只有自上而下的碰撞,沒有自下而上的碰撞為了講清楚這個原理,先看下面的例子:
[-----] [-----] | A | → | B | [-----] [-----]如上所示,將方塊 A 移動到方塊 B 的位置,如果此時 A 的 Y 軸位置小于等于 B,則會將 B 擠下去。結果如下所示:
[-----]| A |[-----][-----]| B |[-----]如果 A 的 Y 軸位置比 B 大,則碰撞結果是 A 跑到了 B 的下面:
[-----] [-----] | B | | A | → [-----] [-----]結果如下所示:
[-----]| B |[-----][-----]| A |[-----]如果 A 擠到 B 和 C 中間會如何呢?見下圖:
[-----] [-----] | B | | A | → [-----] [-----] [-----][ C ][-----]很容易想到,A 會落到 B 與 C 的中間位置。那問題來了,實現的時候,當時 A 放到 B 的下方,還是認為 A 放到 C 的上方?
乍一看會覺得,這不一樣嗎?對這個例子來說是的,但對其他例子就不同了。實際上應該始終認為是 A 放到了 B 的下方。原因的話,我們舉一個反例就行,假設認為 A 放到了 C 的上方,那么看下面的例子:
[-----] [-----] | B | | A | → [-----] [-----] [ X ][-----][ C ][-----]如上圖所示,B 和 C 中間夾了一個狹長的 X,此時 A 拖入 B 和 C 的中間,并未與 X 產生碰撞,那結果一定是 A 落在了 B 的下方。如果落在 C 的上方,A 就懸空了。
所以磁貼布局模式下,組件始終只能落在另一個組件下面,除了 Y 軸為 0 的情況下,可以定到組件上方。
連續碰撞
連續碰撞是指當磁貼布局產生碰撞而導致位置變化后,需要重新調整整體位置,或者繼續與其他組件位置產生碰撞的情況,首先看下面這個簡單例子:
[-----] | A | [-----]↓ [-----] | B | [-----] [-----] | C | [-----]如果把 A 拖動到 B 位置,遵循簡單碰撞原理,必須 Y 軸高于 B 的 Y 軸才會置于 B 下方,此時會把 C 頂上去。但僅做到這一步,A 原來的位置會產生空缺,需要重新吸附到頂部,這就是連續碰撞:
[ ] [-----] |Empty| | B | [ ] [-----] [-----] [-----] | B | [ A ] [-----] → Remove Empty [-----] [-----] [-----] | A | [ C ] [-----] [-----] [-----] | C | [-----]這時候你可能會想,結果不就是 B 和 A 交換了位置嘛,實際上用 react-grid-layout 看起來效果也是如此,那么代碼實現時是不是不用這么麻煩?直接判斷 A 與 B 是否產生位置交換,如果交換了,按照交換的方式處理不就行了嗎?
聽上去很美好,因為按照 A 與 B 交換的思路處理效果一致,而且性能更優,因為不用重新計算 C 組件被擠走,然后 A、B、C 再重新擠上去。但實際上交換方案是不可行的,我們看下面的例子:
[-----]| A |[-----]↓ [-------------] | B | [-------------] [-----] | C | [-----]如果把 A 和 B 位置交換,會發現 C 懸空了。之所以上面的例子可以用交換思路,是因為 A 與 B 交換后,A 還可以 “擋住” C 的上移。但這個例子因為 B 很長,但 A 很短,A、B 交換后,A 就擋不住 C 的上移了:
[-------------] | B | [-------------] [-----] [-----] | C | | A | [-----] [-----]所以為了保證任何時候位移都不會產生 BUG,需要老老實實的分兩步來判斷:1. 判斷 A 移到 B 的底部。2. 新的 A 把下面組件擠走,同時如果上面還有空位置,需要整體向上位移。
看起來還是比較消耗性能的,但通過一些優化手段是可以極大減少計算量的,我們到系列的 “性能優化” 部分再說。
碰撞邊界 case
我們再考慮兩個極端情況,第一種是要碰撞的組件過于矮的時候,第二種是要碰撞的組件過高的時候。
首先是過矮的情況,我們看下面 5 種情況:
[-----] | | ← [ A ] | B | | | [-----] [-----] | | | C | | | [-----]上面的情況插入到 B 的上方(假設 B 上方沒有元素了,如果有的話,假設 B 上方為 X,那么應該認為 A 插入到 X 的底部)。
[-----] | | | B | | | ← [ A ] [-----] [-----] | | | C | | | [-----]上面的情況插入到 B 的下方。
[-----] | | | B | | | [-----] [-----] | | ← [ A ] | C | | | [-----]上面的情況插入到 B 的下方。
[-----] | | | B | | | [-----] [-----] | | | C | | | ← [ A ] [-----]上面的情況插入到 C 的下方。
[-----] | | | B | | | [---] [-----] ← [ A ] [-----] [---] | | | C | | | [-----]上面的情況和簡單碰撞里提到的例子一樣,碰撞位置在 B 與 C 之間,還是會認為插入到 B 的下方。
總結一下,過矮的情況下很多時候拖動組件只會與一個組件產生碰撞,當拖拽中心點在碰撞組件中心點上方時,插入到碰撞組件上方的組件下面(如果上方沒有組件則插入到頂部)。當然插入到上方組件下面也不是真的找到上方組件是什么,具體如何做我們等到【實現分析】篇再講。反之,如果中心點相對在下方,就插入到碰撞組件的下方。如果同時碰撞了多個組件,則忽略中心點偏移量靠上的碰撞,僅考慮中心點偏移量靠下的碰撞。
關于中心點上方其實也可以進一步優化,比如當目標碰撞組件太長的時候,可能比較難移到下方,此時在還沒有拖拽到中心點下方時就要做下方碰撞判定了,此時判斷依據可以優化為:碰撞時,拖拽組件的 Y 只要比目標組件的 Y 大(或者再加一個常數閾值,該閾值由拖拽組件高度決定,比如是高度的 1/3),那么就認為拖入到目標組件底部,比如:
[-----] | | [---] | | ← [ A ] | B | [---] | | | | [-----]如上圖所示,雖然 A 的中心點在 B 中心點上方,但因為 A.y - B.y > A.height / 3,所以判定插入到 B 的下方。當然這也會導致拖入超高組件上方很困難,所以要不要這么設定看用戶喜好。
再看組件過高的情況:
[---] [-----] [ ] | B | ← [ A ] [-----] [ ] [-----] [---] | C | [-----]上面的情況插入 B 的上方(如果 B 上面還有組件 X,則判定為插入該 X 下方)。
[-----] [---] | B | [ ] [-----] ← [ A ] [-----] [ ] | C | [---] [-----]上面的情況插入 B 的下方。
[-----] | B | [-----] [-----] [---] | C | [ ] [-----] ← [ A ][ ][---]上面的情況插入 C 的下方。
總結一下,當拖拽組件過高時,還是維持中心點判斷規則,但更可能同時碰撞到多個組件,此時沿用 “中心點偏移量靠上的碰撞” 的原則就行了。但這里有一個較大的區別,拖拽組件矮的時候最多同時和兩個組件碰撞,但拖拽組件高的時候,可能同時和 N 個組件碰撞,如下圖所示:
[-----] [---] | B | [ ] [-----] [ ] [-----] [ ] | C | [ ] [-----] ← [ A ] [-----] [ ] | D | [ ] [-----] [ ] [-----] [ ] | E | [---] [-----]此時就要看和哪個組件碰撞的優先級最高了。我們單從 B、C、D、E 的角度看,A 分別應該放在 B 下方、C 下方、D 上方、E 上方,其中 B 下方與 C 上方是同一個位置,但與 D 上方、E 上方都不是同一個位置,此時就要看拖拽到哪個位置產生的位移最小了,因為最小的位移是最不突兀的,最符合用戶的預期。
另一個邊界情況就是拖拽組件過高時,如果中心點還未移動到下方,但高度卻超出了下面組件下方,也要視為拖拽到下方:
[-----] | | | | | | | A | | | | | | | [-----]↓ [-----] | B | [-----]如上圖所示,A 非常高,B 很矮,當 A 往下移動時,可能 A 的底部都超出 B 底部了(可以優化為 B 的中間),但 A 的中心點仍然在 B 中心點上方,此時在用戶已經認為可以交換位置了,所以判斷是否移動到下方多了一個優先判斷條件:拖拽組件底部超出目標組件底部。同理拖拽到上方也類似。
要注意的是,這個例子與下面的例子表現并不一致,下面的例子 A 向左移時,應該放置 B 的上方,而上面的例子卻放置 B 的下方:
[-----]| || || |← | A | [-----] | | | B | | | [-----] | |[-----]發現了嗎?單從垂直位置來看,都是 A 的底部超過了 B 底部,但有時候和 B 互換,有時候卻不互換。區分方法就是該碰撞發生時,這兩個區塊是否已經發生過碰撞。如果未發生過碰撞則嚴格根據中心點偏移量判斷,偏移量靠上則放在上方,反之下方;已經處于碰撞狀態則根據頂部或底部判斷,頂部超出目標中心點則放上方,底部超出目標中心點則放下方。
碰撞邊界與靜態區塊
如果沒有靜態組件,碰撞邊界就只有容器頂部。加上靜態組件后,產生位移時要判斷加上一段位移是否會把靜態組件擠走,如果會擠走,則該拖拽位置無效。
固定步長
磁貼布局為了方便對齊,往往會把父容器切割為 12 或者 6 等分,此時拖拽位置就不會完全跟手,當拖拽沒有超過臨界點的時候,實際拖拽位置不會跟隨移動。
總結
磁貼布局的功能主要聚焦在組件間碰撞邏輯上,目標是讓用戶能夠自然的布局,所以組件間碰撞邏輯也要盡可能自然,符合直覺。
討論地址是:精讀《磁貼布局 - 功能分析》· Issue #458 · dt-fe/weekly
如果你想參與討論,請 點擊這里,每周都有新的主題,周末或周一發布。前端精讀 - 幫你篩選靠譜的內容。
關注 前端精讀微信公眾號
版權聲明:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證)
總結
以上是生活随笔為你收集整理的精读《磁贴布局 - 功能分析》的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 5G钢铁渐入佳境:中国电信推动从“1”迈
- 下一篇: 2017离职