Flutter 学习记3 - Widget 框架

通过 widgets 构建 UI,描述当前的配置和状态,当状态改变时,框架找出前后的变化,以确定底层 Render Tree 要做的最小更改,在内部变成另一个状态来更新外面的 Widget Tree。

一个最简单的 Flutter 程序就是在入口方法 main 中调用 runApp,这个函数将参数的 Widget 作为整个应用的 Widget Tree 的根 Widget,将这个根 Widget 完全覆盖到屏幕上。

Widget 分两种:StatelessWidgetStatefulWidget,主要工作就是实现 build 方法,用于描述更低级的 Widget。

基础的 Widget

  • Text
  • Row, Column: flex 布局,类似 Web 的 flexbox,Android 的 FlexboxLayout
  • Stack:堆叠,类似 Web 的 absolute,也像 Android 的 RelativeLayout,内部使用 Positioned 并控制其在 Stack 内的上下左右的边距
  • Container: 创建一个矩形可见元素,可以通过 BoxDecoration 来做样式,如背景,边框,阴影。可以设置 margin,padding 或者尺寸 size。可以通过矩阵转换变成三维的。

下面是一个示例代码,首先 pubspec.yaml 要配置

flutter:
  uses-material-design: true

新创建的工程默认就是这个配置。

import 'package:flutter/material.dart';

class MyAppBar extends StatelessWidget {
  MyAppBar({this.title});

  // Fields in a Widget subclass are always marked "final".

  final Widget title;

  @override
  Widget build(BuildContext context) {
    return new Container(
      height: 56.0, // in logical pixels
      padding: const EdgeInsets.symmetric(horizontal: 8.0),
      decoration: new BoxDecoration(color: Colors.blue[500]),
      // Row is a horizontal, linear layout.
      child: new Row(
        // <Widget> is the type of items in the list.
        children: <Widget>[
          new IconButton(
            icon: new Icon(Icons.menu),
            tooltip: 'Navigation menu',
            onPressed: null, // null disables the button
          ),
          // Expanded expands its child to fill the available space.
          new Expanded(
            child: title,
          ),
          new IconButton(
            icon: new Icon(Icons.search),
            tooltip: 'Search',
            onPressed: null,
          ),
        ],
      ),
    );
  }
}

class MyScaffold extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Material is a conceptual piece of paper on which the UI appears.
    return new Material(
      // Column is a vertical, linear layout.
      child: new Column(
        children: <Widget>[
          new MyAppBar(
            title: new Text(
              'Example title',
              style: Theme.of(context).primaryTextTheme.title,
            ),
          ),
          new Expanded(
            child: new Center(
              child: new Text('Hello, world!'),
            ),
          ),
        ],
      ),
    );
  }
}

void main() {
  runApp(new MaterialApp(
    title: 'My app', // used by the OS task switcher
    home: new MyScaffold(),
  ));
}

现在还不知道怎么加按钮,将上面的代码放到一个新建的 newroute.dart 文件中,去掉 main 方法,在 main.dart 的 AppBar 上添加一个按钮

new IconButton(icon: new Icon(Icons.add), onPressed: _toOther),
void _toOther() {
  Navigator.of(context).push(
    new MaterialPageRoute(
        builder: (context) {
          return new MyScaffold();
        })
  );
}

要使用另一个文件的 MyScaffold 类,需要 import

import 'newroute.dart';

结果成功跳转。

由于语法还不懂,先简单的分析下:

  1. MyScaffoldStatelessWidget,它的 build 方法返回一个 Material,这就是页面显示的内容。
  2. Material 里只有一个 child,就是一个 Column,一个垂直的线性布局。包含一个 MyAppBarExpanded
  3. ExpandedColumeRowFlex 的 child,意思就占据所有剩下的空间,如果多个 child 都是 Expanded,通过 flex 来指定各自的比例,有些类似 Android 的 weight
  4. MyAppBar 是一个 StatelessWidget,内部是 Container,然后设置它的高度,内边距。child 是 Row,和 Column 类似,只是水平的。左右各一个 IconButton

手势处理

class MyButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new GestureDetector(
      onTap: () {
        print('MyButton was tapped!');
        Navigator.of(context).pop(this);
      },
      child: new Container(
        height: 36.0,
        padding: const EdgeInsets.all(8.0),
        margin: const EdgeInsets.symmetric(horizontal: 8.0),
        decoration: new BoxDecoration(
          borderRadius: new BorderRadius.circular(5.0),
          color: Colors.lightGreen[500],
        ),
        child: new Center(
          child: new Text('Engage'),
        ),
      ),
    );
  }
}

当用户触摸 GestureDector 的 child,即这里的 Container,就会调用 onTapGestureDector 可以发现包括 tap,drag 和 scales 事件。

onTap 里让页面返回,效果如下:

IMB_9pMJ4C.GIF

交互

class CounterDisplay extends StatelessWidget {
  CounterDisplay({this.count});

  final int count;

  @override
  Widget build(BuildContext context) {
    return new Text('Count: $count');
  }
}

class CounterIncrementor extends StatelessWidget {
  CounterIncrementor({this.onPressed});

  final VoidCallback onPressed;

  @override
  Widget build(BuildContext context) {
    return new RaisedButton(
      onPressed: onPressed,
      child: new Text('Increment'),
    );
  }
}

class Counter extends StatefulWidget {
  @override
  _CounterState createState() => new _CounterState();
}

class _CounterState extends State<Counter> {
  int _counter = 0;

  void _increment() {
    setState(() {
      ++_counter;
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Row(children: <Widget>[
      new CounterIncrementor(onPressed: _increment),
      new CounterDisplay(count: _counter),
      new RaisedButton(
        onPressed: () {
          Navigator.of(context).pop(this);
        },
        child: new Text('返回'),
      ),
    ]);
  }
}

在前一篇里已经遇到了 StatefulWidget,首先 CountercreateState() 返回一个 _CounterState 实例。

看 CounterIncrementor 里的 CounterIncrementor({this.onPressed});,这一句应该是构造方法,在 _CounterState 里调用是 new CounterIncrementor(onPressed: _increment),因此应该是用 _increment 这个函数赋给 CounterIncrementoronPressed,类型是 VoidCallback

/// Signature of callbacks that have no arguments and return no data. typedef void VoidCallback();

然后当用户点击按压 RaisedButton 时,调用 onPressed 也就是 _increment,将 _counter 加 1,setState 会引起再次 build,于是 CounterDisplay 里的 Text 内容也就变了。

IMB_4rbGFZ.GIF

综合例子

// 一个产品类,一个 name 属性
class Product {
  const Product({this.name});
  final String name;
}

// 这个看后面代码,代表的是一个函数,有点像 C 语言的函数声明,但又不一样,不清楚这个语法
typedef void CartChangedCallback(Product product, bool inCart);

// 一个 StatelessWidget
class ShoppingListItem extends StatelessWidget {
  // 这个构造三个参数,但 product 好像是后面的 product = product 来赋值
  // this.inCart 应该就是传入的参数就覆盖自己的属性 inCart
  ShoppingListItem({Product product, this.inCart, this.onCartChanged})
      : product = product,
        super(key: new ObjectKey(product));

  final Product product;
  final bool inCart;
  final CartChangedCallback onCartChanged; 

  Color _getColor(BuildContext context) {
    // 根据布尔值返回不同的颜色
    return inCart ? Colors.black54 : Theme.of(context).primaryColor;
  }

  TextStyle _getTextStyle(BuildContext context) {
    if (!inCart) return null;

    return new TextStyle(
      color: Colors.black54,
      decoration: TextDecoration.lineThrough,
    );
  }

  @override
  Widget build(BuildContext context) {
    return new ListTile(
      onTap: () {
        // 触摸的时候,inCart 取反,然后回调外界传入的 onCartChanged
        onCartChanged(product, !inCart);
      },
      leading: new CircleAvatar( // 不太清楚干什么的
        backgroundColor: _getColor(context), // 调上面的方法返回不同的背景色
        child: new Text(product.name[0]),
      ),
      title: new Text(product.name, style: _getTextStyle(context)), // 这个 item 的文字
    );
  }
}

这是一个列表的 item,onCartChanged 应该就是一种回调函数,由父 Widget 传入,函数被调用后,父 Widget 更新内部的 State,然后导致利用这个新的 inCart重新创建一个 ShoppingListItem 实例。然后看下这句牛逼哄哄的话:

Although the parent creates a new instance of ShoppingListItem when it rebuilds, that operation is cheap because the framework compares the newly built widgets with the previously built widgets and applies only the differences to the underlying RenderObject.

class ShoppingList extends StatefulWidget {
  ShoppingList({Key key, this.products}) : super(key: key);

  final List<Product> products;

  @override
  _ShoppingListState createState() => new _ShoppingListState();
}

class _ShoppingListState extends State<ShoppingList> {
  Set<Product> _shoppingCart = new Set<Product>();

  // 传给 item 的回调函数,setState 去重新 build
  void _handleCartChanged(Product product, bool inCart) {
    setState(() {
       if (inCart)
        _shoppingCart.add(product);
      else
        _shoppingCart.remove(product);
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Shopping List'),
      ),
      body: new ListView(
        padding: new EdgeInsets.symmetric(vertical: 8.0),
        children: widget.products.map((Product product) {
          return new ShoppingListItem(
            product: product,
            inCart: _shoppingCart.contains(product),
            onCartChanged: _handleCartChanged,
          );
        }).toList(),
      ),
    );
  }
}

// void main() {
//   runApp(new MaterialApp(
//     title: 'Shopping App',
//     home: new ShoppingList(
//       products: <Product>[
//         new Product(name: 'Eggs'),
//         new Product(name: 'Flour'),
//         new Product(name: 'Chocolate chips'),
//       ],
//     ),
//   ));
}

在前面的页面加个按钮跳过来

new RaisedButton(
    onPressed: () {
        Navigator.of(context).push(
            new MaterialPageRoute(
                builder: (context) {
                new ShoppingList(
                    products: <Product>[
                        new Product(name: 'Eggs'),
                        new Product(name: 'Flour'),
                        new Product(name: 'Chocolate chips'),
                    ],
                );
            })  
        );
    },
    ...
)

如果 ShoppingList 的父 Widget 重新构建一个 ShoppingList,但是不会再次调用 createState() 去重新创建一个 _ShoppingListState,而是会复用已经存在的 State 实例。但它会根据新的属性值重新构建,相当于 _ShoppingListState 实例对象只有一个,但是在重新创建 ShoppingList 时会再次调用 State 的 build 方法。嗯,应该是这样。

IMB_OyXeZQ.gif

参考:A Tour of the Flutter Widget Framework

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏生信技能树

两个神奇的R包介绍,外加实用小抄

认识Tidy Data1.Reshape Data2.Handle Missing Values3.Expand Tables4.split cells一、测...

1844
来自专栏达摩兵的技术空间

break与continue跳出的理解

如果你还缺乏对break与continue断点跳出循环的正确理解,请复制粘贴以下代码,思考得出答案(代码涉及标签语句的用法,如果不会的请自行百度)。

992
来自专栏Java帮帮-微信公众号-技术文章全总结

【选择题】Java基础测试题一(10道)

【选择题】Java基础测试题一(10道) 1.下面哪些是合法的变量名? (DEG) A.2variable //不能以数字开头 ...

4648
来自专栏Web行业观察

一句话判断IE浏览器

if(window.addEventListener){ alert("not ie"); }else if(window.attachEvent){ ...

1533
来自专栏一枝花算不算浪漫

[Java拾遗一] XML的书写规范与解析.

48920
来自专栏小狼的世界

Javascript设计模式学习(一)封装和信息隐藏

在我们编程的过程中,我们应该尽可能的把数据和函数处理信息隐藏在对象内部,在Javascript中,我们怎样来做呢?

1024
来自专栏iOSer成长记录

OpenGL ES(二) 三角形

1523
来自专栏前端说吧

【本周面试题】第2周 - 看上去和实际上的代码执行顺序

隐形考点,while小括号内部,会进行隐式转换,将其他类型的值转为Boolean布尔值类型的进行判断

892
来自专栏SeanCheney的专栏

《利用Python进行数据分析·第2版》第6章 数据加载、存储与文件格式6.1 读写文本格式的数据6.2 二进制数据格式6.3 Web APIs交互6.4 数据库交互6.5 总结

访问数据是使用本书所介绍的这些工具的第一步。我会着重介绍pandas的数据输入与输出,虽然别的库中也有不少以此为目的的工具。 输入输出通常可以划分为几个大类:读...

5806
来自专栏空帆船w

Android 编码规范

小驼峰命名(lowerCamelCase):除第一个单词以外,每一个单词的第一个字母大写。

2673

扫码关注云+社区

领取腾讯云代金券