整体布局
@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
https://pub.flutter-io.cn/packages/flip_card
音频播放 just_audio: ^0.6.5
https://pub.flutter-io.cn/packages/just_audio
格式化时间 date_format: ^1.0.8
https://pub.flutter-io.cn/packages/date_format
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();
}