【问题标题】:Flutter Countdown Timer颤振倒数计时器
【发布时间】:2019-07-03 17:47:05
【问题描述】:

如何将传递的值放入构造中,以创建一个四舍五入到小数点后一位并显示在我的 RaisedButton 子文本中的计时器?我试过但没有运气。我设法使用一个简单的 Timer 使回调函数工作,但没有周期性,也没有在文本中实时更新值......

import 'package:flutter/material.dart';
import 'dart:ui';
import 'dart:async';

class TimerButton extends StatefulWidget {
  final Duration timerTastoPremuto;


  TimerButton(this.timerTastoPremuto);

  @override
  _TimerButtonState createState() => _TimerButtonState();
}

class _TimerButtonState extends State<TimerButton> {
  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.all(5.0),
      height: 135.0,
      width: 135.0,
      child: new RaisedButton(
        elevation: 100.0,
        color: Colors.white.withOpacity(.8),
        highlightElevation: 0.0,
        onPressed: () {
          int _start = widget.timerTastoPremuto.inMilliseconds;

          const oneDecimal = const Duration(milliseconds: 100);
          Timer _timer = new Timer.periodic(
              oneDecimal,
                  (Timer timer) =>
                  setState(() {
                    if (_start < 100) {
                      _timer.cancel();
                    } else {
                      _start = _start - 100;
                    }
                  }));

        },
        splashColor: Colors.red,
        highlightColor: Colors.red,
        //shape: RoundedRectangleBorder e tutto il resto uguale
        shape: BeveledRectangleBorder(
            side: BorderSide(color: Colors.black, width: 2.5),
            borderRadius: new BorderRadius.circular(15.0)),
        child: new Text(
          "$_start",
          style: new TextStyle(fontFamily: "Minim", fontSize: 50.0),
        ),
      ),
    );
  }
}

【问题讨论】:

  • 请添加一些代码,您的问题没有被清除。
  • 是的,只需使用setState 更新值,不要在其中放入任何小部件
  • 计时器的代码是什么?可以给我链接吗?
  • 使用Timer 类,它表示可以使用Timer.periodic 重复触发的计时器。你也可以看看Stopwatch类。

标签: timer dart flutter countdown


【解决方案1】:

这是使用Timer.periodic 的示例:

点击按钮时,倒计时从10 开始到0

import 'dart:async';

[...]

Timer _timer;
int _start = 10;

void startTimer() {
  const oneSec = const Duration(seconds: 1);
  _timer = new Timer.periodic(
    oneSec,
    (Timer timer) {
      if (_start == 0) {
        setState(() {
          timer.cancel();
        });
      } else {
        setState(() {
          _start--;
        });
      }
    },
  );
}

@override
void dispose() {
  _timer.cancel();
  super.dispose();
}

Widget build(BuildContext context) {
  return new Scaffold(
    appBar: AppBar(title: Text("Timer test")),
    body: Column(
      children: <Widget>[
        RaisedButton(
          onPressed: () {
            startTimer();
          },
          child: Text("start"),
        ),
        Text("$_start")
      ],
    ),
  );
}

结果:

你也可以使用quiver.async库中的CountdownTimer类,使用更简单:

import 'package:quiver/async.dart';

[...]

int _start = 10;
int _current = 10;

void startTimer() {
  CountdownTimer countDownTimer = new CountdownTimer(
    new Duration(seconds: _start),
    new Duration(seconds: 1),
  );

  var sub = countDownTimer.listen(null);
  sub.onData((duration) {
    setState(() { _current = _start - duration.elapsed.inSeconds; });
  });

  sub.onDone(() {
    print("Done");
    sub.cancel();
  });
}

Widget build(BuildContext context) {
  return new Scaffold(
    appBar: AppBar(title: Text("Timer test")),
    body: Column(
      children: <Widget>[
        RaisedButton(
          onPressed: () {
            startTimer();
          },
          child: Text("start"),
        ),
        Text("$_current")
      ],
    ),
  );
}

编辑:关于 cmets 中关于按钮点击行为的问题

使用上面使用Timer.periodic的代码,每次按钮点击都会启动一个新的计时器,所有这些计时器都会更新相同的_start变量,从而更快地减少计数器。

有多种解决方案可以改变这种行为,具体取决于您想要实现的目标:

  • 点击后禁用按钮,这样用户就不会再打扰倒计时了(可能会在定时器取消后重新启用)
  • 用非空条件包装Timer.periodic 创建,以便多次单击按钮无效
if (_timer != null) {
  _timer = new Timer.periodic(...);
}
  • 如果您想在每次点击时重新启动计时器,请取消计时器并重置倒计时:
if (_timer != null) {
  _timer.cancel();
  _start = 10;
}
_timer = new Timer.periodic(...);
  • 如果您希望该按钮起到播放/暂停按钮的作用:
if (_timer != null) {
  _timer.cancel();
  _timer = null;
} else {
  _timer = new Timer.periodic(...);
}

您也可以使用这个官方的async 包,它提供了一个从Timer 扩展而来的RestartableTimer 类并添加了reset 方法。

所以每次点击按钮时只需调用_timer.reset();

最后,Codepen 现在支持 Flutter 了!所以这里是一个活生生的例子,让每个人都可以玩它:https://codepen.io/Yann39/pen/oNjrVOb

【讨论】:

  • 嗯,看到你的代码有助于我理解。现在我有一些问题。 Timer 和 Timer.periodic 有什么区别?如果我想做一个像你一样倒计时但也显示第一个小数的计时器。
  • Timer 创建一个新的计时器,其回调函数仅在指定的持续时间后调用一次(例如延迟操作)。 Timer.periodic 创建一个新的 repeating 计时器,以便根据指定的持续时间间隔重复调用回调函数。如果要显示十进制数,请使用 double 而不是 int。如果您的问题是更改间隔以使其减少 0.1,那么只需在上面的代码中将 1 更改为 0.1。最后,如果您需要格式化输出,请参阅NumberFormat
  • 您可以简单地将计时器开始时间存储在共享首选项中,然后在应用启动时,将计时器值设置为当前时间减去开始时间(以秒为单位)。
  • @AndrewCaprario 我在答案末尾添加了更多信息,因为评论太多了。我还添加了一个 codepen 链接。
  • @Coltuxumab 只需将您的计数器放在具有自己构建方法的不同小部件中。
【解决方案2】:

我创建了一个通用计时器小部件,它可以用来显示任何类型的计时器,而且它也很灵活。

这个小部件具有以下属性

  1. secondsRemaining:计时器需要运行的持续时间(以秒为单位)
  2. whenTimeExpires:如果计时器结束需要执行什么操作
  3. countDownStyle:你想赋予定时器的任何样式
  4. countDownFormatter:用户想要显示倒数计时器的方式,例如 hh mm ss 类似 01 hours: 20 minutes: 45 seconds 的字符串

您可以提供一个默认格式化程序 (formatHHMMSS),以防您不想从每个地方都提供它。

//为此提供实现 - formatHHMMSS(duration.inSeconds); 或使用以下我提供的实现。

import 'package:flutter/material.dart';
class CountDownTimer extends StatefulWidget {
  const CountDownTimer({
    Key key,
    int secondsRemaining,
    this.countDownTimerStyle,
    this.whenTimeExpires,
    this.countDownFormatter,
  })  : secondsRemaining = secondsRemaining,
        super(key: key);

  final int secondsRemaining;
  final Function whenTimeExpires;
  final Function countDownFormatter;
  final TextStyle countDownTimerStyle;

  State createState() => new _CountDownTimerState();
}

class _CountDownTimerState extends State<CountDownTimer>
    with TickerProviderStateMixin {
  AnimationController _controller;
  Duration duration;

  String get timerDisplayString {
    Duration duration = _controller.duration * _controller.value;
    return widget.countDownFormatter != null
        ? widget.countDownFormatter(duration.inSeconds)
        : formatHHMMSS(duration.inSeconds);
      // In case user doesn't provide formatter use the default one
     // for that create a method which will be called formatHHMMSS or whatever you like
  }

  @override
  void initState() {
    super.initState();
    duration = new Duration(seconds: widget.secondsRemaining);
    _controller = new AnimationController(
      vsync: this,
      duration: duration,
    );
    _controller.reverse(from: widget.secondsRemaining.toDouble());
    _controller.addStatusListener((status) {
      if (status == AnimationStatus.completed || status == AnimationStatus.dismissed) {
        widget.whenTimeExpires();
      }
    });
  }

  @override
  void didUpdateWidget(CountDownTimer oldWidget) {
    if (widget.secondsRemaining != oldWidget.secondsRemaining) {
      setState(() {
        duration = new Duration(seconds: widget.secondsRemaining);
        _controller.dispose();
        _controller = new AnimationController(
          vsync: this,
          duration: duration,
        );
        _controller.reverse(from: widget.secondsRemaining.toDouble());
        _controller.addStatusListener((status) {
          if (status == AnimationStatus.completed) {
            widget.whenTimeExpires();
          } else if (status == AnimationStatus.dismissed) {
            print("Animation Complete");
          }
        });
      });
    }
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return new Center(
        child: AnimatedBuilder(
            animation: _controller,
            builder: (_, Widget child) {
              return Text(
                timerDisplayString,
                style: widget.countDownTimerStyle,
              );
            }));
  }
}

用法:

 Container(
       width: 60.0,
       padding: EdgeInsets.only(top: 3.0, right: 4.0),
         child: CountDownTimer(
           secondsRemaining: 30,
           whenTimeExpires: () {
              setState(() {
                hasTimerStopped = true;
              });
            },
            countDownTimerStyle: TextStyle(
                color: Color(0XFFf5a623),
                fontSize: 17.0,
                height: 1.2,
            ),
          ),
        )

格式HHMMSS的示例:

String formatHHMMSS(int seconds) {
  int hours = (seconds / 3600).truncate();
  seconds = (seconds % 3600).truncate();
  int minutes = (seconds / 60).truncate();

  String hoursStr = (hours).toString().padLeft(2, '0');
  String minutesStr = (minutes).toString().padLeft(2, '0');
  String secondsStr = (seconds % 60).toString().padLeft(2, '0');

  if (hours == 0) {
    return "$minutesStr:$secondsStr";
  }

  return "$hoursStr:$minutesStr:$secondsStr";
}

上述代码的空安全版本

import 'package:flutter/material.dart';

class CountDownTimer extends StatefulWidget {
  const CountDownTimer({
    Key? key,
    required this.secondsRemaining,
    required this.whenTimeExpires,
    this.countDownFormatter,
    this.countDownTimerStyle,
  }) : super(key: key);

  final int secondsRemaining;
  final VoidCallback whenTimeExpires;
  final TextStyle? countDownTimerStyle;
  final Function(int seconds)? countDownFormatter;

  @override
  State createState() => _CountDownTimerState();
}

class _CountDownTimerState extends State<CountDownTimer>
    with TickerProviderStateMixin {
  late final AnimationController _controller;
  late final Duration duration;

  String get timerDisplayString {
    final duration = _controller.duration! * _controller.value;
    if (widget.countDownFormatter != null) {
      return widget.countDownFormatter!(duration.inSeconds) as String;
    } else {
      return formatHHMMSS(duration.inSeconds);
    }
  }

  String formatHHMMSS(int seconds) {
    final hours = (seconds / 3600).truncate();
    seconds = (seconds % 3600).truncate();
    final minutes = (seconds / 60).truncate();

    final hoursStr = (hours).toString().padLeft(2, '0');
    final minutesStr = (minutes).toString().padLeft(2, '0');
    final secondsStr = (seconds % 60).toString().padLeft(2, '0');

    if (hours == 0) {
      return '$minutesStr:$secondsStr';
    }

    return '$hoursStr:$minutesStr:$secondsStr';
  }

  @override
  void initState() {
    super.initState();
    duration = Duration(seconds: widget.secondsRemaining);
    _controller = AnimationController(
      vsync: this,
      duration: duration,
    );
    _controller
      ..reverse(from: widget.secondsRemaining.toDouble())
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed ||
            status == AnimationStatus.dismissed) {
          widget.whenTimeExpires();
        }
      });
  }

  @override
  void didUpdateWidget(CountDownTimer oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.secondsRemaining != oldWidget.secondsRemaining) {
      setState(() {
        duration = Duration(seconds: widget.secondsRemaining);
        _controller.dispose();
        _controller = AnimationController(
          vsync: this,
          duration: duration,
        );
        _controller
          ..reverse(from: widget.secondsRemaining.toDouble())
          ..addStatusListener((status) {
            if (status == AnimationStatus.completed) {
              widget.whenTimeExpires();
            }
          });
      });
    }
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: AnimatedBuilder(
        animation: _controller,
        builder: (_, Widget? child) {
          return Text(
            timerDisplayString,
            style: widget.countDownTimerStyle,
          );
        },
      ),
    );
  }
}

【讨论】:

  • 我在苦苦挣扎,我们可以添加运行时启动和停止计时器吗?
  • 是的,您可以添加以下类似的方法。
  • 如果你能举个例子会很好
  • 通过设置 setState.. 它帮助了我:)
  • countDownFormatter的实现是什么?
【解决方案3】:

晚会有点晚了,但是你们为什么不尝试动画。不,我不是在告诉你们管理动画控制器并将它们处理掉以及所有这些东西,有一个名为 TweenAnimationBuilder 的内置小部件。您可以在任何类型的值之间进行动画处理,这里有一个 Duration 类的示例

TweenAnimationBuilder<Duration>(
  duration: Duration(minutes: 3),
  tween: Tween(begin: Duration(minutes: 3), end: Duration.zero),
  onEnd: () {
    print('Timer ended');
  },
  builder: (BuildContext context, Duration value, Widget? child) {
    final minutes = value.inMinutes;
    final seconds = value.inSeconds % 60;
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 5),
      child: Text('$minutes:$seconds',
               textAlign: TextAlign.center,
               style: TextStyle(
               color: Colors.black,
               fontWeight: FontWeight.bold,
               fontSize: 30)));
    }),

您还会收到 onEnd 回调,它会在动画完成时通知您;

这是输出

【讨论】:

  • 这里显示的示例代码api.flutter.dev/flutter/widgets/… 非常适合简单流畅的动画。我将它用于线性进度指示条。
  • 如果您的项目是空安全的,请将构建器更改为Widget? child
  • 时间结束后如何重新启动?
  • @SULPHURICACID 不幸的是,我们无法重新启动,因为我们没有任何动画控制器。可能你应该考虑定义animationController并使用它。
【解决方案4】:

这是我的计时器小部件,与问题无关,但可以帮助某人。

import 'dart:async';

import 'package:flutter/material.dart';

class OtpTimer extends StatefulWidget {
  @override
  _OtpTimerState createState() => _OtpTimerState();
}

class _OtpTimerState extends State<OtpTimer> {
  final interval = const Duration(seconds: 1);

  final int timerMaxSeconds = 60;

  int currentSeconds = 0;

  String get timerText =>
      '${((timerMaxSeconds - currentSeconds) ~/ 60).toString().padLeft(2, '0')}: ${((timerMaxSeconds - currentSeconds) % 60).toString().padLeft(2, '0')}';

  startTimeout([int milliseconds]) {
    var duration = interval;
    Timer.periodic(duration, (timer) {
      setState(() {
        print(timer.tick);
        currentSeconds = timer.tick;
        if (timer.tick >= timerMaxSeconds) timer.cancel();
      });
    });
  }

  @override
  void initState() {
    startTimeout();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: <Widget>[
        Icon(Icons.timer),
        SizedBox(
          width: 5,
        ),
        Text(timerText)
      ],
    );
  }
}

你会得到这样的东西

【讨论】:

  • 你必须像 if(mounted){startTimeout();} 这样对你的 startTimeout 函数进行挂载检查,否则你会因为 setState 和 async 的影响而导致内存泄漏代码
【解决方案5】:

没有直接回答你的问题。但对那些想在一段时间后开始做某事的人很有帮助。

Future.delayed(Duration(seconds: 1), () {
            print('yo hey');
          });

【讨论】:

  • 延迟动作能否在其宿主程序在 Flutter/Mobile 应用程序中被杀死后幸存下来?
  • 与这个问题无关,但正是我正在寻找的。谢谢
【解决方案6】:

如果您只需要一个简单的倒数计时器,那么这是一个不错的选择,而不是安装软件包。编码愉快!

countDownTimer() async {
 int timerCount;
 for (int x = 5; x > 0; x--) {
   await Future.delayed(Duration(seconds: 1)).then((_) {
     setState(() {
       timerCount -= 1;
    });
  });
 }
}

【讨论】:

    【解决方案7】:

    我正在使用https://pub.dev/packages/flutter_countdown_timer

    依赖: flutter_countdown_timer: ^1.0.0

    $flutter pub 获取

    CountdownTimer(endTime: 1594829147719)
    

    1594829147719 是以毫秒为单位的时间戳

    【讨论】:

    • 嗨,Ricardo,我也在用这个。我有一个问题,可能你能帮忙。你能告诉我如何更改计时器结束时出现的默认文本吗?我尝试使用 OnEnd() 函数返回文本,但它似乎不起作用...
    • 我的问题是您如何创建和处置控制器?我不明白这个包的文档。他们初始化控制器,然后重新分配一切?
    【解决方案8】:

    已经提供了许多答案。我建议一种捷径-

    使用这个包Custom_timer

    将此添加到您的包的 pubspec.yaml 文件中:

    dependencies:
      custom_timer: ^0.0.3 
    

    (使用最新版本)

    而且实现起来非常简单

    @override
      Widget build(BuildContext context) {
        return MaterialApp(
          debugShowCheckedModeBanner: false,
          home: Scaffold(
            appBar: AppBar(
              title: Text("CustomTimer example"),
            ),
            body: Center(
              child: CustomTimer(
                from: Duration(hours: 12),
                to: Duration(hours: 0),
                onBuildAction: CustomTimerAction.auto_start,
                builder: (CustomTimerRemainingTime remaining) {
                  return Text(
                    "${remaining.hours}:${remaining.minutes}:${remaining.seconds}",
                    style: TextStyle(fontSize: 30.0),
                  );
                },
              ),
            ),
          ),
        );
      }
    

    【讨论】:

    • 谢谢 Abir,这个包裹可以计数而不是计数吗?当按下按钮并且页面导航离开时,如何保存经过的时间(计数)?
    【解决方案9】:
    import 'dart:async';
    import 'package:flutter/material.dart';
    
    class CustomTimer extends StatefulWidget {
      @override
      _CustomTimerState createState() => _CustomTimerState();
    }
    
    class _CustomTimerState extends State<CustomTimer> {
      final _maxSeconds = 61;
      int _currentSecond = 0;
      Timer _timer;
    
      String get _timerText {
        final secondsPerMinute = 60;
        final secondsLeft = _maxSeconds - _currentSecond;
    
        final formattedMinutesLeft =
            (secondsLeft ~/ secondsPerMinute).toString().padLeft(2, '0');
        final formattedSecondsLeft =
            (secondsLeft % secondsPerMinute).toString().padLeft(2, '0');
    
        print('$formattedMinutesLeft : $formattedSecondsLeft');
        return '$formattedMinutesLeft : $formattedSecondsLeft';
      }
    
      @override
      void initState() {
        super.initState();
        _startTimer();
      }
    
      @override
      void dispose() {
        _timer.cancel();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Center(
            child: Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Icon(Icons.timer),
                Text(_timerText),
              ],
            ),
          ),
        );
      }
    
      void _startTimer() {
        final duration = Duration(seconds: 1);
        _timer = Timer.periodic(duration, (Timer timer) {
          setState(() {
            _currentSecond = timer.tick;
            if (timer.tick >= _maxSeconds) timer.cancel();
          });
        });
      }
    }
    

    【讨论】:

      【解决方案10】:

      你可以使用这个插件timer_builder

      timer_builder 小部件,可根据计划、定期或动态生成的时间事件进行自我重建。

      示例

      定期重建

      import 'package:timer_builder/timer_builder.dart';
      
      class ClockWidget extends StatelessWidget {
      
        @override
        Widget build(BuildContext context) {
          return TimerBuilder.periodic(Duration(seconds: 1),
            builder: (context) {
              return Text("${DateTime.now()}");
            }
          );
        }
        
      }
      

      按计划重建

      import 'package:timer_builder/timer_builder.dart';
      
      class StatusIndicator extends StatelessWidget {
      
        final DateTime startTime;
        final DateTime endTime;
        
        StatusIndicator(this.startTime, this.endTime);
        
        @override
        Widget build(BuildContext context) {
          return TimerBuilder.scheduled([startTime, endTime],
            builder: (context) {
              final now = DateTime.now();
              final started = now.compareTo(startTime) >= 0;
              final ended = now.compareTo(endTime) >= 0;
              return Text(started ? ended ? "Ended": "Started": "Not Started");
            }
          );
        }
        
      }
      

      【讨论】:

      • 为什么加了和你提交的代码无关的gif?
      【解决方案11】:

      要以这种格式 hh:mm:ss 显示您的总秒数,您可以使用以下方法:

       String getDuration(int totalSeconds) {
          String seconds = (totalSeconds % 60).toInt().toString().padLeft(2, '0');
          String minutes =
         ((totalSeconds / 60) % 60).toInt().toString().padLeft(2, '0');
         String hours = (totalSeconds ~/ 3600).toString().padLeft(2, '0');
      
         return "$hours\:$minutes\:$seconds";
      
      
      }
      

      【讨论】:

        【解决方案12】:

        我创建了一个没有任何插件的惊人计时器,在这里你也可以得到倒计时计时器。 并且不要忘记在按下后停止计时器。

        这是我的计时器完整项目的链接。 *Hope this will help someone. Thank you. *

        【讨论】:

        • 虽然此链接可能会回答问题,但最好在此处包含答案的基本部分并提供链接以供参考。如果链接页面发生更改,仅链接答案可能会失效。 - From Review
        【解决方案13】:

        一行倒计时

        CountdownTimer(Duration(seconds: 5), Duration(seconds: 1)).listen((data){
        })..onData((data){
          print('data $data');
        })..onDone((){
          print('onDone.........');
        });
        

        【讨论】:

        • CountdownTimer 这个类是来自第三方库吗?请包含库包
        • 请添加更多解释。
        猜你喜欢
        • 2019-08-18
        • 2020-05-13
        • 2022-11-20
        • 2021-10-28
        • 2020-06-18
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多