阅读 520

万字长文 讲解 flutter 常用Widget

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战
持续更新,未完待续。  
Flutter 官方demo

通用属性

指的是多个组件之间都有该属性,为了不重复讲解,通用属性都放在该章节讲解。

1.1 textDirection

对应的值为TextDirection.rtlTextDirection.ltr,默认是rtl,除非需要适配阿拉伯语,因为阿语看文章的方式是我们是相反的,我们看文章是从左到右,他们是从右到左。所以如果不需要适配阿语,无视该属性。

一 🐬基础组件

1.1 Text 文本

构造函数如下:大部分属性都是类封装的。

 Text(
  this.data, {
  Key key,
  this.textDirection = TextDirection,
  this.style = TextStyle, // 颜色 字体
  this.strutStyle = StrutStyle ,
  this.textAlign = TextAlign,
  this.locale = Locale,
  this.softWrap = bool,
  this.overflow = TextOverflow,
  this.textScaleFactor = double,
  this.maxLines = int,
  this.semanticsLabel = String,
  this.textWidthBasis = TextWidthBasis ,
  this.textHeightBehavior = TextHeightBehavior,
}
复制代码

1.1.1 基本属性

  Text("111111",
        textAlign: TextAlign.left,
  ),
  Text(
    "222222222" , 
    textAlign: TextAlign.center,
  ),
  Text(
    "3333333333" * 3,
    textScaleFactor: 1.5,
    overflow: TextOverflow.ellipsis,
    maxLines: 1,
  ),
复制代码

效果见图1.1,如果把第2个Text控件把文本*5,效果见图1.2 image.png

  • textAlign:文本的对齐方式;可以选择左对齐、右对齐还是居中。注意,对齐的参考系是Text widget本身。上面代码虽然是指定了居中对齐,但因为Text文本内容宽度不足一行,Text的宽度和文本内容长度相等,那么这时指定对齐方式是没有意义的,只有Text宽度大于文本内容长度时指定此属性才有意义。

  • maxLinesoverflow:指定文本显示的最大行数,默认情况下,文本是自动折行的,如果指定此参数,则文本最多不会超过指定的行。如果有多余的文本,可以通过overflow来指定截断方式,默认是直接截断,本例中指定的截断方式TextOverflow.ellipsis,它会将多余文本截断后以省略符“...”表示;TextOverflow的其它截断方式请参考SDK文档。

  • textScaleFactor:代表文本相对于当前字体大小的缩放因子,相对于去设置文本的样式style属性的fontSize,它是调整字体大小的一个快捷方式。

1.1.2 TextStyle 样式

用于设置Text组件本身的背景色、字体大小、字体颜色等。

const TextStyle({
    this.inherit: true,  // 为false的时候不显示
    this.color,    // 颜色 
    this.fontSize, // 字号
    this.fontWeight,  // 字重,加粗也用这个字段  FontWeight.w700
    this.fontStyle, // FontStyle.normal  FontStyle.italic斜体
    this.letterSpacing, // 字符间距  就是单个字母或者汉字之间的间隔,可以是负数
    this.wordSpacing,   // 字间距 句字之间的间距
    this.textBaseline,   // 基线,两个值,字面意思是一个用来排字母的,一人用来排表意字的(类似中文)
    this.height,  // 当用来Text控件上时,行高(会乘以fontSize,所以不以设置过大)
    this.decoration,   // 添加上划线,下划线,删除线 
    this.decorationColor, // 划线的颜色
    this.decorationStyle, // 这个style可能控制画实线,虚线,两条线,点, 波浪线等
    this.debugLabel,
    String fontFamily, // 字体
    String package,
  })
复制代码

image.png

  • height:作用是将Text组件的高度设置为:fontSize的高度 * height。fontSize默认高度见下图文本为height:1.0,Axy的红色间距,红色间距的上下范围的高度如何计算的,我也没搞懂,有知道的同学请指教下。

image.png

1.1.3 strutStyle 支柱样式

 在使用支柱样式之前,先将Text的高度拆成几部分。 image.png   Text组件正常高度是横线2 到 横线4的距离。
如果使用了StrutStyle则要注意下顶部 和 底部 的 leading 间距。

  • ascent: 是2 到 3的高度(由字体决定)
  • descent:是3 到 4的距离(由字体决定)
  • leading:是1 到 2 和 4 到 5的距离。是支柱样式独有的属性。
  • fontSize: 是 2 到 4 的距离。
strutStyle: StrutStyle(
   height: 5,
   leading: 1,
)
复制代码

1.2 按钮

继承关系如下:

StatefulWidget
|-- ButtonStyleButton // 下面3个用的最多
    |-- ElevatedButton // 有阴影
    |-- OutlinedButton // 四周有边框
    |-- TextButton // 没有阴影
|-- CupertinoButton 
|-- DropdownButton 
|-- PopupMenuButton 
|-- RawMaterialButton 

StatelessWidget
|-- BackButton 
|-- BackButtonIcon 
|-- ButtonBar 
|-- CloseButton
|-- CupertinoButton
复制代码

用的最多的是ButtonStyleButton的子类,它有如下特点:

  1. 按下时都会有“水波动画”(又称“涟漪动画”,就是点击时按钮上会出现水波荡漾的动画)。
  2. 必须设置onPressed点击回调。

1.2.1 ButtonStyleButton

 ButtonStyleButton({
    required VoidCallback? onPressed, // 点击回调
    required Widget child,  
    VoidCallback? onLongPress, // 长按回调
    ButtonStyle? style, // 样式
    FocusNode? focusNode, // 焦点模式
    bool autofocus = false, // 是否自动获取焦点
    Clip clipBehavior = Clip.none,// 裁剪策略
}
复制代码

ElevatedButtonOutlinedButtonTextButton 的样式默认如下图,其中想要创建带icon的按钮,提供ElevatedButton.icon()创建。 image.png

a ButtonStyle

    ButtonStyle (
TextStyle textStyle,// 文字样式
Color backgroundColor,//  背景色
Color foregroundColor, // 前景色,替换testStyle
Color overlayColor, //按钮处于焦点、悬停或按下状态的突出显示颜色
Color shadowColor, // 阴影色
double elevation,// 阴影大小
EdgeInsetsGeometry //padding, 内部文字离当前组件的间距
Size minimumSize, // 按钮最小尺寸
Size fixedSize,// 按钮默认尺寸
Size side, //shape形状太小
OutlinedBorder shape, //按钮形状
MouseCursor mouseCursor, //pc上的
VisualDensity visualDensity, //密度,使用原生平台即可
MaterialTapTargetSize tapTargetSize, //响应点击的范围尺寸
Duration  animationDuration,//shape和elevation的动画变化的持续时间
bool enableFeedback,// 提供特定于平台的反馈,如震动、声音
AlignmentGeometry alignment,//默认居中显示
InteractiveInkFeatureFactory splashFactory//点击时,水波动画中水波的颜色
    )
复制代码

1.3 图片及ICON

Flutter中,我们可以通过Image组件来加载并显示图片,Image的数据源可以是 本地图片、资源图片、及网络。Flutter框架对加载过的图片是有缓存的(内存),默认最大缓存数量是1000,最大缓存空间为100M。

1.3.1 ImageProvider

ImageProvider 是一个抽象类,主要定义了图片数据获取的接口load(),它的子类NetworkImage从网络加载图片,MemoryImage从内存加载图片。 相应的类继承关系如下:

ImageProvider // 不同子类从不同途径加载图片
|-- AssetBundleImageProvider
    |-- AssetImage  // 从本地asset文件夹
    |-- ExactAssetImage
|-- FileImage  // 文件中
|-- MemoryImage  // 内存中
|-- NetworkImage // 网络
|-- ResizeImage 
|-- ScrollAwareImageProvider
复制代码

1.3.2 Image

StatefulWidget
|-- Image 
复制代码

Image widget 有一个必选的image参数,它对应一个ImageProvider。下面我们分别演示一下如何从asset和网络加载图片。

Image(
  image: AssetImage("images/ic_arrow_down.png"),
  width: 100.0
  
Image(
  image: NetworkImage(
      "https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/054f69d471054d8b91123ac73152fbae~tplv-k3u1fbpfcp-watermark.image"),
)
复制代码

image.png

Image的构造

Image({
  ...
width, //图片的宽
height, //图片高度
Color color,// 图片的前景色,一般不设置或设置透明色,会覆盖掉图片,一般会和colorBlendMode结合使用
BlendMode colorBlendMode, // 一般和color结合使用,设置color的混合模式
BoxFit fit,// 图片的显示模式
AlignmentGeometry alignment, // 对齐方式
ImageRepeat repeat, //重复方式
  ...
})
复制代码

1.3.3 Icon

StatelessWidget
|-- Icon  不能点击,只能加载ttf图片
|-- ImageIcon 不能点击, 可以加载本地、网络图片
|-- IconButton  可以点击,child可以是任意widget,如:Icon 和 ImageIcon 
复制代码
  • .svg,是矢量图,Flutter默认不支持解析,需要引入flutter_svg库才能在Flutter中显示svg格式的图片。
  • .ttf,除了是字体格式的后缀,也可以当成字体图标来显示,只能支持单色。Flutter自带系统字体图标给我们使用,通过Icons.xx,可以获取。特别注意的是:如果自己添加的ttf,必须要指定具体的Unicode符号才能正确显示,因为一个ttf字体图标文件可以包含多个图标。

image.png

flutter:
  fonts:
    - family: traffic_font
      fonts:
        - asset: fonts/traffic_font.ttf
Icon(
  Icons.arrow_back,
  color: Colors.green,
  size: 100,
),
Icon(
  Icons.add_ic_call,
  color: Colors.blue,
  size: 100,
),
ImageIcon(
  AssetImage("images/ic_arrow_down.png"),
 color: Colors.red,
  size: 100,
),

Icon(
  IconData(0xe614, fontFamily: 'traffic_font'),
  color: Colors.blue,
  size:100,
),
Icon(
  IconData(0xe615, fontFamily: 'traffic_font'),
  color: Colors.red,
  size:100,
)
复制代码

image.png

在Flutter开发中,Icon和图片相比有如下优势:

  1. 体积小:可以减小安装包大小。
  2. 矢量的:iconfont都是矢量图标,放大不会影响其清晰度。
  3. 可以应用文本样式:可以像文本一样改变字体图标的颜色、大小对齐等。
  4. 可以通过TextSpan和文本混用。

Flutter默认包含了一套Material Design的字体图标,通过Icon.xx可以获取。

1.3.4 小结

阿里图标网

  • Image,需要通过ImageProvider加载图片。
  • Icon,适合加载本地ttf格式资源和系统Icons下的矢量图标(ttfotf)。
  • ImageIcon,用于显示来自AssetImage(本地png)或其他ImageProvider的图标。
  • IconButton,支持Icon点击。
  • Image是想要实现点击效果,得套一层GestureDetector,Icon想实现点击效果,可以换成IconButton

1.4 输入框

继承关系如下:

StatefulWidget
|-- Form  // 容器类控件,可以包含多个FormField表单控件
|-- FormField //包含表单的状态,方便更新UI,常用TextFormField
    |-- CupertinoTextFormFieldRow //
    |-- DropdownButtonFormField // 下拉框,类似Android的Spinner
    |-- TextFormField //  一个输入框表单,因此TextFormField中有很多关于TextField的属性
|-- TextField // Material风格
|-- CupertinoTextField // Ios 风格
|-- EditableText // 无焦点管理,一般不用
复制代码

1.4.1 TextField

最常用的输入框,没有参数也能进行显示。

TextField()
复制代码

image.png
构造

    TextField(
TextEditingController controller, // 监听焦点改变 & 文字改变
FocusNode focusNode, // 设置焦点改变
InputDecoration ecoration, // hint文本、错误提示、帮助提示等
TextInputType keyboardType, // 控制软键盘的类型
TextInputAction textInputAction, // 控制软键盘右下角的按键
TextCapitalization textCapitalization,// 配置键盘是大写还是小写
TextStyle style, // 见Text组件中的说明。
TextStyle strutStyle, // 见Text组件中的说明。
TextAlign textAlign = TextAlign.start,// 见Text组件中的说明。
TextAlignVertical textAlignVertical,
TextDirection textDirection, 
bool readOnly = false, // 是否只读
ToolbarOptions toolbarOptions,// 表示长按时弹出的菜单copy、cut、paste、selectAll
bool showCursor, // 是否显示光标
bool autofocus = false, // 自动获取焦点
String obscuringCharacter = '•',
bool obscureText = false, // 是否设置为密码框
bool autocorrect = true, 
SmartDashesType smartDashesType,
SmartQuotesType smartQuotesType,
bool enableSuggestions = true,
int maxLines = 1,
int minLines,
bool expands = false,
int maxLength,
this.maxLengthEnforcement,
ValueChanged<String> onChanged,// 是当内容发生变化时回调
VoidCallback onEditingComplete,// 用户按下键盘上的“完成”按钮
ValueChanged<String> onSubmitted, // 点击回车或者点击软键盘上的完成回调
AppPrivateCommandCallback onAppPrivateCommand,
List<TextInputFormatter> inputFormatters, // 过滤输入的内容
bool enabled,
double cursorWidth = 2.0,
double cursorHeight,
Radius cursorRadius,
Colors cursorColor,
BoxHeightStyle selectionHeightStyle ,
BoxHeightStyle selectionWidthStyle ,
Brightness keyboardAppearance,
EdgeInsets scrollPadding = const EdgeInsets.all(20.0),
DragStartBehavior dragStartBehavior,
bool enableInteractiveSelection = true,
TextSelectionControls selectionControls,
GestureTapCallback onTap,
InputCounterWidgetBuilder buildCounter,// 输入框右下角字数统计
ScrollController scrollController,
ScrollPhysics scrollPhysics,
Iterable<String> autofillHints,
String restorationId,
    )
复制代码

1.4.2 Form

Form({
  @required Widget child,
  VoidCallback onChanged,
})
复制代码

onChangedForm的任意一个子FormField内容发生变化时会触发此回调 Form的子孙元素必须是FormField类型.

FormState

FormStateFormState类,可以通过Form.of()GlobalKey获得。我们可以通过它来对Form的子孙FormField进行统一操作。我们看看其常用的三个方法:

  • FormState.validate():调用此方法后,会调用Form子孙FormField的validate回调,如果有一个校验失败,则返回false,所有校验失败项都会返回用户返回的错误提示。
  • FormState.save():调用此方法后,会调用Form子孙FormFieldsave回调,用于保存表单内容
  • FormState.reset():调用此方法后,会将子孙FormField的内容清空

1.4.3 TextFormField

TextFormField(
  autovalidateMode: AutovalidateMode.always, // 验证模式,和validator配合使用
  decoration: InputDecoration(hintText: '输入手机号'),
  validator: (String value) {
    return value.length != 11 ? '必须11个字符' : null;
  },
),
TextFormField(
  autovalidateMode: AutovalidateMode.always,
  decoration: InputDecoration(hintText: '输入密码'),
  validator: (String value) {
    return value.length >= 6 ? null : '密码最少6位';
  },
)
复制代码

image.png

二 🐬布局类组件

布局类组件都会包含一个或多个子组件,布局类组件会按照一定的排列方式来对其子Widget进行排列。

2.1 🐎Stack 层叠布局

Stack组件可以将子组件叠加显示,相当于Android 里的 FrameLayout布局,StackPositioned配合实现绝对定位。

Stack({
  this.alignment = AlignmentDirectional.topStart,   // 子布局对齐方向
  this.textDirection,  // 大部分场景不需要设置,默认子布局从左到右摆放
  this.fit = StackFit.loose,   // 见示例
  this.overflow = Overflow.clip,   // 裁剪策略:裁剪 或 保留
  List<Widget> children = const <Widget>[],  // 子布局
})
复制代码
  • fit:此参数用于确定没有定位的子组件如何去适应Stack的大小。StackFit.loose表示使用子组件的大小,StackFit.expand表示扩伸到Stack的大小。

  • overflow:此属性决定如何显示超出Stack显示空间的子组件;值为Overflow.clip时,超出部分会被剪裁(隐藏),值为Overflow.visible 时则不会。

2.1.1 默认用法

  child: Stack(
    alignment: AlignmentDirectional.topStart,  // 还有 topCenter topEnd ..
    fit: StackFit.loose,
    overflow: Overflow.clip,
    clipBehavior: Clip.hardEdge,
    children: <Widget>[
      Container(
        height: 200,
        width: 200,
        color: Colors.red, // 红
      ),
      Positioned(
        left: 10,
        top: 10,
        height: 100,
        width: 100,
        child: Container(
          color: Colors.yellow, // 黄
        ),
      ),
      Container(
        height: 50,
        width: 50,
        color: Colors.green,  // 绿
      )
    ],
);
复制代码

效果图如下:Stack根据子控件的宽高进行显示。如果红色控件的宽高设置的值超过屏幕大小,默认以屏幕宽高进行显示。

image.png

2.1.2 fit 属性

a StackFit.loose

默认Stack的大小由子布局决定,如果想让Stack默认铺满屏幕,使用 ConstrainedBoxContainer等包裹 Stack。

 Container(
  child: Stack(
    fit: StackFit.loose
    alignment: Alignment.center, //指定没有定位或部分定位widget的对齐方式
    children: <Widget>[
      Container(
        child: Text("11111111", style: TextStyle(color: Colors.white)),
        color: Colors.red,
      ),
      Positioned(
        left: 18,
        child: Text("22222222"),
      ),
      Positioned(
        top: 18,
        child: Text("33333333"),
      )
    ],
  ),
)
复制代码

image.png

  • 第一个组件Text("11111111")没有指定定位,并且alignmentAlignment.center,所以它会居中显示。

  • 第二个组件Text("22222222")只指定了水平方向的定位(left),所以属于部分定位,即垂直方向上没有定位,那么它在垂直方向的对齐方式会按照alignment指定的对齐方式对齐,即center

  • 第三个组件Text("33333333"),和第二个Text原理一样,只不过是水平方向没有定位,则水平方向居中。

b StackFit.expand

 Stack(
    fit: StackFit.expand
    ...  同上代码
复制代码

效果见上图。

未定位的widget占满Stack整个空间,上图只有Text("11111111")没有被Positioned包裹和定位.

c StackFit.passthrough

passthrough 中文意思是穿过、贯通。

Row(
  children: [
    Expanded(
      child: Stack(
        fit: StackFit.passthrough, // loose  expand    
        children: <Widget>[
          Container(
            child: Text("11111111"),
            ...
          ),
          Positioned(
            left: 18,
            child: Text("22222222")
          ),
          Positioned(
            right: 18,
            child: Text("33333333")
          )
        ],
      ),
    ),
  ],
);
复制代码

image.png

因为Row的宽度是屏幕宽,高是跟随子控件,

  • loose: 是默认值
  • expand: 会让未定位的widget占满Stack整个空间
  • passthrough: 会让未定位的widget占满 父布局的主轴方向最大值。Row的主轴是横向,其最大值是屏幕宽。只有Stack作为 Row-> Expanded 的子布局才能发挥其效果。见下图。

2.1.3 overflow 属性

Stack(
  overflow: Overflow.clip, // visible
  children: <Widget>[
    Container(
      height: 200,
      width: 200,
      color: Colors.red,
    ),
    Positioned(
      left: 100,
      top: 100,
      height: 200,
      width: 200,
      child: Container(
        color: Colors.green,
      ),
    )
  ],
)
复制代码

image.png

a clip

裁剪超出Stack布局的视图,效果见上图。

b visible

显示超出Stack布局的视图,效果见上图。

2.2 🐂IndexedStack

Stack 的子类

IndexedStack(
    index: _index,
    children: <Widget>[
      Container(
        height: 300,
        width: 300,
        color: Colors.red,
        alignment: Alignment.center,
      ),
      Center(
        child: Container(
          height: 300,
          width: 300,
          color: Colors.green,
        ),
      ),
      Container(
        height: 300,
        width: 300,
        color: Colors.yellow,
        alignment: Alignment.center,
      ),
    ],
  )
复制代码

image.png

只显示children中索引为index的布局,所以只会显示1个子布局,而Stack的子布局默认会全部显示

2.3 🐏Positioned

Positioned是Stack的子组件使用起来才有意义
image.png

Stack(
  children: <Widget>[
    Positioned(
      left: 30,
      right: 30,
      top: 30,
      bottom: 30,
     // width: 300,
     // height: 300,

      child: Container(color: Colors.red),
    ),
  ],
)
复制代码

lefttop 、right、 bottom分别代表离Stack左、上、右、底四边的距离。
widthheight用于指定需要定位元素的宽度和高度。注意,Positionedwidthheight 和其它地方的意义稍微有点区别,此处用于配合lefttop 、right、 bottom来定位组件.
举个例子,在水平方向时,你只能指定leftrightwidth三个属性中的两个,如指定leftwidth后,child的宽度就是 left + width,如果同时指定三个属性则会报错,垂直方向同理。

2.4 🚀Row水平线性布局

Flex  // 弹性布局
|-- Column 
|-- Row 

Flexible
|-- Expanded  // 扩展Row、Column,动态设置权重。
复制代码

先理解下主轴和交叉轴 image.png

类似于Android中的LinearLayout控件。RowColumn都继承自Flex ,沿水平或垂直方向排布子组件。Row中的交叉轴是垂直方向,Column中交叉轴是水平方向。

Row({
TextDirection textDirection,    
MainAxisSize mainAxisSize = MainAxisSize.max,    // Row布局的宽度
MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,  // Row 内子布局对齐方向
VerticalDirection verticalDirection = VerticalDirection.down, 
CrossAxisAlignment crossAxisAlignment = center,  // 子布局众向对齐
List<Widget> children = const <Widget>[],  // 子布局
})
复制代码

2.4.1 mainAxisSize

  定义Row的宽度,max表示Row为屏幕宽,相当于android里的match_parent,min表示Row宽度等于children的总宽度,相当于android里的wrap_content

2.4.2 mainAxisAlignment

  主轴对齐方向,Row主轴是水平方向,如果mainAxisSize值为min,此属性无意义.

image.png 上图代码过于简单,就不列出了。Row中有3个宽100,高50的Container组件,黑色边框为Row的实际宽度

2.4.3 verticalDirection

表示Row纵轴(垂直)的对齐方向,默认是down表示从顶部开始,然后朝底部垂直堆叠。up从底部开始,垂直向上堆放。 在Row中暂时没发现其作用。

2.4.4 crossAxisAlignment

交叉轴对齐方向。startendcenterstretchbaseline

Row(
  crossAxisAlignment:CrossAxisAlignment.xxx,
  children: <Widget>[
    Container(
      height: 50,
      width: 100,
      color: Colors.red,
    ),
    Container(
      height: 100,
      width: 100,
      color: Colors.green,
    ),
    Container(
      height: 150,
      width: 100,
      color: Colors.yellow,
    ),
  ],
);
复制代码

效果图:
image.png

根据上图显示,start、end、center的作用一目了然,下面重点讲下stretchbaseline

a CrossAxisAlignment.stretch

如果Row的父布局,没有约束高度,那么Row的高度默认为屏幕高,每个子View的高度和Row的高保持一致。见上图。

b CrossAxisAlignment.baseline

Container(
width: 1000,
height: 300,
color:Colors.grey,
child:Row(
  crossAxisAlignment:CrossAxisAlignment.baseline,
  children: <Widget>[
    Container(
      height: 50,
      width: 100,
      color: Colors.red,
      child: Text("1111")
    ),
    Container(
      height: 100,
      width: 100,
      color: Colors.green,
      child: Text("2222"),
      alignment:Alignment.center
    ),
    Container(
      height: 150,
      width: 100,
      color: Colors.yellow,
      child: Text("3333")
    ),
  ],
 )
);
复制代码

image.png 上面代码中,由于第2个布局设置了Aligment.center属性,所以第1个布局里的内容也要和2222对齐,第3个黄色布局不惜突破父布局约束的150高度,只为了和2222对齐,所以baseline只有在Row的子View中存在Text等包含基线属性的布局时才生效。

crossAxisAlignment 改成其它值时,效果图如下:

image.png

2.5 🔥Column 垂直线性布局

属性和Row是一样多的,所以每个属性的含义参考Row。

2.6 🚒Wrap 流式布局

在介绍Row和Colum时,如果子widget超出屏幕范围,则会报溢出错误,如

// 布局1
Row(
  children: <Widget>[
    Text("xxxx"*100)
  ],
);
// 布局2
Row(
  children: <Widget>[
    Container(
      width: 3333,
      height: 150,
      color: Colors.red,
    )
  ],
);
复制代码

效果图如下:

image.png 因为Row默认只有一行,如果子View的宽度超过屏幕后,溢出部分则报错。我们把超出屏幕显示范围会自动折行的布局称为流式布局。Flutter中通过WrapFlow来支持流式布局,将上例中的Row换成Wrap后溢出部分则会自动折行,下面我们分别介绍WrapFlow

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>[],
})
复制代码

我们可以看到Wrap的很多属性在Row中也有,如crossAxisAlignmenttextDirectionverticalDirection等,这些参数意义是相同的,我们不再重复介绍.

2.6.1 direction

horizontal 表示主轴横向排列
vertical 表示主轴纵向排列

2.6.2 spacing

主轴(水平)方向间距

2.6.3 runSpacing

纵轴(垂直)方向间距

Container(
      width: 1000,
      color: Colors.black26,
      child:Wrap(
          direction: Axis.horizontal, 主轴方向为横向
          spacing: 16, // 主轴(水平)方向间距
          runSpacing: 20, // 纵轴(垂直)方向间距
          alignment: WrapAlignment.start, // 主轴方向对齐方向 
          children: <Widget>[
            Container(
              width: 100,
              height: 40,
              color: Colors.red,
            ),
            Container(
              width: 100,
              height: 40,
              color: Colors.green,
            ),
            Container(
              width: 100,
              height: 40,
              color: Colors.yellow,
            ),
            Container(
              width: 150,
              height: 40,
              color: Colors.blue,
            ),
          ],
        )
);
复制代码

image.png

2.6.4 aligment

spaceAround: 将空间均匀放置在对象之间以及第一个和最后一个对象前后的一半空间。
spaceBetween: 在对象之间均匀放置可用空间。
spaceEvenly: 在对象之间以及第一个和最后一个对象之前和之后均匀放置可用空间。 image.png

2.7 ⭐️Flow布局

我们一般很少会使用Flow,因为其过于复杂,需要自己实现子widget的位置转换,在很多场景下首先要考虑的是Wrap是否满足需求。Flow主要用于一些需要自定义布局策略或性能要求较高(如动画中)的场景。Flow有如下优点:

  • 性能好;Flow是一个对子组件尺寸以及位置调整非常高效的控件,Flow用转换矩阵在对子组件进行位置调整的时候进行了优化:在Flow定位过后,如果子组件的尺寸或者位置发生了变化,在FlowDelegate中的paintChildren()方法中调用context.paintChild 进行重绘,而context.paintChild在重绘时使用了转换矩阵,并没有实际调整组件位置。
  • 灵活;由于我们需要自己实现FlowDelegatepaintChildren()方法,所以我们需要自己计算每一个组件的位置,因此,可以自定义布局策略。

缺点:

  • 使用复杂。
  • 不能自适应子组件大小,必须通过指定父容器大小或实现TestFlowDelegategetSize返回固定大小
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,),
  ],
)
复制代码
class TestFlowDelegate extends FlowDelegate {
  EdgeInsets margin = EdgeInsets.zero;
  TestFlowDelegate({this.margin});
  @override
  void paintChildren(FlowPaintingContext context) {
    var x = margin.left;
    var y = margin.top;
    //计算每一个子widget的位置  
    for (int i = 0; i < context.childCount; i++) {
      var w = context.getChildSize(i).width + x + margin.right;
      if (w < context.size.width) {
        context.paintChild(i,
            transform: new Matrix4.translationValues(
                x, y, 0.0));
        x = w + margin.left;
      } else {
        x = margin.left;
        y += context.getChildSize(i).height + margin.top + margin.bottom;
        //绘制子widget(有优化)  
        context.paintChild(i,
            transform: new Matrix4.translationValues(
                x, y, 0.0));
         x += context.getChildSize(i).width + margin.left + margin.right;
      }
    }
  }

  @override
  getSize(BoxConstraints constraints){
    //指定Flow的大小  
    return Size(double.infinity,200.0);
  }

  @override
  bool shouldRepaint(FlowDelegate oldDelegate) {
    return oldDelegate != this;
  }
}
复制代码

image.png 可以看到我们主要的任务就是实现paintChildren,它的主要任务是确定每个子widget位置。由于Flow不能自适应子widget的大小,我们通过在getSize返回一个固定大小来指定Flow的大小。

2.8 🔥Align 对齐

上面我们讲过通过StackPositioned,我们可以指定一个或多个子元素相对于父元素各个边的精确偏移,并且可以重叠。但如果我们只想简单的调整一个子元素在父元素中的位置的话,使用Align组件会更简单一些。

  • Align 组件可以调整子组件的位置。
  • Align 组件可以根据子组件的宽高来确定自身的的宽高。
Align({
  Key key,
  this.alignment = Alignment.center,
  this.widthFactor, 
  this.heightFactor,
  Widget child,
})
复制代码

2.8.1 widthFactor | heightFactor

Container(
  height: 120,
  width: 120,
  color: Colors.black26,
  child: Align(
    child: Container(
      width: 60,
      height: 60,
      color: Colors.red,
    ),
  ),
)
复制代码

效果图见图1 image.png

当我们把Container里的宽高属性去掉后,Align又没指定widthFactorheightFactor属性,它会撑满父布局,这点要注意,见上图2

Container(
  color: Colors.black26,
  child: Align(
    widthFactor: 3,
    heightFactor: 2,
    child: Container(
      width: 60,
      height: 60,
      color: Colors.red,
    ),
  ),
)
复制代码

效果图: 灰色背景是Align的大小 image.png 因为Container没有设置 width 和 height。
所以Align的宽 = child组件的宽 * widthFactor
所以Align的高 = child组件的高 * heightFactor

再强调下:如果widthFactor 和 heightFactor都没设置,那么Align的宽高撑满父布局,父布局默认是屏幕宽高。

2.8.2 alignment

Alignment继承自AlignmentGeometry,表示矩形内的一个点,他有两个属性xy,分别表示在水平和垂直方向的偏移,Alignment定义如下

Alignment(this.x, this.y)
复制代码

image.png

Alignment Widget会以矩形的中心点作为坐标原点,即Alignment(0.0, 0.0) 。xy的值从 -1 到 1 分别代表矩形左边到右边的距离和顶部到底边的距离,因此2个水平(或垂直)单位则等于矩形的宽(或高),如Alignment(-1.0, -1.0) 代表矩形的左侧顶点,而Alignment(1.0, 1.0)代表右侧底部终点,而Alignment(1.0, -1.0) 则正是右侧顶点,即Alignment.topRight。为了使用方便,矩形的原点、四个顶点,以及四条边的终点在Alignment类中都已经定义为了静态常量。

Alignment可以通过其坐标转换公式将其坐标转为子元素的具体偏移坐标

( Alignment.x * childWidth / 2 + childWidth / 2,  Alignment.y * childHeight / 2 + childHeight / 2 )

image.png

2.8.3 FractionalOffset

FractionalOffset 继承自 Alignment,它和 Alignment唯一的区别就是坐标原点不同!FractionalOffset 的坐标原点为矩形的左侧顶点,这和布局系统的一致,所以理解起来会比较容易。FractionalOffset的坐标转换公式为:

实际偏移 = (FractionalOffse.x * childWidth, FractionalOffse.y * childHeight)

image.png

2.8.4 Align和Stack对比

可以看到,AlignStack/Positioned都可以用于指定子元素相对于父元素的偏移,但它们还是有两个主要区别:

  1. 定位参考系统不同;Stack/Positioned定位的的参考系可以是父容器矩形的四个顶点;而Align则需要先通过alignment 参数来确定坐标原点,不同的alignment会对应不同原点,最终的偏移是需要通过alignment的转换公式来计算出。
  2. Stack可以有多个子元素,并且子元素可以堆叠,而Align只能有一个子元素,不存在堆叠。

2.9 🌞Center组件

Center 继承 Align,只能有1个子布局。比Align只少了一个alignment 参数;由于Align的构造函数中alignment 值默认为Alignment.center,所以,我们可以认为Center组件其实是对齐方式确定(Alignment.center)了的Align。默认属性如下:

class Center extends Align {
    Center({ 
      Key key, 
      double widthFactor, 
      double heightFactor, 
      Widget child 
      }): super(..);
}
复制代码

示例

Column(
  children: [
    Container(
      color: Colors.red,
      child: Center(
        child: Text("11111"),
      ),
    ),
    Container(
      color: Colors.green,
      child: Center(
        widthFactor: 1,
        heightFactor: 2,
        child: Text("222222"),
      ),
    )
  ],
)
复制代码

image.png   因为组件Center(Colors.red)没有指定widthFactor,所以它的宽高撑满父布局,父布局是Container,属于Column的子布局,默认宽是屏幕宽,高是跟随子布局的高。

三 容器类组件

EdgeInsetsGeometry // 封装组件上下左右间距
|-- EdgeInsets  // 最常用
|-- _MixedEdgeInsets 
|-- EdgeInsetsDirectional 
复制代码

3.1 Padding

Padding({
  ...
  EdgeInsetsGeometry padding,
  Widget child,
})
复制代码

举个例子:

Padding(
  padding: EdgeInsets.all(10),
  child:  Container(
    color: Colors.red,
    width: 100,
    height: 100,
  )
)
复制代码

image.png

3.2 尺寸限制容器

这些组件可以约束子组件的尺寸

SingleChildRenderObjectWidget
|-- AspectRatio  
|-- ConstrainedBox 
|-- SizedBox
|-- FractionallySizedBox
|-- LimitedBox
|-- 

StatelessWidget
|-- Container
|-- UnconstrainedBox 
复制代码

3.2.1 Container

Container是一个组合类容器,它本身不对应具体的RenderObject,它是DecoratedBoxConstrainedBox、TransformPaddingAlign等组件组合的一个多功能容器,所以我们只需通过一个Container组件可以实现同时需要装饰、变换、限制的场景。下面是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,
})
复制代码

Container的大部分属性,我们之前已经介绍过了,不再赘述,但有几点需要说明下。

  • 容器的大小可以通过widthheight属性来指定,也可以通过constraints来指定;如果它们同时存在时,widthheight优先。实际上Container内部会根据widthheight来生成一个constraints。但是width设置了100,不一定它的宽度就是100,还会根据它的父组件的宽进行判断。

  • colordecoration是互斥的,如果同时设置它们则会报错!实际上,当指定color时,Container内会自动创建一个decoration

Container(
  margin: EdgeInsets.only(top: 50.0, left: 120.0), //容器外填充
  constraints: BoxConstraints.tightFor(width: 200.0, height: 150.0), //卡片大小
  decoration: BoxDecoration(//背景装饰
      gradient: RadialGradient( //背景径向渐变
          colors: [Colors.green, Colors.pink],
          center: Alignment.topLeft,
          radius: .98
      ),
      boxShadow: [ //卡片阴影
        BoxShadow(
            color: Colors.yellow,
            offset: Offset(2.0, 2.0),
            blurRadius: 4.0 // 阴影圆角
        )
      ]
  ),
  transform: Matrix4.rotationZ(.3), //卡片倾斜变换
  alignment: Alignment.center, //卡片内文字居中
  child: Text( //卡片文字
    "6.66", style: TextStyle(color: Colors.white, fontSize: 40.0),
  ),
);
复制代码

image.png

a Padding和Margin

...
Container(
  margin: EdgeInsets.all(20.0), //容器外补白
  color: Colors.red,
  child: Text("Hello world!"),
),
Container(
  padding: EdgeInsets.all(20.0), //容器内补白
  color: Colors.red,
  child: Text("Hello world!"),
),
...
复制代码

image.png

3.2.2 SizedBox

SizedBox是具有固定宽高的组件,适合控制2个组件之间的空隙,用法如下:

SizedBox(
    width: 200.0,
    height: 200.0,
    child: Container(
      color: Colors.red,
    )),
Container(color: Colors.green, height: 20),
SizedBox(
  height: 10,
),
Container(color: Colors.blue, height: 20),
复制代码

image.png

3.2.3 AspectRatio

固定宽高比的组件,如果组件的宽度固定,希望高是宽的1/2,可以用AspectRatio实现此效果,用法如下:

AspectRatio(
  aspectRatio: 2 / 1, // 宽:高 = 2: 1
  child: Container(color: Colors.green),
),

AspectRatio(
  aspectRatio: 3 / 1, // 宽:高 = 3: 1
  child: Container(color: Colors.red),
)
复制代码

image.png

3.2.4 FractionallySizedBox

当我们需要一个控件的尺寸是相对尺寸时,比如当前按钮的宽度占父组件的70%,

使用FractionallySizedBox包裹子控件,设置widthFactor宽度系数或者heightFactor高度系数,系数值的范围是0-1,0.7表示占父组件的70%,用法如下:

...
FractionallySizedBox(
  widthFactor: 0.5,  // 当前组件宽度是父组件一半
  child: ElevatedButton(child: Text("hello world")),
),
ElevatedButton(child: Text("hello world")),
...
复制代码

image.png

3.2.5 ConstrainedBox

约束子组件的最大宽高和最小宽高,即使子组件设置了宽高5000,也没用,最大宽高被父组件 ConstrainedBox的constraints属性约束了。

ConstrainedBox(
  constraints: BoxConstraints(maxHeight: 100, maxWidth: 200),
  child: Container(height: 5000, width: 5000, color: Colors.green,child: Text("111")),
),
复制代码

image.png

3.2.6 小结

这么多约束类的容器组件,平时要使用哪一个组件呢?总结如下:

  • ConstrainedBox:适用于需要设置最大/小宽高,组件大小以来子组件大小,但不能超过设置的界限。
  • UnconstrainedBox:用到情况不多,当作ConstrainedBox的子组件可以“突破”ConstrainedBox的限制,超出界限的部分会被截取。
  • SizedBox:适用于固定宽高的情况,常用于当作2个组件之间间隙组件。
  • AspectRatio:适用于固定宽高比的情况。
  • FractionallySizedBox:适用于占父组件百分比的情况。
  • LimitedBox:适用于没有父组件约束的情况。
  • Container:使用最广,适用于不仅有尺寸的约束,还有装饰(颜色、边框、等)、内外边距等需求的情况。

3.3 DecoratedBox 装饰类组件

SingleChildRenderObjectWidget
|-- DecoratedBox // 装饰组件

Decoration // 装饰的抽象类
|-- BoxDecoration  

复制代码

举个例子

DecoratedBox(
  decoration: BoxDecoration(
  shape: BoxShape.rectangle, // 背景是矩形。还可设置圆形
  color: Colors.green, // 背景色
  borderRadius: BorderRadius.circular(20), // 圆角20
    border: Border.all( // 边框
      color: Colors.red,
      width: 2,
    ),
  ),
  child: Text('hello world'),
)
复制代码

image.png

3.4 Scaffold 脚手架

一个完整的路由页可能会包含导航栏、抽屉菜单(Drawer)以及底部Tab导航菜单等。如果每个路由页面都需要开发者自己手动去实现这些,这会是一件非常麻烦且无聊的事。幸运的是,Flutter Material组件库提供了一些现成的组件来减少我们的开发任务。Scaffold是一个路由页的骨架,我们使用它可以很容易地拼装出一个完整的页面。

四 可滚动类组件

当组件内容超过当前显示视口(ViewPort)时,如果没有特殊处理,Flutter则会提示Overflow错误,页面也会显示。 image.png 为此,Flutter提供了多种可滚动组件(Scrollable Widget)用于显示列表和长布局。在本章中,我们先介绍一下常用的可滚动组件(如ListViewGridView等),然后介绍一下ScrollController。可滚动组件都直接或间接包含一个Scrollable组件,因此它们包括一些共同的属性,为了避免重复介绍,我们在此统一介绍一下:

Scrollable({
  ...
  this.axisDirection = AxisDirection.down,
  this.controller,
  this.physics,
  @required this.viewportBuilder, //后面介绍
})
复制代码
  • axisDirection滚动方向。

  • physics:此属性接受一个ScrollPhysics类型的对象,它决定可滚动组件如何响应用户操作,比如用户滑动完抬起手指后,继续执行动画;或者滑动到边界时,如何显示。默认情况下,Flutter会根据具体平台分别使用不同的ScrollPhysics对象,应用不同的显示效果,如当滑动到边界时,继续拖动的话,在iOS上会出现弹性效果,而在Android上会出现微光效果。如果你想在所有平台下使用同一种效果,可以显式指定一个固定的ScrollPhysics,Flutter SDK中包含了两个ScrollPhysics的子类,他们可以直接使用:

    • ClampingScrollPhysics:Android下微光效果。
    • BouncingScrollPhysics:iOS下弹性效果。
  • controller:此属性接受一个ScrollController对象。ScrollController的主要作用是控制滚动位置和监听滚动事件。默认情况下,Widget树中会有一个默认的PrimaryScrollController,如果子树中的可滚动组件没有显式的指定controller,并且primary属性值为true时(默认就为true),可滚动组件会使用这个默认的PrimaryScrollController。这种机制带来的好处是父组件可以控制子树中可滚动组件的滚动行为,例如,Scaffold正是使用这种机制在iOS中实现了点击导航栏回到顶部的功能。我们将在本章后面“滚动控制”一节详细介绍ScrollController

a ViewPort视口

在很多布局系统中都有ViewPort的概念,在Flutter中,术语ViewPort(视口),如无特别说明,则是指一个Widget的实际显示区域。例如,一个ListView的显示区域高度是800像素,虽然其列表项总高度可能远远超过800像素,但是其ViewPort仍然是800像素。

b 基于Sliver的延迟构建

通常可滚动组件的子组件可能会非常多、占用的总高度也会非常大;如果要一次性将子组件全部构建出将会非常昂贵!为此,Flutter中提出一个Sliver(中文为“薄片”的意思)概念,如果一个可滚动组件支持Sliver模型,那么该滚动可以将子组件分成好多个“薄片”(Sliver),只有当Sliver出现在视口中时才会去构建它,这种模型也称为“基于Sliver的延迟构建模型”。可滚动组件中有很多都支持基于Sliver的延迟构建模型,如ListViewGridView,但是也有不支持该模型的,如SingleChildScrollView

c 主轴和纵轴

在可滚动组件的坐标描述中,通常将滚动方向称为主轴,非滚动方向称为纵轴。由于可滚动组件的默认方向一般都是沿垂直方向,所以默认情况下主轴就是指垂直方向,水平方向同理。

4.1 SingleChildScrollView

类似于Android中的ScrollView,它只能接收一个子组件。定义如下:

SingleChildScrollView({
  this.scrollDirection = Axis.vertical, //滚动方向,默认是垂直方向
  this.reverse = false, 
  this.padding, 
  bool primary, 
  this.physics, 
  this.controller,
  this.child,
})
复制代码

举个例子:

Scrollbar( // 显示进度条
  isAlwaysShown:true, // 让滚动条一直显示
  child: SingleChildScrollView(
    padding: EdgeInsets.all(16.0),
    child: Center(
      child: Column(
        // 动态创建一个List<Widget>
        children: str.split("")
            // 每一个字母都用一个Text显示,字体为原来的两倍
            .map((c) => Text(c, textScaleFactor: 2.0,))
            .toList(),
      ),
    ),
  ),
);
复制代码
                    ![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6d1e32ef93f748d2add697b960c3488a~tplv-k3u1fbpfcp-watermark.image)
复制代码

4.2 ListView

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

ListView({
  ...  
  // 滚动组件公共参数
  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>[],
})
复制代码

上面参数分为两组:第一组是可滚动组件的公共参数,上面已经介绍过,不再赘述;第二组是ListView各个构造函数(ListView有多个构造函数)的共同参数,我们重点来看看这些参数,:

  • 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

注意:上面这些参数并非ListView特有,在本章后面介绍的其它可滚动组件也可能会拥有这些参数,它们的含义是相同的。

4.2.1 ListView.builder

适合列表项比较多(或者无限)的情况,因为只有当子组件真正显示的时候才会被创建,也就说通过该构造函数创建的ListView是支持基于Sliver的懒加载模型的.

Column(children: [
    ListTile(title: Text("列表的头部")),
    ListView.builder(
        itemCount: null, // null指的是无限个
        itemExtent: 50, // 指定每个item的高度
        itemBuilder: (BuildContext context, int index) {
          return ListTile(title: Text("$index"));
        }),
]);
复制代码

上面代码执行后会报一个异常

Error caught by rendering library, thrown during performResize()。
Vertical viewport was given unbounded height ...
复制代码

因为ListView高度边界无法确定引起。所以我们指定ListView的高度。 有2个方法。

// 方法一:固定ListView的高度为300
SizedBox(
    height: 300,
    child: ListView.builder()...,
),

// 方法2:Expanded的flex默认=1, Column继承自Flex,所以可以使用Column + Expanded实现动态扩展ListView的高度。
Expanded(
      child:  ListView.builder()...,
),
复制代码

image.png

4.2.2 ListView.separated

创建包含分割线的列表

  //下划线widget预定义以供复用。  
 Widget divider1=Divider(color: Colors.red,);
 Widget divider2=Divider(color: Colors.blue);
Column(children: [
  Expanded(
    child: ListView.separated(
      //列表项构造器
      itemBuilder: (BuildContext context, int index) {
        return ListTile(title: Text("$index"));
      },
      //分割器构造器
      separatorBuilder: (BuildContext context, int index) {
        return index % 2 == 0 ? divider1 : divider2;
      },
      itemCount: 30,
    ),
  )
]);
复制代码

image.png

4.2.3 ListView构造(不建议使用)

  不建议用此方法创建ListView。因为通过构造创建ListView没有基于Sliver的懒加载模型,所以默认会把children里所有的widget都创建好,和使用SingleChildScrollView+Column的方式没有本质的区别。

  基于Sliver的懒加载模型创建的ListView,只有页面被用户看到了,才会去创建Widget,可以减少资源浪费。

Column(children: [
  ListView(
    shrinkWrap: true,
    children: [
      ListTile(title: Text("1111")),
      ListTile(title: Text("2222")),
      ListTile(title: Text("3333")),
      ListTile(title: Text("4444")),
    ],
  ),
]);
复制代码

image.png

4.3 GridView

可以构建一个二维网格列表,其默认构造函数如下:

GridView({
   ... 和ListView重复的属性就不再赘述
  @required SliverGridDelegate gridDelegate, //控制子widget layout的委托
  List<Widget> children = const <Widget>[],
})
复制代码

我们唯一需要关注的是gridDelegate参数,类型是SliverGridDelegate,它的作用是控制GridView子组件如何排列(layout)。

SliverGridDelegate
|-- SliverGridDelegateWithFixedCrossAxisCount // 固定交叉轴数量
|-- SliverGridDelegateWithMaxCrossAxisExtent // 最大交叉轴长度
复制代码

4.3.1 SliverGridDelegateWithFixedCrossAxisCount

该类实现了一个横轴为固定数量子元素的layout算法,其构造函数为:

SliverGridDelegateWithFixedCrossAxisCount({
  @required double crossAxisCount, 
  double mainAxisSpacing = 0.0,
  double crossAxisSpacing = 0.0,
  double childAspectRatio = 1.0,
})
复制代码
  • crossAxisCount:横轴子元素的数量。此属性值确定后子元素在横轴的长度就确定了,即ViewPort横轴长度除以crossAxisCount的商。
  • mainAxisSpacing:主轴方向的间距。
  • crossAxisSpacing:横轴方向子元素的间距。
  • childAspectRatio:子元素在横轴长度和主轴长度的比例。由于crossAxisCount指定后,子元素横轴长度就确定了,然后通过此参数值就可以确定子元素在主轴的长度。

可以发现,子元素的大小是通过crossAxisCountchildAspectRatio两个参数共同决定的。注意,这里的子元素指的是子组件的最大显示空间,注意确保子组件的实际大小不要超出子元素的空间。

举个例子:

GridView(
  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
      crossAxisCount: 3, // 横轴显示三个子widget
      childAspectRatio: 1.0 // 宽高比为1
  ),
  children:<Widget>[
    Container(color:Colors.green,child: Icon(Icons.add)),
    Icon(Icons.airport_shuttle),
    Icon(Icons.all_inclusive),
    Icon(Icons.beach_access),
    Icon(Icons.cake),
    Icon(Icons.free_breakfast)
  ]
);
复制代码

image.png

4.3.2 SliverGridDelegateWithMaxCrossAxisExtent

该子类实现了一个横轴子元素为固定最大长度的layout算法,其构造函数为:

SliverGridDelegateWithMaxCrossAxisExtent({
  double maxCrossAxisExtent,
  double mainAxisSpacing = 0.0,
  double crossAxisSpacing = 0.0,
  double childAspectRatio = 1.0,
})
复制代码
  • maxCrossAxisExtent。控制显示几列,举个例子,屏幕宽410,如果该值设置为120,  410 / 120 = 3.41,GridView内部会向上取整,设置成4列。如果该值为130,410 / 130 = 3.15,也会设置成4列。

  • childAspectRatio:子View的宽高比。

  • 其它参数和SliverGridDelegateWithFixedCrossAxisCount相同。

GridView(
  gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
      maxCrossAxisExtent: 130,
      childAspectRatio: 2 / 1, // 宽高比为2
  ),
  children: <Widget>[
     Container(color:Colors.green,child: Icon(Icons.add)),
    Icon(Icons.airport_shuttle),
    Icon(Icons.all_inclusive),
    Icon(Icons.beach_access),
    Icon(Icons.cake),
    Icon(Icons.free_breakfast),
  ],
);
复制代码

image.png

4.3.3 GridView.count

GridView.count构造函数内部使用了SliverGridDelegateWithFixedCrossAxisCount,我们通过它可以快速的创建横轴固定数量子元素的GridView.下面方式1和方式2是等价的。

// 方式1
GridView(
  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
      crossAxisCount: 3, // 横轴三个子widget
      childAspectRatio: 1.0 // 宽高比为1,子widget
  );

// 方式2
GridView.count( 
  crossAxisCount: 3, 
  childAspectRatio: 1.0,
  ...
)
复制代码

4.3.4 GridView.builder

上面我们介绍的GridView都需要一个widget数组作为其子元素,这些方式都会提前将所有子widget都构建好,所以只适用于子widget数量比较少时,当子widget比较多时,我们可以通过GridView.builder来动态创建子widget。

GridView.builder(
        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: 3, // 每行三列
            childAspectRatio: 1 / 1 // 显示区域宽高相等
        ),
        itemCount: _icons.length,
        itemBuilder: (context, index) {
          return Icon(Icons.add); // item布局
        }
    );
复制代码

4.4 CustomScrollView

  • 举个例子,假设有一个页面,顶部需要一个GridView,底部需要一个ListView,要求整个页面当成一个整体滑动,如果使用GridView+ListView来实现的话,就不能保证一致的滑动效果,因为它们的滚动效果是分离的,所以这时就需要一个"胶水",把这些彼此独立的可滚动组件"粘"起来,而CustomScrollView的功能就相当于“胶水”。
  • CustomScrollView要求只有实现了Sliver功能的组件才能作为它的子View。

实现了Sliver功能的View如下

SliverAppBar
SliverPadding
SliverFixedExtentList
SliverList
SliverGrid
...
复制代码

举个例子:

Material(
  child: CustomScrollView(
    slivers: <Widget>[
      //AppBar,包含一个导航栏2
      SliverAppBar(
        pinned: true,
        expandedHeight: 250.0,
        flexibleSpace: FlexibleSpaceBar(
          title: const Text('Demo'),
          background: Image.asset(
            "./images/peri.png", fit: BoxFit.cover,),
        ),
      ),

      SliverPadding(
        padding: const EdgeInsets.all(8.0),
        sliver: new SliverGrid( //Grid
          gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: 2, //Grid按两列显示
            mainAxisSpacing: 10.0,
            crossAxisSpacing: 10.0,
            childAspectRatio: 4.0,
          ),
          delegate: new SliverChildBuilderDelegate(
                (BuildContext context, int index) {
              //创建子widget
              return new Container(
                alignment: Alignment.center,
                color: Colors.cyan[100 * (index % 9)],
                child: new Text('grid item $index'),
              );
            },
            childCount: 20,
          ),
        ),
      ),
      //List
      new SliverFixedExtentList(
        itemExtent: 50.0,
        delegate: new SliverChildBuilderDelegate(
                (BuildContext context, int index) {
              //创建列表项
              return new Container(
                alignment: Alignment.center,
                color: Colors.lightBlue[100 * (index % 9)],
                child: new Text('list item $index'),
              );
            },
            childCount: 50 //50个列表项
        ),
      ),
    ],
  ),
);
复制代码

image.png

4.5 ScrollController

控制可滚动组件的滚动位置

ScrollController({
  double initialScrollOffset = 0.0, //初始滚动位置
  this.keepScrollOffset = true,//是否保存滚动位置
  ...
})
复制代码

举个例子

class ScrollControllerTestWidget extends StatefulWidget {
  @override
  ScrollControllerTestState createState() {
    return new ScrollControllerTestState();
  }
}
class ScrollControllerTestState extends State<ScrollControllerTestWidget> {
  ScrollController _controller = new ScrollController();
  bool showToTopBtn = false; // 是否显示“返回到顶部”按钮

  @override
  void initState() {
    super.initState();
    // 监听滚动事件,打印滚动位置
    _controller.addListener(() {
      print("当前滚动位置:"+_controller.offset); // 打印滚动位置
      if (_controller.offset < 200 && showToTopBtn) {
        setState(() {
          showToTopBtn = false;
        });
      } else if (_controller.offset >= 200 && showToTopBtn == false) {
        setState(() {
          showToTopBtn = true;
        });
      }
    });
  }

  @override
  void dispose() {
    //为了避免内存泄露,需要调用_controller.dispose
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Scrollbar(
        child: ListView.builder(
            itemCount: 100,
            itemExtent: 50, //列表项高度固定时,显式指定高度是一个好习惯(性能消耗小)
            controller: _controller,
            itemBuilder: (context, index) {
              return ListTile(title: Text("$index"),);
            }
        ),
      ),
      floatingActionButton: !showToTopBtn ? null : FloatingActionButton(
          child: Icon(Icons.arrow_upward),
          onPressed: () {
            //返回到顶部时执行动画
            _controller.animateTo(.0,
                duration: Duration(milliseconds: 200),
                curve: Curves.ease
            );
          }
      ),
    );
  }
}
复制代码

ezgif.com-gif-maker.gif

文章分类
Android
文章标签