Flutter中Stepper内CustomPainter引发无限循环与索引越界问题
Flutter Stepper结合CustomPainter时的无限重绘与索引越界问题
在Flutter的Stepper组件中使用CustomPainter时遇到以下问题:
- 页面加载后CustomPainter持续重绘,触发无限循环
- 抛出「RangeError (index): Index out of range: no indices are valid: 0」错误
包含Stepper的页面逻辑代码
void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return const MaterialApp( home: MyHomePage(), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({Key? key}) : super(key: key); @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { int currentStep = 0; List<LayoutBuilderWidget> listDynamic = []; addDynamic() { numberofwalls ++; listDynamic.add(LayoutBuilderWidget(numberofwalls)); LengthFT = []; LengthIN = []; Angle = []; setState(() {}); } submitdata(){ double MathLengthFT; double finalLength; LengthFT = []; LengthIN = []; Angle = []; listDynamic.forEach((widget) => LengthFT.add(widget.lengthFT.text)); listDynamic.forEach((widget) => LengthIN.add(widget.lengthIN.text)); listDynamic.forEach((widget) => Angle.add(widget.dropdownValue)); for(int i=0; i < numberofwalls; i++) { if(LengthIN[i] == null) { double MathLengthFT = int.parse(LengthFT[i]) * 12; FinalLength.add(MathLengthFT.toString()); } if(LengthFT[i] == null) { finalLength = double.parse(LengthIN[i]); FinalLength.add(finalLength.toString()); } MathLengthFT = int.parse(LengthFT[i]) * 12; finalLength = int.parse(LengthIN[i]) + MathLengthFT; FinalLength.add(finalLength.toString()); } } @override Widget build(BuildContext context) { return Scaffold( body:Stepper( type: StepperType.horizontal, steps: getSteps(), currentStep: currentStep, onStepContinue: () { final isLastStep = currentStep == getSteps().length - 1; if(isLastStep){ print('Complete'); } else{ setState(() { currentStep += 1; }); } }, onStepCancel: currentStep == 0 ? null : () => setState(() => currentStep -= 1), ), ); } List<Step> getSteps() => [ Step( isActive: currentStep >= 0, title: const Text('Layout'), content: Container( child: Row( children: [ Container( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( alignment: Alignment.center, margin: const EdgeInsets.all(20), width: 200, child: Text( 'Wall', style: Theme.of(context).textTheme.headline4, ), ), Container( alignment: Alignment.center, margin: const EdgeInsets.all(20), width: 200, child: Text( 'Length', style: Theme.of(context).textTheme.headline4, ), ), Container( alignment: Alignment.center, margin: const EdgeInsets.all(20), width: 200, child: Text( 'Angle', style: Theme.of(context).textTheme.headline4, ), ), ], ), Container( height: 600, width: 1000, child: ListView.builder( itemCount: listDynamic.length, itemBuilder: ((_, index) => listDynamic[index]) ) ), FloatingActionButton.extended(onPressed: () => addDynamic(), label: const Text('Add Wall'), icon: const Icon(Icons.add),), FloatingActionButton.extended(onPressed: () {submitdata();}, icon: const Icon(Icons.visibility), label: const Text(''),), ], ), ), Column( children: [ Container( color: Color.fromARGB(255, 241, 241, 241), width: 450, height: 450, child: CustomPaint( foregroundPainter: OpenPainter(), ), ), Container( margin: EdgeInsets.only(top: 20), width: 450, height: 450, child: CustomPaint( painter: CanvasLegend(), ), ), ], ) ], ), ), ), Step( isActive: currentStep >= 1, title: const Text('Doors & Options'), content: Container(), ), Step( isActive: currentStep >= 2, title: const Text('Panel Config'), content: Container(), ), Step( isActive: currentStep >= 3, title: const Text('Accessories'), content: Container(), ), Step( isActive: currentStep >= 4, title: const Text('Summary & Prices'), content: Container(), ), ]; }
CustomPainter实现代码
class OpenPainter extends CustomPainter { OpenPainter(); @override void paint(Canvas canvas, Size size) { var line1 = Paint()..strokeWidth = 5; var line2 = Paint()..strokeWidth = 5; double lastXPosition = 225; double lastYPosition = 225; double NextXPosition = 225; double NextYPosition = 225; int i = 0; if(numberofwalls != 0){ for(i = 0; i < numberofwalls; i++) { NextYPosition = lastYPosition + double.parse(FinalLength[i]) * cos(AngleToRadians(double.parse(Angle[i]))); NextXPosition = lastXPosition + double.parse(FinalLength[i]) * sin(AngleToRadians(double.parse(Angle[i]))); canvas.drawLine( Offset(lastXPosition,lastYPosition), Offset(NextXPosition, NextYPosition), line1, ); lastYPosition = NextYPosition; lastXPosition = NextXPosition; } i = 0; } } @override bool shouldRepaint(CustomPainter oldDelegate) => true; } double AngleToRadians(double angle){ return (pi / 180) * angle; }
问题原因与修复方案
1. 无限重绘问题
原因:OpenPainter的shouldRepaint直接返回true,每次页面刷新都会强制重绘;加上Stepper本身会因状态变化触发重建,形成无限循环。
修复:让OpenPainter接收绘制所需数据,仅在数据变化时重绘:
class OpenPainter extends CustomPainter { final int numberOfWalls; final List<String> finalLength; final List<String> angle; OpenPainter({required this.numberOfWalls, required this.finalLength, required this.angle}); @override bool shouldRepaint(OpenPainter oldDelegate) { return oldDelegate.numberOfWalls != numberOfWalls || oldDelegate.finalLength != finalLength || oldDelegate.angle != angle; } // 其余paint方法逻辑不变 }
2. 索引越界错误
原因:
submitdata中未清空FinalLength就重复添加数据,导致长度不匹配;同时条件判断逻辑错误(重复执行添加代码)OpenPainter依赖全局变量,数据更新不同步,导致绘制时索引超出数组长度
修复:- 修正
submitdata逻辑:
submitdata(){ double mathLengthFT; double finalLength; LengthFT = []; LengthIN = []; Angle = []; FinalLength.clear(); // 先清空旧数据 listDynamic.forEach((widget) => LengthFT.add(widget.lengthFT.text)); listDynamic.forEach((widget) => LengthIN.add(widget.lengthIN.text)); listDynamic.forEach((widget) => Angle.add(widget.dropdownValue)); for(int i=0; i < numberofwalls; i++) { // 判断空字符串而非null(TextField的text默认是空字符串) if(LengthIN[i].isEmpty) { mathLengthFT = int.parse(LengthFT[i]) * 12; FinalLength.add(mathLengthFT.toString()); } else if(LengthFT[i].isEmpty) { finalLength = double.parse(LengthIN[i]); FinalLength.add(finalLength.toString()); } else { mathLengthFT = int.parse(LengthFT[i]) * 12; finalLength = int.parse(LengthIN[i]) + mathLengthFT; FinalLength.add(finalLength.toString()); } } setState(() {}); // 触发UI更新,同步数据到CustomPainter }
- 传递参数给
OpenPainter:
CustomPaint( foregroundPainter: OpenPainter( numberOfWalls: numberofwalls, finalLength: FinalLength, angle: Angle, ), ),
3. 其他优化建议
- 将全局变量(
numberofwalls、LengthFT、LengthIN、Angle、FinalLength)移至_MyHomePageState类中作为成员变量,避免全局状态混乱 LayoutBuilderWidget中的输入框改用TextEditingController管理,确保状态同步正确
内容的提问来源于stack exchange,提问作者Aiden P
相关产品推荐
相关产品推荐

