3.3、Flutter:组件Widget

一、概述

声明式UI 和 响应式UI

  • Flutter的页面编写风格,属于声明式UI风格
    这与iOS的 UIKit响应式 差距很大,不过后面的 SwiftUI 苹果拥抱 声明式UI 的努力。

  • Flutter的UI框架吸取了React的理念,即 UI 是关于状态的函数。
    所谓的状态,可以理解为页面所需要的数据。可以分为短时状态,如StatefullWidget里面的state,其不能直接被其他widget获取。应用级别状态,如多个页面共享的state。

image.png

Flutter组件的分类

1、基础组件

  • 文本,Text
  • 按钮,TextButton
  • 图片图标,ImageIcon
  • 输入框 和 表单,TextFieldForm
  • 进度指示器,LinearProgressIndicatorCircularProgressIndicator
  • 单选框和复选框,SwitchCheckbox

2、布局之容器组件

  • 装饰,DecoratedBoxBoxDecoration等,如颜色、背景、边框、渐变、阴影等
  • 复合容器,Container,它是结合align、padding、margin、box、变换等多种功能。
  • 尺寸,主要是设置页面的宽高大小 或 宽高比例。
    宽高SizedBoxAspectRatioLimitedBoxFractionallySizedBox等;
    限制类ConstrainedBoxBoxConstraintsUnconstrainedBox
  • 对齐,Align
  • 居中,Center
  • 填充,Padding
  • 剪裁,ClipOvalClipPathClipRectClipRRect
  • 变换(如旋转等),Transform
  • 适配(如换行,即超出父组件边界约束),FittedBox

3、布局组件

  • 线性布局,RowColumn
  • 弹性布局,FlexExpanded
  • 流式布局(子元素自动换行),WrapFlow
  • 层叠布局,StackPositioned

4、复合组件(滚动、列表等)

  • 简单滚动列表(类似iOS的ScrollView),SingleChildScrollView
  • 列表,ListViewAnimatedList
  • 二维列表(类似iOS的CollectionView),GridView
  • 自定义滚动列表,CustomScrollViewNestedScrollView
  • TabBar,TabBarView

5、交互组件

  • 手势,GestureDetectorGestureRecognizer等相关类
  • 动画,AnimationCurveAnimationControllerTweenTicker等相关类

二、组件相关类

主要汇总组件对应的类以及继承关系,对于组件学习和内部原理有一定的帮助。

一、组件基类
abstract class DiagnosticableTree with Diagnosticable
abstract class Widget extends DiagnosticableTree

二、普通组件
abstract class StatelessWidget extends Widget
abstract class StatefulWidget extends Widget

// 状态
abstract class State<T extends StatefulWidget> with Diagnosticable


三、布局组件
// 布局类基类
abstract class RenderObjectWidget extends Widget 

// 包含一个子Widget,如:ConstrainedBox、DecoratedBox等
abstract class SingleChildRenderObjectWidget extends RenderObjectWidget 
  通过属性child关联单个widget

// 包含多个子Widget,一般都有一个children参数,接受一个Widget数组。如Row、Column、Stack等
abstract class MultiChildRenderObjectWidget extends RenderObjectWidget 
  通过属性children关联多个widget。

// Widget树的叶子节点,用于没有子节点的widget,通常基础组件都属于这一类,如Image。
abstract class LeafRenderObjectWidget extends RenderObjectWidget 

// Widget代理
abstract class ProxyWidget extends Widget
abstract class ParentDataWidget<T extends ParentData> extends ProxyWidget

// 弹性布局,flex
class Flex extends MultiChildRenderObjectWidget

class Flexible extends ParentDataWidget<FlexParentData>
class Expanded extends Flexible

// 线性布局
class Column extends Flex
class Row extends Flex

// 设置padding
class Padding extends SingleChildRenderObjectWidget
// SizedBox 设置宽高
class SizedBox extends SingleChildRenderObjectWidget
// Spacer 基于flex的空白,内部基于Expanded实现的,它只是Expanded的一个包装类
class Spacer extends StatelessWidget

// 流式布局
class Wrap extends MultiChildRenderObjectWidget
class Flow extends MultiChildRenderObjectWidget

// 层叠布局
class Stack extends MultiChildRenderObjectWidget
class Positioned extends ParentDataWidget<StackParentData>

// 对齐与相对定位
class Align extends SingleChildRenderObjectWidget
class Center extends Align

/* 容器类组件*/
// 填充
class Padding extends SingleChildRenderObjectWidget

// 尺寸大小限制
class ConstrainedBox extends SingleChildRenderObjectWidget
class SizedBox extends SingleChildRenderObjectWidget
class AspectRatio extends SingleChildRenderObjectWidget
class FractionallySizedBox extends SingleChildRenderObjectWidget
class LimitedBox extends SingleChildRenderObjectWidget

class UnconstrainedBox extends StatelessWidget

// 装饰类组件
class DecoratedBox extends SingleChildRenderObjectWidget
class BoxDecoration extends Decoration

// 组合类容器
class Container extends StatelessWidget


/* Material常用组件类 */
class Scaffold extends StatefulWidget

abstract class PreferredSizeWidget implements Widget
class AppBar extends StatefulWidget implements PreferredSizeWidget

class Drawer extends StatelessWidget
class FloatingActionButton extends StatelessWidget

class BottomNavigationBar extends StatefulWidget 
class BottomNavigationBarItem

三、基础组件

1.1、Widget 和 Element

Widget只是UI元素的一个配置数据,并且一个Widget可以对应多个Element。
注意1:Widget实际上是Element的配置数据树,而真正的UI渲染树是由Element构成。
注意2:一个Widget对象可以对应多个Element对象。

1.2、Widget主要接口

// Widget 是一个抽象类。在Flutter开发中,我们一般都不用直接继承Widget类来实现一个新组件。
@immutable
abstract class Widget extends DiagnosticableTree { // 继承与DiagnosticableTree“诊断树”,主要作用是提供调试信息。
  // 类似于React/Vue中的key,主要的作用是决定是否在下一次build时复用旧的widget
  const Widget({ this.key });
  final Key key;
    
  // Flutter Framework隐式调用的,在我们开发过程中基本不会调用到。
  @protected
  Element createElement();

  @override
  String toStringShort() {
    return key == null ? '$runtimeType' : '$runtimeType-$key';
  }

  // 复写父类的方法,主要是设置诊断树的一些特性。
  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
  }
  
  // 静态方法,它主要用于在Widget树重新build时复用旧的widget
  // 源码分析可知:只要newWidget与oldWidget的runtimeType和key同时相等时就会用newWidget去更新Element对象的配置,否则就会创建新的Element。
  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }
}

1.3、核心Widget子类:StatelessWidget 和 StatefulWidget

StatelessWidget

  • 它继承自Widget类,重写了createElement()方法:
// StatelessElement 间接继承自Element类,与StatelessWidget相对应(作为其配置数据)。
@override
StatelessElement createElement() => new StatelessElement(this);
  • 用于不需要维护状态的场景,它通常在build方法中通过嵌套其它Widget来构建UI,在构建过程中会递归的构建其嵌套的Widget。

  • 初始BuildContext
    表示当前widget在widget树中的上下文,每一个widget都会对应一个context对象(因为每一个widget都是widget树上的一个节点)。实际上,context是当前widget在widget树中位置中执行”相关操作“的一个句柄(例如,它提供了从当前widget开始向上遍历widget树以及按照widget类型查找父级widget的方法。)。

StatefulWidget

abstract class StatefulWidget extends Widget {
  const StatefulWidget({ Key key }) : super(key: key);
    
  // StatefulElement 间接继承自Element类,与StatefulWidget相对应(作为其配置数据)。
  // StatefulElement中可能会多次调用createState()来创建状态(State)对象。
  @override
  StatefulElement createElement() => new StatefulElement(this);
    
  // 比StatelessWidget类多添加了一个新的接口createState()。
  // 用于创建和Stateful widget相关的状态,它在Stateful widget的生命周期中可能会被多次调用。
  @protected
  State createState();
}
  • 注意:StatefulWidget的组件一般会将 Widget build(BuildContext context) 放在State中实现。

理解State

  • 核心作用:
    构建Widget的时候可以从State读取信息,同时State变化的时候可以同步更新对应Widget。

  • State中有两个常用属性:
    widget,它表示与该State实例关联的widget实例;context,当前widget对应的BuildContext上下文。

  • 深刻理解「State生命周期」

class _CounterWidgetState extends State<CounterWidget> {  
  int _counter;

  // step 1、
  // 当Widget第一次插入到Widget树时会被调用,对于每一个State对象,Flutter framework只会调用一次该回调,
  // 所以,通常在该回调中做一些一次性的操作,如状态初始化、订阅子树的事件通知等。
  @override
  void initState() {
    super.initState();
    //初始化状态  
    _counter=widget.initValue;
    print("initState");
  }

  // step 2、
  // 当State对象的依赖发生变化时会被调用;
  // 典型的场景是当系统语言Locale或应用主题改变时,Flutter framework会通知widget调用此回调。
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    print("didChangeDependencies");
  }

  // step 3、
  // 主要是用于构建Widget子树的,会在如下场景被调用:
  // 
  // 1、在调用initState()之后。
  // 2、在调用didUpdateWidget()之后。
  // 3、在调用setState()之后。
  // 4、在调用didChangeDependencies()之后。
  // 5、在State对象从树中一个位置移除后(会调用deactivate)又重新插入到树的其它位置之后。
  @override
  Widget build(BuildContext context) {
    print("build");
    return Scaffold(
      body: Center(
        child: FlatButton(
          child: Text('$_counter'),
          //点击后计数器自增
          onPressed:()=>setState(()=> ++_counter,
          ),
        ),
      ),
    );
  }

  // step 4、
  // 当State对象从树中被移除时,会调用此回调。
  @override
  void deactivate() {
    super.deactivate();
    print("deactive");
  }

  // step 5、
  // 当State对象从树中被永久移除时调用;通常在此回调中释放资源。
  @override
  void dispose() {
    super.dispose();
    print("dispose");
  }




  // step 3.1、
  // 是专门为了开发调试而提供的,在热重载(hot reload)时会被调用,此回调在Release模式下永远不会被调用。
  @override
  void reassemble() {
    super.reassemble();
    print("reassemble");
  }
  // step 3.2、
  // 在widget重新构建时,Flutter会调用Widget.canUpdate来检测Widget树中同一位置的新旧节点,然后决定是否需要更新,  
  // 如果Widget.canUpdate返回true则会调用此回调。
  @override
  void didUpdateWidget(CounterWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    print("didUpdateWidget");
  }
}

在Widget树中获取State对象

  • 通过Context获取
    context对象有一个findAncestorStateOfType()方法,该方法可以从当前节点沿着widget树向上查找指定类型的StatefulWidget对应的State对象。
// 查找父级最近的Scaffold对应的ScaffoldState对象
ScaffoldState _state = context.findAncestorStateOfType<ScaffoldState>();

注意:在Flutter开发中便有了一个默认的约定,如果StatefulWidget的状态是希望暴露出的,应当在StatefulWidget中提供一个of静态方法来获取其State对象,开发者便可直接通过该方法来获取;如果State不希望暴露,则不提供of方法。

  • 通过GlobalKey
    Flutter还有一种通用的获取State对象的方法——通过GlobalKey来获取!
// 1、定义一个globalKey, 由于GlobalKey要保持全局唯一性,我们使用静态变量存储
static GlobalKey<ScaffoldState> _globalKey= GlobalKey();
...
Scaffold(
    key: _globalKey , //设置key
    ...  
)

// 2、通过GlobalKey来获取State对象
_globalKey.currentState.openDrawer()

Flutter SDK内置组件库介绍

基础组件库

  • Text:带格式的文本
  • Row/Column:弹性控件布局类,其设计是基于Web开发中的Flexbox布局模型。
  • Stack:线性布局,使用 Positioned来定位他们相对于Stack的上下左右四条边的位置。基于Web开发中的绝对定位(absolute positioning )布局模型设计的。
  • Container:矩形视觉元素,具有边距(margins)、填充(padding)和应用于其大小的约束(constraints)。可以使用矩阵在三维空间中对其进行变换。

Material组件库(Android风格,遵循Material Design设计规范)

Material应用程序以MaterialApp组件开始的。

Cupertino组件(iOS风格)

丰富的Cupertino风格的组件,尽管目前还没有Material 组件那么丰富,但是它仍在不断的完善中。

状态管理

以下是管理状态的最常见的方法:

  • Widget管理自己的状态。
  • Widget管理子Widget状态。
  • 混合管理(父Widget和子Widget都管理状态)。

如何决定使用哪种管理方法?下面是官方给出的一些原则可以帮助你做决定:

  • 如果状态是用户数据,如复选框的选中状态、滑块的位置,则该状态最好由父Widget管理。
  • 如果状态是有关界面外观效果的,例如颜色、动画,那么状态最好由Widget本身来管理。
  • 如果某一个状态是不同Widget共享的则最好由它们共同的父Widget管理。

基础组件库

1、Text

  • 基本属性
Text(
          'Welcome to first page!',
          textAlign: TextAlign.left, // 对齐方式
          maxLines: 1, // 最大行数
          overflow: TextOverflow.ellipsis, // 截断方式
          textScaleFactor: 1.5, // 当前字体大小的缩放因子
          style: TextStyle(
              //用于指定文本显示的样式如颜色、字体、粗细、背景等。
              color: Colors.blue,
              fontSize: 18.0,
              height: 1.2,
              fontFamily: "Courier",
              background: new Paint()..color = Colors.yellow,
              decoration: TextDecoration.underline,
              decorationStyle: TextDecorationStyle.dashed),
),
  • TextSpan
    如果我们需要对一个Text内容的不同部分按照不同的样式显示,这时就可以使用TextSpan,它代表文本的一个“片段”。
Text.rich(TextSpan(
    children: [
     TextSpan(
       text: "Home: "
     ),
     TextSpan(
       text: "https://flutterchina.club",
       style: TextStyle(
         color: Colors.blue
       ),  
       recognizer: _tapRecognizer
     ),
    ]
))
  • DefaultTextStyle
    在Widget树中,文本的样式默认是可以被继承的(子类文本类组件未指定具体样式时可以使用Widget树中父级设置的默认样式),因此,如果在Widget树的某一个节点处设置一个默认的文本样式,那么该节点的子树中所有文本都会默认使用这个样式,而DefaultTextStyle正是用于设置默认文本样式的。

2、按钮(Material 组件库)

  • Material组件库中的按钮
    Material 组件库中提供了多种按钮组件如RaisedButton、FlatButton、OutlineButton等,它们都是直接或间接对RawMaterialButton组件的包装定制,所以他们大多数属性都和RawMaterialButton一样。
    这些按钮在按下时都会有“水波动画”。并且有一个onPressed属性来设置点击回调,当按钮按下时会执行该回调,如果不提供该回调则按钮会处于禁用状态,禁用状态不响应用户点击。

3、图片及ICON

  • ImageProvider
    其是一个抽象类,主要定义了图片数据获取的接口load(),从不同的数据源获取图片需要实现不同的ImageProvider ,如AssetImage是实现了从Asset中加载图片的ImageProvider,而NetworkImage实现了从网络加载图片的ImageProvider。

  • Image组件

/* 本地加载 */ 
Image(
  image: AssetImage("images/avatar.png"),
  width: 100.0
);
// 快捷加载
Image.asset("images/avatar.png",
  width: 100.0,
)

/* 网络加载 */ 
Image(
  image: NetworkImage(
      "https://avatars2.githubusercontent.com/u/20411648?s=460&v=4"),
  width: 100.0,
)
// 快捷加载
Image.network(
  "https://avatars2.githubusercontent.com/u/20411648?s=460&v=4",
  width: 100.0,
)
  • Image组件的属性
const Image({
  ...
  this.width, //图片的宽
  this.height, //图片高度
  this.color, //图片的混合色值
  this.colorBlendMode, //混合模式
  this.fit,//缩放模式
  this.alignment = Alignment.center, //对齐方式
  this.repeat = ImageRepeat.noRepeat, //重复方式
  ...
})
  • 关于Image缓存
    Flutter框架对加载过的图片是有缓存的(内存),默认最大缓存数量是1000,最大缓存空间为100M。

ICON

Flutter中,可以像Web开发一样使用iconfont,iconfont即“字体图标”,它是将图标做成字体文件,然后通过指定不同的字符而显示不同的图片。

在字体文件中,每一个字符都对应一个位码,而每一个位码对应一个显示字形,不同的字体就是指字形不同,即字符对应的字形是不同的。而在iconfont中,只是将位码对应的字形做成了图标,所以不同的字符最终就会渲染成不同的图标。

  • 在Flutter开发中,iconfont和图片相比有如下优势:
    1、体积小:可以减小安装包大小。
    2、矢量的:iconfont都是矢量图标,放大不会影响其清晰度。
    3、可以应用文本样式:可以像文本一样改变字体图标的颜色、大小对齐等。
    4、可以通过TextSpan和文本混用。

4、单选开关和复选框(Material 组件库)

Material 组件库中提供了Material风格的单选开关Switch和复选框Checkbox,虽然它们都是继承自StatefulWidget,但它们本身不会保存当前选中状态,选中状态都是由父组件来管理的。当Switch或Checkbox被点击时,会触发它们的onChanged回调,我们可以在此回调中处理选中状态改变逻辑。
Checkbox的大小是固定的,无法自定义,而Switch只能定义宽度,高度也是固定的。

class SwitchAndCheckBoxTestRoute extends StatefulWidget {
  @override
  _SwitchAndCheckBoxTestRouteState createState() => new _SwitchAndCheckBoxTestRouteState();
}

class _SwitchAndCheckBoxTestRouteState extends State<SwitchAndCheckBoxTestRoute> {
  bool _switchSelected=true; //维护单选开关状态
  bool _checkboxSelected=true;//维护复选框状态
  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Switch(
          value: _switchSelected,//当前状态
          onChanged:(value){
            //重新构建页面  
            setState(() {
              _switchSelected=value;
            });
          },
        ),
        Checkbox(
          value: _checkboxSelected,
          activeColor: Colors.red, //选中时的颜色
          onChanged:(value){
            setState(() {
              _checkboxSelected=value;
            });
          } ,
        )
      ],
    );
  }
}

5、输入框及表单(Material 组件库)

  • TextField

  • Form
    它可以对输入框进行分组,然后进行一些统一操作,如输入内容校验、输入框重置以及输入内容保存。

6、进度指示器(Material 组件库)

  • LinearProgressIndicator是一个线性、条状的进度条。
LinearProgressIndicator({
  double value,
  Color backgroundColor,
  Animation<Color> valueColor,
  ...
})
  • CircularProgressIndicator是一个圆形进度条
CircularProgressIndicator({
  double value,
  Color backgroundColor,
  Animation<Color> valueColor,
  this.strokeWidth = 4.0,
  ...   
}) 

三、布局类组件

在Flutter中,根据Widget是否需要包含子节点将Widget分为了三类,分别对应三种Element:

线性布局(Row和Column)

Row和Column都继承自Flex,类似于Android中的LinearLayout控件。

Row({
  ...  
  TextDirection textDirection,    // 表示水平方向子组件的布局顺序
  MainAxisSize mainAxisSize = MainAxisSize.max,    // 表示Row在主轴(水平)方向占用的空间
  MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start, // 表示子组件在Row所占用的水平空间内对齐方式
  VerticalDirection verticalDirection = VerticalDirection.down,   // 表示Row纵轴(垂直)的对齐方向
  CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center, // 表示子组件在纵轴方向的对齐方式
  List<Widget> children = const <Widget>[], // 子组件数组。
})

注意:如果Row里面嵌套Row,或者Column里面再嵌套Column,那么只有最外面的Row或Column会占用尽可能大的空间,里面Row或Column所占用的空间为实际大小。

弹性布局(Flex)

弹性布局允许子组件按照一定比例来分配父容器空间。Flutter中的弹性布局主要通过FlexExpanded来配合实现。
弹性布局的概念在其它UI系统中也都存在,如H5中的弹性盒子布局,Android中的FlexboxLayout等。

流式布局

注意:如果Row和Colum时,如果子widget超出屏幕范围,则会报溢出错误。

Flutter中通过Wrap和Flow来支持流式布局。

Wrap({
  ...
  this.direction = Axis.horizontal,
  this.alignment = WrapAlignment.start,
  this.spacing = 0.0,
  this.runAlignment = WrapAlignment.start,
  this.runSpacing = 0.0,
  this.crossAxisAlignment = WrapCrossAlignment.start,
  this.textDirection,
  this.verticalDirection = VerticalDirection.down,
  List<Widget> children = const <Widget>[],
})

Flow(
  delegate: TestFlowDelegate(margin: EdgeInsets.all(10.0)),
  children: <Widget>[
    new Container(width: 80.0, height:80.0, color: Colors.red,),
    new Container(width: 80.0, height:80.0, color: Colors.green,),
    new Container(width: 80.0, height:80.0, color: Colors.blue,),
    new Container(width: 80.0, height:80.0,  color: Colors.yellow,),
    new Container(width: 80.0, height:80.0, color: Colors.brown,),
    new Container(width: 80.0, height:80.0,  color: Colors.purple,),
  ],
)

层叠布局 Stack、Positioned

层叠布局和Web中的绝对定位、Android中的Frame布局是相似的,子组件可以根据距父容器四个角的位置来确定自身的位置。绝对定位允许子组件堆叠起来(按照代码中声明的顺序)。

Stack({
  this.alignment = AlignmentDirectional.topStart,
  this.textDirection,
  this.fit = StackFit.loose,
  this.overflow = Overflow.clip,
  List<Widget> children = const <Widget>[],
})

const Positioned({
  Key key,
  this.left, 
  this.top,
  this.right,
  this.bottom,
  this.width,
  this.height,
  @required Widget child,
})

对齐与相对定位(Align 和 Center)

Align({
  Key key,
  this.alignment = Alignment.center,
  this.widthFactor,
  this.heightFactor,
  Widget child,
})

// Alignment继承自AlignmentGeometry,表示矩形内的一个点,他有两个属性x、y,分别表示在水平和垂直方向的偏移。
// FractionalOffset 继承自 Alignment,它和 Alignment唯一的区别就是坐标原点不同!FractionalOffset 的坐标原点为矩形的左侧顶点,这和布局系统的一致,所以理解起来会比较容易。

四、容器类Widget

填充(Padding)

Padding({
  EdgeInsetsGeometry padding,
  Widget child,
})

尺寸限制类容器

  • ConstrainedBox
    用于对子组件添加额外的约束。

  • SizedBox
    用于给子元素指定固定的宽高。

  • UnconstrainedBox
    不会对子组件产生任何限制,它允许其子组件按照其本身大小绘制。
    一般情况下,我们会很少直接使用此组件,但在"去除"多重限制的时候也许会有帮助。

  • AspectRatio,它可以指定子组件的长宽比

  • LimitedBox 用于指定最大宽高

  • FractionallySizedBox 可以根据父容器宽高的百分比来设置子组件宽高

装饰容器DecoratedBox

可以在其子组件绘制前(或后)绘制一些装饰(Decoration),如背景、边框、渐变等。

  • BoxDecoration
    我们通常会直接使用BoxDecoration类,它是一个Decoration的子类,实现了常用的装饰元素的绘制。

变换(Transform)

Transform可以在其子组件绘制时对其应用一些矩阵变换来实现一些特效。
Matrix4是一个4D矩阵,通过它我们可以实现各种矩阵操作。

组合类容器(Container)

一个组合类容器,它本身不对应具体的RenderObject,它是DecoratedBox、ConstrainedBox、Transform、Padding、Align等组件组合的一个多功能容器,所以我们只需通过一个Container组件可以实现同时需要装饰、变换、限制的场景。

Container({
  this.alignment,
  this.padding, //容器内补白,属于decoration的装饰范围
  Color color, // 背景色
  Decoration decoration, // 背景装饰
  Decoration foregroundDecoration, //前景装饰
  double width,//容器的宽度
  double height, //容器的高度
  BoxConstraints constraints, //容器大小的限制条件
  this.margin,//容器外补白,不属于decoration的装饰范围
  this.transform, //变换
  this.child,
})

五、Material组件库常用

Scaffold

Scaffold是一个路由页的骨架,包含导航栏、抽屉菜单(Drawer)以及底部Tab导航菜单等。

TabBar

AppBar是一个Material风格的导航栏,通过它可以设置导航栏标题、导航栏菜单、导航栏底部的Tab标题等。

AppBar({
  Key key,
  this.leading, //导航栏最左侧Widget,常见为抽屉菜单按钮或返回按钮。
  this.automaticallyImplyLeading = true, //如果leading为null,是否自动实现默认的leading按钮
  this.title,// 页面标题
  this.actions, // 导航栏右侧菜单
  this.bottom, // 导航栏底部菜单,通常为Tab按钮组
  this.elevation = 4.0, // 导航栏阴影
  this.centerTitle, //标题是否居中 
  this.backgroundColor,
  ...   //其它属性见源码注释
})

抽屉菜单Drawer

Scaffold的drawer和endDrawer属性可以分别接受一个Widget来作为页面的左、右抽屉菜单。

FloatingActionButton

是Material设计规范中的一种特殊Button,通常悬浮在页面的某一个位置作为某种常用动作的快捷入口,如本节示例中页面右下角的"➕"号按钮。

底部导航

通过Material组件库提供的BottomNavigationBar和BottomNavigationBarItem两种组件来实现Material风格的底部导航栏。


六、可滚动组件和列表组件

abstract class ScrollView extends StatelessWidget
abstract class BoxScrollView extends ScrollView
class ListView extends BoxScrollView
class GridView extends BoxScrollView
class CustomScrollView extends ScrollView


class Scrollable extends StatefulWidget
class SingleChildScrollView extends StatelessWidget


abstract class Listenable
class ChangeNotifier implements Listenable
class ScrollController extends ChangeNotifier

可滚动组件简介

当组件内容超过当前显示视口(ViewPort)时,如果没有特殊处理,Flutter则会提示Overflow错误。

  • Scrollable组件
    可滚动组件都直接或间接包含一个Scrollable组件,因此它们包括一些共同的属性
Scrollable({
  ...
  this.axisDirection = AxisDirection.down, // 滚动方向
  this.controller, 
  this.physics, 
  @required this.viewportBuilder, // 
})

// ScrollController的主要作用是控制滚动位置和监听滚动事件。
// 默认情况下,Widget树中会有一个默认的PrimaryScrollController

// ScrollPhysics 决定可滚动组件如何响应用户操作。
// Flutter SDK中包含了两个ScrollPhysics的子类,他们可以直接使用:ClampingScrollPhysics(Android下微光效果);BouncingScrollPhysics(iOS下弹性效果)。
  • Scrollbar 和 CupertinoScrollbar
    Scrollbar一个Material风格的滚动指示器(滚动条),如果要给可滚动组件添加滚动条,只需将Scrollbar作为可滚动组件的任意一个父级组件即可。
    CupertinoScrollbar是iOS风格的滚动条,如果你使用的是Scrollbar,那么在iOS平台它会自动切换为CupertinoScrollbar

  • ViewPort视口
    指一个Widget的实际显示区域。
    在很多布局系统中都有ViewPort的概念,在Flutter中,术语ViewPort(视口),如无特别说明。

  • 基于Sliver的延迟构建
    通常可滚动组件的子组件可能会非常多、占用的总高度也会非常大;如果要一次性将子组件全部构建出将会非常昂贵!

    为此,Flutter中提出一个Sliver(中文为“薄片”的意思)概念,如果一个可滚动组件支持Sliver模型,那么该滚动可以将子组件分成好多个“薄片”(Sliver),只有当Sliver出现在视口中时才会去构建它,这种模型也称为“基于Sliver的延迟构建模型”。

    可滚动组件中有很多都支持基于Sliver的延迟构建模型,如ListView、GridView,但是也有不支持该模型的,如SingleChildScrollView。

SingleChildScrollView

类似于Android中的ScrollView,它只能接收一个子组件。

SingleChildScrollView({
  this.scrollDirection = Axis.vertical, //滚动方向,默认是垂直方向
  this.reverse = false,  // 是否按照阅读方向相反的方向滑动,此属性本质上是决定可滚动组件的初始滚动位置是在“头”还是“尾”,取false时,初始滚动位置在“头”,反之则在“尾”,读者可以自己试验。
  this.padding, 
  bool primary,  // 是否使用widget树中默认的PrimaryScrollController;
  this.physics, 
  this.controller,
  this.child,
})

ListView

最常用的可滚动组件之一,它可以沿一个方向线性排布所有子组件,并且它也支持基于Sliver的延迟构建模型。

ListView({
  ...  
  //可滚动widget公共参数
  Axis scrollDirection = Axis.vertical,
  bool reverse = false,
  ScrollController controller,
  bool primary,
  ScrollPhysics physics,
  EdgeInsetsGeometry padding,
  
  //ListView各个构造函数的共同参数  
  double itemExtent, 
  bool shrinkWrap = false,
  bool addAutomaticKeepAlives = true,
  bool addRepaintBoundaries = true,
  double cacheExtent,
    
  //子widget列表
  List<Widget> children = const <Widget>[],
})

itemExtent:该参数如果不为null,则会强制children的“长度”为itemExtent的值;这里的“长度”是指滚动方向上子组件的长度,也就是说如果滚动方向是垂直方向,则itemExtent代表子组件的高度;如果滚动方向为水平方向,则itemExtent就代表子组件的宽度。在ListView中,指定itemExtent比让子组件自己决定自身长度会更高效,这是因为指定itemExtent后,滚动系统可以提前知道列表的长度,而无需每次构建子组件时都去再计算一下,尤其是在滚动位置频繁变化时(滚动系统需要频繁去计算列表高度)。

shrinkWrap:该属性表示是否根据子组件的总长度来设置ListView的长度,默认值为false 。默认情况下,ListView的会在滚动方向尽可能多的占用空间。当ListView在一个无边界(滚动方向上)的容器中时,shrinkWrap必须为true。

addAutomaticKeepAlives:该属性表示是否将列表项(子组件)包裹在AutomaticKeepAlive 组件中;典型地,在一个懒加载列表中,如果将列表项包裹在AutomaticKeepAlive中,在该列表项滑出视口时它也不会被GC(垃圾回收),它会使用KeepAliveNotification来保存其状态。如果列表项自己维护其KeepAlive状态,那么此参数必须置为false。

addRepaintBoundaries:该属性表示是否将列表项(子组件)包裹在RepaintBoundary组件中。当可滚动组件滚动时,将列表项包裹在RepaintBoundary中可以避免列表项重绘,但是当列表项重绘的开销非常小(如一个颜色块,或者一个较短的文本)时,不添加RepaintBoundary反而会更高效。和addAutomaticKeepAlive一样,如果列表项自己维护其KeepAlive状态,那么此参数必须置为false。

  • 通过children添加cell(适用于少量数据)

  • 通过ListView.builder添加cell(适合列表项比较多或者无限的情况,是支持基于Sliver的懒加载模型)

ListView.builder({
  // ListView公共参数已省略  
  ...
  @required IndexedWidgetBuilder itemBuilder, // 它是列表项的构建器,返回值为一个widget。当列表滚动到具体的index位置时,会调用该构建器构建列表项。
  int itemCount, // 列表项的数量,如果为null,则为无限列表。
  ...
})

// 例如
ListView.builder(
    itemCount: 100,
    itemExtent: 50.0, //强制高度为50.0
    itemBuilder: (BuildContext context, int index) {
      return ListTile(title: Text("$index"));
    }
);
  • ListView.separated
    可以在生成的列表项之间添加一个分割组件,它比ListView.builder多了一个separatorBuilder参数,该参数是一个分割组件生成器。

GridView

用于构建一个二维网格列表,其默认构造函数定义如下:

GridView({
  Axis scrollDirection = Axis.vertical,
  bool reverse = false,
  ScrollController controller,
  bool primary,
  ScrollPhysics physics,
  bool shrinkWrap = false,
  EdgeInsetsGeometry padding,
  @required SliverGridDelegate gridDelegate, //控制子widget layout的委托
  bool addAutomaticKeepAlives = true,
  bool addRepaintBoundaries = true,
  double cacheExtent,
  List<Widget> children = const <Widget>[],
})

CustomScrollView



参考

Flutter实战
Material风格的组件展示

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容