flutter第二篇:布局上

5、Container、Text

利用Container的alignment属性,可以设置其子组件在Container中的位置,如居中。Container设置alignment后,其将会充满父组件,而不再是根据子组件自适应。

如果想让child决定Container的尺寸,那么就不要设置Container的alignment。Container是矩形的,除非设置了borderRadius。

如果想在固定尺寸的Container中对齐child,那么就得设置Container的alignment。

如果既想让child决定Container的尺寸,又想对齐child,那么就得用Align包裹Container了,设置Align的alignment属性,而不是Container的alignment属性。Align的alignment属性控制Container在Align中的位置,即Container的child在Align中的位置,因为Container是紧紧包裹其child的。

根据https://docs.flutter.dev/ui/layout/constraints中Example6、7,知,在Container没有指定宽高的情况下,如果没有child,那么它和父组件一样大。反之,如果没有alignment,那么它将和子组件一样大,否则它和父组件一样大。

逻辑伪代码如下:

if no explicit size {
    if no child {
        size = parent's size
    }else{
        if no alignment {
            size = child's size
        }else{
            size = parent's size
        }
    }
}

设置Container的decoration属性为一个BoxDecoration实例:

利用BoxDecoration实例的color属性,可以给Container设置背景色。

利用BoxDecoration实例的border属性,可以给Container设置边框。如赋值为Border.all(),此时边框宽度是1,颜色是黑色。如果想自定义,则可以在这个all()方法中用width指定宽度、用color指定颜色。

利用BoxDecoration实例的borderRadius属性,可以给Container设置圆角,如赋值为BorderRadius.circular(10)。

利用BoxDecoration实例的boxShadow属性,可以给Container设置阴影。

利用BoxDecoration实例的gradient属性,可以给Container设置渐变的背景色,如设置为LinearGradient(colors: [Colors.red, Colors.black]),则背景色会线性渐变,由红色变为黑色。设置为RadialGradient(colors: [Colors.red, Colors.black]),则背景色会径向渐变,从中心到四周,由红色变为黑色。gradient设置后,会忽略color属性。

Container的padding是内边距,是其child组件与container边框的距离,margin是外边距,是container边框与其父组件边框的距离。Container的margin、padding不能是负数。如果给Container加背景色,那么padding的区域是会加背景色的,而margin的区域不会。

constraints属性用于向子组件传递约束信息,值是个BoxConstraints实例,包含最小最大宽高信息。

Container嵌套的情况下,如果外部Container设置了宽高,但没有设置alignment,那么内部的Container会自动扩展成和外部Container一样大,即使设置了比较小的宽高。给外部Container设置alignment后,内部Container大小才正常。

Text

Text默认可以换行,如果字数很多,则当文字到Text外部容器(如果没有容器,则是屏幕)的右边界时,就会往下换一行,直到Text外部容器(如果没有容器,则是屏幕)的底边界。如果想限制成仅一行,则需要设置maxLines属性值为1。如果设置了TextStyle的overflow属性为TextOverflow.ellipsis,则文本最后会变成三个点,即省略号。设置overflow后,文本不会自动换行了,即文字再多也是单独一行,最后会变成三个点。要换行,需要再设置maxLines为一个大于1的数。

Row中的Text不会换行,字数过多会越界,如果要自动换行,需要把Text用Expanded包起来,即Row中有一个Expanded,Expanded的child是Text,此Text会自动换行。

字体加粗要利用TextStyle的fontWeight属性。fontSize默认是14。如果文本下面有双黄线,想去掉的话,需要设置TextStyle的decoration值为TextDecoration.none。

如何实现一个自适应尺寸的Container,其宽高随内部的Text自适应,且把Text放到右上角?

Align(
  alignment: Alignment.topRight,
  child: Container(
    padding: const EdgeInsets.all(8),
    decoration: BoxDecoration(
      color: Colors.blue.shade100,
      border: Border.all(color: Colors.blue),
    ),
    child: const Text('自适应文本'),
  ),
)

Align负责定位,Container根据Text的大小自适应尺寸。

6、Image

加载远程图片:Image.network("")、Image(image: NetworkImage(""))

加载本地图片:Image.asset("")、Image(image: AssetImage(""))

常用参数有:

width、height:用于设置Image的宽高(可以用一个裸的Container包裹Image,给Container设置背景色观察Image的宽高)。注意是Image的宽高,而不是图片最终肉眼可见的宽高。图片最终肉眼可见的宽高很可能和Image的宽高不一样,如果fit设置为BoxFit.fill或BoxFit.cover,就一样,设置为BoxFit.fitWidth或者BoxFit.fitHeight,就很可能不一样。

alignment:用于在图片肉眼可见宽高和Image的宽高不一样时,设置图片的位置。

fit:用于在图片的父容器和图片尺寸不同时,指定图片的适应模式,可选值如下:

①BoxFit.fitWidth、BoxFit.fitHeight:如为fitWidth,则图片会等比缩放,直到在水平方向上铺满容器,在竖直方向上,图片可能因为很高而被裁剪,或者不够高而居中。fitHeight同理。

②BoxFit.contain:等比例缩放图片,使得图片完全放到容器中。容器可能有部分空白。contain不会裁剪。contain可以记成容器contains图片。

③BoxFit.cover:等比例缩放图片,使得图片完全覆盖容器区域。图片可能有部分看不见。cover可能会裁剪。cover可以记成图片cover容器。

④BoxFit.fill:图片会缩放,直到在两个方向上都铺满容器。如果容器与图片宽高比不同的话,图片会变形。

repeat:平铺。在图片尺寸比父容器小,且在某方向上还没铺满容器时有效,平铺就好像地板砖一样,一个一个铺。默认不平铺。

有一张图片,如果想把图片设为圆角,则需要1、设置Image的width、height、fit,且fit设置为BoxFit.fill或BoxFit.cover,即图片要把Image填满。2、把图片用ClipRRect包裹,设置其borderRadius。

最佳实践:width、height、fit都设置。

测试图:

1080*2341:https://avg-oss.xndm.tech/test/op/CvE2wA4fLhiSRLwCXsXPJm.png

300*178:https://avg-oss.xndm.tech/test/op/WX6MWVSEvRmVjsKixKCX96.jpg

360*214:https://avg-oss.xndm.tech/test/op/Ki83VJnyCuTgu3Gee5EDPH.jpg

720*428:https://avg-oss.xndm.tech/test/op/njn3uGVP2vDYZieCLiy4cX.jpg

创建圆形图片的三种方式:

①、使用Container:

Container默认是矩形,可以设置其decoration属性,设置BoxDecoration的shape属性值为BoxShape.circle,使其变为圆形。

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 40,
      height: 40,
      decoration: const BoxDecoration(
          shape: BoxShape.circle,
          image: DecorationImage(
              fit: BoxFit.cover,
              image: NetworkImage(
                "http://oss.echowa.xndm.tech/test/op/98ir9yjgJc8krUcyp9Ztd4.png",
              ))),
    );
  }

②、使用ClipOval

Oval是椭圆的意思

  @override
  Widget build(BuildContext context) {
    return ClipOval(
      child: Image.network(
          "http://oss.echowa.xndm.tech/test/op/98ir9yjgJc8krUcyp9Ztd4.png",
          width: 40,
          height: 40,
          fit: BoxFit.cover),
    );
  }

③、使用CircleAvatar:

这种方式最简单,CircleAvatar其实是对AnimatedContainer的封装。

  @override
  Widget build(BuildContext context) {
    return const CircleAvatar(
      radius: 20,
      backgroundImage: NetworkImage(
          "http://oss.echowa.xndm.tech/test/op/98ir9yjgJc8krUcyp9Ztd4.png"),
    );
  }

CircleAvatar默认的半径是20dp,如果觉得太小或太多,可以用radius属性自定义。

Image.network()每次加载都要重新从服务器获取图片,没有缓存到本地。在实际开发中,cached_network_image插件使用的更多,https://pub.dev/packages/cached_network_image,这个插件会把图片缓存到本地,并且利用其placeholder属性还可以设置占位图(一般是一张本地图片),利用其errorWidget属性还可以设置兜底图(原图跪了的时候展示兜底图)。

7、Icon

如果内置的图标不能满足我们的需求,那么我们可以去阿里巴巴矢量图标库寻找合适的图标。

登录后,根据关键字搜索想要的图标,找到后,点击加入购物车,然后去购物车,点击下载代码,下载到本地。解压后,把文件夹中的iconfont.json和iconfont.ttf文件复制到项目中的fonts目录(如果没有就新建),也可以是assets/fonts目录。然后修改项目的pubspec.yaml文件,找到fonts块,解除注释,修改其family和asset的值,family可以理解为是一个唯一名称,asset是上述ttf文件的路径。然后新建一个类,在其中定义一个常量,示例如下:

import 'package:flutter/material.dart';

class Fonts {
  static const IconData xiaomi =
      IconData(0xe66e, fontFamily: "Schyler", matchTextDirection: true);
}

IconData构造方法中,第一个参数是码点,固定以0x开头,0x后面是iconfont.json文件中unicode的值,第二个参数fontFamily的值就是pubspec.yaml文件中指定的family的值。

此时,就可以通过Icon(Fonts.xiaomi)来使用这个图标了。

8+9、ListView

要创建ListView实例,可以调用ListView()方法,但是最好不要在有大量元素的情况下使用这个方法,因为这个方法不是按需加载的,而是一下子就渲染了。元素太多时,性能不好。除此之外,要创建一个ListView实例,还可以使用其builder()命名构造器,itemBuilder参数用于构造元素,itemCount参数用于指定元素数量,必须指定,不然是无限多的。除了builder()命名构造器外,还有separated()命名构造器、custom()命名构造器。利用separated()命名构造器,可以在每个元素下面都自动添加一个组件,来分隔元素。利用custom()命名构造器,需要指定一个SliverChildDelegate,有SliverChildBuilderDelegate和SliverChildListDelegate可选,前者是按需加载的,后者不是,而是一下子就渲染了。

常用属性或参数有:

itemExtent:用于指定元素的长度,这个长度是在滚动方向上的长度,默认是高度。Extent的意思是长度。如果ListView中所有元素的高度都一样,那么建议用itemExtent设置高度,而不是在创建元素时指定高度,前者有更好的性能。设置itemExtent的话,里面的元素即使自己设置了高度也不会生效。

cacheExtent:预渲染区域长度,即预渲染区域高度,默认值是250dp。

shrinkWrap,是否根据子组件的总长度来设置ListView的长度,默认值为false。当ListView在一个无边界(滚动方向上)的容器中时,shrinkWrap为true,可以解决报Vertical viewport was given unbounded height的问题。例如,有一个column,在此column中有一个listView,那么要么设置listView的shrinkWrap值为true,要么用Expanded或指定高度的Container包裹。

在ListView中,如果ListView是上下滚动的,那么如果其中有Container,则Container的width属性会失效,不管设成多少,在水平方向上都会铺满。为了自定义宽度,我们可以把ListView用指定宽度的SizedBox包裹(每个元素都一样宽,且边框从左侧开始),或者使用ListView的padding属性,给左右两边留点空隙。是的,ListView也有padding属性,如果想让滚动区域和屏幕两边有点间距的话,可以使用padding属性。

如果想让ListView中某元素在水平方向上居中,则可以用Center包裹该元素。ListView有一个默认的top padding,如果要去掉,则可以设置其padding属性,或者用MediaQuery.removePadding包裹,设置removeTop属性值为true。

当ListView的元素超过一屏时,ListView不会越界,而是会可以滚动。如果要禁止滚动,则需要把其physics属性设为一个NeverScrollableScrollPhysics实例。有些场景下,默认不能滚动,设置了physics反而能滚动。总之,涉及到滚动,设置并调整physics就好了。

ListTile是一个固定高度的行,在这个行中可以放标题(title属性)和子标题(subTitle属性),在标题/子标题之前(利用leading属性)、之后(利用trailing属性)还可以放一个Icon或图片或者其他任意组件。leading与左边界的距离、trailing与右边界的距离,可以通过contentPadding调整,默认都是16dp。使用contentPadding还可以让ListTile在上下有点空间。

ListTile经常用在ListView中,也可以单独当做行使用。ListTile的trailing属性,如果是个Row的话,比如想放多个元素的情况,那么必须设置Row的mainAxisSize属性为MainAxisSize.min,否则会报Trailing widget consumes the entire tile width (including ListTile.contentPadding)。ListTile有onTap属性,可以定义点击事件。

问题:title是个有很多元素的Column时,leading和trailing在竖直方向上会自动居中,如何调整使其居上?

 

如果要获取滚动条的位置,则需要利用ListView的controller属性,赋值为一个ScrollController实例。通过ScrollController实例的addListener()方法,可以监听滚动条的位置变化。scrollController.position.pixels可以获得滚动条顶端与appBar底部(或者说body顶部)的距离。注意不是与屏幕顶部的距离,滚动条顶端与屏幕顶部中间还隔着状态栏、appBar。

ListView是显示不了滚动条的,如果要显示,则需要用Scrollbar包裹。

如果要实现像淘宝的商品详情页那样,页面在往上滑的过程中,展示的依次是宝贝部分、评价部分、详情部分、推荐部分,且点击顶部的宝贝、评价、详情、推荐时,页面可以直接跳到相应部分,那么需要用SingleChildScrollView包裹Column,在Column的children放各部分,各部分都要用Container包裹,且Container要配置key,假如key分别是k0、k1、k2、k3。点击评价时,直接调用Scrollable的ensureVisible()静态方法,入参是k1.currentContext as BuildContext,如Scrollable.ensureVisible(controller.k1.currentContext as BuildContext)。

如果想在ListView某一行中居中放一个小图片,如128×128,则可以:

第一种方法:用一个宽128的Container包裹此ListView,但是此ListView的其他行的宽度也最多只有128了,且如果还得把Container用Center包裹,否则是图片是居左显示。

第二种方法:在ListView中放一个alignment属性为Alignment.center的Container,在此Container中再放一个包裹了图片的Container。外部Container的宽度会自动扩展成和ListView的宽度一样,不管设不设其alignment属性。如果不设外部Container的alignment属性,那么内部的Container的宽度也会自动扩展成和外部Container的宽度一样。

10、GridView

GridView是网格,好像方格本一样,一行有几个格子,一行一行的,格子里面有内容。我们把格子叫做单元格。

GridView的count()命名构造方法用于构建一个可以指定一行有多少个单元格的网格,利用crossAxisCount属性指定,如指定为2,那么在任意设备上,一行都只有2个单元格。

GridView的extent()命名构造方法用于构建一个可以指定单元格在水平方向上最大宽度的网格,利用maxCrossAxisExtent属性指定,如指定为120,这种网格,一行最终能展示多少个单元格是不确定的,在不同设备上,可能不一样。

GridView的children属性用于指定网格的内容。GridView的crossAxisSpacing属性用于指定在同一行中相邻单元格的间距,默认没有任何间距。GridView的mainAxisSpacing属性用于指定行间距,默认没有任何间距。GridView的childAspectRatio属性用于指定宽高比,默认是1,即单元格是正方形。

GridView的builder()命名构造器、custom()命名构造器、GridView(),这三种方式,都既可以构造一个count类型的GridView,又可以构造一个extent类型的Gridview实例,取决于给gridDelegate属性赋值为一个SliverGridDelegateWithFixedCrossAxisCount实例,还是一个SliverGridDelegateWithMaxCrossAxisExtent实例。在builder()中,用itemCount属性指定单元格的总量,用itemBuilder属性指定各单元格的内容,itemBuilder属性所对应的函数的第二个入参是索引,利用这个参数,我们可以根据索引的不同返回不同的组件。在custom()中,利用childrenDelegate指定各单元格的内容。

GridView的physics属性、shrinkWrap属性同ListView。如果ListView中有GridView,那么要么用Container包裹GridView,并设置高度,要么设置GridView的shrinkWrap属性为true。

11、Padding、Flex、Row、Column、Flexible、Expanded

有一些容器组件有padding属性,如Container,如果想设置其child与容器壁的距离,就可以给padding赋值。站在非容器组件角度,如果我们想它离上下左右有点间距,那么除了用Container包裹外,还可以用Padding包裹。比如我们想实现文本框离左边10dp,就可以用Padding包裹文本框,然后设置Padding组件的padding属性。padding属性值为一个EdgeInsets实例,可以通过调用EdgeInsets的only()方法、all()方法、fromLTRB()方法来得到。

Padding(
        padding: EdgeInsets.only(left: 10),
        child: Text("离左侧10dp"),
      )

线性布局:Flex、Row、Column

mainAxisAlignment是在主轴方向上子元素的对齐方式,Row的主轴方向是水平方向,Column的主轴方向是竖直方向,默认是MainAxisAlignment.start,即左对齐。MainAxisAlignment.center表示居中、MainAxisAlignment.end表示右对齐。特殊的,

①MainAxisAlignment.spaceBetween表示分散对齐,最左边元素贴着左边框,最右边元素贴着右边框。如果Row的宽度是x,元素个数是n,那么元素间距是x/(n-1)。如果children中只有两个子元素,那么将会形成一左一右的效果。

②MainAxisAlignment.spaceAround表示最左边元素与左边框间距是元素间距1/2、最右边元素与右边框间距是元素间距1/2的分散对齐,如果Row的宽度是x,元素个数是n,那么元素间距是x/n。

③MainAxisAlignment.spaceEvenly表示最左边元素与左边框间距、最右边元素与右边框间距都等于元素间距的分散对齐,如果Row的宽度是x,元素个数是n,那么元素间距是x/(n+1)。evenly是平均的意思。

如果想更个性化地分配空间,可以使用Spacer。

mainAxisSize是行的宽度,默认是MainAxisSize.max,即行宽默认等于外层容器的宽度,哪怕只有少量的元素。设为MainAxisSize.min,则等于元素的宽度。

crossAxisAlignment是在交叉轴方向上子元素的对齐方式,Row的交叉轴方向是竖直方向,Column的主轴方向是水平方向,默认是CrossAxisAlignment.center,即居中对齐,即同一行的元素在竖直方向上是居中的,同一列的元素在水平方向上是居中的。

Row和Column都是Flex的子类。

弹性布局:Flex/Column/Row + Flexible/Expanded

如果我们要实现的布局是在一行或一列中,某些元素的宽度是固定的,某些元素的宽度是自适应的,或是每个元素的宽度都是固定比例,那么就要用到Flex。具体来说,把要自适应的元素用Expanded包裹,并放到Flex的children中。而由于Row、Column都是特定的Flex,所以也可以放到Row或Column的children中,看具体需求。以把Expanded放到Row的children中为例,Expanded有一个flex属性,值是个整数,表示占据宽度的份数,多个Expanded指定flex后,每一个Expanded的实际宽度就可以计算出来。如在Row的children中只有两个Expanded,第一个Expanded的flex为1,第二个Expanded的flex为2,那么第一个Expanded的实际宽度则为Row宽度的1/3,第二个Expanded的实际宽度为Row宽度的2/3。如果除了这两个Expanded外,还有一个固定宽度的Container,假设为100,那么第一个Expanded的实际宽度则为(Row宽度-100)/3,第二个Expanded的实际宽度为2*(Row宽度-100)/3,即在给Expanded分配宽度时,要先把固定宽度的组件的宽度减出来。Expanded是Flexible的子类,用到Expanded的地方,都可以用Flexible,反之亦然。它俩的唯一区别是Expanded的child的尺寸恒等于Expanded的尺寸,而Flexible的child的尺寸是由其(Flexible的child)子元素撑起来的,小于等于Flexible的尺寸,可以通过对Expanded/Flexible的child设置背景色确认。

在Row的children中,用Expanded包裹的元素要指定高度,如果此组件没有高度属性,则需要用Container或是SizedBox包裹起来。同理,在Column的children中,用Expanded包裹的元素要指定宽度,如果此组件没有宽度属性,也需要用Container或是SizedBox包裹起来。

问题:有一个行,其中有一个用Expanded包裹的列,列中有一个Expanded。把这个行作为ListView的元素时,会报RenderFlex children have non-zero flex but incoming height constraints are unbounded。为什么,如何解决?

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        backgroundColor: Colors.yellow,
        appBar: AppBar(),
        body: Row(
          children: [
            const Column(
              children: [
                CircleAvatar(
                  radius: 20,
                  backgroundImage: NetworkImage("http://oss.echowa.xndm.tech/test/op/98ir9yjgJc8krUcyp9Ztd4.png"),
                )
              ],
            ),
            Expanded(
              child: Container(
                  padding: EdgeInsets.only(left: 10, right: 10),
                  child: const Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        "张三",
                        style: TextStyle(color: Colors.white),
                      ),
                      Expanded(
                          child: Text(
                        "如果要获取滚动条的位置,则需要利用ListView的controller属性,赋值为一个ScrollController实例。通过ScrollController实例的addListener()方法,可以监听滚动条的位置变化。scrollController.position.pixels可以获得滚动条顶端与appBar底部(或者说body顶部)的距离。注意不是与屏幕顶部的距离,滚动条顶端与屏幕顶部中间还隔着状态栏、appBar。",
                        style: TextStyle(color: Colors.white),
                      )),
                    ],
                  )),
            ),
          ],
        ));
  }

上面代码,在外面套一个ListView就报错了。

12、Stack Positioned Align

Stack用于实现层叠布局。Stack组件的children中各元素会堆叠,一层叠一层,后面的叠前面的。如果我们想自定义元素的位置,那么可以把元素用Positioned包裹,通过指定Positioned组件的left、right、top、bottom属性即可调整元素的位置(设置left值为0、right值为0就会占满整行),通过指定Positioned的width、height即可调整元素的宽高。在水平方向上,left、right、width这三个属性不能同时都设置,都不设置、设置任意一个、任意两个都可以。在竖直方向上同理。left、right、top、bottom都可以是负数。Positioned必须用在Stack中。

Stack的fit属性表示没有定位或者部分定位的子组件如何适应,默认值为StackFit.loose,表示使用子组件大小,StackFit.expand表示扩展到Stack的大小。clipBehavior属性表示超出Stack的子组件是否被剪掉,默认值Clip.hardEdge表示被剪掉,Clip.hardEdge表示不被剪掉。

有一些组件有alignment属性,可以通过此属性调整其child在组件中的位置,比如Container组件,其alignment值如果设为Alignment.center的话,其child就会位于Container的正中间,即横向和纵向都居中。如果组件没有alignment属性,但又想设置其child的位置,则可以把该组件的child设置为一个Align组件,Align组件的child属性设置为原来的child,通过设置Align的alignment的属性调整原child的位置。Center是Align的子类,表示横向和纵向都居中。

posted on 2024-08-27 19:18  koushr  阅读(290)  评论(0)    收藏  举报

导航