Skip to content

Latest commit

 

History

History
678 lines (524 loc) · 20.2 KB

05. Flutter用户交互 - Layouts1.md

File metadata and controls

678 lines (524 loc) · 20.2 KB

Layouts in Flutter

重点 Widgets类用来构建UI Widgets用来做布局和UI元素 由简单的widget构建复杂widget

Flutter布局的核心机制是widgets. Flutter中, 几乎所有东西都是widget, layout模型也是widget. app上看得到的如images, icons, and text 也是widget. 看不到, 如rows, columns, and grids that arrange, constrain, align 也是widget.

通过组合widgets成复杂的widget来创建布局.

widget tree

图解:

widget tree

Container widget 允许你可以自定义它的子widget. 当想要增加padding, margins, borders, or background color 可以使用Container.

这个例子中, 每个Text widget放在一个Container中来添加margin. 整个Row也放在一个Container中来添加row周围的padding.

这个例子中, 其他的UI由属性控制. 通过color属性设置Icon的颜色. 通过Text.style属性设置字体, 颜色, 粗细等等. Columns 和 rows 属性, 如有vertically或horizontally对齐方式, 子widget占据的空间等等.

Lay out a widget

1. 选择布局widget Select a layout widget

根据可见widget的对齐或约束方式, 选择对应的layout widgets.

下面例子使用 Center, 将内容水平和垂直居中.

2. 创建可见widget Create a visible widget

创建Text widget

Text('Hello World'),

创建Image widget

Image.asset(
  'images/lake.jpg',
  fit: BoxFit.cover,
),

创建Icon widget

Icon(
  Icons.star,
  color: Colors.red[500],
),

3. 将可见widget方法布局widget中 Add the visible widget to the layout widget

所有的layout widget都有以下两种属性之一

  • child属性, 如Center or Container
  • children属性, 如Row, Column, ListView, or Stack
Center(
  child: Text('Hello World'),
),

4. 将layout widget添加到页面 Add the layout widget to the page

Flutter app本身就是一个widget, 大部分widget有一个build()方法. 在这个方法中实例化并返回一个widget来显示这个widget.

Material apps

Material app中, 可以使用 Scaffold widget; 提供一个默认的banner, 背景色, API(adding drawers, snack bars, and bottom sheets). 设置Center widget到body属性来展示主页面.

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter layout demo',
      home: Scaffold(
        appBar: AppBar(
          title: Text('Flutter layout demo'),
        ),
        body: Center(
          child: Text('Hello World'),
        ),
      ),
    );
  }
}
Non-Material apps
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(color: Colors.white),
      child: Center(
        child: Text(
          'Hello World',
          textDirection: TextDirection.ltr,
          style: TextStyle(
            fontSize: 32,
            color: Colors.black87,
          ),
        ),
      ),
    );
  }
}

non-Material app 默认不包含 AppBar, title, or background color. 如果想要有这些特性需要自己构建.

水平和垂直方向布局多个widget Lay out multiple widgets vertically and horizontally

使用Row widget安排水平布局, Column widget安排垂直布局

添加一个children widgets list 到 Row 或 Column widget中. 每个child也可以是一个 row 或 column, 以此类推.

以下layout由一个Row, 包含两个children: 左边的column, 右边的image

widget tree

左边的column内嵌了rows and columns

widget tree

Aligning widgets

使用mainAxisAlignment和crossAxisAlignment属性控制一个row或column中children的对齐方式.

对于row, 主轴是水平的, 交叉轴是垂直的.

widget tree

对于column, 主轴是垂直的, 交叉轴是水平的.

widget tree

下面例子, 3张图片宽度都是100px, 渲染用的盒子(该案例为整个屏幕)超过300px. 设置MainAxisAlignment为spaceEvenly, 将水平的空间按前,中,后平均分配.

widget tree

Row(
  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  children: [
    Image.asset('images/pic1.jpg'),
    Image.asset('images/pic2.jpg'),
    Image.asset('images/pic3.jpg'),
  ],
);

Columns 和 Row 类似. 设置MainAxisAlignment为spaceEvenly, 将水平的空间按上,中,下平均分配.

widget tree

Column(
  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  children: [
    Image.asset('images/pic1.jpg'),
    Image.asset('images/pic2.jpg'),
    Image.asset('images/pic3.jpg'),
  ],
);

Sizing widgets

使用Expanded widget让尺寸适应row或者column. 将图片包在Expanded widget中来修复图片过大的问题.

Row(
  crossAxisAlignment: CrossAxisAlignment.center,
  children: [
    Expanded(
      child: Image.asset('images/pic1.jpg'),
    ),
    Expanded(
      child: Image.asset('images/pic2.jpg'),
    ),
    Expanded(
      child: Image.asset('images/pic3.jpg'),
    ),
  ],
);

Expanded widget的flex属性, 默认值为1. 设置这个值为2, 可以让这个widget占用空间是其他的两倍.

widget tree

Row(
  crossAxisAlignment: CrossAxisAlignment.center,
  children: [
    Expanded(
      child: Image.asset('images/pic1.jpg'),
    ),
    Expanded(
      flex: 2,
      child: Image.asset('images/pic2.jpg'),
    ),
    Expanded(
      child: Image.asset('images/pic3.jpg'),
    ),
  ],
);

Packing widgets

默认情况下, row或column会尽可能占用主轴的空间. 如果想要所有的子widget紧密的打包一起, 设置mainAxisSize为MainAxisSize.min

widget tree

Row(
  mainAxisSize: MainAxisSize.min,
  children: [
    Icon(Icons.star, color: Colors.green[500]),
    Icon(Icons.star, color: Colors.green[500]),
    Icon(Icons.star, color: Colors.green[500]),
    Icon(Icons.star, color: Colors.black),
    Icon(Icons.star, color: Colors.black),
  ],
)

行和列嵌套 Nesting rows and columns

布局框架允许根据需求在行和列中嵌套行和列.

widget tree

红框内容区域可以分为两个Row. 评价Row包含5个星星和评论数. 图标Row包含3个Column, 每一列包含icon和text.

评价Row布局如下:

widget tree

ratings变量创建一个row包含row(包含5个星星图标)和text:

var stars = Row(
  mainAxisSize: MainAxisSize.min,
  children: [
    Icon(Icons.star, color: Colors.green[500]),
    Icon(Icons.star, color: Colors.green[500]),
    Icon(Icons.star, color: Colors.green[500]),
    Icon(Icons.star, color: Colors.black),
    Icon(Icons.star, color: Colors.black),
  ],
);

final ratings = Container(
  padding: EdgeInsets.all(20),
  child: Row(
    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
    children: [
      stars,
      Text(
        '170 Reviews',
        style: TextStyle(
          color: Colors.black,
          fontWeight: FontWeight.w800,
          fontFamily: 'Roboto',
          letterSpacing: 0.5,
          fontSize: 20,
        ),
      ),
    ],
  ),
);

可以通过变量和函数来减少多层嵌套造成的视觉困扰

评分row下面的图标row中包含3个column; 每个column包含一个图标和两行字.

widget tree

final descTextStyle = TextStyle(
  color: Colors.black,
  fontWeight: FontWeight.w800,
  fontFamily: 'Roboto',
  letterSpacing: 0.5,
  fontSize: 18,
  height: 2,
);

// DefaultTextStyle.merge() allows you to create a default text
// style that is inherited by its child and all subsequent children.
// DefaultTextStyle.merge() 允许你创建一个所有子类都可以继承的默认文本样式
final iconList = DefaultTextStyle.merge(
  style: descTextStyle,
  child: Container(
    padding: EdgeInsets.all(20),
    child: Row(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: [
        Column(
          children: [
            Icon(Icons.kitchen, color: Colors.green[500]),
            Text('PREP:'),
            Text('25 min'),
          ],
        ),
        Column(
          children: [
            Icon(Icons.timer, color: Colors.green[500]),
            Text('COOK:'),
            Text('1 hr'),
          ],
        ),
        Column(
          children: [
            Icon(Icons.restaurant, color: Colors.green[500]),
            Text('FEEDS:'),
            Text('4-6'),
          ],
        ),
      ],
    ),
  ),
);

leftColumn变量包含标题, 子标题, 评分row和图标Row.

final leftColumn = Container(
  padding: EdgeInsets.fromLTRB(20, 30, 20, 20),
  child: Column(
    children: [
      titleText,
      subTitle,
      ratings,
      iconList,
    ],
  ),
);

左边的列放在Container中来约束他的宽度. 最后, 装整个row(包含左边的列和右边的图)放在一个Card部件中.

body: Center(
  child: Container(
    margin: EdgeInsets.fromLTRB(0, 40, 0, 30),
    height: 600,
    child: Card(
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Container(
            width: 440,
            child: leftColumn,
          ),
          mainImage,
        ],
      ),
    ),
  ),
),

常用布局部件 Common layout widgets

  • widgets library 标准部件
  • Material library 特殊部件

Standard widgets

  • Container: 给部件添加 padding, margins, borders, background color, 或其他装饰.
  • GridView: 可滚动的格子.
  • ListView: 可滚动的列表
  • Stack: 在其他部件上面叠加部件

Material widgets

  • Card: 将相关信息组织到带圆角和阴影的盒子中
  • ListTile: 组织3行以内的文本, 可选的leading和trailing图标到一行中

Container

Containers用法自由, 可以通过padding, borders 或 margins来分隔不同部件. 可以将一个layout部件放到整个Container中, 然后改变颜色或图片来作为背景.

image

总结
  • 添加 padding, margins, borders
  • 改变背景色或图片
  • 包含一个子部件(widget), 子部件可以是Row, Column,或部件树的根.
案例

一个column中包含两row. Container用来改变背景色

image

Widget _buildImageColumn() => Container(
      decoration: BoxDecoration(
        color: Colors.black26,
      ),
      child: Column(
        children: [
          _buildImageRow(1),
          _buildImageRow(3),
        ],
      ),
    );

Container也可以用来设置圆角, 边框

Widget _buildDecoratedImage(int imageIndex) => Expanded(
      child: Container(
        decoration: BoxDecoration(
          border: Border.all(width: 10, color: Colors.black38),
          borderRadius: const BorderRadius.all(const Radius.circular(8)),
        ),
        margin: const EdgeInsets.all(4),
        child: Image.asset('images/pic$imageIndex.jpg'),
      ),
    );

Widget _buildImageRow(int imageIndex) => Row(
      children: [
        _buildDecoratedImage(imageIndex),
        _buildDecoratedImage(imageIndex + 1),
      ],
    );

GridView

二维列表布局. GridView默认提供两个列表,也可以自定义网格. GridView检测到内容太长, 会自动设置滚动.

总结
  • 网格布局
  • 监听column内容何时超出盒子, 可以自动滚动
  • 自定义网格, 或者使用提供好的网格
    • GridView.count 指定column数
    • GridView.extent 指定每一个tile的最大像素
案例
  • 使用GridView.extent创建一个grid, 并设置最大像素为150

image

Widget _buildGrid() => GridView.extent(
    maxCrossAxisExtent: 150,
    padding: const EdgeInsets.all(4),
    mainAxisSpacing: 4,
    crossAxisSpacing: 4,
    children: _buildGridTileList(30));

// The images are saved with names pic0.jpg, pic1.jpg...pic29.jpg.
// The List.generate() constructor allows an easy way to create
// a list when objects have a predictable naming pattern.
List<Container> _buildGridTileList(int count) => List.generate(
    count, (i) => Container(child: Image.asset('images/pic$i.jpg')));
  • 使用GridView.count创建一个grid, 设置垂直模式下宽度为2个tile, 水平模式下3个tile.

image

grid_list_demo.dart

ListView

ListView组件列, 当内容超出盒子会自动可滚动.

总结
  • 特殊的Column, 用于组织盒子列表
  • 可以水平或垂直布局
  • 自动检测内容可滚动化
  • 易用, 支持滚动, 相对Column没那么可配置
案例

image

使用ListView展示一列ListTile. 使用Divider作分隔线

Widget _buildList() => ListView(
      children: [
        _tile('CineArts at the Empire', '85 W Portal Ave', Icons.theaters),
        _tile('The Castro Theater', '429 Castro St', Icons.theaters),
        _tile('Alamo Drafthouse Cinema', '2550 Mission St', Icons.theaters),
        _tile('Roxie Theater', '3117 16th St', Icons.theaters),
        _tile('United Artists Stonestown Twin', '501 Buckingham Way',
            Icons.theaters),
        _tile('AMC Metreon 16', '135 4th St #3000', Icons.theaters),
        Divider(),
        _tile('Kescaped_code#39;s Kitchen', '757 Monterey Blvd', Icons.restaurant),
        _tile('Emmyescaped_code#39;s Restaurant', '1923 Ocean Ave', Icons.restaurant),
        _tile(
            'Chaiya Thai Restaurant', '272 Claremont Blvd', Icons.restaurant),
        _tile('La Ciccia', '291 30th St', Icons.restaurant),
      ],
    );

ListTile _tile(String title, String subtitle, IconData icon) => ListTile(
      title: Text(title,
          style: TextStyle(
            fontWeight: FontWeight.w500,
            fontSize: 20,
          )),
      subtitle: Text(subtitle),
      leading: Icon(
        icon,
        color: Colors.blue[500],
      ),
    );

image

使用ListView展示Material Design palette的颜色

colors_demo

Stack

使用Stack将一个widget部件(通常是图片)放在基础部件widget上面. 可以完全或部分覆盖基础的部件.

总结
  • 用于一个部件覆盖另一个部件
  • 子部件列表中的第一个widget为基础部件. 随后的部件会覆盖在该基础部件的上面.
  • Stack的内容无法滚动
  • 可以裁剪超出盒子的子部件
案例

image

使用stack叠加一个Container

Widget _buildStack() => Stack(
    alignment: const Alignment(0.6, 0.6),
    children: [
      CircleAvatar(
        backgroundImage: AssetImage('images/pic.jpg'),
        radius: 100,
      ),
      Container(
        decoration: BoxDecoration(
          color: Colors.black45,
        ),
        child: Text(
          'Mia B',
          style: TextStyle(
            fontSize: 20,
            fontWeight: FontWeight.bold,
            color: Colors.white,
          ),
        ),
      ),
    ],
  );

image

使用Stack进行梯度叠加到图片上, 确保toolbar的icon和图片区分开来

contacts_demo

Card

Card, 属于Material库, 包含许多有价值的信息, 可以由任何widget组成, 但是通常使用ListTile. Card只有一个子部件, 但子部件可以是column, row, list, grid,或其他支持多子部件的部件. 默认Card会收缩尺寸为0x0像素. 可以使用SizedBox来约束它的尺寸.

Card通过轻微圆角, 带阴影来提现3D效果. 修改elevation属性来控制阴影效果.

总结
  • Implements a Material card
  • Used for presenting related nuggets of information
  • Accepts a single child, but that child can be a Row, Column, or other widget that holds a list of children
  • Displayed with rounded corners and a drop shadow
  • A Card’s content can’t scroll
  • From the Material library
案例

image

Card中包含3个ListTiles, 然后包裹在SizedBox中设置尺寸. Divider设置分割线.

Widget _buildCard() => SizedBox(
    height: 210,
    child: Card(
      child: Column(
        children: [
          ListTile(
            title: Text('1625 Main Street',
                style: TextStyle(fontWeight: FontWeight.w500)),
            subtitle: Text('My City, CA 99984'),
            leading: Icon(
              Icons.restaurant_menu,
              color: Colors.blue[500],
            ),
          ),
          Divider(),
          ListTile(
            title: Text('(408) 555-1212',
                style: TextStyle(fontWeight: FontWeight.w500)),
            leading: Icon(
              Icons.contact_phone,
              color: Colors.blue[500],
            ),
          ),
          ListTile(
            title: Text('[email protected]'),
            leading: Icon(
              Icons.contact_mail,
              color: Colors.blue[500],
            ),
          ),
        ],
      ),
    ),
  );

image

Card中包含图片和文本

cards_demo

ListTile

Material库中特殊的行部件.最多3行文本, 可选的左边/右边的图标. 通常用在Card 或 ListView中, 也可以用在其他地方.

总结

A specialized row that contains up to 3 lines of text and optional icons Less configurable than Row, but easier to use From the Material library

案例

A Card containing 3 ListTiles.

image

Uses ListTile to list 3 drop down button types.

image

button_demo