Flutter 实现钉钉打卡效果

2,039 阅读3分钟

整体布局


@override
Widget build(BuildContext context) {
    return SingleChildScrollView(
        child: Container(
            padding: EdgeInsets.symmetric(horizontal: 10).copyWith(top: 10),
            child: Column(
                children: [
                    _buildFlipCard(),
                    // 预留.... 可以放一些其他需要显示的
                ],
            ),
        ),
    );
}

用到了第三方依赖

翻转卡片 flip_card: ^0.4.4 pub.flutter-io.cn/packages/fl…

音频播放 just_audio: ^0.6.5 pub.flutter-io.cn/packages/ju…

格式化时间 date_format: ^1.0.8 pub.flutter-io.cn/packages/da…

FlipCard weiget 分为 front 和 back,可以实现前后旋转效果

FlipCard(
  direction: FlipDirection.HORIZONTAL, // default
  front: Container(
        child: Text('Front'),
    ),
    back: Container(
        child: Text('Back'),
    ),
);

_buildFlipCard 封装构建 FlipCard

构建 front 使用 Column 包裹,划分不同小的 widget

构建第一部分, 使用flutter 自带 Card 包裹,可以提现卡片效果
Card(
            elevation: 0.0,
            child: Container(
              margin: EdgeInsets.symmetric(horizontal: 16.0),
              padding: EdgeInsets.symmetric(vertical: 16.0),
              child: Row(
                children: [
                  Expanded(
                      flex: 4,
                      child: Container(
                        child: Row(
                          children: [
                            ClipOval(
                              child: Container(
                                width: 60,
                                height: 60,
                                alignment: Alignment.center,
                                color: kDTPrimary,
                                child: Text(
                                  "江景",
                                  style: TextStyle(
                                    color: Colors.white,
                                    fontSize: 18.0,
                                  ),
                                ),
                              ),
                            ),
                            Container(
                              margin: EdgeInsets.only(left: 16),
                              child: Column(
                                crossAxisAlignment: CrossAxisAlignment.start,
                                children: [
                                  Text(
                                    "江景",
                                    style: TextStyle(fontSize: 24.0),
                                  ),
                                  Row(
                                    children: [
                                      Text("MOOSE"),
                                      Padding(
                                        padding: EdgeInsets.only(left: 8.0),
                                        child: Text(
                                          "(查看规则)",
                                          style: TextStyle(color: kDTPrimary),
                                        ),
                                      ),
                                    ],
                                  )
                                ],
                              ),
                            )
                          ],
                        ),
                      )),
                  Expanded(
                      flex: 1,
                      child: Container(
                        child: Text(
                          "申请",
                          style: TextStyle(fontSize: 16.0),
                        ),
                      )),
                ],
              ),
            ),
          ),
第二部分使用 Card 包裹,里边嵌套 Column
Card(
            elevation: 0.0,
            child: Column(
              children: [
                Container(
                  margin: EdgeInsets.only(
                    top: 20.0,
                  ),
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.spaceAround,
                    children: [
                      Container(
                        width: 150,
                        padding: EdgeInsets.symmetric(
                            vertical: 8.0, horizontal: 8.0),
                        margin: EdgeInsets.only(left: 8.0),
                        decoration: BoxDecoration(
                            color: kDTNormal,
                            borderRadius:
                                BorderRadius.all(Radius.circular(8.0))),
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Text(
                              "上班08:30",
                              style: TextStyle(fontSize: 18),
                            ),
                            Row(
                              children: [
                                SvgPicture.asset(
                                  'assets/icons/icon_right.svg',
                                  width: 32,
                                  color: kDTPrimary,
                                ),
                                Text("08:36已打卡")
                              ],
                            ),
                          ],
                        ),
                      ),
                      Expanded(
                        flex: 1,
                        child: Container(
                          padding: EdgeInsets.symmetric(
                              vertical: 8.0, horizontal: 8.0),
                          margin: EdgeInsets.only(left: 8.0, right: 8.0),
                          decoration: BoxDecoration(
                              color: kDTNormal,
                              borderRadius:
                                  BorderRadius.all(Radius.circular(8.0))),
                          child: Column(
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: [
                              Text(
                                "下班 18:30(弹性)",
                                style: TextStyle(fontSize: 18),
                              ),
                              Row(
                                children: [
                                  SvgPicture.asset(
                                    'assets/icons/icon_right.svg',
                                    width: 32,
                                    color: kDTPrimary,
                                  ),
                                  Row(
                                    children: [
                                      Text(
                                        "08:36已打卡",
                                        style: TextStyle(fontSize: 16.0),
                                      ),
                                      Text(
                                        "更新打卡",
                                        style: TextStyle(
                                            fontSize: 16.0, color: kDTPrimary),
                                      ),
                                    ],
                                  )
                                ],
                              ),
                            ],
                          ),
                        ),
                      )
                    ],
                  ),
                ),
               
                // 构建扫描部分
                _buildStack(),
                
                Container(
                  margin: EdgeInsets.only(
                    top: 20.0,
                    bottom: 20.0,
                  ),
                  child: Text("已进入考勤范围 MOOSE"),
                ),
              ],
            ),
          ),
_buildStack() 部分

使用 Stack 层叠布局放置两个大小相同 ClipOval widget

第一个 Container decoration 使用 LinearGradient 渐变

第二个 Container decoration 使用 SweepGradient 渐变

给第二个加上 RotationTransition 动画效果,控制隐藏和显示

给第一个加上 GestureDetector 事件触发动画执行

Container(
    padding: EdgeInsets.only(top: 60.0),
    child: Stack(
        children: [
            Center(
                child: GestureDetector(
                    onTap: _onClick,
                    child: ClipOval(
                        child: Container(
                            width: 180,
                            height: 180,
                            alignment: Alignment.center,
                            decoration: BoxDecoration(
                                color: kDTPrimary,
                                gradient: LinearGradient(
                                    begin: Alignment.topCenter,
                                    end: Alignment.topCenter,
                                    colors: [
                                        kDTPrimary.withOpacity(0.8),
                                        kDTPrimary.withOpacity(1),
                                    ]),
                            ),
                            child: Column(
                                mainAxisAlignment: MainAxisAlignment.center,
                                children: [
                                    Text(
                                        "上班打卡",
                                        style: TextStyle(
                                            color: Colors.white, fontSize: 24),
                                    ),
                                    Text(_currentTime,
                                         style: TextStyle(
                                             color: Colors.white, fontSize: 24)),
                                ],
                            ),
                        ),
                    ),
                ),
            ),
            Center(
                child: _show
                ? RotationTransition(
                    turns: _animationController,
                    child: ClipOval(
                        child: Container(
                            width: 180,
                            height: 180,
                            decoration: BoxDecoration(
                                gradient: SweepGradient(colors: [
                                    Colors.white.withOpacity(0.4),
                                    Colors.white.withOpacity(0.6),
                                ]),
                            ),
                        ),
                    ),
                )
                : Container(),
            )
        ],
    ),
),
初始化定时器 Timer,动画控制器 AnimationController
class _DTWorksetBodyState extends State<DTWorksetBody>
    with SingleTickerProviderStateMixin {
  GlobalKey<FlipCardState> _flipCardKey = GlobalKey<FlipCardState>();

  AnimationController _animationController;

  bool _show = false;

  Timer _timer;

  String _currentTime = formatDate(DateTime.now(), [HH, ':', nn, ':', ss]);

  String _downTime = formatDate(DateTime.now(), [HH, ':', nn, ':', ss]);
    
  .......
      
  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
        vsync: this, duration: Duration(milliseconds: 2000));

    _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
      setState(() {
        _currentTime = formatDate(DateTime.now(), [HH, ':', nn, ':', ss]);
      });
    });
  }
点击事件
void _onClick() {
    
    // 控制第二个圆 显示
    setState(() {
      _show = true;
    });

    Future.delayed(Duration.zero, () {
      _animationController.repeat();

      Future.delayed(Duration(seconds: 3), () {
        setState(() {
            
          _downTime = formatDate(DateTime.now(), [HH, ':', nn, ':', ss]);
            // 结束扫描动画
          _animationController.stop();
            // 使用 FlipCard 开始 翻转
          _flipCardKey.currentState.toggleCard();
            
            // 隐藏扫描 widget
          _show = false;
        });
      });
    });
  }
FlipCard back 视图简单实现
back: Card(
    elevation: 0.0,
    child: Container(
        padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
        child: Column(
            children: [
                Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                        Column(
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: [
                                Text(
                                    "下班打卡成功",
                                    style: TextStyle(fontSize: 26.0),
                                ),
                                Padding(
                                    padding: const EdgeInsets.all(8.0),
                                    child: Text(
                                        "打卡时间 $_downTime",
                                        style: TextStyle(fontSize: 16.0),
                                    ),
                                ),
                            ],
                        ),
                        GestureDetector(
                            child: Icon(Icons.close),
                            onTap: () {
                                // 控制 翻转 FlipCard
                                _flipCardKey.currentState.toggleCard();
                            },
                        )
                    ],
                )
            ],
        ),
    ),
),
加上音效

初始化 AudioPlayer

AudioPlayer _player;

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

   	_player = AudioPlayer();
    
    // 监听 AudioPlayer 播放状态,播放完成之后结束播放,重置播放时间
    _player.playerStateStream.listen((state) {
        switch (state.processingState) {
            case ProcessingState.completed:
                _player.seek(Duration.zero, index: _player.effectiveIndices.first);
                _player.stop();
                break;
            case ProcessingState.idle:
                break;
            case ProcessingState.loading:
                break;
            case ProcessingState.buffering:
                break;
            case ProcessingState.ready:
                break;
        }
    });
    _init();
}

  _init() async {
    final session = await AudioSession.instance;
    await session.configure(AudioSessionConfiguration.music());
    try {
      var duration = await _player.setAsset('assets/sound/on_duty.mp3');
    } catch (e) {
      // catch load errors: 404, invalid url ...
      print("An error occured $e");
    }
  }
点击时间播放音频
void _onClick() {
    setState(() {
      _show = true;
    });

    if (_player.playing) {
      return;
    }

    Future.delayed(Duration.zero, () {
      _animationController.repeat();

      Future.delayed(Duration(seconds: 3), () {
        setState(() {
          _downTime = formatDate(DateTime.now(), [HH, ':', nn, ':', ss]);
          _animationController.stop();
          _player.play(); // add code
          _flipCardKey.currentState.toggleCard();
          _show = false;
        });
      });
    });
  }
组件销毁生命周期放过释放资源
@override
void dispose() {
    if (null != _timer && _timer.isActive) {
        _timer.cancel();
    }

    if (null != _animationController) {
        _animationController.dispose();
    }

    if (null != _player) {
        _player.pause();
        _player.dispose();
    }
    super.dispose();
}

源代码: gitee.com/shizidada/f…