”工欲善其事,必先利其器。“—孔子《论语.录灵公》
首页 > 编程 > 在 React 中构建响应式会议图块的动态网格系统

在 React 中构建响应式会议图块的动态网格系统

发布于2024-11-06
浏览:485

Building a Dynamic Grid System for Responsive Meeting Tiles in React

In the era of remote work and virtual meetings, creating a responsive and dynamic grid system for displaying participant video tiles is crucial. Inspired by platforms like Google Meet, I recently developed a flexible grid system in React that adapts seamlessly to varying numbers of participants and different screen sizes. In this blog post, I'll walk you through the implementation, explaining the key components and how they work together to create an efficient and responsive layout.

Table of Contents

  1. Introduction
  2. Grid Layout Definitions
  3. Selecting the Appropriate Grid Layout
  4. The useGridLayout Hook
  5. Example Usage
  6. Styling the Grid
  7. Conclusion

Introduction

Creating a dynamic grid system involves adjusting the layout based on the number of items (or "tiles") and the available screen real estate. For video conferencing applications, this ensures that each participant's video feed is displayed optimally, regardless of the number of participants or the device being used.

The solution I developed leverages React hooks and CSS Grid to manage and render the grid layout dynamically. Let's dive into the core components of this system.

Grid Layout Definitions

First, we define the possible grid layouts that our system can use. Each layout specifies the number of columns and rows, as well as constraints on the minimum and maximum number of tiles it can accommodate.

import { useState, useEffect, RefObject } from 'react';

export type GridLayoutDefinition = {
  name: string;
  columns: number;
  rows: number;
  minTiles: number;
  maxTiles: number;
  minWidth: number;
  minHeight: number;
};

export const GRID_LAYOUTS: GridLayoutDefinition[] = [
  { columns: 1, rows: 1, name: '1x1', minTiles: 1, maxTiles: 1, minWidth: 0, minHeight: 0 },
  { columns: 1, rows: 2, name: '1x2', minTiles: 2, maxTiles: 2, minWidth: 0, minHeight: 0 },
  { columns: 2, rows: 1, name: '2x1', minTiles: 2, maxTiles: 2, minWidth: 900, minHeight: 0 },
  { columns: 2, rows: 2, name: '2x2', minTiles: 3, maxTiles: 4, minWidth: 560, minHeight: 0 },
  { columns: 3, rows: 3, name: '3x3', minTiles: 5, maxTiles: 9, minWidth: 700, minHeight: 0 },
  { columns: 4, rows: 4, name: '4x4', minTiles: 10, maxTiles: 16, minWidth: 960, minHeight: 0 },
  { columns: 5, rows: 5, name: '5x5', minTiles: 17, maxTiles: 25, minWidth: 1100, minHeight: 0 },
];

Explanation

  • GridLayoutDefinition: A TypeScript type that defines the properties of each grid layout.
  • GRID_LAYOUTS: An array of predefined layouts, ordered by complexity. Each layout specifies:
    • columns and rows: The number of columns and rows in the grid.
    • name: A descriptive name for the layout (e.g., '2x2').
    • minTiles and maxTiles: The range of tile counts that the layout can accommodate.
    • minWidth and minHeight: The minimum container dimensions required for the layout.

Selecting the Appropriate Grid Layout

The core logic for selecting the right grid layout based on the number of tiles and container size is encapsulated in the selectGridLayout function.

function selectGridLayout(
  layouts: GridLayoutDefinition[],
  tileCount: number,
  width: number,
  height: number,
): GridLayoutDefinition {
  let currentLayoutIndex = 0;
  let layout = layouts.find((layout_, index, allLayouts) => {
    currentLayoutIndex = index;
    const isBiggerLayoutAvailable = allLayouts.findIndex((l, i) => 
      i > index && l.maxTiles === layout_.maxTiles
    ) !== -1;
    return layout_.maxTiles >= tileCount && !isBiggerLayoutAvailable;
  });

  if (!layout) {
    layout = layouts[layouts.length - 1];
    console.warn(`No layout found for: tileCount: ${tileCount}, width/height: ${width}/${height}. Fallback to biggest available layout (${layout?.name}).`);
  }

  if (layout && (width  0) {
      const smallerLayout = layouts[currentLayoutIndex - 1];
      layout = selectGridLayout(
        layouts.slice(0, currentLayoutIndex),
        smallerLayout.maxTiles,
        width,
        height,
      );
    }
  }

  return layout || layouts[0];
}

How It Works

  1. Initial Selection: The function iterates through the layouts array to find the first layout where maxTiles is greater than or equal to tileCount and ensures there's no larger layout with the same maxTiles available.

  2. Fallback Mechanism: If no suitable layout is found, it defaults to the largest available layout and logs a warning.

  3. Responsive Adjustment: If the selected layout's minWidth or minHeight constraints aren't met by the container dimensions, the function recursively selects a smaller layout that fits within the constraints.

  4. Final Return: The selected layout is returned, ensuring that the grid is both adequate for the number of tiles and fits within the container's size.

The useGridLayout Hook

To encapsulate the grid selection logic and make it reusable across components, I created the useGridLayout custom hook.

export function useGridLayout(
  gridRef: RefObject,
  tileCount: number
): { layout: GridLayoutDefinition } {
  const [layout, setLayout] = useState(GRID_LAYOUTS[0]);

  useEffect(() => {
    const updateLayout = () => {
      if (gridRef.current) {
        const { width, height } = gridRef.current.getBoundingClientRect();
        const newLayout = selectGridLayout(GRID_LAYOUTS, tileCount, width, height);
        setLayout(newLayout);

        gridRef.current.style.setProperty('--col-count', newLayout.columns.toString());
        gridRef.current.style.setProperty('--row-count', newLayout.rows.toString());
      }
    };

    updateLayout();

    window.addEventListener('resize', updateLayout);
    return () => window.removeEventListener('resize', updateLayout);
  }, [gridRef, tileCount]);

  return { layout };
}

Hook Breakdown

  • Parameters:

    • gridRef: A reference to the grid container element.
    • tileCount: The current number of tiles to display.
  • State Management: Uses useState to keep track of the current layout, initializing with the first layout in GRID_LAYOUTS.

  • Effect Hook:

    • updateLayout Function: Retrieves the container's width and height, selects the appropriate layout using selectGridLayout, and updates the state. It also sets CSS variables --col-count and --row-count on the container for styling.
    • Event Listener: Adds a resize event listener to update the layout whenever the window size changes. Cleans up the listener on component unmount.
  • Return Value: Provides the current layout object to the consuming component.

Example Usage

To demonstrate how this dynamic grid system works in practice, here's an example React component that uses the useGridLayout hook.

'use client'

import React, { useState, useRef, useEffect } from 'react'
import { Button } from "@/components/ui/button"
import { useGridLayout, GridLayoutDefinition } from './useGridLayout'

export default function Component() {
  const [tiles, setTiles] = useState([1, 2, 3, 4]);
  const [containerWidth, setContainerWidth] = useState(typeof window !== 'undefined' ? window.innerWidth : 1000);
  const gridRef = useRef(null);

  const { layout } = useGridLayout(gridRef, tiles.length);

  useEffect(() => {
    const handleResize = () => {
      setContainerWidth(window.innerWidth);
    };

    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  const addTile = () => setTiles(prev => [...prev, prev.length   1]);
  const removeTile = () => setTiles(prev => prev.slice(0, -1));

  return (
    
{tiles.slice(0, layout.maxTiles).map((tile) => (
Tile {tile}
))}

Current Layout: {layout.name} ({layout.columns}x{layout.rows})

Container Width: {containerWidth}px

Visible Tiles: {Math.min(tiles.length, layout.maxTiles)} / Total Tiles: {tiles.length}

) }

Component Breakdown

  1. State Management:

    • tiles: An array representing the current tiles. Initially contains four tiles.
    • containerWidth: Tracks the container's width, updating on window resize.
  2. Refs:

    • gridRef: A reference to the grid container, passed to the useGridLayout hook.
  3. Using the Hook:

    • Destructures the layout object from the useGridLayout hook, which determines the current grid layout based on the number of tiles and container size.
  4. Event Handling:

    • Add Tile: Adds a new tile to the grid.
    • Remove Tile: Removes the last tile from the grid.
    • Resize Listener: Updates containerWidth on window resize.
  5. Rendering:

    • Controls: Buttons to add or remove tiles.
    • Grid Container:
      • Uses CSS Grid with dynamic gridTemplateColumns and gridTemplateRows based on CSS variables set by the hook.
      • Renders tiles up to the layout.maxTiles limit.
    • Info Section: Displays the current layout, container width, and the number of visible versus total tiles.

What Happens in Action

  • Adding Tiles: As you add more tiles, the useGridLayout hook recalculates the appropriate grid layout to accommodate the new number of tiles while respecting the container's size.
  • Removing Tiles: Removing tiles triggers a layout recalculation to potentially use a smaller grid layout, optimizing space.
  • Resizing: Changing the window size dynamically adjusts the grid layout to ensure that the tiles remain appropriately sized and positioned.

Styling the Grid

The grid's responsiveness is primarily handled via CSS Grid properties and dynamically set CSS variables. Here's a brief overview of how the styling works:

/* Example Tailwind CSS classes used in the component */
/* The actual styles are managed via Tailwind, but the key dynamic properties are set inline */

.grid {
  display: grid;
  gap: 1rem; /* Adjust as needed */
}

.grid > div {
  /* Example styles for tiles */
  background-color: var(--color-primary, #3490dc);
  color: var(--color-primary-foreground, #ffffff);
  padding: 1rem;
  border-radius: 0.5rem;
  display: flex;
  align-items: center;
  justify-content: center;
}

Dynamic CSS Variables

In the useGridLayout hook, the following CSS variables are set based on the selected layout:

  • --col-count: Number of columns in the grid.
  • --row-count: Number of rows in the grid.

These variables are used to define the gridTemplateColumns and gridTemplateRows properties inline:

style={{
  gridTemplateColumns: `repeat(var(--col-count), 1fr)`,
  gridTemplateRows: `repeat(var(--row-count), 1fr)`,
}}

This approach ensures that the grid layout adapts seamlessly without the need for extensive CSS media queries.

Conclusion

Building a dynamic grid system for applications like video conferencing requires careful consideration of both the number of elements and the available display space. By defining a set of responsive grid layouts and implementing a custom React hook to manage layout selection, we can create a flexible and efficient system that adapts in real-time to user interactions and screen size changes.

This approach not only enhances the user experience by providing an optimal viewing arrangement but also simplifies the development process by encapsulating the layout logic within reusable components. Whether you're building a video conferencing tool, a dashboard, or any application that requires dynamic content arrangement, this grid system can be a valuable addition to your toolkit.

Feel free to customize and extend this system to suit your specific needs. Happy coding!

版本声明 本文转载于:https://dev.to/vaib215/building-a-dynamic-grid-system-for-responsive-meeting-tiles-in-react-443l?1如有侵犯,请联系[email protected]删除
最新教程 更多>
  • 左连接为何在右表WHERE子句过滤时像内连接?
    左连接为何在右表WHERE子句过滤时像内连接?
    左JOIN CONUNDRUM:WITCHING小时在数据库Wizard的领域中变成内在的加入很有趣,当将c.foobar条件放置在上面的Where子句中时,据说左联接似乎会转换为内部连接。仅当满足A.Foo和C.Foobar标准时,才会返回结果。为什么要变形?关键在于其中的子句。当左联接的右侧值...
    编程 发布于2025-07-16
  • C++中如何将独占指针作为函数或构造函数参数传递?
    C++中如何将独占指针作为函数或构造函数参数传递?
    在构造函数和函数中将唯一的指数管理为参数 unique pointers( unique_ptr [2启示。通过值: base(std :: simelor_ptr n) :next(std :: move(n)){} 此方法将唯一指针的所有权转移到函数/对象。指针的内容被移至功能中,在操作...
    编程 发布于2025-07-16
  • C++成员函数指针正确传递方法
    C++成员函数指针正确传递方法
    如何将成员函数置于c [&& && && && && && && && && && &&&&&&&&&&&&&&&&&&&&&&&华仪的函数时,在接受成员函数指针的函数时,要在函数上既要提供指针又可以提供指针和指针到函数的函数。需要具有一定签名的功能指针。要通过成员函数,您需要同时提供对象指针(此...
    编程 发布于2025-07-16
  • 为什么使用Firefox后退按钮时JavaScript执行停止?
    为什么使用Firefox后退按钮时JavaScript执行停止?
    导航历史记录问题:JavaScript使用Firefox Back Back 此行为是由浏览器缓存JavaScript资源引起的。要解决此问题并确保在后续页面访问中执行脚本,Firefox用户应设置一个空功能。 警报'); }; alert('inline Alert')...
    编程 发布于2025-07-16
  • 在GO中构造SQL查询时,如何安全地加入文本和值?
    在GO中构造SQL查询时,如何安全地加入文本和值?
    在go中构造文本sql查询时,在go sql queries 中,在使用conting and contement和contement consem per时,尤其是在使用integer per当per当per时,per per per当per. 在GO中实现这一目标的惯用方法是使用fmt.spr...
    编程 发布于2025-07-16
  • 如何使用Regex在PHP中有效地提取括号内的文本
    如何使用Regex在PHP中有效地提取括号内的文本
    php:在括号内提取文本在处理括号内的文本时,找到最有效的解决方案是必不可少的。一种方法是利用PHP的字符串操作函数,如下所示: 作为替代 $ text ='忽略除此之外的一切(text)'; preg_match('#((。 &&& [Regex使用模式来搜索特...
    编程 发布于2025-07-16
  • 找到最大计数时,如何解决mySQL中的“组函数\”错误的“无效使用”?
    找到最大计数时,如何解决mySQL中的“组函数\”错误的“无效使用”?
    如何在mySQL中使用mySql 检索最大计数,您可能会遇到一个问题,您可能会在尝试使用以下命令:理解错误正确找到由名称列分组的值的最大计数,请使用以下修改后的查询: 计数(*)为c 来自EMP1 按名称组 c desc订购 限制1 查询说明 select语句提取名称列和每个名称...
    编程 发布于2025-07-16
  • 如何使用Depimal.parse()中的指数表示法中的数字?
    如何使用Depimal.parse()中的指数表示法中的数字?
    在尝试使用Decimal.parse(“ 1.2345e-02”中的指数符号表示法表示的字符串时,您可能会遇到错误。这是因为默认解析方法无法识别指数符号。 成功解析这样的字符串,您需要明确指定它代表浮点数。您可以使用numbersTyles.Float样式进行此操作,如下所示:[&& && && ...
    编程 发布于2025-07-16
  • 同实例无需转储复制MySQL数据库方法
    同实例无需转储复制MySQL数据库方法
    在同一实例上复制一个MySQL数据库而无需转储在同一mySQL实例上复制数据库,而无需创建InterMediate sqql script。以下方法为传统的转储和IMPORT过程提供了更简单的替代方法。 直接管道数据 MySQL手动概述了一种允许将mysqldump直接输出到MySQL clie...
    编程 发布于2025-07-16
  • 如何使用node-mysql在单个查询中执行多个SQL语句?
    如何使用node-mysql在单个查询中执行多个SQL语句?
    Multi-Statement Query Support in Node-MySQLIn Node.js, the question arises when executing multiple SQL statements in a single query using the node-mys...
    编程 发布于2025-07-16
  • 如何解决AppEngine中“无法猜测文件类型,使用application/octet-stream...”错误?
    如何解决AppEngine中“无法猜测文件类型,使用application/octet-stream...”错误?
    appEngine静态文件mime type override ,静态文件处理程序有时可以覆盖正确的mime类型,在错误消息中导致错误消息:“无法猜测mimeType for for file for file for [File]。 application/application/octet...
    编程 发布于2025-07-16
  • Go语言垃圾回收如何处理切片内存?
    Go语言垃圾回收如何处理切片内存?
    Garbage Collection in Go Slices: A Detailed AnalysisIn Go, a slice is a dynamic array that references an underlying array.使用切片时,了解垃圾收集行为至关重要,以避免潜在的内存泄...
    编程 发布于2025-07-16
  • 大批
    大批
    [2 数组是对象,因此它们在JS中也具有方法。 切片(开始):在新数组中提取部分数组,而无需突变原始数组。 令ARR = ['a','b','c','d','e']; // USECASE:提取直到索引作...
    编程 发布于2025-07-16
  • Java是否允许多种返回类型:仔细研究通用方法?
    Java是否允许多种返回类型:仔细研究通用方法?
    在Java中的多个返回类型:一种误解类型:在Java编程中揭示,在Java编程中,Peculiar方法签名可能会出现,可能会出现,使开发人员陷入困境,使开发人员陷入困境。 getResult(string s); ,其中foo是自定义类。该方法声明似乎拥有两种返回类型:列表和E。但这确实是如此吗...
    编程 发布于2025-07-16
  • 如何有效地选择熊猫数据框中的列?
    如何有效地选择熊猫数据框中的列?
    在处理数据操作任务时,在Pandas DataFrames 中选择列时,选择特定列的必要条件是必要的。在Pandas中,选择列的各种选项。选项1:使用列名 如果已知列索引,请使用ILOC函数选择它们。请注意,python索引基于零。 df1 = df.iloc [:,0:2]#使用索引0和1 c...
    编程 发布于2025-07-16

免责声明: 提供的所有资源部分来自互联网,如果有侵犯您的版权或其他权益,请说明详细缘由并提供版权或权益证明然后发到邮箱:[email protected] 我们会第一时间内为您处理。

Copyright© 2022 湘ICP备2022001581号-3