效果
源代码
组件代码 animator_button.dart
import 'package:flutter/material.dart';
class MyController extends ChangeNotifier {
DownloadState _downloadState = DownloadState.idle;
resetState() {
_downloadState = DownloadState.idle;
notifyListeners();
}
}
class AnimatorButtonWidget extends StatefulWidget {
final Color mainColor;
final MyController controller;
final double btnWidth;
const AnimatorButtonWidget({
super.key,
required this.mainColor,
required this.btnWidth,
required this.controller,
});
@override
State<StatefulWidget> createState() {
return AnimatorButtonWidgetState();
}
}
enum DownloadState { idle, downloading, completed }
class AnimatorButtonWidgetState extends State<AnimatorButtonWidget>
with TickerProviderStateMixin {
/// 下载进度控制器
late AnimationController _progressController;
/// 缩放控制, 点击按钮时进行短暂缩放
late AnimationController _scaleController;
/// 下载完成之后 下载按钮的消失动画
late AnimationController _fadeController;
late Animation _scaleAnimation;
late Animation _progressAnimation;
late Animation _fadeAnimation;
late final double _btnWidth;
/// 遮罩层透明度
double _barColorOpacity = .4;
/// 按钮状态
DownloadState _downloadState = DownloadState.idle;
final TextStyle _textStyle =
const TextStyle(color: Colors.white, fontSize: 18);
/// 当前的下载进度
int _downloadProgressValue = 0;
@override
void initState() {
super.initState();
widget.controller.addListener(() {
if (widget.controller._downloadState == DownloadState.idle) {
resetStatue();
}
});
_btnWidth = widget.btnWidth;
_progressController = AnimationController(
vsync: this,
duration: const Duration(seconds: 3),
);
_scaleController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 200),
);
_fadeController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
_fadeAnimation = Tween<double>(begin: 50, end: 0).animate(_fadeController)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
_scaleController.reverse();
_fadeController.forward();
_progressController.forward();
}
});
_scaleAnimation =
Tween<double>(begin: 1, end: 1.05).animate(_scaleController)
..addStatusListener((status) async {
if (status == AnimationStatus.completed) {
_scaleController.reverse();
_fadeController.forward();
_progressController.reset();
_progressController.forward();
}
});
_progressAnimation =
Tween<double>(begin: 0, end: _btnWidth).animate(_progressController)
..addListener(() {
setState(() {
_downloadProgressValue =
((100 * _progressAnimation.value / _btnWidth)).floor();
});
})
..addStatusListener((status) {
debugPrint('进度状态发生变化');
if (status == AnimationStatus.forward) {
setState(() {
_downloadState = DownloadState.downloading;
_barColorOpacity = 0.4;
});
} else if (status == AnimationStatus.completed) {
setState(() {
_downloadState = DownloadState.completed;
_barColorOpacity = 0;
});
}
});
}
@override
void dispose() {
super.dispose();
_progressController.dispose();
_scaleController.dispose();
_fadeController.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
debugPrint('当前状态是 $_downloadState');
if (_downloadState == DownloadState.idle) {
startDownload();
} else if (_downloadState == DownloadState.completed) {
resetStatue();
} else {
debugPrint('下载中,点了没用 $_downloadProgressValue');
}
},
child: Stack(children: [
_mainLayout(),
_loadingLayout(),
]),
);
}
void startDownload() {
_scaleController.forward();
}
void resetStatue() {
setState(() {
_downloadState = DownloadState.idle;
});
_fadeController.reverse();
_progressController.reset();
}
void setDownloading() {
setState(() {
_downloadState = DownloadState.downloading;
});
}
/// 主要布局
Widget _mainLayout() {
return AnimatedBuilder(
animation: _scaleController,
builder: (BuildContext context, Widget? child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Container(
width: _btnWidth,
height: 50,
color: widget.mainColor.withOpacity(.7),
child: Row(children: [
_textActionButton(),
_downloadActionBtn()
]))));
});
}
/// loading遮罩层
Widget _loadingLayout() {
return AnimatedBuilder(
animation: _progressController,
builder: (BuildContext context, Widget? child) {
return Positioned(
left: 0,
top: 0,
height: 50,
width: _progressAnimation.value,
child: AnimatedOpacity(
opacity: _barColorOpacity,
duration: const Duration(milliseconds: 200),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Container(color: Colors.white))));
});
}
/// 文字部分
Widget _textActionButton() {
Widget w;
switch (_downloadState) {
case DownloadState.idle:
w = Text('开始下载', style: _textStyle);
break;
case DownloadState.downloading:
w = Text('$_downloadProgressValue%', style: _textStyle);
break;
case DownloadState.completed:
w = Text('已完成', style: _textStyle);
break;
}
return Expanded(child: Center(child: w));
}
/// 下载箭头按钮
Widget _downloadActionBtn() {
var decoration = const BorderRadius.only(
topLeft: Radius.circular(0),
bottomLeft: Radius.circular(0),
topRight: Radius.circular(10),
bottomRight: Radius.circular(10));
return AnimatedBuilder(
builder: (BuildContext context, Widget? child) {
return Container(
decoration: BoxDecoration(
color: widget.mainColor, borderRadius: decoration),
width: _fadeAnimation.value,
height: 50,
child: const Icon(Icons.arrow_downward, color: Colors.white));
},
animation: _fadeController);
}
}
main.dart代码:
import 'package:flutter/material.dart';
import 'package:flutter_neumorphism/widget/animator_button.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '动态按钮',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final MyController _myController = MyController();
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[300],
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
_myController.resetState();
},
child: const Text('重置状态'),
),
const SizedBox(height: 20),
AnimatorButtonWidget(
mainColor: Colors.blue,
btnWidth: 200,
controller: _myController,
),
const SizedBox(height: 20),
AnimatorButtonWidget(
mainColor: Colors.orange,
btnWidth: 200,
controller: _myController,
),
const SizedBox(height: 20),
AnimatorButtonWidget(
mainColor: Colors.black87,
btnWidth: 300,
controller: _myController,
),
const SizedBox(height: 20),
AnimatorButtonWidget(
mainColor: Colors.deepPurple,
btnWidth: 300,
controller: _myController,
),
],
),
));
}
}
实现原理
这是Flutter封装组件的常见写法,自定义StatefulWidget。
- 给一个widget设计多种状态(
idle, downloading, completed)分别对应初始状态,下载中,已完成。 - 每一种状态对应了一种静态布局
/// 文字部分
Widget _textActionButton() {
Widget w;
switch (_downloadState) {
case DownloadState.idle:
w = Text('开始下载', style: _textStyle);
break;
case DownloadState.downloading:
w = Text('$_downloadProgressValue%', style: _textStyle);
break;
case DownloadState.completed:
w = Text('已完成', style: _textStyle);
break;
}
return Expanded(child: Center(child: w));
}
-
结合 动画组件 AnimatedBuilder ,动画 Animation,以及 动画控制器 AnimationController 构建动态效果,多个动画组件 结合 Widget的自身状态, 创造出 widget状态发生变化时的动态效果。
-
最后开放一个外部控制器,可以由外部代码控制Widget的自身状态。