在这里插入图片描述

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

🎯 欢迎来到 Flutter for OpenHarmony 社区!本文将深入讲解 Flutter 中 two_dimensional_scrollables 二维滚动组件的使用方法,带你全面掌握在 Flutter 中实现表格、电子表格等需要双向滚动的界面的技巧。


一、two_dimensional_scrollables 组件概述

在 Flutter for OpenHarmony 应用开发中,two_dimensional_scrollables 是一个专门用于实现二维滚动的组件库。它提供了 TableView 组件,支持同时在水平和垂直方向上滚动,非常适合用于显示表格、电子表格、日历等需要双向滚动的数据展示场景。

📋 two_dimensional_scrollables 组件特点

特点 说明
双向滚动 支持同时水平和垂直滚动
懒加载 只渲染可见区域的单元格,性能优化
固定行列 支持固定行和列,滚动时保持可见
装饰支持 支持行列背景色、边框等装饰效果
手势处理 支持自定义手势和指针事件处理
跨平台支持 支持 Android、iOS、Linux、macOS、Windows、OpenHarmony
灵活构建 提供多种构建方式适应不同需求

💡 使用场景:电子表格、数据表格、日历视图、时间表、财务报表、课程表等需要双向滚动的数据展示场景。


二、OpenHarmony 平台适配说明

2.1 兼容性信息

本项目基于 two_dimensional_scrollables@0.0.2 开发,适配 Flutter 3.27.5-ohos-1.0.4。

2.2 支持的功能

在 OpenHarmony 平台上,two_dimensional_scrollables 支持以下功能:

功能 说明 OpenHarmony 支持
TableView 二维表格视图 ✅ yes
水平垂直滚动 同时支持两个方向滚动 ✅ yes
固定行列 固定行和列 ✅ yes
行列装饰 背景色、边框等装饰 ✅ yes
懒加载 按需构建单元格 ✅ yes
手势处理 自定义手势事件 ✅ yes

三、项目配置与安装

3.1 添加依赖配置

首先,需要在你的 Flutter 项目的 pubspec.yaml 文件中添加 two_dimensional_scrollables 依赖。

打开项目根目录下的 pubspec.yaml 文件,找到 dependencies 部分,添加以下配置:

dependencies:
  flutter:
    sdk: flutter

  # 添加 two_dimensional_scrollables 依赖
  two_dimensional_scrollables:
    git:
      url: "https://atomgit.com/openharmony-tpc/flutter_packages.git"
      path: "packages/two_dimensional_scrollables"

配置说明:

  • 使用 git 方式引用开源鸿蒙适配的 flutter_packages 仓库
  • url:指定 GitCode 托管的仓库地址
  • path:指定 two_dimensional_scrollables 包的具体路径
  • 本项目基于 two_dimensional_scrollables@0.0.2 开发,适配 Flutter 3.27.5-ohos-1.0.4

⚠️ 重要:对于 OpenHarmony 平台,必须使用 git 方式引用适配版本,不能直接使用 pub.dev 的版本号。

3.2 下载依赖

配置完成后,需要在项目根目录执行以下命令下载依赖:

flutter pub get

执行成功后,你会看到类似以下的输出:

Running "flutter pub get" in my_cross_platform_app...
Resolving dependencies...
Got dependencies!

四、two_dimensional_scrollables 基础用法

4.1 导入包

在使用 two_dimensional_scrollables 之前,首先需要导入相关包:

import 'package:flutter/material.dart';
import 'package:two_dimensional_scrollables/two_dimensional_scrollables.dart';
import 'package:flutter/gestures.dart';

💡 提示:如果需要在行列上添加手势识别(如点击、长按等),需要导入 package:flutter/gestures.dart

4.2 创建基本的 TableView

4.2.1 使用 builder 创建
TableView.builder(
  diagonalDragBehavior: DiagonalDragBehavior.free,
  columnCount: 10,
  rowCount: 20,
  columnBuilder: (int column) {
    return TableSpan(
      extent: const FixedTableSpanExtent(100),
      backgroundDecoration: TableSpanDecoration(
        color: column % 2 == 0 ? Colors.grey.shade100 : Colors.white,
      ),
    );
  },
  rowBuilder: (int row) {
    return TableSpan(
      extent: const FixedTableSpanExtent(50),
      backgroundDecoration: TableSpanDecoration(
        color: row % 2 == 0 ? Colors.grey.shade50 : Colors.white,
      ),
    );
  },
  cellBuilder: (BuildContext context, TableVicinity vicinity) {
    return Center(
      child: Text('${vicinity.column}, ${vicinity.row}'),
    );
  },
)

参数说明:

参数 说明 类型 默认值
columnCount 列数 int -
rowCount 行数 int -
columnBuilder 列构建器 TableSpan Function -
rowBuilder 行构建器 TableSpan Function -
cellBuilder 单元格构建器 Widget Function -
diagonalDragBehavior 对角拖拽行为 DiagonalDragBehavior free
pinnedColumnCount 固定列数 int 0
pinnedRowCount 固定行数 int 0
mainAxis 主轴方向 Axis vertical
cellDimensions 单元格尺寸 TableDimensions? null
4.2.2 使用 delegate 创建
TableView.builder(
  delegate: TwoDimensionalChildBuilderDelegate(
    maxColumnCount: 10,
    maxRowCount: 20,
    builder: (BuildContext context, TableVicinity vicinity) {
      return Container(
        color: (vicinity.row + vicinity.column) % 2 == 0
            ? Colors.grey.shade100
            : Colors.white,
        child: Center(
          child: Text('${vicinity.column}, ${vicinity.row}'),
        ),
      );
    },
  ),
  columnBuilder: (int column) {
    return TableSpan(
      extent: const FixedTableSpanExtent(100),
    );
  },
  rowBuilder: (int row) {
    return TableSpan(
      extent: const FixedTableSpanExtent(50),
    );
  },
)

4.3 设置行列装饰

4.3.1 背景颜色
columnBuilder: (int column) {
  return TableSpan(
    extent: const FixedTableSpanExtent(100),
    backgroundDecoration: TableSpanDecoration(
      color: column == 0 ? Colors.blue.shade100 : Colors.white,
      border: TableSpanBorder(
        trailing: BorderSide(color: Colors.grey.shade300),
      ),
    ),
  );
},

rowBuilder: (int row) {
  return TableSpan(
    extent: const FixedTableSpanExtent(50),
    backgroundDecoration: TableSpanDecoration(
      color: row == 0 ? Colors.blue.shade50 : Colors.white,
      border: TableSpanBorder(
        trailing: BorderSide(color: Colors.grey.shade300),
      ),
    ),
  );
},
4.3.2 边框样式
backgroundDecoration: TableSpanDecoration(
  color: Colors.white,
  border: TableSpanBorder(
    leading: BorderSide(color: Colors.grey.shade300, width: 1),
    trailing: BorderSide(color: Colors.grey.shade300, width: 1),
  ),
)

4.4 固定行列

TableView.builder(
  pinnedColumnCount: 1,  // 固定第一列
  pinnedRowCount: 1,     // 固定第一行
  columnCount: 10,
  rowCount: 20,
  columnBuilder: (int column) {
    return TableSpan(
      extent: const FixedTableSpanExtent(100),
      backgroundDecoration: TableSpanDecoration(
        color: column == 0 ? Colors.blue.shade100 : Colors.white,
      ),
    );
  },
  rowBuilder: (int row) {
    return TableSpan(
      extent: const FixedTableSpanExtent(50),
      backgroundDecoration: TableSpanDecoration(
        color: row == 0 ? Colors.blue.shade50 : Colors.white,
      ),
    );
  },
  cellBuilder: (BuildContext context, TableVicinity vicinity) {
    return Center(
      child: Text('${vicinity.column}, ${vicinity.row}'),
    );
  },
)

4.5 自定义单元格尺寸

// 固定尺寸
extent: const FixedTableSpanExtent(100),

// 百分比尺寸
extent: const FractionalTableSpanExtent(0.2),

// 自适应尺寸
extent: const IntrinsicTableSpanExtent(),

// 自定义尺寸
extent: TableSpanExtent.fixed(100),

五、完整示例代码

下面是一个完整的示例应用,展示了 two_dimensional_scrollables 的各种用法:

import 'package:flutter/material.dart';
import 'package:two_dimensional_scrollables/two_dimensional_scrollables.dart';
import 'package:flutter/gestures.dart';

void main() {
  runApp(const TwoDimensionalScrollablesDemo());
}

class TwoDimensionalScrollablesDemo extends StatelessWidget {
  const TwoDimensionalScrollablesDemo({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Two Dimensional Scrollables 示例',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const TableViewPage(),
    );
  }
}

class TableViewPage extends StatelessWidget {
  const TableViewPage({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('TableView 示例'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: const TableViewDemo(),
    );
  }
}

class TableViewDemo extends StatelessWidget {
  const TableViewDemo({super.key});

  static const int columnCount = 10;
  static const int rowCount = 20;
  static const int pinnedColumnCount = 1;
  static const int pinnedRowCount = 1;

  
  Widget build(BuildContext context) {
    return TableView.builder(
      diagonalDragBehavior: DiagonalDragBehavior.free,
      columnCount: columnCount,
      rowCount: rowCount,
      pinnedColumnCount: pinnedColumnCount,
      pinnedRowCount: pinnedRowCount,
      columnBuilder: _buildColumnSpan,
      rowBuilder: _buildRowSpan,
      cellBuilder: _buildCell,
    );
  }

  TableSpan _buildColumnSpan(int column) {
    final bool isPinned = column < pinnedColumnCount;
    return TableSpan(
      extent: const FixedTableSpanExtent(100),
      backgroundDecoration: TableSpanDecoration(
        color: isPinned ? Colors.blue.shade100 : Colors.white,
        border: TableSpanBorder(
          trailing: BorderSide(
            color: Colors.grey.shade300,
            width: 1,
          ),
        ),
      ),
    );
  }

  TableSpan _buildRowSpan(int row) {
    final bool isPinned = row < pinnedRowCount;
    return TableSpan(
      extent: const FixedTableSpanExtent(50),
      backgroundDecoration: TableSpanDecoration(
        color: isPinned ? Colors.blue.shade50 : Colors.white,
        border: TableSpanBorder(
          trailing: BorderSide(
            color: Colors.grey.shade300,
            width: 1,
          ),
        ),
      ),
    );
  }

  Widget _buildCell(BuildContext context, TableVicinity vicinity) {
    final bool isPinnedColumn = vicinity.column < pinnedColumnCount;
    final bool isPinnedRow = vicinity.row < pinnedRowCount;

    return Container(
      padding: const EdgeInsets.all(8),
      child: Center(
        child: Text(
          _getCellValue(vicinity),
          style: TextStyle(
            fontWeight: isPinnedColumn || isPinnedRow
                ? FontWeight.bold
                : FontWeight.normal,
            color: isPinnedColumn || isPinnedRow
                ? Colors.blue.shade900
                : Colors.black87,
          ),
        ),
      ),
    );
  }

  String _getCellValue(TableVicinity vicinity) {
    // 表头行
    if (vicinity.row == 0) {
      if (vicinity.column == 0) return '';
      return '列 ${vicinity.column}';
    }

    // 行标题列
    if (vicinity.column == 0) {
      return '行 ${vicinity.row}';
    }

    // 普通单元格
    return '${vicinity.column}-${vicinity.row}';
  }
}

// 带有数据的完整表格示例
class DataTableExample extends StatefulWidget {
  const DataTableExample({super.key});

  
  State<DataTableExample> createState() => _DataTableExampleState();
}

class _DataTableExampleState extends State<DataTableExample> {
  static const int columnCount = 8;
  static const int rowCount = 15;
  static const int pinnedColumnCount = 1;
  static const int pinnedRowCount = 1;

  // 模拟数据
  final List<List<String>> _data = List.generate(
    rowCount - 1,
    (row) => List.generate(
      columnCount - 1,
      (col) => '数据 ${row + 1}-${col + 1}',
    ),
  );

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('数据表格'),
        actions: [
          IconButton(
            icon: const Icon(Icons.refresh),
            onPressed: () {
              setState(() {});
            },
          ),
        ],
      ),
      body: TableView.builder(
        diagonalDragBehavior: DiagonalDragBehavior.free,
        columnCount: columnCount,
        rowCount: rowCount,
        pinnedColumnCount: pinnedColumnCount,
        pinnedRowCount: pinnedRowCount,
        columnBuilder: _buildColumnSpan,
        rowBuilder: _buildRowSpan,
        cellBuilder: _buildCell,
      ),
    );
  }

  TableSpan _buildColumnSpan(int column) {
    final bool isPinned = column < pinnedColumnCount;
    final List<String> columnNames = ['序号', '姓名', '年龄', '部门', '职位', '电话', '邮箱', '状态'];

    return TableSpan(
      extent: const FixedTableSpanExtent(120),
      backgroundDecoration: TableSpanDecoration(
        color: isPinned ? Colors.blue.shade100 : Colors.white,
        border: TableSpanBorder(
          trailing: BorderSide(
            color: Colors.grey.shade300,
            width: 1,
          ),
        ),
      ),
      recognizerFactories: <Type, GestureRecognizerFactory>{
        TapGestureRecognizer: GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
          () => TapGestureRecognizer(),
          (TapGestureRecognizer recognizer) {
            recognizer.onTap = () {
              _showColumnDetails(column);
            };
          },
        ),
      },
    );
  }

  TableSpan _buildRowSpan(int row) {
    final bool isPinned = row < pinnedRowCount;

    return TableSpan(
      extent: const FixedTableSpanExtent(60),
      backgroundDecoration: TableSpanDecoration(
        color: isPinned ? Colors.blue.shade50 : (row % 2 == 0 ? Colors.grey.shade50 : Colors.white),
        border: TableSpanBorder(
          trailing: BorderSide(
            color: Colors.grey.shade300,
            width: 1,
          ),
        ),
      ),
      recognizerFactories: <Type, GestureRecognizerFactory>{
        TapGestureRecognizer: GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
          () => TapGestureRecognizer(),
          (TapGestureRecognizer recognizer) {
            recognizer.onTap = () {
              _showRowDetails(row);
            };
          },
        ),
      },
    );
  }

  Widget _buildCell(BuildContext context, TableVicinity vicinity) {
    final bool isPinnedColumn = vicinity.column < pinnedColumnCount;
    final bool isPinnedRow = vicinity.row < pinnedRowCount;

    String? cellValue;

    // 表头行
    if (vicinity.row == 0) {
      final List<String> columnNames = ['序号', '姓名', '年龄', '部门', '职位', '电话', '邮箱', '状态'];
      cellValue = columnNames[vicinity.column];
    }
    // 行标题列
    else if (vicinity.column == 0) {
      cellValue = '${vicinity.row}';
    }
    // 普通单元格
    else {
      cellValue = _data[vicinity.row - 1][vicinity.column - 1];
    }

    return Container(
      padding: const EdgeInsets.all(8),
      decoration: BoxDecoration(
        color: (isPinnedColumn || isPinnedRow)
            ? Colors.transparent
            : (vicinity.row % 2 == 0 ? Colors.white : Colors.grey.shade100),
      ),
      child: Center(
        child: Text(
          cellValue,
          style: TextStyle(
            fontWeight: isPinnedColumn || isPinnedRow ? FontWeight.bold : FontWeight.normal,
            color: isPinnedColumn || isPinnedRow ? Colors.blue.shade900 : Colors.black87,
            fontSize: isPinnedRow ? 16 : 14,
          ),
        ),
      ),
    );
  }

  void _showColumnDetails(int column) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text('点击了列 $column'),
        duration: const Duration(seconds: 2),
      ),
    );
  }

  void _showRowDetails(int row) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text('点击了行 $row'),
        duration: const Duration(seconds: 2),
      ),
    );
  }
}

六、高级用法

6.1 动态加载数据

class DynamicTableView extends StatefulWidget {
  const DynamicTableView({super.key});

  
  State<DynamicTableView> createState() => _DynamicTableViewState();
}

class _DynamicTableViewState extends State<DynamicTableView> {
  final Map<TableVicinity, String> _data = {};
  final int _columnCount = 10;
  final int _rowCount = 20;

  
  Widget build(BuildContext context) {
    return TableView.builder(
      columnCount: _columnCount,
      rowCount: _rowCount,
      columnBuilder: (int column) {
        return TableSpan(
          extent: const FixedTableSpanExtent(100),
        );
      },
      rowBuilder: (int row) {
        return TableSpan(
          extent: const FixedTableSpanExtent(50),
        );
      },
      cellBuilder: (BuildContext context, TableVicinity vicinity) {
        // 动态加载数据
        final cellValue = _data.putIfAbsent(
          vicinity,
          () => _loadCellData(vicinity),
        );

        return Center(child: Text(cellValue));
      },
    );
  }

  String _loadCellData(TableVicinity vicinity) {
    // 模拟异步数据加载
    return '${vicinity.column}-${vicinity.row}';
  }
}

6.2 合并单元格

Widget _buildCell(BuildContext context, TableVicinity vicinity) {
  // 检查是否是合并单元格的起始位置
  if (vicinity.row == 1 && vicinity.column == 1) {
    return Container(
      color: Colors.blue.shade200,
      child: const Center(
        child: Text('合并单元格 (2x2)'),
      ),
    );
  }

  // 检查是否是被合并的单元格,返回空
  if ((vicinity.row == 1 || vicinity.row == 2) &&
      (vicinity.column == 1 || vicinity.column == 2)) {
    return const SizedBox.shrink();
  }

  return Center(
    child: Text('${vicinity.column}, ${vicinity.row}'),
  );
}

6.3 自定义滚动行为

class CustomScrollingTable extends StatelessWidget {
  const CustomScrollingTable({super.key});

  
  Widget build(BuildContext context) {
    return TableView.builder(
      diagonalDragBehavior: DiagonalDragBehavior.none, // 禁用对角拖拽
      mainAxis: Axis.horizontal, // 主轴为水平方向
      columnCount: 10,
      rowCount: 20,
      columnBuilder: (int column) {
        return TableSpan(
          extent: const FixedTableSpanExtent(100),
        );
      },
      rowBuilder: (int row) {
        return TableSpan(
          extent: const FixedTableSpanExtent(50),
        );
      },
      cellBuilder: (BuildContext context, TableVicinity vicinity) {
        return Center(
          child: Text('${vicinity.column}, ${vicinity.row}'),
        );
      },
    );
  }
}

七、常见问题与最佳实践

7.1 常见问题

Q1: 为什么表格性能不好?

A: 可能的原因和解决方案:

  • 单元格过于复杂:简化单元格内容
  • 没有使用懒加载:确保使用 builder 模式
  • 列数行数过多:考虑分页或虚拟滚动
// ✅ 使用 builder 模式
TableView.builder(
  columnCount: columnCount,
  rowCount: rowCount,
  cellBuilder: (context, vicinity) {
    return SimpleCell(vicinity);
  },
)

// ❌ 避免一次性构建所有单元格
ListView(
  children: List.generate(columnCount * rowCount, (index) {
    return ComplexCell(index);
  }),
)
Q2: 如何实现单元格选择?

A: 使用 GestureDetector 和状态管理:

class SelectableTable extends StatefulWidget {
  const SelectableTable({super.key});

  
  State<SelectableTable> createState() => _SelectableTableState();
}

class _SelectableTableState extends State<SelectableTable> {
  final Set<TableVicinity> _selectedCells = {};

  Widget _buildCell(BuildContext context, TableVicinity vicinity) {
    final isSelected = _selectedCells.contains(vicinity);

    return GestureDetector(
      onTap: () {
        setState(() {
          if (isSelected) {
            _selectedCells.remove(vicinity);
          } else {
            _selectedCells.add(vicinity);
          }
        });
      },
      child: Container(
        decoration: BoxDecoration(
          color: isSelected ? Colors.blue.withOpacity(0.3) : Colors.white,
          border: Border.all(color: Colors.grey.shade300),
        ),
        child: Center(
          child: Text('${vicinity.column}, ${vicinity.row}'),
        ),
      ),
    );
  }
}
Q3: 如何实现滚动到指定位置?

A: 使用 ScrollController:

class ScrollToCellTable extends StatefulWidget {
  const ScrollToCellTable({super.key});

  
  State<ScrollToCellTable> createState() => _ScrollToCellTableState();
}

class _ScrollToCellTableState extends State<ScrollToCellTable> {
  final ScrollController _verticalController = ScrollController();
  final ScrollController _horizontalController = ScrollController();

  void scrollToCell(int column, int row) {
    // 计算目标位置
    final double targetX = column * 100.0;
    final double targetY = row * 50.0;

    // 滚动到目标位置
    _verticalController.animateTo(
      targetY,
      duration: const Duration(milliseconds: 300),
      curve: Curves.easeInOut,
    );

    _horizontalController.animateTo(
      targetX,
      duration: const Duration(milliseconds: 300),
      curve: Curves.easeInOut,
    );
  }

  
  Widget build(BuildContext context) {
    return TableView.builder(
      verticalDetails: ScrollableDetails.vertical(
        controller: _verticalController,
      ),
      horizontalDetails: ScrollableDetails.horizontal(
        controller: _horizontalController,
      ),
      columnCount: 10,
      rowCount: 20,
      cellBuilder: (context, vicinity) {
        return Center(
          child: Text('${vicinity.column}, ${vicinity.row}'),
        );
      },
      columnBuilder: (int column) {
        return TableSpan(
          extent: const FixedTableSpanExtent(100),
        );
      },
      rowBuilder: (int row) {
        return TableSpan(
          extent: const FixedTableSpanExtent(50),
        );
      },
    );
  }

  
  void dispose() {
    _verticalController.dispose();
    _horizontalController.dispose();
    super.dispose();
  }
}

7.2 最佳实践

1. 优化单元格渲染
Widget _buildCell(BuildContext context, TableVicinity vicinity) {
  // ✅ 使用 const 构造函数
  return const CellContent();

  // ❌ 避免在 build 中创建新对象
  return CellContent(
    color: Colors.white,
    padding: EdgeInsets.all(8),
  );
}
2. 使用状态管理
// 使用 Provider 管理表格数据
class TableProvider extends ChangeNotifier {
  final Map<TableVicinity, String> _data = {};

  void updateCell(TableVicinity vicinity, String value) {
    _data[vicinity] = value;
    notifyListeners();
  }

  String? getCell(TableVicinity vicinity) {
    return _data[vicinity];
  }
}

// 使用
Consumer<TableProvider>(
  builder: (context, provider, child) {
    return TableView.builder(
      cellBuilder: (context, vicinity) {
        final value = provider.getCell(vicinity);
        return Text(value ?? '');
      },
    );
  },
)
3. 实现懒加载
class LazyLoadingTable extends StatefulWidget {
  const LazyLoadingTable({super.key});

  
  State<LazyLoadingTable> createState() => _LazyLoadingTableState();
}

class _LazyLoadingTableState extends State<LazyLoadingTable> {
  final Map<TableVicinity, Future<String>> _loadingCells = {};

  Widget _buildCell(BuildContext context, TableVicinity vicinity) {
    final future = _loadingCells.putIfAbsent(
      vicinity,
      () => _loadCellData(vicinity),
    );

    return FutureBuilder<String>(
      future: future,
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return const CircularProgressIndicator();
        }
        if (snapshot.hasError) {
          return const Icon(Icons.error);
        }
        return Text(snapshot.data ?? '');
      },
    );
  }

  Future<String> _loadCellData(TableVicinity vicinity) async {
    // 模拟异步加载
    await Future.delayed(const Duration(milliseconds: 100));
    return '${vicinity.column}-${vicinity.row}';
  }
}
4. 实现响应式布局
class ResponsiveTable extends StatelessWidget {
  const ResponsiveTable({super.key});

  
  Widget build(BuildContext context) {
    final screenWidth = MediaQuery.of(context).size.width;
    final isTablet = screenWidth > 600;

    return LayoutBuilder(
      builder: (context, constraints) {
        final columnCount = isTablet ? 10 : 5;
        final columnWidth = (constraints.maxWidth / columnCount).clamp(80.0, 150.0);

        return TableView.builder(
          columnCount: columnCount,
          rowCount: 20,
          columnBuilder: (int column) {
            return TableSpan(
              extent: FixedTableSpanExtent(columnWidth),
            );
          },
          rowBuilder: (int row) {
            return TableSpan(
              extent: const FixedTableSpanExtent(50),
            );
          },
          cellBuilder: (context, vicinity) {
            return Center(
              child: Text('${vicinity.column}, ${vicinity.row}'),
            );
          },
        );
      },
    );
  }
}

八、总结

恭喜你!通过这篇文章的学习,你已经掌握了 Flutter 中 two_dimensional_scrollables 二维滚动组件的全面知识。

🎯 核心要点

  1. 双向滚动:支持同时水平和垂直滚动
  2. 懒加载:只渲染可见区域的单元格,性能优化
  3. 固定行列:支持固定行和列,滚动时保持可见
  4. 装饰支持:支持行列背景色、边框等装饰效果
  5. 灵活构建:提供多种构建方式适应不同需求

📚 使用场景指南

场景 推荐配置 说明
电子表格 固定行列 + 懒加载 大量数据展示
数据表格 固定表头 + 分页加载 结构化数据展示
日历视图 固定行 + 自适应列宽 时间相关数据展示
时间表 固定行 + 固定列 固定行列的时间表
财务报表 固定表头 + 格式化单元格 数值型数据展示
课程表 固定行列 + 颜色区分 时间 + 位置信息展示

🚀 进阶方向

掌握了 two_dimensional_scrollables 后,你还可以探索以下方向:

  1. 高级交互:实现单元格拖拽、排序、筛选等功能
  2. 数据编辑:实现单元格内联编辑、批量编辑等功能
  3. 导出功能:实现表格导出为 Excel、CSV 等格式
  4. 图表集成:在表格中集成图表展示
  5. 实时更新:实现数据的实时更新和同步

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐