在算法工程化开发中,折叠面板是 KMP 匹配结果展示、模式串分组筛选、匹配日志分类的核心高频组件(如多模式串匹配结果折叠、分段匹配日志、算法参数设置分组)。原生无现成折叠面板实现方案,手动基于 AnimatedContainer+GestureDetector 封装时,易出现动画卡顿、内容高度适配失效、多面板联动逻辑混乱、样式碎片化等问题。本文封装的 KmpExpandablePanelWidget 整合平滑高度自适应动画 + 单 / 多面板灵活切换 + 全样式细粒度自定义 + 内容无限扩展 四大核心能力,支持外部强制控制面板状态、深色模式一键适配,一行代码集成即可覆盖 90%+ KMP 相关折叠场景!
一、核心优势(精准解决 KMP 开发痛点)
- 高度自适应平滑动画:基于
AnimatedContainer实现展开 / 折叠隐式动画,内容高度自动适配 KMP 匹配结果、日志文本等动态内容,过渡自然无卡顿,支持自定义动画时长(300-500ms 最优) - 单 / 多面板灵活切换:单面板模式(仅一个展开,自动关闭其他)、多面板模式(多个可同时展开)一键切换,适配 KMP 多模式串匹配结果独立展示场景
- 全样式细粒度自定义:面板背景、边框、圆角、标题样式、展开 / 折叠图标、分隔线颜色均可独立配置,完美贴合算法工具类应用视觉主题
- 内容无限扩展能力:支持任意 Widget 作为面板内容(KMP 匹配结果文本、模式串列表、算法参数表单等),内置内容滚动适配,避免长日志、多结果溢出问题
- 状态双向可控:内部维护展开状态,同时提供
expandPanel外部控制接口,支持根据 KMP 匹配进度强制展开 / 折叠指定面板(如匹配完成后自动展开结果面板) - 全场景适配:自动兼容深色模式、不同屏幕尺寸,面板间距 / 内边距独立配置,无多设备适配成本,适配算法可视化工具多端展示需求
二、核心配置速览(关键参数一目了然)
| 配置分类 | 核心参数 | 类型 / 默认值 | 核心作用 |
|---|---|---|---|
| 必选配置 | panels |
List<KmpExpandablePanel>(必填) |
折叠面板列表(包含标题、KMP 相关内容、初始展开状态、自定义图标 / 操作组件) |
| 功能配置 | isSingleExpand |
bool(true) |
是否单面板模式(true = 仅一个展开,false = 多面板同时展开) |
| 功能配置 | initialExpandedIndex |
int?(null) |
单面板模式初始展开索引(需在面板列表范围内,如默认展开 KMP 匹配结果面板) |
| 功能配置 | animationDuration |
Duration(milliseconds:300) |
展开 / 折叠动画时长(建议 300-500ms) |
| 功能配置 | onPanelToggle |
Function(int, bool)?(null) |
面板状态切换回调(参数:面板索引、是否展开,适配 KMP 匹配状态联动) |
| 功能配置 | enableAnimation |
bool(true) |
是否启用动画(禁用时无过渡效果,适配性能敏感的算法实时展示场景) |
| 样式配置 | panelHeight |
double(50.0) |
面板标题栏高度 |
| 样式配置 | borderRadius |
double(8.0) |
面板整体圆角半径 |
| 样式配置 | bgColor |
Color(Colors.white) |
面板背景色 |
| 样式配置 | borderColor |
Color(0xFFF5F5F5) |
面板边框色 |
| 样式配置 | dividerColor |
Color(0xFFF5F5F5) |
标题栏与 KMP 内容的分隔线颜色 |
| 样式配置 | titleStyle |
TextStyle(fontSize:16, color:0xFF333333) |
标题文本样式(如 "KMP 匹配结果"、"模式串列表" 标题) |
| 样式配置 | expandedIcon |
Icon(Icons.keyboard_arrow_down) |
展开状态图标 |
| 样式配置 | collapsedIcon |
Icon(Icons.keyboard_arrow_right) |
折叠状态图标 |
| 样式配置 | iconSize |
double(24.0) |
展开 / 折叠图标大小 |
| 样式配置 | contentPadding |
EdgeInsets.symmetric(horizontal:16, vertical:12) |
KMP 相关内容区域内边距(如匹配文本、日志内容) |
| 样式配置 | panelPadding |
EdgeInsets.symmetric(horizontal:16, vertical:8) |
面板外部间距 |
| 适配配置 | adaptDarkMode |
bool(true) |
是否自动适配深色模式(适配算法工具夜间使用场景) |
三、生产级完整代码(可直接复制,开箱即用)
dart
import 'package:flutter/material.dart';
/// KMP 折叠面板数据模型(单面板配置)
class KmpExpandablePanel {
final String title; // 面板标题(如 "KMP 匹配结果"、"模式串配置")
final Widget content; // 面板内容(支持 KMP 匹配文本、列表、表单等任意Widget)
final bool isInitialExpanded; // 多面板模式下初始展开状态
final Widget? prefixIcon; // 标题左侧自定义图标(如 KMP 算法图标)
final Widget? suffixWidget; // 标题右侧自定义操作组件(优先级高于展开/折叠图标)
final bool isDisabled; // 是否禁用当前面板(禁用后不可点击)
KmpExpandablePanel({
required this.title,
required this.content,
this.isInitialExpanded = false,
this.prefixIcon,
this.suffixWidget,
this.isDisabled = false,
});
}
/// KMP 通用折叠面板组件(支持单/多面板、平滑动画、深色模式适配)
class KmpExpandablePanelWidget extends StatefulWidget {
// 必选参数
final List<KmpExpandablePanel> panels;
// 功能配置
final bool isSingleExpand;
final int? initialExpandedIndex;
final Duration animationDuration;
final Function(int, bool)? onPanelToggle;
final bool enableAnimation;
final double? contentMaxHeight; // 内容区域最大高度(避免 KMP 长日志溢出)
// 样式配置
final double panelHeight;
final double borderRadius;
final Color bgColor;
final Color borderColor;
final Color dividerColor;
final TextStyle titleStyle;
final Widget expandedIcon;
final Widget collapsedIcon;
final double iconSize;
final EdgeInsetsGeometry contentPadding;
final EdgeInsetsGeometry panelPadding;
final Color disabledColor; // 禁用状态面板颜色
// 适配配置
final bool adaptDarkMode;
const KmpExpandablePanelWidget({
super.key,
required this.panels,
// 功能配置
this.isSingleExpand = true,
this.initialExpandedIndex,
this.animationDuration = const Duration(milliseconds: 300),
this.onPanelToggle,
this.enableAnimation = true,
this.contentMaxHeight,
// 样式配置
this.panelHeight = 50.0,
this.borderRadius = 8.0,
this.bgColor = Colors.white,
this.borderColor = const Color(0xFFF5F5F5),
this.dividerColor = const Color(0xFFF5F5F5),
this.titleStyle = const TextStyle(
fontSize: 16,
color: Color(0xFF333333),
),
this.expandedIcon = const Icon(Icons.keyboard_arrow_down),
this.collapsedIcon = const Icon(Icons.keyboard_arrow_right),
this.iconSize = 24.0,
this.contentPadding = const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
this.panelPadding = const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
this.disabledColor = const Color(0xFFF8F8F8),
// 适配配置
this.adaptDarkMode = true,
})
: assert(panels.isNotEmpty, "KMP 面板列表不可为空!"),
assert(
!isSingleExpand || (initialExpandedIndex == null || (initialExpandedIndex >= 0 && initialExpandedIndex < panels.length)),
"单面板模式下初始展开索引需在 KMP 面板列表范围内!",
);
// 外部控制面板展开/折叠的方法(适配 KMP 算法状态联动)
void expandPanel(BuildContext context, int index, {required bool isExpanded}) {
final state = context.findAncestorStateOfType<_KmpExpandablePanelWidgetState>();
state?._expandPanel(index, isExpanded: isExpanded);
}
@override
State<KmpExpandablePanelWidget> createState() => _KmpExpandablePanelWidgetState();
}
class _KmpExpandablePanelWidgetState extends State<KmpExpandablePanelWidget> {
late List<bool> _expandedStates; // 面板展开状态列表
@override
void initState() {
super.initState();
// 初始化展开状态
_initExpandedStates();
}
@override
void didUpdateWidget(covariant KmpExpandablePanelWidget oldWidget) {
super.didUpdateWidget(oldWidget);
// KMP 面板列表/初始索引/模式变化时重新初始化状态
if (widget.panels != oldWidget.panels ||
widget.initialExpandedIndex != oldWidget.initialExpandedIndex ||
widget.isSingleExpand != oldWidget.isSingleExpand) {
_initExpandedStates();
}
}
/// 初始化面板展开状态
void _initExpandedStates() {
_expandedStates = List.generate(widget.panels.length, (index) {
final panel = widget.panels[index];
// 单面板模式:优先使用初始索引,禁用面板默认折叠
if (widget.isSingleExpand) {
return !panel.isDisabled && widget.initialExpandedIndex == index;
}
// 多面板模式:使用面板自身初始状态,禁用面板默认折叠
return !panel.isDisabled && panel.isInitialExpanded;
});
}
/// 外部控制面板展开/折叠(适配 KMP 算法外部状态控制)
void _expandPanel(int index, {required bool isExpanded}) {
if (index < 0 || index >= widget.panels.length || widget.panels[index].isDisabled) return;
setState(() {
if (widget.isSingleExpand && isExpanded) {
// 单面板模式:仅展开指定 KMP 面板,关闭其他
_expandedStates = List.generate(widget.panels.length, (i) => i == index);
} else {
// 多面板模式:仅修改指定面板状态
_expandedStates[index] = isExpanded;
}
widget.onPanelToggle?.call(index, isExpanded);
});
}
/// 切换面板展开/折叠状态(内部点击触发)
void _togglePanel(int index) {
final panel = widget.panels[index];
if (panel.isDisabled) return; // 禁用面板不响应点击
setState(() {
if (widget.isSingleExpand) {
// 单面板模式:关闭所有,展开当前 KMP 面板
_expandedStates = List.generate(widget.panels.length, (i) => i == index);
} else {
// 多面板模式:切换当前面板状态
_expandedStates[index] = !_expandedStates[index];
}
// 触发外部回调(适配 KMP 算法状态更新)
widget.onPanelToggle?.call(index, _expandedStates[index]);
});
}
/// 深色模式颜色适配
Color _adaptDarkMode(Color lightColor, Color darkColor) {
if (!widget.adaptDarkMode) return lightColor;
return MediaQuery.platformBrightnessOf(context) == Brightness.dark ? darkColor : lightColor;
}
/// 构建单个 KMP 折叠面板
Widget _buildSinglePanel(int index) {
final panel = widget.panels[index];
final isExpanded = _expandedStates[index];
final isDisabled = panel.isDisabled;
// 深色模式适配后的颜色
final adaptedBgColor = _adaptDarkMode(
isDisabled ? widget.disabledColor : widget.bgColor,
isDisabled ? const Color(0xFF3A3A3A) : const Color(0xFF333333),
);
final adaptedBorderColor = _adaptDarkMode(
widget.borderColor,
const Color(0xFF444444),
);
final adaptedTitleStyle = widget.titleStyle.copyWith(
color: _adaptDarkMode(
widget.titleStyle.color ?? const Color(0xFF333333),
Colors.white70,
),
opacity: isDisabled ? 0.6 : 1.0,
);
final adaptedDividerColor = _adaptDarkMode(
widget.dividerColor,
const Color(0xFF444444),
);
final adaptedIconColor = _adaptDarkMode(
const Color(0xFF999999),
const Color(0xFF777777),
);
// 展开/折叠图标(适配颜色和大小)
Widget toggleIcon = isExpanded ? widget.expandedIcon : widget.collapsedIcon;
toggleIcon = IconTheme(
data: IconThemeData(
size: widget.iconSize,
color: adaptedIconColor,
opacity: isDisabled ? 0.6 : 1.0,
),
child: toggleIcon,
);
// 标题栏:整行可点击,禁用状态灰化
final titleBar = GestureDetector(
onTap: () => _togglePanel(index),
behavior: HitTestBehavior.opaque, // 确保整行可点击
child: Container(
height: widget.panelHeight,
padding: const EdgeInsets.symmetric(horizontal: 16),
alignment: Alignment.center,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// 左侧:前缀图标 + KMP 相关标题
Row(
children: [
if (panel.prefixIcon != null) ...[
IconTheme(
data: IconThemeData(
size: widget.iconSize,
color: adaptedIconColor,
opacity: isDisabled ? 0.6 : 1.0,
),
child: panel.prefixIcon!,
),
const SizedBox(width: 8),
],
Text(
panel.title,
style: adaptedTitleStyle,
),
],
),
// 右侧:自定义操作组件 > 展开/折叠图标
panel.suffixWidget ?? toggleIcon,
],
),
),
);
// 面板内容:动画控制高度,支持滚动和最大高度限制(适配 KMP 长日志)
final content = AnimatedContainer(
duration: widget.enableAnimation ? widget.animationDuration : Duration.zero,
height: isExpanded ? null : 0,
padding: isExpanded ? widget.contentPadding : EdgeInsets.zero,
constraints: widget.contentMaxHeight != null
? BoxConstraints(maxHeight: widget.contentMaxHeight!)
: null,
child: isExpanded
? SingleChildScrollView(
physics: const ClampingScrollPhysics(), // 避免滚动冲突
child: panel.content,
)
: null,
);
// 面板容器:包含边框、圆角、背景色
return Container(
margin: const EdgeInsets.only(bottom: 1), // 面板间分隔线效果
decoration: BoxDecoration(
color: adaptedBgColor,
border: Border.all(color: adaptedBorderColor),
borderRadius: BorderRadius.circular(widget.borderRadius),
),
child: Column(
mainAxisSize: MainAxisSize.min, // 自适应高度
children: [
titleBar,
// 展开状态显示分隔线
if (isExpanded && !isDisabled)
Container(
height: 1,
color: adaptedDividerColor,
),
content,
],
),
);
}
@override
Widget build(BuildContext context) {
return Padding(
padding: widget.panelPadding,
child: Column(
mainAxisSize: MainAxisSize.min,
children: List.generate(widget.panels.length, (index) => _buildSinglePanel(index)),
),
);
}
}
四、三大 KMP 高频场景实战示例(直接复制可用)
场景 1:KMP 匹配结果面板(单面板模式)
适用场景:KMP 单模式串匹配结果、分段匹配日志、算法执行详情等需逐个展开的场景
dart
class KmpMatchResultDemo extends StatelessWidget {
const KmpMatchResultDemo({super.key});
// 构建 KMP 匹配结果面板列表
final List<KmpExpandablePanel> _kmpMatchPanels = [
KmpExpandablePanel(
title: "KMP 匹配基本信息",
isInitialExpanded: true,
prefixIcon: const Icon(Icons.code_outlined),
content: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("文本串:ABCABDABABCC", style: TextStyle(height: 1.5)),
SizedBox(height: 8),
Text("模式串:ABABC", style: TextStyle(height: 1.5)),
SizedBox(height: 8),
Text("匹配方式:单模式串精确匹配", style: TextStyle(height: 1.5)),
SizedBox(height: 8),
Text("匹配耗时:12ms", style: TextStyle(height: 1.5, color: Colors.greenAccent)),
],
),
),
KmpExpandablePanel(
title: "部分匹配表(PMT)",
prefixIcon: const Icon(Icons.table_chart_outlined),
content: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("模式串索引:0 1 2 3 4", style: TextStyle(height: 1.5)),
SizedBox(height: 8),
Text("模式串内容:A B A B C", style: TextStyle(height: 1.5)),
SizedBox(height: 8),
Text("PMT 值: 0 0 1 2 0", style: TextStyle(height: 1.5, color: Color(0xFF0066FF))),
],
),
),
KmpExpandablePanel(
title: "匹配结果详情",
prefixIcon: const Icon(Icons.find_in_page_outlined),
content: const Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.check_circle_outlined, size: 60, color: Colors.greenAccent),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("匹配成功", style: TextStyle(fontWeight: FontWeight.w500)),
SizedBox(height: 4),
Text("匹配起始位置:7(0-based)", style: TextStyle(color: Color(0xFF666666))),
SizedBox(height: 4),
Text("匹配长度:5(与模式串长度一致)", style: TextStyle(color: Color(0xFF666666))),
SizedBox(height: 4),
Text("匹配子串:ABABC", style: TextStyle(color: Colors.redAccent)),
],
),
),
],
),
),
// 禁用面板示例(日志导出已完成)
KmpExpandablePanel(
title: "匹配日志导出",
prefixIcon: const Icon(Icons.download_outlined),
isDisabled: true,
content: const Text("KMP 匹配日志已导出至本地(路径:/storage/kmp_logs/20250618.txt)"),
),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("KMP 匹配结果详情")),
body: SingleChildScrollView( // 避免面板过多溢出
child: KmpExpandablePanelWidget(
panels: _kmpMatchPanels,
isSingleExpand: true,
initialExpandedIndex: 0,
onPanelToggle: (index, isExpanded) {
debugPrint("KMP 面板${_kmpMatchPanels[index].title} ${isExpanded ? "展开" : "折叠"}");
},
panelHeight: 55,
borderRadius: 12,
bgColor: const Color(0xFFFAFAFA),
borderColor: const Color(0xFFF0F0F0),
titleStyle: const TextStyle(fontSize: 17, fontWeight: FontWeight.w500),
animationDuration: const Duration(milliseconds: 400),
contentMaxHeight: 300, // 限制内容最大高度(适配长日志)
),
),
);
}
}
场景 2:KMP 多模式串筛选面板(多面板模式)
适用场景:KMP 多模式串匹配筛选、匹配结果过滤、算法参数配置等需同时展开多个条件的场景
dart
class KmpFilterPanelDemo extends StatefulWidget {
const KmpFilterPanelDemo({super.key});
@override
State<KmpFilterPanelDemo> createState() => _KmpFilterPanelDemoState();
}
class _KmpFilterPanelDemoState extends State<KmpFilterPanelDemo> {
late List<KmpExpandablePanel> _kmpFilterPanels;
List<String> _selectedPatterns = []; // 选中的模式串
List<String> _selectedMatchTypes = []; // 选中的匹配类型
@override
void initState() {
super.initState();
_initFilterPanels();
}
/// 初始化 KMP 筛选面板
void _initFilterPanels() {
_kmpFilterPanels = [
KmpExpandablePanel(
title: "模式串选择",
isInitialExpanded: true,
content: Wrap(
spacing: 8,
runSpacing: 8,
children: ["ABABC", "ABCABD", "ABABCC", "ABD", "ABC"]
.map((pattern) => _buildFilterTag(
text: pattern,
isSelected: _selectedPatterns.contains(pattern),
onTap: () => setState(() {
_selectedPatterns.contains(pattern)
? _selectedPatterns.remove(pattern)
: _selectedPatterns.add(pattern);
}),
))
.toList(),
),
),
KmpExpandablePanel(
title: "匹配类型",
isInitialExpanded: true,
content: Wrap(
spacing: 8,
runSpacing: 8,
children: ["精确匹配", "前缀匹配", "后缀匹配", "模糊匹配(允许1个误差)"]
.map((type) => _buildFilterTag(
text: type,
isSelected: _selectedMatchTypes.contains(type),
onTap: () => setState(() {
_selectedMatchTypes.contains(type)
? _selectedMatchTypes.remove(type)
: _selectedMatchTypes.add(type);
}),
))
.toList(),
),
),
KmpExpandablePanel(
title: "匹配选项",
content: const Column(
children: [
ListTile(
title: Text("显示部分匹配表"),
trailing: Icon(Icons.check, color: Color(0xFF0066FF)),
onTap: null,
),
ListTile(
title: Text("显示匹配过程日志"),
onTap: null,
),
ListTile(
title: Text("忽略大小写"),
onTap: null,
),
],
),
),
];
}
/// 构建筛选标签
Widget _buildFilterTag({
required String text,
required bool isSelected,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFFE6F7FF) : Colors.white,
border: Border.all(
color: isSelected ? const Color(0xFF0066FF) : const Color(0xFFE0E0E0),
),
borderRadius: BorderRadius.circular(16),
),
child: Text(
text,
style: TextStyle(
color: isSelected ? const Color(0xFF0066FF) : const Color(0xFF333333),
fontSize: 14,
),
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("KMP 多模式串筛选")),
body: Column(
children: [
Expanded(
child: SingleChildScrollView(
child: KmpExpandablePanelWidget(
panels: _kmpFilterPanels,
isSingleExpand: false, // 多面板模式
panelHeight: 50,
borderRadius: 8,
bgColor: Colors.white,
borderColor: const Color(0xFFF5F5F5),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
),
),
// 筛选确认按钮
Padding(
padding: const EdgeInsets.all(16),
child: ElevatedButton(
onPressed: () {
debugPrint("KMP 筛选条件:模式串=$_selectedPatterns,匹配类型=$_selectedMatchTypes");
// 实际业务:执行 KMP 多模式串匹配
// KmpAlgorithm.multiPatternMatch(text: "目标文本串", patterns: _selectedPatterns, matchTypes: _selectedMatchTypes);
Navigator.pop(context);
},
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 44),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text("执行 KMP 匹配"),
),
),
],
),
);
}
}
场景 3:KMP 算法设置面板(带自定义图标 / 操作)
适用场景:KMP 算法参数设置、工具配置、个人中心等带图标 / 自定义操作的折叠面板
dart
class KmpSettingsPanelDemo extends StatelessWidget {
const KmpSettingsPanelDemo({super.key});
final List<KmpExpandablePanel> _kmpSettingPanels = [
KmpExpandablePanel(
title: "KMP 核心参数",
prefixIcon: const Icon(Icons.tune_outlined, color: Color(0xFF0066FF)),
content: const Column(
children: [
ListTile(
title: Text("匹配超时时间"),
subtitle: Text("默认 5000ms,长文本串可延长"),
trailing: Icon(Icons.arrow_forward_ios, size: 16),
onTap: null,
),
ListTile(
title: Text("最大匹配次数"),
subtitle: Text("默认无限制,多模式串可限制"),
trailing: Icon(Icons.arrow_forward_ios, size: 16),
onTap: null,
),
ListTile(
title: Text("PMT 计算优化"),
subtitle: Text("启用后提升长模式串匹配性能"),
trailing: Icon(Icons.arrow_forward_ios, size: 16),
onTap: null,
),
],
),
),
KmpExpandablePanel(
title: "结果展示设置",
prefixIcon: const Icon(Icons.visibility_outlined, color: Color(0xFF0066FF)),
content: const Column(
children: [
ListTile(
title: Text("显示匹配位置索引"),
trailing: Switch(value: true, onChanged: null),
),
ListTile(
title: Text("显示匹配耗时统计"),
trailing: Switch(value: true, onChanged: null),
),
ListTile(
title: Text("显示 PMT 表"),
trailing: Switch(value: false, onChanged: null),
),
],
),
),
KmpExpandablePanel(
title: "高级配置",
prefixIcon: const Icon(Icons.settings_applications_outlined, color: Color(0xFF0066FF)),
suffixWidget: const Text("更多", style: TextStyle(color: Color(0xFF0066FF), fontSize: 14)),
content: const Column(
children: [
ListTile(
title: Text("内存优化模式"),
subtitle: Text("低内存模式下降低匹配速度,减少内存占用"),
trailing: Icon(Icons.arrow_forward_ios, size: 16),
onTap: null,
),
ListTile(
title: Text("清除匹配缓存"),
trailing: Text("3.2MB", style: TextStyle(color: Colors.grey)),
onTap: null,
),
ListTile(
title: Text("算法版本选择"),
subtitle: Text("当前版本:KMP v2.1(支持多模式串并行匹配)"),
trailing: Icon(Icons.arrow_forward_ios, size: 16),
onTap: null,
),
],
),
),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("KMP 算法设置")),
body: SingleChildScrollView(
child: KmpExpandablePanelWidget(
panels: _kmpSettingPanels,
isSingleExpand: true,
panelHeight: 55,
borderRadius: 0, // 无边角,贴合设置页风格
borderColor: Colors.transparent, // 无边框
dividerColor: const Color(0xFFF5F5F5),
titleStyle: const TextStyle(fontSize: 17),
expandedIcon: const Icon(Icons.keyboard_arrow_down, color: Color(0xFF999999)),
collapsedIcon: const Icon(Icons.keyboard_arrow_right, color: Color(0xFF999999)),
panelPadding: EdgeInsets.zero, // 取消外部间距
),
),
);
}
}
五、核心封装技巧(适配 KMP 算法场景)
- 隐式动画最优实践:基于
AnimatedContainer实现高度自适应动画,通过mainAxisSize: MainAxisSize.min让面板高度跟随 KMP 匹配结果、日志文本等动态内容自适应,避免手动计算高度导致的动画卡顿 - 状态分层管理:内部维护
_expandedStates列表存储面板状态,单 / 多面板模式通过不同的状态更新逻辑实现,适配 KMP 多模式串独立展示需求 - 内容溢出防护:面板内容默认包裹
SingleChildScrollView,并提供contentMaxHeight参数限制最大高度,适配 KMP 长日志、多匹配结果展示场景 - 交互体验优化:标题栏设置
HitTestBehavior.opaque确保整行可点击,禁用面板自动灰化并屏蔽点击,符合算法工具类应用操作习惯 - 样式解耦设计:将标题、边框、图标、分隔线样式拆分为独立参数,支持细粒度自定义,降低 KMP 工具多主题维护成本
- 鲁棒性增强:通过断言校验面板列表非空、初始索引合法,避免运行时异常,适配 KMP 算法动态更新场景
六、避坑指南(解决 KMP 开发 90% 痛点)
- 动画卡顿 / 不流畅:使用 Column/Wrap 等自适应高度 Widget 展示 KMP 结果,动画时长设置 300-500ms,简化内容 Widget 层级
- 内容溢出 / 空白:内容区域包裹
SingleChildScrollView,通过contentMaxHeight限制最大高度(建议 200-400px) - 单面板模式状态混乱:
initialExpandedIndex仅指定一个合法索引,单面板模式下统一通过该参数配置初始展开状态 - 面板点击无响应:设置
behavior: HitTestBehavior.opaque,确保标题栏全屏可点击 - 深色模式样式异常:所有可视化颜色通过
_adaptDarkMode处理,确保深色模式下文本与背景对比度≥4.5:1 - 多面板联动失效:单面板模式下更新状态时重新生成整个
_expandedStates列表,确保只有当前 KMP 面板展开 - 布局溢出(面板过多):将
KmpExpandablePanelWidget包裹在SingleChildScrollView中,设置physics: ClampingScrollPhysics()避免滚动冲突
七、扩展能力(KMP 场景按需定制)
- 面板禁用功能:已内置
isDisabled参数,适配 KMP 日志导出完成、算法执行中禁止操作等场景 - 内容懒加载:扩展
contentBuilder参数,实现 KMP 匹配结果展开时才构建内容 Widget,提升首屏加载性能 - 自定义动画效果:支持替换
AnimatedContainer为AnimatedSize/AnimatedCrossFade,实现 KMP 结果展示自定义动画 - 状态持久化:结合
SharedPreferences将 KMP 面板展开状态持久化存储,重启工具后恢复上次状态 - 面板拖拽排序:集成
flutter_reorderable_list实现 KMP 模式串面板拖拽排序,适配个性化配置场景
欢迎大家加入[开源鸿蒙跨平台开发者社区](https://openharmonycrossplatform.csdn.net),一起共建开源鸿蒙跨平台生态。