You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter Duration Picker包手动编辑后滚轮异常问题求助

问题根源
  1. 错误的时长叠加逻辑:从编辑弹窗返回后,isAddedManually被设为true,此时滚轮的onChange回调会执行_duration = _duration + val,但val是滚轮当前选中的总时长,不是增量,这会导致时长数值错误,进而引发滚动异常。
  2. 状态未及时重置:手动编辑完成后,没有在滚轮开始交互时重置isAddedManually,导致后续滚轮操作一直走错误的叠加逻辑。
修复方案
  1. 移除错误的叠加逻辑,统一在onChange中直接用滚轮返回的val更新_duration。
  2. 给DurationPicker添加onScrollStart回调,在滚轮开始滚动时重置isAddedManually为false,确保滚轮操作始终使用正确的更新逻辑。
  3. 优化编辑弹窗的时长管理,用局部变量保存编辑状态,避免修改外部原始值。
修改后的完整代码
import 'package:duration_picker/duration_picker.dart';
import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter App'),
      ),
      body: Center(
          child: ElevatedButton(
        onPressed: () {
          activityDurationDialog(context);
        },
        child: const Text('click'),
      )),
    );
  }
}

Future<void> activityDurationDialog(BuildContext context, {double topBottomPadding = 10}) async {
  Duration _duration = const Duration(hours: 0, minutes: 0);
  final TextEditingController _hourController = TextEditingController(text: _duration.inHours.toString());
  final TextEditingController _minuteController = TextEditingController(text: (_duration.inMinutes.remainder(60)).toString());
  bool isAddedManually = false;

  void setIsAddedManually(bool newValue) {
    isAddedManually = newValue;
  }

  await showDialog<void>(
    context: context,
    barrierDismissible: true,
    builder: (BuildContext context) {
      return StatefulBuilder(
        builder: (context, setState) {
          return AlertDialog(
            title: const Text('duration'),
            content: SingleChildScrollView(
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  children: [
                    const Divider(color: Colors.black),
                    Padding(
                      padding: const EdgeInsets.only(top: 25.0),
                      child: DurationPicker(
                        width: MediaQuery.of(context).size.width * 0.9,
                        duration: _duration,
                        onChange: (val) {
                          setState(() {
                            _duration = val;
                            _hourController.text = _duration.inHours.toString();
                            _minuteController.text = (_duration.inMinutes.remainder(60)).toString();
                          });
                        },
                        onScrollStart: () {
                          setState(() {
                            isAddedManually = false;
                          });
                        },
                        snapToMins: 5.0,
                      ),
                    ),
                  ],
                ),
              ),
            ),
            actions: <Widget>[
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                crossAxisAlignment: CrossAxisAlignment.center,
                children: [
                  Padding(
                    padding: const EdgeInsets.only(left: 8.0),
                    child: IconButton(
                      icon: const Icon(
                        Icons.keyboard,
                        size: 30,
                      ),
                      onPressed: () {
                        activityDurationEditingDialog(
                          context,
                          _hourController,
                          _minuteController,
                          _duration,
                          setIsAddedManually,
                          (newDuration) {
                            setState(() {
                              _duration = newDuration;
                              _hourController.text = _duration.inHours.toString();
                              _minuteController.text = (_duration.inMinutes.remainder(60)).toString();
                            });
                          },
                        );
                      },
                    ),
                  ),
                ],
              ),
            ],
          );
        },
      );
    },
  );
}

Future<void> activityDurationEditingDialog(
  BuildContext context,
  TextEditingController _hourController,
  TextEditingController _minuteController,
  Duration initialDuration,
  Function(bool) setIsAddedManually,
  Function(Duration) onUpdate, {
  double topBottomPadding = 10,
}) async {
  Duration _editingDuration = initialDuration;

  return showDialog<void>(
    context: context,
    barrierDismissible: true,
    builder: (BuildContext context) {
      return StatefulBuilder(
        builder: (context, setState) {
          return AlertDialog(
            title: const Text('edit'),
            content: SingleChildScrollView(
              child: Column(
                children: [
                  const Divider(color: Colors.black),
                  const SizedBox(height: 10),
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceAround,
                    children: [
                      Column(
                        children: [
                          const Text('hour'),
                          const SizedBox(height: 10),
                          SizedBox(
                            width: 100,
                            child: TextField(
                              decoration: const InputDecoration(
                                border: OutlineInputBorder(),
                              ),
                              maxLength: 3,
                              controller: _hourController,
                              keyboardType: TextInputType.number,
                              textAlign: TextAlign.center,
                              onChanged: (value) {
                                int hours = int.tryParse(value) ?? 0;
                                setState(() {
                                  _editingDuration = Duration(
                                    hours: hours,
                                    minutes: _editingDuration.inMinutes.remainder(60),
                                  );
                                });
                              },
                            ),
                          ),
                        ],
                      ),
                      Column(
                        children: [
                          const Text('minute'),
                          const SizedBox(height: 10),
                          SizedBox(
                            width: 100,
                            child: TextField(
                              decoration: const InputDecoration(
                                border: OutlineInputBorder(),
                              ),
                              maxLength: 2,
                              controller: _minuteController,
                              keyboardType: TextInputType.number,
                              textAlign: TextAlign.center,
                              onChanged: (value) {
                                int minutes = int.tryParse(value) ?? 0;
                                minutes = minutes.clamp(0, 59);
                                setState(() {
                                  _editingDuration = Duration(
                                    hours: _editingDuration.inHours,
                                    minutes: minutes,
                                  );
                                  _minuteController.text = minutes.toString();
                                });
                              },
                            ),
                          ),
                        ],
                      ),
                    ],
                  ),
                ],
              ),
            ),
            actions: <Widget>[
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                crossAxisAlignment: CrossAxisAlignment.center,
                children: [
                  Padding(
                    padding: const EdgeInsets.only(left: 8.0),
                    child: IconButton(
                      icon: const Icon(
                        Icons.access_time,
                        size: 30,
                      ),
                      onPressed: () {
                        setIsAddedManually(true);
                        onUpdate(_editingDuration);
                        Navigator.of(context).pop();
                      },
                    ),
                  ),
                ],
              ),
            ],
          );
        },
      );
    },
  );
}
额外优化点
  • 给分钟输入添加了clamp(0,59)限制,避免输入超过60的非法值。
  • 编辑弹窗使用局部变量_editingDuration保存编辑状态,逻辑更清晰。
  • 添加const修饰符优化Widget性能。

内容的提问来源于stack exchange,提问作者behnam

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 16:35:54