Flutter - Weight 大全

3,136 阅读13分钟

新东西都需要一个记忆的过程,加之 Flutter Weight 设置的东西更繁杂,所以我这里尽量全的列举出 Flutter 常用的 Weight 的所有设置和用法

官方文档:


吐槽下

Flutter 中的 Weight 异常之多,相互之间非常混乱,实现相同功能的 Weight 一般都有好几个,并且 Weight 使用起来一般都比较别扭,android 中一个 layout 能实现的,在 Flutter 里一般你都得套好多层 Weight 才行。这体现出 Flutter Weight 设计的草率、随性和经验不足,生搬硬套 web 端,但是现在 Flutter 逗正式版 1.2 了,也没法改了,大家适应吧

另外 Flutter 把 android 中大多数 view 的限制属性全部提升到 Weight 的级别,作为容器使用,这样进一步加剧了 Flutter Weight 编写的复杂读,大家先心里有个底,Flutter 注定是像 web 一样了一层 dvi 套一层 dvi

为了便于学习、记忆、查找,我把 Flutter Weight 按照功能分成几种:

  • 完成布局任务的 - 布局 Weight
  • 负责具体绘制的 - 内容 Weight
  • 实现控制属性的 - 单容器 weight
  • 设置尺寸大小的 - 大小 weight

对于内容较多的 Weight 我开单章了,介绍里面方链接,要不看起来太费劲了,剩下的那些内容少的就写在本文里了


涉及 Weight 如下:

由 Container 拓展出来的单个属性的容器 Weight (比如 padding,sizebox)都比较简单,除了需要特殊说明的,剩下的我都没列在这里

布局 Weight:
  • Row Column - 一行,一列,只能是一行或者一列,不能换行,换列,具体的请看:Flutter - Row、Column / 一行、一列
  • Stack - 堆叠布局,允许子 weight 相互重叠现实,相当于 Android 的 FrameLayout
  • IndexedStack - 指定显示第几个子 view 的 Stack,Stack 大小占位不会因此变化
  • WrapFlow - 都是流式布局,区别是 Flow 性能相对好一些,但是得自己写换行得代码,Wrap 使用起来最方便,具体请看:Flutter UI - Wrap、Flow 实现流式布局
  • FlexibleExpanded - 权重比 Weight,一般配合 row、column 使用
实现尺寸设置的容器 Weight:
  • FractionallySizedBox - 可以让其中包裹的子 view 的宽高是外层父容器宽高的倍数
  • AspectRatio - 设置子view宽高比例
  • FittedBox - 根据父容器的大小和预设模式自动缩放子view
  • ConstrainedBox - 约束子控件,可以设置宽高的最大最小值
单容器 Weight:
  • Container - 综合修饰容器,view 的边距,背景,旋转,位移都是设置在 Container 上的,具体的请看:Flutter - Container
  • Transform - 相当于 android 的 tween 动画,作用与draw阶段,weight 位置不会随着动画变动
  • RotatedBox - 作用与layout阶段,weight 位置会随着动画变化
  • Opacity - 控制子 widget 透明度,在 0-1 之间变化
  • Baseline - 容器,子 view 文字基线对齐,一般都是配合 row 这种布局 weight 使用得
  • Offstage - 控制子view 显示与否
内容 Weight:
  • Decoration - Flutter 版的 shape,用于绘制各种背景,边框,因为东西比较多,不合适放在这里,专门单开一章,详细请看:Flutter - Decoration
  • Chip - 简单的小标签
  • Divider - 分割线

基础设置

1. Flutter 单位

Flutter 里没有单位一说了,直接写数值,比如设置 width

  Widget build(BuildContext context) {
    return Container(
      width: 50,
      color: Colors.lightBlueAccent,
      child: oneRow(),
    );
  }

既然没有单位了,那么这个 50 表示什么,表示 50个逻辑单位,Flutter 的逻辑单位其实就是 android 的像素密度

dm = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(dm);
density  = dm.density;      // 屏幕密度(像素比例:0.75/1.0/1.5/2.0 )
  • density 1.0,1dp 此时 = 1px
  • density 2.0,1dp 此时 = 2px

然后我们再回过头来看 Flutter 的这个逻辑密度,其实还是 dp,只不过不写 dp Z合格单位了,换汤不换药。为了匹配 android 和 ios 端,Flutter 统一把单位称为几X,和 UI 给我们切的 IOS 那个几X的切图的几X是一个意思

android 和Flutter 像素比例:

android ios
ldpi 0.75x
mdpi 1.0x
hdpi 1.5x
xhdpi 2.0x
xxhdpi 3.0x
xxxhdpi 4.0x

我想很自然的大家都会想到 Flutter 屏幕适配怎么做对吧,这个问题不再本文之列,以后研究...

2. EdgeInsets

Flutter 里面很多时候不能直接写数值的,这点让人大感意外,比如 padding、maiger 就不能直接写数字,必须借助 EdgeInsets,EdgeInsets 的初衷是为了省大家事

EdgeInsets 的使用如下:

ALT + 回车快捷键可以自动提示,不用手打

  Widget build(BuildContext context) {
    // TODO: implement build
    return Container(
      padding: EdgeInsets.all(10),
      width: 50,
      color: Colors.lightBlueAccent,
      child: oneRow(),
    );
  }

EdgeInsets 是一个类,提供了设置 LTRB 4个方向的方法:

  • EdgeInsets.all() = 全方向统一值
  • EdgeInsets.fromLTRB() = 自定义4个方向,4个方向值必须写
  • EdgeInsets.only(left,top,right,bottom) = 自定义单个方向的值
  • EdgeInsets.symmetric(vertical, horizontal) = 自定义垂直,水平对称边距
  • EdgeInsets.fromWindowPadding(ui.WindowPadding padding, double devicePixelRatio) = 根据机型屏幕尺寸定义,这个我不知道啥意思
3. 颜色值设置

我不详细写 weight,大家看设置应该能明白

1. Colors 配置好的颜色,包括透明度
color: Colors.grey
Colors.black12 //这种就是带透明度的,12=12%不透明

2. 自己加透明度
数值从 100 到 900,增量为 100,数字越小,颜色越浅,数字越大,颜色越深
color:Colors.green[200],
color: Colors.green.shade200,

3. ARGB 16进制
Color.fromARGB(0xFF, 0xEE, 0x50, 0x48)

4. ARGB 10进制
Color.fromARGB(255, 240, 80, 72)

5. RGBO 16进制:最后一个参数是透明度
Color.fromRGBO(0xEE, 0x50, 0x48, 1.0)

6. RGBO 10进制
Color.fromRGBO(240, 80, 72, 1.0)

7. 直接写16进制
Color(0xFFEE5048)

注意:


插段吐槽~

Flutter 的 UI 设计太过于依赖 Weight 了,什么逗靠 Weight 实现,搞得 Flutter 的 Weight 数量非常多,实现相同功能的 Weight 有好几个,学习起来很花时间,有的还要仔细思考其差异,到底那不一样...

但这就是 Flutter 的特色,瑕不掩瑜 Flutter 本身是非常 Nice,在 Ui 层面复杂一些是可以接收的,那个语言能没有不被人吐槽的地方呢~。Flutter 目前都是正式版了,以后这种 UI风格也肯定是固定的了,大家学就行了。有网友猜测 Flutter 这样设计是为了实现统一渲染,目的是提升渲染效率,我想了好久也没想到性能上能有多少优势,也许有但是不是我能以下就理解的,还得大N多来解惑啊

日常中用的多的,也就是 Container、Padding、Center、Align、Row、Column、Stack、ListView、image、icon、text 等也十来种,大家用心学就行了,其实也没麻烦多少,代码上结构上比 android 可是方便多了,差距那是一天一地啊...


Transform tween 动画 Weight

android 中我们再 xml 里是写不了动画变换的,Flutter 中就省事了,没那么复杂了,直接就有一个操作 view 属性的 Weight:Transform,借助 setValue,我们可实现动画效果

  • 旋转 - 正数是顺时针旋转,左边翘起;负数是逆时针旋转,右边翘起
class oneContainer extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Transform.rotate(
      angle: -math.pi / 9,
      child: Container(
        width: 100,
        height: 40,
        child: Text("AAAAAA"),
        alignment: Alignment.center,
        color: Colors.lightBlueAccent,
      ),
    );
  }
}

  • 位移
class oneContainer extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Transform.translate(
      offset: Offset(20, 20),
      child: Container(
        width: 100,
        height: 40,
        child: Text("AAAAAA"),
        alignment: Alignment.center,
        color: Colors.lightBlueAccent,
      ),
    );
  }
}
  • 缩放
class oneContainer extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Transform.scale(
     scale: 0.9,
      child: Container(
        width: 100,
        height: 40,
        child: Text("AAAAAA"),
        alignment: Alignment.center,
        color: Colors.lightBlueAccent,
      ),
    );
  }
}

注意:

Transform 是应用在绘制阶段,而并不是应用在布局(layout)阶段,所以无论对子组件应用何种变化,其占用空间的大小和在屏幕上的位置都是固定不变的,因为这些是在布局阶段就确定的

相当于 android 的 tween 动画,虽然 view 的样式变了,但是位置不变,比如点击的话还是只能相应原位置,甚至有时 view 密集的话会出问题

想 view 自适应的话就得用专门的动画 Weight


RotatedBox

旋转变换 Weight,和 Transform 不同的是 RotatedBox应用与 layout 阶段

Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: <Widget>[
    DecoratedBox(
      decoration: BoxDecoration(color: Colors.red),
      //将Transform.rotate换成RotatedBox  
      child: RotatedBox(
        // int类型
        quarterTurns: 1, //旋转90度(1/4圈)
        child: Text("Hello world"),
      ),
    ),
    Text("你好", style: TextStyle(color: Colors.green, fontSize: 18.0),)
  ],
),


Opacity

Opacity 控制透明度变化,很简单的

class EE extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Opacity(
      opacity: 0.2,
      child: Container(
        width: 350,
        height: 350,
        alignment: Alignment.center,
        decoration: BoxDecoration(
            image: DecorationImage(
                image: AssetImage("assets/icons/icon_3.png"),
                fit: BoxFit.cover),
            borderRadius: BorderRadius.circular(1000)),
        child: Text(
          "AAAAAAAAAA",
          style: TextStyle(
            fontSize: 30,
            color: Colors.lightGreenAccent,
          ),
        ),
      ),
    );
  }
}

Opacity 的特点,即便完全透明不可见了,依然不会失去位置:


FittedBox

FittedBox 没别的属性,就是一个 fit 缩放模式,和 android imageview scaleType 异曲同工啊,不过FittedBox 的应用场景我也只知道啊

new Container(
  color: Colors.amberAccent,
  width: 300.0,
  height: 300.0,
  child: new FittedBox(
    fit: BoxFit.contain,
    alignment: Alignment.topLeft,
    child: new Container(
      color: Colors.red,
      child: new Text("FittedBox"),
    ),
  ),
)

这里面也就有限的2-3个实用,none是没有缩放,contain能保持子view 自身比例的前提下缩放,scaleDown在父容器不够大时缩放子view也是保持子view比例的。有一点要说一下,文字大小也是能同时缩放的,这点很不错的


ConstrainedBox

可以设置宽高的最大值,最小值

return new ConstrainedBox(
  constraints: BoxConstraints(
    minHeight: 80,
    maxHeight: 100  
  ),
  child: Container(
    width: 100,
    color: Colors.red,
  ),
); 

AspectRatio

AspectRatio 可以设置子 view 的宽高比例,我们有时使用图片时是有设置宽高比例的需求的,另外摄像笔视频这块也是有需求的

没别的属性,就 aspectRatio 这一个

class AA extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      width: 200.0,
      child: new AspectRatio(
        aspectRatio: 3 / 4,
        child: new Container(
          color: Colors.red,
        ),
      ),
    );
  }
}

注意这里的比例是:宽:长,比如例子里面 aspectRatio: 3 / 4,那么宽占3份,长占4份,长比宽多,例子里提供了具体的 width值,那么就会按照这个比例就去计算 height 的值

别跟手机屏幕比例搞混了,手机屏幕比例是:长:宽,比如 16:9。但是把图片的分辨率又是:宽*长表示的,所以大家一定要搞清楚,要不就容易出问题

AspectRatio 的宽高比其实和 ConstraintLayout 约束布局的 view 宽高比一样,搞不懂的可以去参考那个


Stack 堆叠布局

Stack 和 Android 的 FrameLayout 很像,并且另外有些扩展。Flutter 里面只要有 weight 重叠的布局基本都要用 Stack,当然用其他更多的 weight 组合也能实现 Stack 的效果,但是想要最简单的实现 weigjt 的堆叠还是要用 Stack 的

Stack 属性如下:

  Stack({
    Key key,
    this.alignment = AlignmentDirectional.topStart,
    this.textDirection,
    this.fit = StackFit.loose,
    this.overflow = Overflow.clip,
    List<Widget> children = const <Widget>[],
  }) 
  • alignment - 是 Stack 里面所有子 view 的对齐方式,只有被 Positioned 包裹的子 view 除外,一般多用在 Stack 只有 2个子 view 时,超过2个子 view 就要考虑 Positioned 了
  • textDirection - 子view 的排列顺序,TextDirection.ltr- 从左往右,TextDirection.rtl-从右往左
  • overflow - 超出部分的处理方式,有 Overflow.clipOverflow.visible
  • fit - Stack 大小的判定,StackFit.loose- Stack 大小取子view 的最大值;StackFit.expand- Stack 大小占父控件的最大值;StackFit.passthrough- 这个不知道啥意思。Positioned 包裹的子 view 不再考虑之列
  • Positioned - 用于包裹子 view,让该子 view 脱离 Stack alignment 属性的约束,内部使用具体是指指定子view 到 Stack 4个边距的距离
class AA extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: AlignmentDirectional(0, 0),
      fit: StackFit.loose,
      children: <Widget>[
        Container(
          color: Colors.blueAccent,
          width: 200,
          height: 200,
        ),
        Container(
          color: Colors.red,
          child: Text("AAAAAA"),
        ),
        Positioned(
          left: 20,
          bottom: 20,
          child: Container(
            color: Colors.yellowAccent,
            child: Text("BBBBBB"),
          ),
        ),
      ],
    );
  }
}


IndexedStack

和 stack 一样,区别是可以控制显示 stack 里面的第几个子 view,就是通过index这个属性操作的,特点是虽然只能显示一个子 view,但是整个 stack 的大小占位并不会因为 index 的变化而改变

class AA extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      mainAxisSize: MainAxisSize.max,
      children: <Widget>[
        Container(
          height: 50,
          color: Colors.deepOrange,
        ),
        IndexedStack(
          alignment: AlignmentDirectional(0, 0),
          index: 0,
          children: <Widget>[
            Container(
              color: Colors.blueAccent,
              width: 200,
              height: 200,
            ),
            Container(
              color: Colors.red,
              child: Text("AAAAAA"),
            ),
            Positioned(
              left: 20,
              bottom: 20,
              child: Container(
                color: Colors.yellowAccent,
                child: Text("BBBBBB"),
              ),
            ),
          ],
        ),
      ],
    );
  }
}

index = 0 时

index = 1 时,statck 占位大小并发不会变化


Baseline 文字对齐

Baseline 作为一个容器存在,重点在于向 row 这样得布局 weight 里对齐子 view,Baseline控件布局行为分为两种情况:

  • 如果child有baseline,则根据child的baseline属性,调整child的位置
  • 如果child没有baseline,则根据child的bottom,来调整child的位置
new Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
  children: <Widget>[
    new Baseline(
      baseline: 50.0,
      baselineType: TextBaseline.alphabetic,
      child: new Text(
        'TjTjTj',
        style: new TextStyle(
          fontSize: 20.0,
          textBaseline: TextBaseline.alphabetic,
        ),
      ),
    ),
    new Baseline(
      baseline: 50.0,
      baselineType: TextBaseline.alphabetic,
      child: new Container(
        width: 30.0,
        height: 30.0,
        color: Colors.red,
      ),
    ),
    new Baseline(
      baseline: 50.0,
      baselineType: TextBaseline.alphabetic,
      child: new Text(
        'RyRyRy',
        style: new TextStyle(
          fontSize: 35.0,
          textBaseline: TextBaseline.alphabetic,
        ),
      ),
    ),
  ],
)


FractionallySizedBox 宽高倍数容器

FractionallySizedBox 可以让期中包裹得子 view 得宽高是外层父容器宽高得倍数,我不用知道有什么常用会用到这个,但是要小心,一旦外层父容器没有使用具体是指指定宽高大小,使用 FractionallySizedBox 得话很容易就占满全屏了。另外 FractionallySizedBox 允许子 view 超出父容器

FractionallySizedBox 依靠其中得 widthFactor、heightFactor 设置宽高比例倍数,当设置 null 的时候会使用其中包裹得子 view 得具体宽高值,若是子 view 提供不了宽高值,那么侯就会占满父容器

class BB extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.topLeft,
      width: 200,
      height: 200,
      color: Colors.blueAccent,
      child: FractionallySizedBox(
        widthFactor: 1.5,
        heightFactor: 0.5,
        child: Container(
          width: 100,
          height: 100,
          color: Colors.yellow,
        ),
      ),
    );
  }
}

使用具体宽高倍数时,蓝色是父容器

宽高倍数设置为`null`时

这是其他人得演示图


Offstage

Offstage 可以控制 view 的显示与否,view 不显示的话是不占位的,但是若是动画的话,view 在不显示的同时要手动停止动画

class CC extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return CCState();
  }
}

class CCState extends State<CC> {
  var isCanShow = true;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        padding: EdgeInsets.all(10),
        color: Colors.red,
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            Offstage(
              offstage: !isCanShow,
              child: Container(
                width: 100,
                height: 50,
                color: Colors.lightBlueAccent,
              ),
            ),
            Container(
              width: 100,
              height: 50,
              color: Colors.yellowAccent,
              child: RaisedButton(
                  child: Text("AAAAAA"),
                  onPressed: () {
                    setState(() {
                      isCanShow = !isCanShow;
                    });
                  }),
            ),
          ],
        ),
      ),
    );
  }
}


OverflowBox

子 view 可以超出父控件

const OverflowBox({
    Key key,
    this.alignment = Alignment.center,
    this.minWidth,
    this.maxWidth,
    this.minHeight,
    this.maxHeight,
    Widget child,
  })

OverflowBox 其实并不像想象的那么好,OverflowBox 要是不设置最大,最小宽高,仅仅靠子 view 确定宽高的话是没有超过父控件的效果的,并且子 view 超过父控件还不能随心所遇,必须是子 view 宽高有一个比父控件大才行

  • 子 view 的宽高比 OverflowBox 限定的大的话,子 view 的宽高受OverflowBox 限定限制
class DD extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.topLeft,
      width: 200,
      height: 200,
      color: Colors.redAccent,
      child: OverflowBox(
//        maxWidth: 300,
        alignment: Alignment.topLeft,
        child: Container(
          width: 300,
          height: 100,
          color: Colors.blueAccent,
        ),
      ),
    );
  }
}

view 宽高有一个比父控件大才能超出父控件

OverflowBox 不设置宽高限制子 view 就不能超过父控件,即便子 view 的宽高更大


Flexible、Expanded

FlexibleExpanded 权重比 Weight,一般配合 row、column 使用,他俩的区别是 Expanded 默认会占据父控件的全部控件,Flexible 不会,并且 ExpandedFlexible 的子类,一般 Expanded 使用的最多,他俩通过设置flex来设置权重比,和 android 的线性布局的权重比一样

class FF extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Row(
      children: <Widget>[
        Expanded(
          child: new Container(
            alignment: Alignment(0, 0),
            color: Colors.redAccent,
            height: 80,
            child: Text(
              "AAAAA",
              style: TextStyle(fontSize: 30),
            ),
          ),
          flex: 2,
        ),
        Expanded(
          child: new Container(
            alignment: Alignment(0, 0),
            color: Colors.blueAccent,
            height: 80,
            child: Text(
              "BBBBB",
              style: TextStyle(fontSize: 30),
            ),
          ),
          flex: 3,
        ),
      ],
    );
  }
}

Chip

Chip - 简单的小标签,就是文本可以添加左边右边图标,参考理解 android 的 textview

属性如下:

Chip({
    this.avatar,//左侧小图标
    this.label,//中间文版
    this.labelStyle,
    this.labelPadding,
    this.deleteIcon//删除图标
    this.onDeleted//删除回调,为空时不显示删除图标,
    this.deleteIconColor//删除图标的颜色
    this.shape//形状
    this.backgroundColor//背景颜色
    this.padding
  })

class EE extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      width: 200,
      alignment: Alignment.center,
      child: Chip(
          avatar: Icon(Icons.access_alarm),
          padding: EdgeInsets.all(10),
          label: Text(
            "AAA",
            style: TextStyle(fontSize: 20),
          ),
          backgroundColor: Colors.blueAccent,
          deleteIcon: Icon(Icons.clear),
          deleteIconColor: Colors.white,
          elevation: 10,
          onDeleted: () {
            print("AAA");
          }),
    );
  }
}

注意:不设置删除事件,右侧图标不会显示的,因为没用呀~

Chip 是典型的应用就是配合 warp


Divider

Divider 大家在熟悉不过啦,分割线嘛

属性如下:

  const Divider({
    this.height, //距 top 的距离
    this.indent, //距 left 的距离
    this.endIndent, //距 right 的距离
    this.color, 
  })

注意:若是居中布局的话,height 是不起作用的

class EE extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Divider(
      endIndent: 100,
      indent: 100,
      height: 100,
      color: Colors.blueAccent,
    );
  }
}