当前位置: 首页 > news >正文

react-grid-layout 实现原理介绍

简介

React-grid-layout 是一个基于 React 的网格布局库,它可以帮助我们轻松地在网格中管理和排列组件。它提供了一个直观的 API,可以通过配置网格的大小、列数和组件的位置来实现复杂的布局,还支持对网格中组件的拖拽和重新排列。

实现

诉求

  • 绘制:编排之后,可以根据配置对页面进行正确的绘制

  • 拖拽:支持位置调整,拖拽之后,调整对应配置参数

  • 缩放:支持大小调整,缩放之后,调整对应配置参数

  • 自动调整:当配置有错误、拖拽时有碰撞时,进行自动调整;布局空间的自动优化

示例:

https://react-grid-layout.github.io/react-grid-layout/examples/0-showcase.html

使用方式

const layout = [{ i: 'a', x: 0, y: 1, w: 1, h: 1, static: true },{ i: 'b', x: 1, y: 0, w: 3, h: 2 },{ i: 'c', x: 4, y: 0, w: 1, h: 2 },
];<GridLayoutlayouts={layout}margin={[10, 10]}containerPadding={[10,10]}cols=12rowHeight=150width=1200style={{}}
>{layout.map((item) => (<div key={item.i} className="item-style"><div>{item.i}</div><div>xy{item.x}/{item.y}</div><div>wh{item.w}/{item.h}</div></div>))}
</GridLayout>

绘制

容器

高度计算

const containerHeight = () => {// autoSize的情况下,不计算容器高度    if (!this.props.autoSize) { return; }// 底部坐标, 获取布局中y+h的最大值,即为容器的最大坐标h值    const nbRow = bottom(this.state.layout); const containerPaddingY = this.props.containerPadding ? this.props.containerPadding[1] : this.props.margin[1];// 计算包含margin/padding的真实高度  return ${nbRow * this.props.rowHeight + (nbRow - 1) * this.props.margin[1] + containerPaddingY * 2}px;
}

compact布局

如果直接安装上述的计算方式绘制,可能由于配置的问题,会带来页面的浪费。因此react-grid-layout会默认对纵向空间进行压缩,以避免空间的浪费,也支持设置为横向压缩,或者不压缩。在压缩过程中,很关键的一部分在于对模块间碰撞的处理。

function compact(layout, compactType, cols) {// 对静态模块不进行压缩const compareWith = getStatics(layout); // 排序 horizontal | verticalconst sorted = sortLayoutItems(layout, compactType); const out = Array(layout.length); for (let i = 0, len = sorted.length; i < len; i++) {let l = cloneLayoutItem(sorted[i]);if (!l.static) {l = compactItem(compareWith, l, compactType, cols, sorted);compareWith.push(l);}// 放回正确的位置out[layout.indexOf(sorted[i])] = l;// 在处理冲突的时候设置flagl.moved = false;}return out;
}// 压缩方法
function compactItem(compareWith: Layout,l: LayoutItem,compactType: CompactType,cols: number,fullLayout: Layout
): LayoutItem {const compactV = compactType === "vertical";const compactH = compactType === "horizontal";if (compactV) {// 垂直方向压缩,静态元素所需容器的高度 与 当前元素的纵向起始位置l.y = Math.min(bottom(compareWith), l.y);while (l.y > 0 && !getFirstCollision(compareWith, l)) {l.y--;}} else if (compactH) {while (l.x > 0 && !getFirstCollision(compareWith, l)) {l.x--;}}// 处理碰撞let collides;while ((collides = getFirstCollision(compareWith, l))) {if (compactH) {resolveCompactionCollision(fullLayout, l, collides.x + collides.w, "x");} else {resolveCompactionCollision(fullLayout, l, collides.y + collides.h, "y");}// 水平方向不超过最大值if (compactH && l.x + l.w > cols) {l.x = cols - l.w;l.y++;}}// 对上述的y--,x--做容错处理,确保没有负值l.y = Math.max(l.y, 0);l.x = Math.max(l.x, 0);return l;
}function getFirstCollision(layout: Layout,layoutItem: LayoutItem
): ?LayoutItem {for (let i = 0, len = layout.length; i < len; i++) {// collides方法: // 当前节点 或者节点出现在另一个节点的上方(下边界 > 其他节点的起始位置)/下方/右侧/左侧if (collides(layout[i], layoutItem)) return layout[i];}
}function resolveCompactionCollision(layout: Layout,item: LayoutItem,moveToCoord: number,axis: "x" | "y"
) {const sizeProp = heightWidth[axis]; // x: w, y: hitem[axis] += 1;const itemIndex = layout.map(layoutItem => {return layoutItem.i;}).indexOf(item.i);// 至少要加1个位置的情况下,其下一个元素开始判断,是否与元素碰撞for (let i = itemIndex + 1; i < layout.length; i++) {const otherItem = layout[i];// 静态元素不能动if (otherItem.static) continue;// 如果在当前元素的下方的话 breakif (otherItem.y > item.y + item.h) break;if (collides(item, otherItem)) {resolveCompactionCollision(layout,otherItem,// moveToCoord想相当于新的ymoveToCoord + item[sizeProp],axis);}}item[axis] = moveToCoord;
}

看一下紧凑布局的实际效果

const layout = [{ i: "a", x: 0, y: 0, w: 5, h: 5 },{ i: "b", x: 7, y: 6, w: 3, h: 2, minW: 2, maxW: 4, static: true },{ i: "c", x: 0, y: 10, w: 12, h: 2 },{ i: "d", x: 2, y: 16, w: 8, h: 2 },{ i: "e", x: 7, y: 9, w: 3, h: 1 }]; // 高度 (16 + 8) * 60 + 23 * 10 + 2 * 10 = 1690

compact调整之后

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7UCuxkBr-1693221682629)(https://tech-proxy.bytedance.net/tos/images/1693221360491_d79de77167d3c591075cdc27711f3ed8.png)]

看一下碰撞检测之后的处理结果

allowOverlap | preventCollision

**allowOverlap 为true时,**可以允许覆盖/重叠,并且不会对布局做压缩处理。

**preventCollision为true时,**阻止碰撞发生,在拖拽/缩放时,会有其作用

容器绘制


render() {const { className, style, isDroppable, innerRef } = this.props;//样式合并const mergedClassName = classNames(layoutClassName, className);const mergedStyle = {height: this.containerHeight(),...style,};return (<divref={innerRef}className={mergedClassName}style={mergedStyle}// 拖拽功能onDrop={isDroppable ? this.onDrop : noop}onDragLeave={isDroppable ? this.onDragLeave : noop}onDragEnter={isDroppable ? this.onDragEnter : noop}onDragOver={isDroppable ? this.onDragOver : noop}>{React.Children.map(this.props.children, child => this.processGridItem(child))}{isDroppable && this.state.droppingDOMNode && this.processGridItem(this.state.droppingDOMNode, true)}{this.placeholder()}</div>);}

GridItem

<GridItem        containerWidth={width}        cols={cols}        margin={margin}        containerPadding={containerPadding || margin}        maxRows={maxRows}        rowHeight={rowHeight}        w={l.w}        h={l.h}        x={l.x}        y={l.y}        i={l.i}        minH={l.minH}        minW={l.minW}        maxH={l.maxH}        maxW={l.maxW}        static={l.static}   cancel={draggableCancel}        handle={draggableHandle}    // 拖拽onDragStop={this.onDragStop}        onDragStart={this.onDragStart}        onDrag={this.onDrag}            isDraggable={draggable}       // 缩放  onResizeStart={this.onResizeStart}        onResize={this.onResize}        onResizeStop={this.onResizeStop}   isResizable={resizable}        resizeHandles={resizeHandlesOptions}        resizeHandle={resizeHandle}  isBounded={bounded}        useCSSTransforms={useCSSTransforms && mounted}        usePercentages={!mounted}        transformScale={transformScale}        droppingPosition={isDroppingItem ? droppingPosition : undefined}        >        {child}      </GridItem>

计算实际位置(top/left/width/height)

function calcGridItemPosition(positionParams,x,y,w,h,state
){const { margin, containerPadding, rowHeight } = positionParams;// 计算列宽 (containerWidth - margin[0] * (cols - 1) - containerPadding[0] * 2) / colsconst colWidth = calcGridColWidth(positionParams);const out = {};// 如果gridItem正在缩放,就采用缩放时state记录的宽高(width,height)。// 通过回调函数获取布局信息if (state && state.resizing) {out.width = Math.round(state.resizing.width);out.height = Math.round(state.resizing.height);}// 反之,基于网格单元计算else {// gridUnits, colOrRowSize, marginPx// Math.round(colOrRowSize * gridUnits + Math.max(0, gridUnits - 1) * marginPx)// margin值计算,会单独留一个margin在下方或者右侧out.width = calcGridItemWHPx(w, colWidth, margin[0]);out.height = calcGridItemWHPx(h, rowHeight, margin[1]);}// 如果gridItem正在拖拽,就采用拖拽时state记录的位置(left,top)// 通过回调函数获取布局信息if (state && state.dragging) {out.top = Math.round(state.dragging.top);out.left = Math.round(state.dragging.left);}// 反之,基于网格单元计算else {out.top = Math.round((rowHeight + margin[1]) * y + containerPadding[1]);out.left = Math.round((colWidth + margin[0]) * x + containerPadding[0]);}return out;
}
// 计算列宽
function calcGridColWidth(positionParams: PositionParams): number {const { margin, containerPadding, containerWidth, cols } = positionParams;return ((containerWidth - margin[0] * (cols - 1) - containerPadding[0] * 2) / cols);
}
// 计算宽高的实际值,最后一个margin会留在当前网格的下面
function calcGridItemWHPx(gridUnits, colOrRowSize, marginPx){// 0 * Infinity === NaN, which causes problems with resize contraintsif (!Number.isFinite(gridUnits)) return gridUnits;return Math.round(colOrRowSize * gridUnits + Math.max(0, gridUnits - 1) * marginPx);
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CQGZ37uY-1693221682630)(https://tech-proxy.bytedance.net/tos/images/1693221360608_b333c2cc2256a2207154e26e5bbea246.png)]

拖拽

布局拖拽处理

onDrag

onDrag: (i: string, x: number, y: number, GridDragEvent) => void = (i,x,y,{ e, node }) => {const { oldDragItem } = this.state;let { layout } = this.state;const { cols, allowOverlap, preventCollision } = this.props;const l = getLayoutItem(layout, i);if (!l) return;// Create placeholder (display only)const placeholder = {w: l.w,h: l.h,x: l.x,y: l.y,placeholder: true,i: i};const isUserAction = true;// 根据实时拖拽 生成布局layout = moveElement(layout,l,x,y,isUserAction,preventCollision,compactType(this.props),cols,allowOverlap);this.props.onDrag(layout, oldDragItem, l, placeholder, e, node);this.setState({layout: allowOverlap? layout: compact(layout, compactType(this.props), cols),activeDrag: placeholder});};function moveElement(layout: Layout,l: LayoutItem,x: ?number,y: ?number,isUserAction: ?boolean,preventCollision: ?boolean,compactType: CompactType,cols: number,allowOverlap: ?boolean
): Layout {if (l.static && l.isDraggable !== true) return layout;if (l.y === y && l.x === x) return layout;log(`Moving element ${l.i} to [${String(x)},${String(y)}] from [${l.x},${l.y}]`);const oldX = l.x;const oldY = l.y;//只 修改 xy 更快速if (typeof x === "number") l.x = x;if (typeof y === "number") l.y = y;l.moved = true;// 排序,以便在发生碰撞的时候找最近碰撞的元素let sorted = sortLayoutItems(layout, compactType);const movingUp =compactType === "vertical" && typeof y === "number"? oldY >= y: compactType === "horizontal" && typeof x === "number"? oldX >= x: false;if (movingUp) sorted = sorted.reverse();const collisions = getAllCollisions(sorted, l);const hasCollisions = collisions.length > 0;if (hasCollisions && allowOverlap) {// 允许覆盖时 不处理return cloneLayout(layout);} else if (hasCollisions && preventCollision) {// 不允许碰撞的时候,撤销本次拖拽log(`Collision prevented on ${l.i}, reverting.`);l.x = oldX;l.y = oldY;l.moved = false;return layout;}//处理碰撞for (let i = 0, len = collisions.length; i < len; i++) {const collision = collisions[i];log(`Resolving collision between ${l.i} at [${l.x},${l.y}] and ${collision.i} at [${collision.x},${collision.y}]`);if (collision.moved) continue;if (collision.static) {// 与静态元素碰撞的时候,只能处理当前移动的元素layout = moveElementAwayFromCollision(layout,collision,l,isUserAction,compactType,cols);} else {layout = moveElementAwayFromCollision(layout,l,collision,isUserAction,compactType,cols);}}return layout;
}function moveElementAwayFromCollision(layout: Layout,collidesWith: LayoutItem,itemToMove: LayoutItem,isUserAction: ?boolean,compactType: CompactType,cols: number
): Layout {const compactH = compactType === "horizontal";const compactV = compactType !== "horizontal";const preventCollision = collidesWith.static;if (isUserAction) {isUserAction = false;// 构建元素 出现在碰撞元素的左上方const fakeItem: LayoutItem = {x: compactH ? Math.max(collidesWith.x - itemToMove.w, 0) : itemToMove.x,y: compactV ? Math.max(collidesWith.y - itemToMove.h, 0) : itemToMove.y,w: itemToMove.w,h: itemToMove.h,i: "-1"};if (!getFirstCollision(layout, fakeItem)) {log(`Doing reverse collision on ${itemToMove.i} up to [${fakeItem.x},${fakeItem.y}].`);return moveElement(layout,itemToMove,compactH ? fakeItem.x : undefined,compactV ? fakeItem.y : undefined,isUserAction,preventCollision,compactType,cols);}}return moveElement(layout,itemToMove,compactH ? itemToMove.x + 1 : undefined,compactV ? itemToMove.y + 1 : undefined,isUserAction,preventCollision,compactType,cols);
}

元素拖拽处理

使用mixinDraggable函数,依赖react-draggable中的DraggableCore,为模块提供拖拽能力支持

onDrag

   onDrag: (Event, ReactDraggableCallbackData) => void = (e,{ node, deltaX, deltaY }) => {const { onDrag } = this.props;if (!onDrag) return;if (!this.state.dragging) {throw new Error("onDrag called before onDragStart.");}let top = this.state.dragging.top + deltaY;let left = this.state.dragging.left + deltaX;const { isBounded, i, w, h, containerWidth } = this.props;const positionParams = this.getPositionParams();// 不得超过边界if (isBounded) {const { offsetParent } = node;if (offsetParent) {const { margin, rowHeight } = this.props;const bottomBoundary =offsetParent.clientHeight - calcGridItemWHPx(h, rowHeight, margin[1]);top = clamp(top, 0, bottomBoundary);const colWidth = calcGridColWidth(positionParams);const rightBoundary =containerWidth - calcGridItemWHPx(w, colWidth, margin[0]);left = clamp(left, 0, rightBoundary);}}const newPosition: PartialPosition = { top, left };this.setState({ dragging: newPosition });// Call callback with this dataconst { x, y } = calcXY(positionParams, top, left, w, h);return onDrag.call(this, i, x, y, {e,node,newPosition});};

缩放

布局缩放处理

onResize

onResize: (i: string, w: number, h: number, GridResizeEvent) => void = (i,w,h,{ e, node }) => {const { layout, oldResizeItem } = this.state;const { cols, preventCollision, allowOverlap } = this.props;// 把缩放之后的新元素set到layout中,cb单独处理不可碰撞时的场景const [newLayout, l] = withLayoutItem(layout, i, l => {let hasCollisions;if (preventCollision && !allowOverlap) {const collisions = getAllCollisions(layout, { ...l, w, h }).filter(layoutItem => layoutItem.i !== l.i);hasCollisions = collisions.length > 0;if (hasCollisions) {// 只能向右下角伸缩,因此取x,y 的最小值,保证拖拽元素在碰撞let leastX = Infinity,leastY = Infinity;collisions.forEach(layoutItem => {if (layoutItem.x > l.x) leastX = Math.min(leastX, layoutItem.x);if (layoutItem.y > l.y) leastY = Math.min(leastY, layoutItem.y);});if (Number.isFinite(leastX)) l.w = leastX - l.x;if (Number.isFinite(leastY)) l.h = leastY - l.y;}}if (!hasCollisions) {// Set new width and height.l.w = w;l.h = h;}return l;});// Shouldn't ever happen, but typechecking makes it necessaryif (!l) return;// Create placeholder element (display only)const placeholder = {w: l.w,h: l.h,x: l.x,y: l.y,static: true,i: i};this.props.onResize(newLayout, oldResizeItem, l, placeholder, e, node);// Re-compact the newLayout and set the drag placeholder.this.setState({layout: allowOverlap? newLayout: compact(newLayout, compactType(this.props), cols),activeDrag: placeholder});};

元素缩放处理

使用mixinResizable函数,依赖react-resizable中的Resizable,为模块提供缩放能力支持

onResizeHandler

 onResizeHandler(e: Event,{ node, size }: { node: HTMLElement, size: Position },handlerName: string // onResizeStart,  onResize, onResizeStop): void {const handler = this.props[handlerName];if (!handler) return;const { cols, x, y, i, maxH, minH } = this.props;let { minW, maxW } = this.props;// 获取新的宽高let { w, h } = calcWH(this.getPositionParams(), // margin, maxRows, cols, rowHeightsize.width,size.height,x,y);//  保证宽度至少为1minW = Math.max(minW, 1);//  最大宽度不应超过当前行剩余的宽度,缩放 可以挤掉其右侧的模块,但是无法对左侧的模块进行改变maxW = Math.min(maxW, cols - x);// 获取在上下边界内的值w = clamp(w, minW, maxW);h = clamp(h, minH, maxH);this.setState({ resizing: handlerName === "onResizeStop" ? null : size });handler.call(this, i, w, h, { e, node, size });}// px 转 格数
function calcWH(positionParams: PositionParams,width: number,height: number,x: number,y: number
): { w: number, h: number } {const { margin, maxRows, cols, rowHeight } = positionParams;const colWidth = calcGridColWidth(positionParams);let w = Math.round((width + margin[0]) / (colWidth + margin[0]));let h = Math.round((height + margin[1]) / (rowHeight + margin[1]));// 在其边界内取值w = clamp(w, 0, cols - x);h = clamp(h, 0, maxRows - y);return { w, h };
}

ResponsiveGridLayout

响应式布局,主要实现的是针对不同宽度的适配。值得注意的是,如果一定不想给每一个breakpoint配置layout的话,请一定给最大的那个breakpoint配置layout。

配置

breakpoints: ?Object = {lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0},
cols: ?Object = {lg: 12, md: 10, sm: 6, xs: 4, xxs: 2},// 固定间隔或者响应式间隔 e.g.[10,10]| `{lg: [10, 10], md: [10, 10], ...}.
margin: [number, number] | {[breakpoint: $Keys<breakpoints>]: [number, number]}
containerPadding: [number, number] | {[breakpoint: $Keys<breakpoints>]: [number, number]}// 由断点查找的 layouts 对象 e.g. {lg: Layout, md: Layout, ...}
layouts: {[key: $Keys<breakpoints>]: Layout}// 回调函数
onBreakpointChange: (newBreakpoint: string, newCols: number) => void,
onLayoutChange: (currentLayout: Layout, allLayouts: {[key: $Keys<breakpoints>]: Layout}) => void,
// 页面宽度变化时的回调,可以根据需要修改 layout 或者处理其他内容
onWidthChange: (containerWidth: number, margin: [number, number], cols: number, containerPadding: [number, number]) => void;

流程

布局生成方法

function findOrGenerateResponsiveLayout(layouts: ResponsiveLayout<Breakpoint>,breakpoints: Breakpoints<Breakpoint>,breakpoint: Breakpoint,lastBreakpoint: Breakpoint,cols: number,compactType: CompactType
): Layout {// 如果有提供对应配置的layout 直接使用if (layouts[breakpoint]) return cloneLayout(layouts[breakpoint]);// 另外生成let layout = layouts[lastBreakpoint];const breakpointsSorted = sortBreakpoints(breakpoints);const breakpointsAbove = breakpointsSorted.slice(breakpointsSorted.indexOf(breakpoint));for (let i = 0, len = breakpointsAbove.length; i < len; i++) {const b = breakpointsAbove[i];if (layouts[b]) {layout = layouts[b];break;}}layout = cloneLayout(layout || []);return compact(correctBounds(layout, { cols: cols }), compactType, cols);
}

总结

**性能好:**200个模块 约268.8ms

独立的是否更新layout的判断机制

用 transform: translateY(-5px) 只是改变了视觉位置,元素本身位置还是在 0px,不改动 css 布局,因为渲染行为大多数情况下在元素本身,所以效率比 top left 要高

功能齐全

https://react-grid-layout.github.io/react-grid-layout/examples/0-showcase.html

https://github.com/react-grid-layout/react-grid-layout/tree/master

http://www.lryc.cn/news/150481.html

相关文章:

  • 集合框架-(Collection/Map)
  • 什么是单文件组件?
  • 国际站阿里云服务器多久会重启一次系统??
  • 低成本32位单片机电动工具无感方波控制方案
  • 安防视频监控/视频集中存储/云存储平台EasyCVR平台无法播放HLS协议该如何解决?
  • MySQL如何查找某个字段值相同的数据
  • 8.react18并发模式与startTransition(搜索高亮思路)
  • 前端Vue自定义得分构成水平柱形图组件 可用于系统专业门类得分评估分析
  • Linux获取纳秒级别时间
  • CSS中你不得不知道的盒子模型
  • 知识储备--基础算法篇-数组
  • zookeeper 理论合集
  • 【pyinstaller 怎么打包python,打包后程序闪退 不打日志 找不到自建模块等问题的踩坑解决】
  • 【Docker】网络
  • Linux :realpath 命令
  • react17:生命周期函数
  • 腾讯内部单边拥塞算法BBR-TCPA一键脚本安装
  • 【LLM】chatglm-6B模型训练和推理
  • 性能可靠it监控系统,性能监控软件的获得来源有哪些
  • TCP/IP网络编程(一) 理解网络编程和套接字
  • Python 潮流周刊#18:Flask、Streamlit、Polars 的学习教程
  • 装备一台ubuntu
  • 为了更好和大家交流,欢迎大家加我的微信账户
  • MS1826A HDMI 多功能视频处理器 HDMI4进1出画面分割芯片
  • 最新文献怎么找|学术最新前沿文献哪里找
  • ctfshow 红包题
  • SpringBoot项目(jar)部署,启动脚本
  • 大数据(四)主流大数据技术
  • 【已解决】激活虚拟环境报错:此时不应有Anaconda3\envs\[envs]\Library\ssl\cacert.pem。
  • Vue安装过程的困惑解答——nodejs和vue关系、webpack、vue-cli、vue的项目结构