Flutter抽屉布局的使用以及实现过程

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); //关闭抽屉面板
                      });
                },
              )),
        ),
      )));
}

效果:

Flutter抽屉布局的使用以及实现过程_第1张图片

Flutter抽屉布局的使用以及实现过程_第2张图片

手势滑动和点击菜单都可以打开抽屉布局

 

使用方式

一、生成抽屉和菜单

1.设置Scaffold的参数drawer对应的控件可以设置开始方向抽屉布局。

2.设置Scaffold的参数endDrawer可以设置结束方向抽屉布局。

系统给出了一个封装好的类Drawer控件作为抽屉布局,使用Drawer以外的控件也是可以的,因为drawer和endDrawer属性都是widget类型。但是使用别的widget需要自己设置背景色和宽度等。因为drawer和endDrawer最后会被放置在一个半透明的外层布局中。Drawer控件设置了背景色和宽度,所以设置成其他控件需要对样式做处理。

下面是直接给drawer设置ListView控件,没有设置宽度和背景色的样子

Flutter抽屉布局的使用以及实现过程_第3张图片

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实现源码

一、菜单

上面提到,如果设置了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,
      );
    }
    
  }

代码1,设置leading

先判断leading等于null并且automaticallyImplyLeading为true,然后判断是否设置了drawer,这几个条件都满足的话则设置一个IconButton,图标是Icons.menu,并且设置按下事件onPressed为_handleDrawerButton方法,_handleDrawerButton实现了什么功能呢,看下代码

  void _handleDrawerButton() {
    Scaffold.of(context).openDrawer();
  }

这个和之前手动设置菜单的点击事件一样

代码2,设置actions

如果给AppBar设置了actions,那么创建Row控件,把他们放到Row中,所以设置的actions是依次横向排列的,如果没有设置actions,就判断是否设置了endDrawer,如果设置了endDrawer就创建与leading同样的IconButton只不过按键事件中打开的是右侧抽屉布局。

 

二、抽屉布局的创建与显示过程

由于结束方向的抽屉endDrawer和开始方向的抽屉drawer创建过程几乎一模一样,所以只分析开始的drawer。

创建过程

创建抽屉时序图如下:

Flutter抽屉布局的使用以及实现过程_第4张图片

 

第一步:创建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。

 

打开抽屉的过程

打开抽屉的时序图如下:

Flutter抽屉布局的使用以及实现过程_第5张图片

打开抽屉从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被移除,移除监听方法被调用,执行关闭过程,关闭过程与打开过程执行的流程很类似。只不过是状态的变化是相反的。

 

你可能感兴趣的:(Flutter,Flutter)