最近产品同学想要改变一下app的首页样式,做一个如下面所示的效果。
之前在做安卓原生项目的时候,看到有人实现过这个效果,这次想了想Flutter下的实现方式,现在记录一下代码和实现思路。
1、创建Flutter工程,创建自定义Drawer类
创建CustomDrawer,将主页面作为一个child传入。
class CustomDrawer extends StatefulWidget {
final Widget child;
static CustomDrawerState of(BuildContext context) =>
context.findRootAncestorStateOfType<CustomDrawerState>();
const CustomDrawer({Key key, this.child}) : super(key: key);
@override
CustomDrawerState createState() => CustomDrawerState();
}
main.dart
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: CustomDrawer(child: MyHomePage(title: 'Flutter Demo Home Page'),),
);
}
}
2、创建AnimationController
因为涉及到动画操作,所以需要创建AnimationController对象,指定动画持续时间,定义一些动画过程中的常量。
class CustomDrawerState extends State<CustomDrawer>
with SingleTickerProviderStateMixin {
//动画持续时间
static const Duration toggleDuration = Duration(microseconds: 250);
//主窗口最大右移距离
static const double maxSlide = 225;
//拖动打开Drawer时,打开窗口的最做左边的位置
static const double minDragStartEdge = 60;
//拖动打开Drawer时,打开窗口操作的最右边的位置
static const double maxDragStartEdge = maxSlide - 16;
AnimationController _animationController;
//是否可以拖动的标志位
bool _canDrag = false;
@override
void initState() {
super.initState();
//创建AnimationController对象,指定动画时间
_animationController = AnimationController(
vsync: this, duration: CustomDrawerState.toggleDuration);
}
3、定义抽屉的打开,关闭操作
void close() {
_animationController.reverse();
}
void open() {
_animationController.forward();
}
void toggleDrawer() {
_animationController.isCompleted ? close() : open();
}
4、动画过程中,刷新页面
拖动过程中,需要用到GestureDector,动画过程中,通过动画执行进度,获得child组件应该平移的位置和缩放程度,更新页面。
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: ()async{
//当抽屉打开时,点击返回先关闭抽屉
if (_animationController.isCompleted) {
close();
return false;
}
return true;
},
child: GestureDetector(
//拖动开始执行事件
onHorizontalDragStart: _onDragStart,
//拖动过程中更新事件
onHorizontalDragUpdate: _onDragUpdate,
//拖动结束时执行事件
onHorizontalDragEnd: _onDragEnd,
child: AnimatedBuilder(
animation: _animationController,
child: widget.child,
builder: (context, child) {
//当前动画执行进度
double animValue = _animationController.value;
//根据进度计算child偏移量
final slideAmout = maxSlide * animValue;
//根据进度计算child缩放程度,最大缩放到70%
final contentScale = 1.0 - 0.3 * animValue;
return Stack(
children: <Widget>[
MyDrawer(),
Transform(
transform: Matrix4.identity()
..translate(slideAmout)
..scale(contentScale,contentScale),
alignment: Alignment.centerLeft,
child: GestureDetector(
//当抽屉打开时,点击child组件,关闭抽屉
onTap: _animationController.isCompleted ? close :null,
child: widget.child,
),
),
],
);
},
),
),
);
}
5、实现onHorizontalDragStart,onHorizontalDragUpdate、onHorizontalDragEnd 方法
void _onDragStart(DragStartDetails details) {
//判断拖动是否是打开抽屉
bool isDragOpenFromLeft = _animationController.isDismissed &&
details.globalPosition.dx < minDragStartEdge;
//判断拖动是否是关闭抽屉
bool isDragCloseFromRight = _animationController.isCompleted &&
details.globalPosition.dx > maxDragStartEdge;
_canDrag = isDragOpenFromLeft || isDragCloseFromRight;
}
void _onDragUpdate(DragUpdateDetails details) {
if(_canDrag){
//根据偏移量得到当前的动画进度
double delta = details.primaryDelta / maxSlide;
_animationController.value += delta;
}
}
void _onDragEnd(DragEndDetails details) {
//临界值加速度
double _kMinFlingVelocity = 365.0;
//如果当前动画在开始时已经结束 或者 已经执行完毕,
if(_animationController.isDismissed || _animationController.isCompleted){
return;
}
if(details.velocity.pixelsPerSecond.dx.abs() >= _kMinFlingVelocity){
//如果当前加速度大于临界值,计算得到可见加速度
double visualVelcoity = details.velocity.pixelsPerSecond.dx /
MediaQuery.of(context).size.width;
//fling()函数允许您提供速度(velocity)、力量(force)、position(通过Force对象)
_animationController.fling(velocity: visualVelcoity);
}else if(_animationController.value < 0.5){
//动画执行不到一半,关闭
close();
}else{
//执行大于一半,打开抽屉
open();
}
}