Flutter - 封装广告页的倒计时圆圈控件

3,310 阅读2分钟

前言

很多 App 的广告页都会有一个倒计时控件,倒计时结束或者用户点击之后就会进入到 App 的主页。最近在搞一个新的 App,正好需要用到,就顺便封装一个,用的是 Flutter 提供的组件 CircularProgressIndicator,比较简单,需要的同学可以直接拿去用。

使用

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

class CountdownCircle extends StatefulWidget {
  const CountdownCircle({
    Key? key,
    this.countdownSeconds = 5,
    this.ringBackgroundColor = Colors.transparent,
    this.ringColor = Colors.deepOrange,
    this.ringStrokeWidth = 3.0,
    this.textStyle = const TextStyle(color: Colors.black),
    this.finished,
  })  : assert(countdownSeconds > 0),
        assert(ringStrokeWidth > 0),
        super(key: key);

  /// 倒计时秒数,默认为 5 秒
  final int countdownSeconds;

  /// 圆环的背景色,ringColor 会逐渐填充背景色,默认为透明色
  final Color ringBackgroundColor;

  /// 圆环逐渐填充的颜色,默认为 Colors.deepOrange
  final Color ringColor;

  /// 圆谎的宽度,默认为3.0
  final double ringStrokeWidth;

  /// 中间文案的字体
  final TextStyle textStyle;

  /// 点击或倒计时结束后的回调
  /// [byUserClick] 为 true,是用户点击,否则是倒计时结束
  final Function({required bool byUserClick})? finished;

  @override
  State<CountdownCircle> createState() => _CountdownCircleState();
}

class _CountdownCircleState extends State<CountdownCircle> {
  Timer? _timer;
  final _currentTimer = ValueNotifier<int>(0);
  final _isVisible = ValueNotifier<bool>(true);

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

    _timer = Timer.periodic(const Duration(milliseconds: 10), (timer) {
      _currentTimer.value += 10;
      if (_currentTimer.value >= widget.countdownSeconds * 1000) {
        _didFinished(byUserClick: false);
      }
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder(
      builder: (context, bool isVisible, child) {
        return Visibility(
          visible: isVisible,
          child: GestureDetector(
            onTap: () => _didFinished(byUserClick: true),
            child: Stack(
              alignment: Alignment.center,
              children: [
                ValueListenableBuilder(
                  builder: ((context, int countdownDuration, child) => CircularProgressIndicator(
                        strokeWidth: widget.ringStrokeWidth,
                        color: widget.ringColor,
                        value: countdownDuration / (widget.countdownSeconds * 1000),
                        backgroundColor: widget.ringBackgroundColor,
                      )),
                  valueListenable: _currentTimer,
                ),
                Text(
                  '跳过',
                  style: widget.textStyle,
                ),
              ],
            ),
          ),
        );
      },
      valueListenable: _isVisible,
    );
  }

  void _didFinished({required bool byUserClick}) {
    if (widget.finished != null) {
      widget.finished!(byUserClick: byUserClick);
    }
    _timer?.cancel();
    _isVisible.value = false;
  }
}

使用方法:

class SplashPage extends StatefulWidget {
  const SplashPage({Key? key}) : super(key: key);

  @override
  State<SplashPage> createState() => _SplashPageState();
}

class _SplashPageState extends State<SplashPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          Positioned(
            right: 200,
            top: 200,
            child: CountdownCircle(
              finished: (byUserClick) {
                print('用户主动点击? $byUserClick');
              },
            ),
          ),
        ],
      ),
    );
  }
}

效果如下:

1.gif

回调 finished 中有一个参数 byUserClick,如果为 true,说明是用户主动点击了跳过,如果为 false,说明是倒计时结束后自动回调的,可以使用它来区分场景,一般埋点时会需要使用。

整个控件使用 Visibility 来包裹住,在倒计时结束或者用户主动触发结束后,会将 visible 设置为 false,隐藏控件。