美文网首页FlutterFlutter圈子Dart
【Flutter实战】定位装饰权重组件及柱状图案例

【Flutter实战】定位装饰权重组件及柱状图案例

作者: 老孟程序员 | 来源:发表于2020-06-22 20:58 被阅读0次

    老孟导读:Flutter中有这么一类组件,用于定位、装饰、控制子组件,比如 Container (定位、装饰)、Expanded (扩展)、SizedBox (固定尺寸)、AspectRatio (宽高比)、FractionallySizedBox (占父组件比例)。这些组件的使用频率非常高,下面一一介绍,最后给出项目中实际案例熟悉其用法。
    【Flutter实战】系列文章地址:http://laomengit.com/guide/introduction/mobile_system.html

    Container

    Container 是最常用的组件之一,它是单容器类组件,即仅能包含一个子组件,用于装饰和定位子组件,例如设置背景颜色、形状等。

    最简单的用法如下:

    Container(
        child: Text('老孟'),
     )
    

    子组件不会发生任何外观上的变化:


    设置背景颜色:

    Container(
        color: Colors.blue,
        child: Text('老孟'),
    )
    

    设置内边距( padding ) 和 外边距( margin )

    Container(
          color: Colors.blue,
          child: Container(
            margin: EdgeInsets.all(10),
            padding: EdgeInsets.all(20),
            color: Colors.red,
            child: Text('老孟'),
          ),
        )
    

    效果如下:


    decoration 属性设置子组件的背景颜色、形状等。设置背景为圆形,颜色为蓝色:

    Container(
      child: Text('老孟,专注分享Flutter技术及应用'),
      decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.blue),
    )
    

    默认情况下,圆形的直径等于 Container 窄边长度,相当于在矩形内绘制内切圆。

    上面的情况明显不是我们希望看到了,希望背景是圆角矩形:

    Container(
            child: Text('老孟,专注分享Flutter技术及应用'),
            padding: EdgeInsets.symmetric(horizontal: 10),
            decoration: BoxDecoration(
                shape: BoxShape.rectangle,
                borderRadius: BorderRadius.all(Radius.circular(20)),
                color: Colors.blue),
          )
    

    除了背景我们可以设置边框效果,代码如下:

    Container(
            child: Text('老孟,专注分享Flutter技术及应用'),
            padding: EdgeInsets.symmetric(horizontal: 10),
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(12),
              border: Border.all(
                color: Colors.blue,
                width: 2,
              ),
            ),
          )
    

    创建圆角图片和圆形图片:

    Container(
          height: 200,
          width: 200,
          decoration: BoxDecoration(
            image:  DecorationImage(
              image: NetworkImage(
                  'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl-2.jpg'),
              fit: BoxFit.cover,
            ),
            border: Border.all(
              color: Colors.blue,
              width: 2,
            ),
            borderRadius: BorderRadius.circular(12),
          ),
        )
    

    修改其形状为圆形,代码如下:

    Container(
          height: 200,
          width: 200,
          decoration: BoxDecoration(
            image: DecorationImage(
              image: NetworkImage(
                  'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl-2.jpg'),
              fit: BoxFit.cover,
            ),
            border: Border.all(
              color: Colors.blue,
              width: 2,
            ),
            shape: BoxShape.circle,
          ),
        )
    

    设置对齐方式为居中,背景色为蓝色,代码如下:

    Container(
            color: Colors.blue,
            child: Text('老孟,一个有态度的程序员'),
            alignment: Alignment.center,
          )
    

    注意:设置对齐方式后,Container将会充满其父控件,相当于Android中 match_parent

    Alignment 已经封装了常用的位置,

    通过名字就知道其位置,这里要介绍一下其他的位置,比如在距离左上角1/4处:

    Container(
      alignment: Alignment(-.5,-.5),
      child: Text('老孟,专注分享Flutter技术及应用'),
    )
    

    所以这里有一个非常重要的坐标系,Alignment 坐标系如下:

    组件的中心为坐标原点。

    设置固定的宽高属性:

    Container(
            color: Colors.blue,
            child: Text('老孟,专注分享Flutter技术及应用'),
            alignment: Alignment.center,
            height: 60,
            width: 250,
          )
    

    通过 constraints 属性设置最大/小宽、高来确定大小,如果不设置,默认最小宽高是0,最大宽高是无限大(double.infinity),约束width代码如下:

    Container(
            color: Colors.blue,
            child: Text('老孟,专注分享Flutter技术及应用'),
            alignment: Alignment.center,
            constraints: BoxConstraints(
              maxHeight: 100,
              maxWidth: 300,
              minHeight: 100,
              minWidth: 100,
            ),
          )
    

    通过transform可以旋转、平移、缩放Container,旋转代码如下:

    Container(
            color: Colors.blue,
            child: Text('老孟,专注分享Flutter技术及应用'),
            alignment: Alignment.center,
            height: 60,
            width: 250,
            transform: Matrix4.rotationZ(0.5),
          )
    

    注意:Matrix4.rotationZ()参数的单位是弧度而不是角度

    SizedBox

    SizedBox 是具有固定宽高的组件,直接指定具体的宽高,用法如下:

    SizedBox(
            height: 60,
            width: 200,
            child: Container(
              color: Colors.blue,
              alignment: Alignment.center,
              child: Text('老孟,专注分享Flutter技术及应用'),
            ),
          )
    

    设置尺寸无限大,如下:

    SizedBox(
      height: double.infinity,
      width: double.infinity,
      ...
    )
    

    虽然设置了无限大,子控件是否会无限长呢?不,不会,子控件依然会受到父组件的约束,会扩展到父组件的尺寸,还有一个便捷的方式设置此方式:

    SizedBox.expand(
      child: Text('老孟,专注分享Flutter技术及应用'),
    )
    

    SizedBox 可以没有子组件,但仍然会占用空间,所以 SizedBox 非常适合控制2个组件之间的空隙,用法如下:

    Column(
              children: <Widget>[
                Container(height: 30,color: Colors.blue,),
                SizedBox(height: 30,),
                Container(height: 30,color: Colors.red,),
              ],
            )
    

    AspectRatio

    AspectRatio 是固定宽高比的组件,用法如下:

    Container(
            height: 300,
            width: 300,
            color: Colors.blue,
            alignment: Alignment.center,
            child: AspectRatio(
              aspectRatio: 2 / 1,
              child: Container(color: Colors.red,),
            ),
          )
    

    aspectRatio 是宽高比,可以直接写成分数的形式,也可以写成小数的形式,但建议写成分数的形式,可读性更高。效果如下:

    FractionallySizedBox

    FractionallySizedBox 是一个相对父组件尺寸的组件,比如占父组件的70%:

    Container(
      height: 200,
      width: 200,
      color: Colors.blue,
      child: FractionallySizedBox(
        widthFactor: .8,
        heightFactor: .3,
        child: Container(
          color: Colors.red,
        ),
      ),
    )
    

    通过 alignment 参数控制子组件显示的位置,默认为居中,用法如下:

    FractionallySizedBox(
      alignment: Alignment.center,
      ...
    )
    

    权重组件

    ExpandedFlexibleSpacer 都是具有权重属性的组件,可以控制 Row、Column、Flex 的子控件如何布局的组件。

    Flexible 组件可以控制 Row、Column、Flex 的子控件占满父组件,比如,Row 中有3个子组件,两边的宽是100,中间的占满剩余的空间,代码如下:

    Row(
          children: <Widget>[
            Container(
              color: Colors.blue,
              height: 50,
              width: 100,
            ),
            Flexible(
                child: Container(
                  color: Colors.red,
                  height: 50,
                )
            ),
            Container(
              color: Colors.blue,
              height: 50,
              width: 100,
            ),
          ],
        )
    

    还是有3个子组件,第一个占1/6,第二个占2/6,第三个占3/6,代码如下:

    Column(
          children: <Widget>[
            Flexible(
              flex: 1,
              child: Container(
                color: Colors.blue,
                alignment: Alignment.center,
                child: Text('1 Flex/ 6 Total',style: TextStyle(color: Colors.white),),
              ),
            ),
            Flexible(
              flex: 2,
              child: Container(
                color: Colors.red,
                alignment: Alignment.center,
                child: Text('2 Flex/ 6 Total',style: TextStyle(color: Colors.white),),
              ),
            ),
            Flexible(
              flex: 3,
              child: Container(
                color: Colors.green,
                alignment: Alignment.center,
                child: Text('3 Flex/ 6 Total',style: TextStyle(color: Colors.white),),
              ),
            ),
          ],
        )
    

    子组件占比 = 当前子控件 flex / 所有子组件 flex 之和。

    Flexible中 fit 参数表示填满剩余空间的方式,说明如下:

    • tight:必须(强制)填满剩余空间。
    • loose:尽可能大的填满剩余空间,但是可以不填满。

    这2个看上去不是很好理解啊,什么叫尽可能大的填满剩余空间?什么时候填满?看下面的例子:

    Row(
          children: <Widget>[
            Container(
              color: Colors.blue,
              height: 50,
              width: 100,
            ),
            Flexible(
                child: Container(
                  color: Colors.red,
                  height: 50,
                  child: Text('Container',style: TextStyle(color: Colors.white),),
                )
            ),
            Container(
              color: Colors.blue,
              height: 50,
              width: 100,
            ),
          ],
        )
    

    这段代码是在最上面代码的基础上给中间的红色Container添加了Text子控件,此时红色Container就不在充满空间,再给Container添加对齐方式,代码如下:

    Row(
          children: <Widget>[
            Container(
              color: Colors.blue,
              height: 50,
              width: 100,
            ),
            Flexible(
                child: Container(
                  color: Colors.red,
                  height: 50,
                  alignment: Alignment.center,
                  child: Text('Container',style: TextStyle(color: Colors.white),),
                )
            ),
            Container(
              color: Colors.blue,
              height: 50,
              width: 100,
            ),
          ],
        )
    

    此时又填满剩余空间。

    大家是否还记得 Container 组件的大小是如何调整的吗?Container 默认是适配子控件大小的,但当设置对齐方式时 Container 将会填满父组件,因此是否填满剩余空间取决于子组件是否需要填满父组件。

    如果把 Flexible 中子组件由 Container 改为 OutlineButton,代码如下:

    Row(
          children: <Widget>[
            Container(
              color: Colors.blue,
              height: 50,
              width: 100,
            ),
            Flexible(
              child: OutlineButton(
                child: Text('OutlineButton'),
              ),
            ),
            Container(
              color: Colors.blue,
              height: 50,
              width: 100,
            ),
          ],
        )
    

    OutlineButton 正常情况下是不充满父组件的,因此最终的效果应该是不填满剩余空间:


    下面再来介绍另一个权重组件 Expanded ,源代码如下:

    class Expanded extends Flexible {
      /// Creates a widget that expands a child of a [Row], [Column], or [Flex]
      /// so that the child fills the available space along the flex widget's
      /// main axis.
      const Expanded({
        Key key,
        int flex = 1,
        @required Widget child,
      }) : super(key: key, flex: flex, fit: FlexFit.tight, child: child);
    }
    

    Expanded 继承字 Flexible,fit 参数固定为 FlexFit.tight,也就是说 Expanded 必须(强制)填满剩余空间。上面的 OutlineButton 想要充满剩余空间可以直接使用 Expanded :

    Row(
          children: <Widget>[
            Container(
              color: Colors.blue,
              height: 50,
              width: 100,
            ),
            Expanded(
              child: OutlineButton(
                child: Text('OutlineButton'),
              ),
            ),
            Container(
              color: Colors.blue,
              height: 50,
              width: 100,
            ),
          ],
        )
    

    Spacer 也是一个权重组件,源代码如下:

    @override
    Widget build(BuildContext context) {
      return Expanded(
        flex: flex,
        child: const SizedBox.shrink(),
      );
    }
    

    Spacer 的本质也是 Expanded 的实现的,和Expanded的区别是:Expanded 可以设置子控件,而 Spacer 的子控件尺寸是0,因此Spacer适用于撑开 Row、Column、Flex 的子控件的空隙,用法如下:

    Row(
      children: <Widget>[
        Container(width: 100,height: 50,color: Colors.green,),
        Spacer(flex: 2,),
        Container(width: 100,height: 50,color: Colors.blue,),
        Spacer(),
        Container(width: 100,height: 50,color: Colors.red,),
      ],
    )
    

    三个权重组建总结如下

    • Spacer 是通过 Expanded 实现的,Expanded继承自Flexible。
    • 填满剩余空间直接使用Expanded更方便。
    • Spacer 用于撑开 Row、Column、Flex 的子组件的空隙。

    仿 掘金-我 效果

    先看下效果:

    拿到效果图先不要慌 (取出手机拍照发个朋友圈😊),整个列表每一行的布局基本一样,所以先写出一行的效果:

    class _SettingItem extends StatelessWidget {
      const _SettingItem(
          {Key key, this.iconData, this.iconColor, this.title, this.suffix})
          : super(key: key);
    
      final IconData iconData;
      final Color iconColor;
      final String title;
      final Widget suffix;
    
      @override
      Widget build(BuildContext context) {
        return Container(
          height: 45,
          child: Row(
            children: <Widget>[
              SizedBox(
                width: 30,
              ),
              Icon(iconData,color: iconColor,),
              SizedBox(
                width: 30,
              ),
              Expanded(
                child: Text('$title'),
              ),
              suffix,
              SizedBox(
                width: 15,
              ),
            ],
          ),
        );
      }
    }
    

    消息中心和其他行最后的样式不一样,单独封装,带红色背景的组件:

    class _NotificationsText extends StatelessWidget {
      final String text;
    
      const _NotificationsText({Key key, this.text}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Container(
          padding: EdgeInsets.symmetric(horizontal: 10),
          decoration: BoxDecoration(
              shape: BoxShape.rectangle,
              borderRadius: BorderRadius.all(Radius.circular(50)),
              color: Colors.red),
          child: Text(
            '$text',
            style: TextStyle(color: Colors.white),
          ),
        );
      }
    }
    

    灰色后缀组件:

    class _Suffix extends StatelessWidget {
      final String text;
    
      const _Suffix({Key key, this.text}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Text(
          '$text',
          style: TextStyle(color: Colors.grey.withOpacity(.5)),
        );
      }
    }
    

    将这些封装好的组件组合起来:

    class SettingDemo extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Column(
          children: <Widget>[
            _SettingItem(
              iconData: Icons.notifications,
              iconColor: Colors.blue,
              title: '消息中心',
              suffix: _NotificationsText(
                text: '2',
              ),
            ),
            Divider(),
            _SettingItem(
              iconData: Icons.thumb_up,
              iconColor: Colors.green,
              title: '我赞过的',
              suffix: _Suffix(
                text: '121篇',
              ),
            ),
            Divider(),
            _SettingItem(
              iconData: Icons.grade,
              iconColor: Colors.yellow,
              title: '收藏集',
              suffix: _Suffix(
                text: '2个',
              ),
            ),
            Divider(),
            _SettingItem(
              iconData: Icons.shopping_basket,
              iconColor: Colors.yellow,
              title: '已购小册',
              suffix: _Suffix(
                text: '100个',
              ),
            ),
            Divider(),
            _SettingItem(
              iconData: Icons.account_balance_wallet,
              iconColor: Colors.blue,
              title: '我的钱包',
              suffix: _Suffix(
                text: '10万',
              ),
            ),
            Divider(),
            _SettingItem(
              iconData: Icons.location_on,
              iconColor: Colors.grey,
              title: '阅读过的文章',
              suffix: _Suffix(
                text: '1034篇',
              ),
            ),
            Divider(),
            _SettingItem(
              iconData: Icons.local_offer,
              iconColor: Colors.grey,
              title: '标签管理',
              suffix: _Suffix(
                text: '27个',
              ),
            ),
          ],
        );
      }
    }
    

    至此就结束了。

    柱状图

    先来看下效果:

    关于动画部分的内容会在后面的章节具体介绍。这个效果分为3大部分:

    1. 坐标轴,左边和底部黑色直线。
    2. 矩形柱状图。
    3. 动画控制部分。

    坐标轴的实现如下:

    class _Axis extends StatelessWidget {
      final Widget child;
    
      const _Axis({Key key, this.child}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Container(
          decoration: BoxDecoration(
            border: Border(
              left: BorderSide(color: Colors.black, width: 2),
              bottom: BorderSide(color: Colors.black, width: 2),
            ),
          ),
          child: child,
        );
      }
    }
    

    单个柱状图实现:

    class _Cylinder extends StatelessWidget {
      final double height;
      final double width;
      final Color color;
    
      const _Cylinder({Key key, this.height, this.width, this.color})
          : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return AnimatedContainer(
          duration: Duration(seconds: 1),
          height: height,
          width: width,
          color: color,
        );
      }
    }
    

    生成多个柱状图:

    final double _width = 20.0;
    List<double> _heightList = [60.0, 80.0, 100.0, 120.0, 140.0];
    
    Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        crossAxisAlignment: CrossAxisAlignment.end,
        children: List.generate(_heightList.length, (index) {
          return _Cylinder(
            height: _heightList[index],
            width: _width,
            color: Colors.primaries[index % Colors.primaries.length],
          );
        }))
    

    将此合并,然后更改每一个柱状图的高度:

    class CylinderChart extends StatefulWidget {
      @override
      _CylinderChartState createState() => _CylinderChartState();
    }
    
    class _CylinderChartState extends State<CylinderChart> {
      final double _width = 20.0;
      List<double> _heightList = [60.0, 80.0, 100.0, 120.0, 140.0];
    
      @override
      Widget build(BuildContext context) {
        return Center(
          child: Container(
            height: 200,
            width: 250,
            child: Stack(
              children: <Widget>[
                _Axis(),
                Positioned.fill(
                  left: 5,
                  right: 5,
                  child: Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      crossAxisAlignment: CrossAxisAlignment.end,
                      children: List.generate(_heightList.length, (index) {
                        return _Cylinder(
                          height: _heightList[index],
                          width: _width,
                          color: Colors.primaries[index % Colors.primaries.length],
                        );
                      })),
                ),
                Positioned(
                  top: 0,
                  left: 30,
                  child: OutlineButton(
                    child: Text('反转'),
                    onPressed: () {
                      setState(() {
                        _heightList = _heightList.reversed.toList();
                      });
                    },
                  ),
                )
              ],
            ),
          ),
        );
      }
    }
    

    搞定。

    交流

    老孟Flutter博客地址(330个控件用法):http://laomengit.com

    欢迎加入Flutter交流群(微信:laomengit)、关注公众号【老孟Flutter】:

    相关文章

      网友评论

        本文标题:【Flutter实战】定位装饰权重组件及柱状图案例

        本文链接:https://www.haomeiwen.com/subject/nziffktx.html