当前位置: 首页 > Web前端 > HTML

精读《磁贴布局 - 功能实现》

时间:2023-03-28 17:39:04 HTML

经过上篇文章精读《磁贴布局 - 功能分析》的分析,这次我们进入实现环节。精读在实现tile布局之前,首先要实现最基本的组件拖拽过程,然后我们就可以在拖拽的基础上添加tile效果了。基本的拖放能力对于布局抽象,它关心的是可拖拽组件和容器的DOM。你不需要关心这些DOM是如何创建的。在此基础上,你甚至可以再做一套构造或布局框架层,专门负责DOM的管理,但本文仍着重于布局的实现层。布局组件首先要收集可拖动的组件和容器,假设业务层生成这些DOM并将它们传递给布局:constelementMap:Record={};constcontainerMap:Record={};elementMap表示可拖拽的Dragged组件信息,包括其DOM实例,以及相对于父容器的x,y,width,height。containerMap表示容器组件信息。之所以存储rectX和rectY相对于浏览器的绝对定位是因为容器的直接父组件可能是element。比如Card组件可以同时渲染Header和Footer,两个位置都可以拖入element中,所以这两个位置是容器,相对于父元素Card定位,存储绝对定位方便计算。接下来,将鼠标按下事件绑定到elementMap的每个组件作为onDragStart时序:开始下降};});然后监听文档中的onMouseMove和onMouseUp分别作为onDrag和onDragEnd的时序,这样我们就抽象出了拖拽前、拖拽中和拖拽后三个阶段:dragComponent=componentId;}functiononDrag(context,event){//根据context响应组件的拖动。dragComponent//将元素x和y更改为event.clientX和event.clientY}functiononDragEnd(context){context.dragComponent=undefined;}这样就完成了最基本的拖拽能力。在实际代码中,为了简化,这里可能会包含进一步的抽象。是时候模拟用户输入了。tile布局影响因素tile布局入场后,只影响onDrag阶段。在之前的逻辑中,拖动是完全自由的,所以tile布局会约束两件事:约束当前拖动的组件位置。可能会挤出其他组件。拖拽组件位置的约束由其背后的“放手DOM”决定,即元素在拖拽时实时跟随手,但如果拖拽位置无法放置,则会修改落地位置放手的时候。这个落地位置我们称之为safePosition,也就是当前组件的安全位置。所以onDrag需要计算一个新的safePosition。应该如何计算由瓦片的碰撞模式决定。我们可以在onDrag函数中做如下抽象://实时改变组件的位置到event.clientX,event.clientY//改变后面的实际DOM位置到safeX,safeY//onDragEnd时,改变组件的位置到safeX,safeY,这样thecomponentfallsinasafeposition}接下来是关键函数collision的实现,需要包含tile布局的所有核心逻辑。碰撞功能包括两个模块,即拖入拖出模块和碰撞模块。拖入拖出判断当前拖拽位置是进入新容器还是离开当前容器;碰撞模块判断当前拖动位置是否与其他元素发生碰撞,并做出相应的碰撞效果。另外,tile布局还允许组件根据重力的影响被向上吸引,所以我们需要做一个runGravity函数来根据重力的影响来排列所有的组件。functioncollision(context,x,y){//先做拖入拖出判断if(judgeDragInOrOut(context,event)){//如果判断是拖入拖出,会有没有碰撞,提前返回//但拖出时,需要对原父节点做runGravity//拖入时,不需要对原父节点做runGravityreturn{safeX:x,safeY:};}//CollisionmodulereturngridCollsion(context,x,y);}为什么拖进来的时候不需要对原来的父节点做runGravity:假设一个元素从上往下移动到一个容器里,那么一旦拖入容器,上面会创建一个空白区域,如果容器马上被重力挤压上去,但是鼠标还没有松手,可能鼠标位置马上落到容器外面,导致组件触发拖出.所以在拖入的时候,不要马上对原来的父容器施加重力,这样才能保持拖入时结构的稳定性。拖入拖出模块判断起来很简单,就是如果x%的元素进入容器,判断为拖入,y%的元素离开容器,判断为离开。碰撞模块碰撞模块gridCollsion比较复杂,这里先说一下。首先需要写一个矩形求交函数,判断两个组件是否发生碰撞:和context.elementMap[componentId]是否相交,如果相交,则认为有碰撞});}如果没有碰撞,那么我们需要根据重力的影响计算着陆点的safeY(水平方向不受重力影响,必须跟随手,所以不需要算作safeX)。这时候直接调用runGravity函数,传入一个extraBox。这个extraBox就是当前鼠标位置生成的框。由于这个盒子没有和任何组件发生碰撞,所以可以直接判断extraBox在重力作用下会落到哪里。这个位置是safeY:functiongridCollision(context,x,y){//在一个父容器中计算重力,同时插入一个extraBox,在这个extraBox的重力生效后返回Y:extraBoxYconst{extraBoxY}=runGravity(context,parentId,extraBox);return{safeY:extraBoxY};}不碰撞的逻辑比较简单。如果有碰撞,逻辑是这样的://是否是初始化碰撞。初始碰撞的优先级最低,所以只要发生非初始碰撞,与其他组件的初始碰撞也被认为是非初始碰撞letisInitCollision=true;Object.keys(context.elementMap).forEach((componentId)=>{//判断context.dragComponent和context.elementMap[componentId]是否相交constintersect=areRectanglesOverlap();//相交if(intersect.isIntersect){//1.context中存储一个全局变量,判断当前组件是否有intersectedbefore这是判断是否修改isInitCollision//2.判断碰撞后,碰撞会导致鼠标位置的框,即extraBox是放在组件上方还是下方}});首先判断当前碰撞是否为初始化碰撞,一旦存在不是初始化碰撞的组件,则认为没有发生初始化碰撞。原因是初始碰撞位置的判断比较简单,直接根据源和目标元素水平中心点的高度来判断落地位置。如果源的水平中心点高于目标的水平中心点,则将其放置在目标上方,否则将放置在目标下方。如果是非初始化的碰撞逻辑会比较复杂,比如下面的例子://[---][C]//[B]//[---]//↑//[-------]//[A]//[------]当A组件向上移动时,因为已经和B发生碰撞,所以会尝试判断是否适合放在上面B,否则会一直把自己锁在B下面。其实我们希望A的上边缘超过B的水平中心点,从而产生交换。此时A的水平中心点仍然低于B的水平中心点,所以此时两种不同的判断规则会产生不同的结果。位置判断,区分的手段是A和B是否已经处于相交状态。现在插入位置终于算出来了(根据是否初始化碰撞来判断extraBox落在哪个元素上或下面),然后进入runGravity函数:functionrunGravity(context,parentId,extraBox){}这个函数是针对某个parentcontainer节点的引力是有效的,所以在不考虑extraBox的情况下逻辑如下:先获取container下的所有子元素,将这些元素按照y从小到大排序,然后依次计算落点,而已经计算出的分量会计算在碰撞的影响范围内,即新分量y尽量小,但是如果在水平方向上与计算出的分量有重叠,则只能被推到这些组件下面。如果有extraBox,问题就更复杂了,见下图://[---][C]//[B]//[---]//↑//[-------]//[A]//[------]//AthisextraBoxbeforeB//这个例子应该按照C->A->B的顺序计算重力//规则:如果有都在ids之前(idsy,bottom相同),然后把排序结果中y>=ids.y&bottomA->B的顺序计算重力//规则:如果ids之后有(idsy,bottom相同),则对结果进行排序y<=ids.y&bottom>ids[0].bottom的组件被拉出来放在ids的最后一个组件之后,因为extraBox是一个插入位置,所以计算方法肯定不一样。以第一个例子为例:当A向上移动可以和B交换时,最终想要的结果从上到下是C->A->B,但是因为C和B的y都为0,如果我们A和B互换解释为A的y变为0将B压低,那么A也会将C压低,从而导致错误的结果。因此,重要的是计算重力的优先级。在上面的例子中,重力计算的顺序应该是先计算C,再计算A,再计算B。这个逻辑是基于上面的注释。上面说的算法就是isInitCollision=false的算法。如果isInitCollision=true,extraBox可以按照y的顺序插入。原因见下图://[------][-]//[][]//[][D]//[A]→[]//[][-]//[][--------]//[----][]//[---][C]//[B][]//[-----][-----------------]当A向右移动直到与C碰撞时,根据y计算重力优先结果是正确的。如果按照extraBox已经产生碰撞的算法,会认为A放在C上面,但是因为B相对于C满足y>=ids.y&bottom版权声明:免费转载-非商业-非衍生保留属性(CreativeCommons3.0License)