Flutter版本:1.2.1
Dart版本:2.2.0
给MaterialApp控件中的Scaffold设置开始方向和结束方向抽屉(开始方向可以理解成左侧,结束方向可以理解成右侧):
import 'package:flutter/material.dart';
void main() {
runApp(new MaterialApp(
//隐藏右上角debug标签
debugShowCheckedModeBanner: false,
home: new Scaffold(
//设置导航栏
appBar: new AppBar(
//leading指的是导航栏左侧的按钮,可以放置返回按钮,菜单按钮等,
// 现在放置的是一个菜单按钮,实现了打开抽屉布局的代码
leading: Builder(builder: (context) {
return new IconButton(
icon: new Icon(Icons.menu),
onPressed: () {
Scaffold.of(context).openDrawer(); //打开开始方向抽屉布局
});
}),
title: new Text("Drawer"),
actions: [
//在actions里面放置了右侧方向菜单的按钮
new Builder(builder: (context) {
return new IconButton(
icon: new Icon(Icons.menu),
onPressed: () {
Scaffold.of(context).openEndDrawer(); //打开结束方向抽屉布局
});
})
],
),
body: new Center(
child: new Text("抽屉布局"),
),
//设置开始方向抽屉
drawer: new Drawer(
child: new Container(
//width: 200.0,
//color: Colors.white,
padding: EdgeInsets.only(top: 30.0),
child: new ListView.builder(
itemCount: 10,
itemBuilder: (context, i) {
return new RaisedButton(
child: new Text("菜单 $i"),
onPressed: () {
Navigator.pop(context); //关闭抽屉面板
});
},
)),
),
//设置了结束方向的抽屉
endDrawer: new Drawer(
child: Container(
//width: 200.0,
//color: Colors.white,
padding: EdgeInsets.only(top: 30.0),
child: new ListView.builder(
itemCount: 10,
itemBuilder: (context, i) {
return new RaisedButton(
child: new Text("菜单 $i"),
onPressed: () {
Navigator.pop(context); //关闭抽屉面板
});
},
)),
),
)));
}
效果:
手势滑动和点击菜单都可以打开抽屉布局
1.设置Scaffold的参数drawer对应的控件可以设置开始方向抽屉布局。
2.设置Scaffold的参数endDrawer可以设置结束方向抽屉布局。
系统给出了一个封装好的类Drawer控件作为抽屉布局,使用Drawer以外的控件也是可以的,因为drawer和endDrawer属性都是widget类型。但是使用别的widget需要自己设置背景色和宽度等。因为drawer和endDrawer最后会被放置在一个半透明的外层布局中。Drawer控件设置了背景色和宽度,所以设置成其他控件需要对样式做处理。
下面是直接给drawer设置ListView控件,没有设置宽度和背景色的样子
3.设置菜单
开始方向菜单:设置AppBar中时给leading设置菜单按钮,然后在按下事件中设置打开开始方向抽屉,如果设置了drawer的控件但是没有设置leading对应的菜单控件,那么系统会自动给leading设置菜单按钮并设置打开抽屉事件
结束方向菜单:设置AppBar时给actions设置一个菜单按钮,然后在按下事件中设置打开结束方向抽屉,如果设置了endrawer的控件但是没有设置actions对应的控件,那么系统会自动给actions设置一个菜单按钮并设置打开抽屉事件
Scaffold.of(context).openDrawer();
注意一下这个context的获取。在设置leading时使用的是Builder这个widget,在创建Builder是设置builder方法参数即可拿到context,我们在设置widget想获取到context都可以使用Builder控件来获取。看一下它的实现思路
class Builder extends StatelessWidget {
const Builder({
Key key,
@required this.builder
}) : assert(builder != null),
super(key: key);
final WidgetBuilder builder;
@override
Widget build(BuildContext context) => builder(context);
}
在build方法中调用builder这个方法类型的成员变量传入context,看下builder的类型
typedef WidgetBuilder = Widget Function(BuildContext context);
这是一个接受context的方法。
Navigator.pop(context);
上面提到,如果设置了drawer或者endDrawer但是没有设置菜单,AppBar会默认设置菜单,下面看下AppBar对应的State类_AppBarState的build方法中是如何设置菜单的
@override
Widget build(BuildContext context) {
........
//1,设置leading
Widget leading = widget.leading;
if (leading == null && widget.automaticallyImplyLeading) {
if (hasDrawer) {
leading = IconButton(
icon: const Icon(Icons.menu),
onPressed: _handleDrawerButton,
tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip,
);
} else {
if (canPop)
leading = useCloseButton ? const CloseButton() : const BackButton();
}
}
if (leading != null) {
leading = ConstrainedBox(
constraints: const BoxConstraints.tightFor(width: _kLeadingWidth),
child: leading,
);
}
......
//2. 设置actions
Widget actions;
if (widget.actions != null && widget.actions.isNotEmpty) {
actions = Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: widget.actions,
);
} else if (hasEndDrawer) {
actions = IconButton(
icon: const Icon(Icons.menu),
onPressed: _handleDrawerButtonEnd,
tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip,
);
}
}
先判断leading等于null并且automaticallyImplyLeading为true,然后判断是否设置了drawer,这几个条件都满足的话则设置一个IconButton,图标是Icons.menu,并且设置按下事件onPressed为_handleDrawerButton方法,_handleDrawerButton实现了什么功能呢,看下代码
void _handleDrawerButton() {
Scaffold.of(context).openDrawer();
}
这个和之前手动设置菜单的点击事件一样
如果给AppBar设置了actions,那么创建Row控件,把他们放到Row中,所以设置的actions是依次横向排列的,如果没有设置actions,就判断是否设置了endDrawer,如果设置了endDrawer就创建与leading同样的IconButton只不过按键事件中打开的是右侧抽屉布局。
由于结束方向的抽屉endDrawer和开始方向的抽屉drawer创建过程几乎一模一样,所以只分析开始的drawer。
创建抽屉时序图如下:
第一步:创建ScaffoldState
Scaffold是一个有状态的widget,系统创建它时会调用createState方法,创建ScaffoldState,然后调用ScaffoldState的build方法。创建ScaffoldState时,会创建两个全局的key,一个代表endDrawer,一个代表drawer。从这两个key中可以拿到DrawerControllerState,它是真正绘制抽屉的类。
final GlobalKey _drawerKey = GlobalKey();
final GlobalKey _endDrawerKey = GlobalKey();
第二步:调用ScaffordState中的build方法
@override
Widget build(BuildContext context) {
。。。。。。
if (_endDrawerOpened) {
_buildDrawer(children, textDirection);
_buildEndDrawer(children, textDirection);
} else {
_buildEndDrawer(children, textDirection);
_buildDrawer(children, textDirection);
}
。。。。。。
}
这里省略了其他的代码,build方法创建了drawer和endDrawer并且两者中哪个打开了就把它放到最后创建,先创建的widget会被后面创建的盖住。
下面只看下_buildDrawer,另一个逻辑差不多
void _buildDrawer(List children, TextDirection textDirection) {
if (widget.drawer != null) {
assert(hasDrawer);
_addIfNonNull(
children,
DrawerController(
key: _drawerKey,
alignment: DrawerAlignment.start,
child: widget.drawer,
drawerCallback: _drawerOpenedCallback,
dragStartBehavior: widget.drawerDragStartBehavior,
),
_ScaffoldSlot.drawer,
// remove the side padding from the side we're not touching
removeLeftPadding: textDirection == TextDirection.rtl,
removeTopPadding: false,
removeRightPadding: textDirection == TextDirection.ltr,
removeBottomPadding: false,
);
}
}
方法中创建了DrawerController这个widget,然后调用_addIfNonNull将DrawerController添加到children中,而我们设置给Scaffold的drawer被放置到了DrawerController的child中,这样才能使抽屉被系统绘制。参数alignment设置成了DrawerAlignment.start,这是开始方向,对应的结束方向是DrawerAlignment.end。还设置了_drawerOpenedCallback回调方法,用于给ScaffordState设置抽屉的状态,并重绘widget。
第三步:创建DrawerControllerState
DrawerController是一个有状态的wiget,系统调用它的createState方法获取DrawerControllerState,DrawerControllerState这个类是创建抽屉的实际执行者,它混合使用了SingleTickerProviderStateMixin类,而SingleTickerProviderStateMixin又实现了
TickerProvider接口。TickerProvider是为了创建Ticker而生的。
第四步:创建AnimationController
创建DrawerControllerState时在它的initState中创建了AnimationController,将DrawerControllerState当做参数vsync传给AnimationController,以创建Ticker,同时设置了两个监听
class DrawerControllerState extends State with SingleTickerProviderStateMixin {
@override
void initState() {
super.initState();
_controller = AnimationController(duration: _kBaseSettleDuration, vsync: this)
..addListener(_animationChanged)
..addStatusListener(_animationStatusChanged);
}
......
}
void _animationChanged() {
//调用setState会让widget重绘
setState(() {
});
}
_animationStatusChanged会根据动画的状态的变化来添加或者移除_historyEntry,后面具体讲解。
下面是AnimationController的构造函数
AnimationController({
double value,
this.duration,
this.debugLabel,
this.lowerBound = 0.0,
this.upperBound = 1.0,
this.animationBehavior = AnimationBehavior.normal,
@required TickerProvider vsync,
}) : assert(lowerBound != null),
assert(upperBound != null),
assert(upperBound >= lowerBound),
assert(vsync != null),
_direction = _AnimationDirection.forward {
_ticker = vsync.createTicker(_tick);
_internalSetValue(value ?? lowerBound);
}
构造函数中创建了Ticker
调用了_internalSetValue方法,这个方法中设置了_status = AnimationStatus.dismissed,DrawerControllerState中的build方法中会根据AnimationController的_status属性来判断显示还是隐藏抽屉。
void _internalSetValue(double newValue) {
_value = newValue.clamp(lowerBound, upperBound);
if (_value == lowerBound) {
_status = AnimationStatus.dismissed;
} else if (_value == upperBound) {
_status = AnimationStatus.completed;
} else {
_status = (_direction == _AnimationDirection.forward) ?
AnimationStatus.forward :
AnimationStatus.reverse;
}
}
第五步:创建Ticker
Ticker的注释的第一句话是:每个动画帧调用一次它的回调。当抽屉被打开时会执行动画,动画会回调给Ticker
AnimationController的构造函数中创建了Ticker,用到的就是传进来的vsync,它的类型是TickerProvider。调用它的createTicker方法,看一下它的实现类的实现
mixin SingleTickerProviderStateMixin on State implements TickerProvider {
Ticker _ticker;
@override
Ticker createTicker(TickerCallback onTick) {
。。。。。。
_ticker = Ticker(onTick, debugLabel: 'created by $this');
return _ticker;
}
}
创建Ticker并设置了回调,再回头看下AnimationController构造函数,传入了_tick方法作为回调。
第六步:调用DrawerControllerState的build方法
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterialLocalizations(context));
return ListTileTheme(
style: ListTileStyle.drawer,
child: _buildDrawer(context),
);
}
主要还是调用了_buildDrawer方法创建抽屉,_buildDrawer方法可以简写成下面的伪代码
Widget _buildDrawer(BuildContext context) {
。。。。。。
if (_controller.status == AnimationStatus.dismissed) {
return Empty;
} else {
return widget{
child : drawer;
};
}
}
判断_controller.status即判断AnimationController的状态,如果是AnimationStatus.dismissed那么就返回空的widget,如果不是就返回正常的包含用户设置的drawer的widget。
打开抽屉的时序图如下:
打开抽屉从Scaffold.of(context).openDrawer()开始
先获取到ScaffoldState然后调用它的openDrawer()方法,如下
void openDrawer() {
if (_endDrawerKey.currentState != null && _endDrawerOpened)
_endDrawerKey.currentState.close();
_drawerKey.currentState?.open();
}
在openDrawer中先判断结束方向的endDrawer是否打开,如果打开就先把它关闭。
然后调用_drawerkey.currentState?.open()。_drawerkey前面创建过程提到过,它是一个全局key,与DrawerControllerState绑定_drawerkey.currentState就是DrawerControllerState,然后调用它的open()方法。
void open() {
_controller.fling(velocity: 1.0);
if (widget.drawerCallback != null)
widget.drawerCallback(true);
}
open方法中调用了AnimationController中的fling方法,代码开始在AnimationController中执行
TickerFuture fling({ double velocity = 1.0, AnimationBehavior animationBehavior }) {
_direction = velocity < 0.0 ? _AnimationDirection.reverse : _AnimationDirection.forward;
。。。。。。
final Simulation simulation = SpringSimulation(_kFlingSpringDescription, value, target, velocity * scale)
..tolerance = _kFlingTolerance;
return animateWith(simulation);
}
fling方法中根据velocity的值来确定_direction的方向,
当velocity大于0时,_direction=_AnimationDirection.forward
当velocity小于0时,_direction=_AnimationDirection.reverse
上面代码中调用fling方法传入的velicity是1,大于0,所以_direction=_AnimationDirection.forward
fling方法的最后调用了animateWith方法,animateWith方法比较简单,它调用了_startSimulation方法,如下所示。
TickerFuture _startSimulation(Simulation simulation) {
assert(simulation != null);
assert(!isAnimating);
_simulation = simulation;
_lastElapsedDuration = Duration.zero;
_value = simulation.x(0.0).clamp(lowerBound, upperBound);
//代码1
final TickerFuture result = _ticker.start();
//代码2
_status = (_direction == _AnimationDirection.forward) ?
AnimationStatus.forward :
AnimationStatus.reverse;
//代码3
_checkStatusChanged();
return result;
}
代码1,_ticker.start,这里的_ticker是Ticker类对象,主要用于接收动画执行时每一帧的回调
代码2,根据_direction的方向设置_status的值,根据前面的分析,这里的_status=AnimationStatus.forward
代码3,调用_checkStatusChanged()检查_status的变化
下面先说下代码3中执行了哪些逻辑,回头再说代码1中Ticker的start()方法,原因是,Ticker中的回调不是同步的,是异步的,也就是调用了start()以及之后的方法需要等待动画执行之后才有回调,这个过程中,代码3的逻辑先被执行了。
看下_checkStatusChanged()方法
AnimationStatus _lastReportedStatus = AnimationStatus.dismissed;
void _checkStatusChanged() {
final AnimationStatus newStatus = status;
if (_lastReportedStatus != newStatus) {
_lastReportedStatus = newStatus;
notifyStatusListeners(newStatus);
}
}
_checkStatusChanged()方法中判断_lastReportedStatus与现在的是否相等,如果不相等就会调用notifyStatusListeners()方法
void notifyStatusListeners(AnimationStatus status) {
final List localListeners = List.from(_statusListeners);
for (AnimationStatusListener listener in localListeners) {
try {
if (_statusListeners.contains(listener))
listener(status);
} catch (exception, stack) {
。。。。。。
}
}
}
循环遍历_statusListener,并调用每一个监听。在第四步DrawerControllerState创建AnimationController时,调用addStatusListener添加了_animationStatusChanged()方法到StatusListener中。
现在_animationStatusChanged()被回调了。
void _animationStatusChanged(AnimationStatus status) {
switch (status) {
case AnimationStatus.forward:
_ensureHistoryEntry();
break;
case AnimationStatus.reverse:
_historyEntry?.remove();
_historyEntry = null;
break;
case AnimationStatus.dismissed:
break;
case AnimationStatus.completed:
break;
}
}
这个方法对status做了判断,共有四种判断分支,分别代表了,打开中forward,关闭中reverse,消失dismissed,完成complete。当前的status是AnimationStatus.forward,这个分支中调用了_ensureHistoryEntry()方法
跟踪_ensureHistoryEntry()方法
void _ensureHistoryEntry() {
if (_historyEntry == null) {
final ModalRoute route = ModalRoute.of(context);
if (route != null) {
//创建LocalHistoryEntry
_historyEntry = LocalHistoryEntry(onRemove: _handleHistoryEntryRemoved);
//添加进route
route.addLocalHistoryEntry(_historyEntry);
FocusScope.of(context).setFirstFocus(_focusScopeNode);
}
}
}
这个方法主要创建了LcalHistoryEntry,并添加进route中。
创建LocalHistoryEntry时设置了onRemove参数为_handleHistoryEntryRemoved()方法,用于LocalHistoryEntry被移除时做回调
至于LocalHistoryEntry的作用,跟踪下route.addLocalHistoryEntry(_historyEntry)的源码就明白了,方法上的注释有很清晰的解释。
/// Adds a local history entry to this route.
///
/// When asked to pop, if this route has any local history entries, this route
/// will handle the pop internally by removing the most recently added local
/// history entry.
///
/// The given local history entry must not already be part of another local
/// history route.
将本地历史记录项添加到此路由。
当被要求弹出时,如果此路由有任何本地历史记录项,则此路由将处理pop内部删除最近添加的本地历史条目。
给定的本地历史记录条目必须不是另一个本地的一部分历史的路由。
分析一下这个过程,当我们打开抽屉时,会添加一个LocalHistoryEntry历史记录到Route中,当我们调用Navigator.pop(context)时,系统会将最近的一条历史记录移除掉,LocalHistoryEntry被移除时会执行onRemove方法进行回调,对应的,在_ensureHistoryEntry()方法中设置的回调方法是_handleHistoryEntryRemoved,它就会被调用。看一下这个方法做了什么处理
void _handleHistoryEntryRemoved() {
_historyEntry = null;
close();
}
在这个方法中调用了close()方法进行关闭抽屉。这就是为什么用Scaffold.of(context).openDrawer()打开抽屉,却只能用Navigator.pop(context)来关闭抽屉。
注意这里并不仅仅是Navigator.pop(context)能关闭抽屉,按返回键也能够关闭抽屉,也就是说添加了LocalHistoryentry之后相当于添加了一个页面,它响应系统的返回按键,我们可以根据LocalHistoryEntry的添加或者移除来切换widget的显示与隐藏,也就相当于添加了新的页面和关闭了页面。
关于LocalHistoryEntry的用法,在LocalHistoryRoute.addLocalHistoryEntry()方法中有一个例子,可以学习一下。代码挺长的就不列出来了。
讲完了代码3的逻辑之后,回到代码1 _ticker.start()方法,看一下动画执行后的回调过程。
TickerFuture start() {
。。。。。。
if (shouldScheduleTick) {
scheduleTick();
}
if (SchedulerBinding.instance.schedulerPhase.index > SchedulerPhase.idle.index &&
SchedulerBinding.instance.schedulerPhase.index < SchedulerPhase.postFrameCallbacks.index)
_startTime = SchedulerBinding.instance.currentFrameTimeStamp;
return _future;
}
start()调用了scheduleTick()方法
@protected
void scheduleTick({ bool rescheduling = false }) {
assert(!scheduled);
assert(shouldScheduleTick);
_animationId = SchedulerBinding.instance.scheduleFrameCallback(_tick, rescheduling: rescheduling);
}
scheduleTick最终调用了SchedulerBinding执行动画,SchedulerBinding用来调度系统的渲染画面功能,它会在每一帧变化时给Ticker回调,也就是会不停的回调,直到动画结束,在这里回调了_tick()方法
void _tick(Duration timeStamp) {
assert(isTicking);
assert(scheduled);
_animationId = null;
_startTime ??= timeStamp;
//继续回调
_onTick(timeStamp - _startTime);
// The onTick callback may have scheduled another tick already, for
// example by calling stop then start again.
if (shouldScheduleTick)
scheduleTick(rescheduling: true);
}
_tick又调用了_onTick。
这个_onTick类型是TickerCallback
final TickerCallback _onTick;
typedef TickerCallback = void Function(Duration elapsed);
_onTick是在AnimationController创建Ticker时传入的,传入的方法也叫_tick(),只不过这个方法在AnimationController中
void _tick(Duration elapsed) {
。。。。。。
//设置了状态
if (_simulation.isDone(elapsedInSeconds)) {
_status = (_direction == _AnimationDirection.forward) ?
AnimationStatus.completed :
AnimationStatus.dismissed;
stop(canceled: false);
}
notifyListeners();
_checkStatusChanged();
}
现在代码又回到了AnimationController中执行
显示设置了状态_status
然后调用notifyListeners()方法,这个方法和前面提到的notifyStatusListeners方法作用类似,
void notifyListeners() {
final List localListeners = List.from(_listeners);
for (VoidCallback listener in localListeners) {
try {
if (_listeners.contains(listener))
//监听回调
listener();
} catch (exception, stack) {
。。。。。。
}
}
}
它回调的是DrawerControllerState创建AnimationController时调用addListener()添加的监听方法,对应的是DrawerControllerState中的_animationChanged方法前面提到过它会重绘widget,执行DrawerControllerState的build方法,然后调用DrawerControllerState的_buildDrawer方法,这些前面也都说过。
最后调用的是_checkStatusChanged()方法,这个也前面也说过。
最后总结一下,我们在使用Scafford时创建了drawer或者endDrawer,然后执行绘制过程,系统创建了ScaffordState,调用它的build方法,在这个方法中创建DrawerController,将其添加进children让它能加入绘制流程中,并设置回调接受打开关闭状态,将我们设置的drawer或者endDrawer传给DrawerController,同时传入全局key,以便于获取到DrawerControllerState,然后DrawerControllerState被创建,DrawerControllerState中创建了AnimationController用于动画的控制,并设置了动画监听,系统调用DrawerControllerState的build方法,build方法又调用了_buildDrawer方法,这个方法根据动画的状态,抽屉开始被真正绘制,或者隐藏。AnimationController使用Ticker开始执行动画并接收回调。
当我们打开抽屉时,先获取ScaffordState,然后调用openDrawer方法,openDrawer方法中又调用了全局key中存放的DrawerControllerState,调用它的open方法,open方法调用了AnimationController开始动画,在AnimationController中执行了一系列方法之后调用了Ticker,Ticker调用了系统的SchedulerBinding开始动画,然后开始不断地回调过程,直到结束。回调给DrawerControllerState时如果是打开状态就添加一个LocalHistoryEntry,用于关闭。
当我们调用Navigator.pop()方法时,LocalHistoryEntry被移除,移除监听方法被调用,执行关闭过程,关闭过程与打开过程执行的流程很类似。只不过是状态的变化是相反的。