[译]Flutter for Android Developers - Views

4,450 阅读11分钟

先说两句

关于Flutter就不多介绍了,它同样是一个致力于开发跨平台移动应用的SDK,使用Dart语言。Dart是Google家的语言,虽然比较低调,但也确实不算难用,学起来也不难。Flutter的实现参考了不少RN的思路。更多关于Flutter可参阅官方网站

前几天Google发布了Dart2了,并且听说Google内部部分应用早已经转用Flutter实现。我这琢磨着Flutter是不是得开始把玩一下了。于是开始一顿瞎操作。过程中在官网发现了这篇结合Android对比Flutter的文档,对之前做Android的同学挺有帮助的,于是决定译之并重新整理为一个系列,也算是一个小结。这系列的文章更适合对Dart和Flutter已经有所了解的Android Developers。

华丽分割线后,这系列文章正式开始


View在Flutter中等价于什么

  • in Android
    1. View是屏幕上可见的基础元素,我们屏幕上的Button,Toolbars等等,每个东西都是一个View。
    2. 系统可以修改整个View的层级结构中的任意一个View。
    3. 一个View被画完之后它不会重绘,除非invalidate方法被调用。
  • in Flutter
    1. View等价于Widget。这是Flutter中的一个概念。
    2. Widget是不可修改的,以至于Widget变得非常的轻量。
    3. Widget只维持一帧,每一帧Flutter框架都会重新创建一个由Widget实例组成的树。

怎么更新Widgets

  • in Android

    • 我们可以直接修改View的属性来更新它们。
  • in Flutter

    • Widget是不可修改的,我们没法直接去更新它们,取而代之我们可以用Widget的State来实现更新。

在Flutter中Widget分为两个类型:

1.StatelessWidget 一个StatelessWidget没有任何状态信息。当你正在描述的界面元素不依赖于任何除了自身对象内的配置外的其他东西时,StatelessWidgets就刚好派上用场。 比如在Android中我们将logo用一个ImageView来展示。这个logo在运行的过程中将不会再改变,因此放到Flutter中的话,我们将用一个StatelessWidget来实现它。

2.StatefulWidget 如果你想在运行的过程中动态的改变界面,比如在想网络请求了数据之后,或者应用与用户发生了一系列交互之后。这个时候就必须要使用带有状态信息的StatefulWidget,它可以告知Flutter框架Widget的状态已经更新进而促使Flutter框架去更新Widget。

Note: StatelessWidget和StatefulWidget的核心逻辑是相同的,就是他们在每一帧都被rebuild,不同的是StatefulWidget有一个State对象来保存状态信息,然后在帧与帧之间它可以通过这个State对象来恢复之前保存的状态信息。

如果你还比较疑惑,那么可以简单记下这个规则:如果用户会与一个Widget交互,那么这个Widget就用StatefulWidget。如果一个Widget响应了一个交互事件,但是只要包含它的Parent Widget没有响应这个交互事件的话,它的Parent Widget依然是StatelessWidget。

接下来我们看下怎样使用StatelessWidget。一个最常见的StatelessWidget就是Text Widget。如果你去看Text Widget的实现的话你会发现他是StatelessWidget的一个子类。

new Text(
  'I like Flutter!',
  style: new TextStyle(fontWeight: FontWeight.bold),
);

就像你看到的一样,Text Widget没有与它关联的State信息,它只是简单的渲染通过构造函数传递给它的信息。 但是如果我们想使**"I like Flutter!"**动态的改变,比如通过点击一个FloatingActionButton,该怎么办呢? 其实很简单,我们可以通过将Text Widget包裹在一个StatefulWidget里面来实现,当FloatingActionButton点击的时候更新StatefulWidget中的状态信息。 代码如下:

import 'package:flutter/material.dart';

void main() {
  runApp(new SampleApp());
}

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Sample App',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

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

class _SampleAppPageState extends State<SampleAppPage> {
  // Default placeholder text
  String textToShow = "I Like Flutter";

  void _updateText() {
    setState(() {
      // update the text
      textToShow = "Flutter is Awesome!";
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text("Sample App"),
      ),
      body: new Center(child: new Text(textToShow)),
      floatingActionButton: new FloatingActionButton(
        onPressed: _updateText,
        tooltip: 'Update Text',
        child: new Icon(Icons.update),
      ),
    );
  }
}

上面的代码中可以看到我们将展示**"I Like Flutter"**的Text Widget在了一个继承于State的_SampleAppPageState类中渲染。我们自定义了一个继承于StatefulWidget的类SampleAppPage,它是一个StatefulWidget,与它关联的State就是_SampleAppPageState。当FloatingActionButton被点击时会回调_updateText方法,_updateText方法通过调用State类的setState方法来修改Text Widget的内容。setState由于修改了状态信息会触发Flutter框架对Widget的更新。

小结: 在Flutter中Widgets Tree是不可变的,并且每一帧Widget都会rebuild。无法直接更新Widget。所以需要使用StatefulWidget来记录State,记录的State可以在帧与帧之间共享(在下一帧时恢复上一帧保存的State信息)。通过改变State来触发Flutter更新Widget。

怎样对Widgets布局,xml布局文件在哪里

  • in Android

    • 我们一般通过xml来写布局。
  • in Flutter

    • 我们通过Widget Tree来写我们的布局。

这有一个例子描述了怎样在屏幕上展示一个Widget,并且给他添加一些padding。

override
Widget build(BuildContext context) {
  return new Scaffold(
    appBar: new AppBar(
      title: new Text("Sample App"),
    ),
    body: new Center(
      child: new MaterialButton(
        onPressed: () {},
        child: new Text('Hello'),
        padding: new EdgeInsets.only(left: 10.0, right: 10.0),
      ),
    ),
  );
}

这里看到其实在Flutter中是没有xml布局文件的存在了,取而代之的是直接在override的build方法中去布局,这其实有点类似RN,这里的build方法就类似RN中的render方法,只不过RN通过JSX使得render方法中通过xml语法来完成布局,而Flutter则是完全通过Dart语法来完成布局。可读性上我个人还是更喜欢Flutter,xml与js混写还是觉得有点别扭。 这里列出了Flutter提供的所有的布局。

小结: 在Flutter中不存在xml的布局形式,Widget的布局在build方法中直接构建。

怎么从布局中添加或者删除一个组件

  • in Android

    • 我们可以调用addChild或者removeChild方法去动态的添加或者删除一个ViewGroup中的View。
  • in Flutter

    • 因为widget是不可变的所以不能直接的addChild或者removeChild。但是可以传递一个返回Widget的方法给它的Parent,然后通过一个boolean值在该方法中控制要返回的Widget。

下面的代码展示了如何通过点击FloatingActionButton来触发在两个Widget之间切换:

import 'package:flutter/material.dart';

void main() {
  runApp(new SampleApp());
}

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Sample App',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

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

class _SampleAppPageState extends State<SampleAppPage> {
  // Default value for toggle
  bool toggle = true;
  void _toggle() {
    setState(() {
      toggle = !toggle;
    });
  }

  _getToggleChild() {
    if (toggle) {
      return new Text('Toggle One');
    } else {
      return new MaterialButton(onPressed: () {}, child: new Text('Toggle Two'));
    }
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text("Sample App"),
      ),
      body: new Center(
        child: _getToggleChild(),
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: _toggle,
        tooltip: 'Update Text',
        child: new Icon(Icons.update),
      ),
    );
  }
}

代码也比较简单,关键在于_SampleAppPageState的build方法中Center的构造方法中的child参数传的是一个_getToggleChild方法。该方法通过一个toggle变量来决定返回给Center的是一个怎样的Widget。而toggle的赋值同样是由点击FloatingActionButton后调用setState来改变的。也就是说将toggle作为SampleAppPage的状态保存下来,在展示的时候由toggle的值来动态决定要展示的是什么Widget。

小结: 在Flutter中不能直接动态的去添加或者删除一个Widget到Widgets Tree中,因为Flutter中的Widget是不可变的。但我们可以依赖StatefulWidget根据State的不同来灵活的构建不同的Widget。

怎样对一个Widget做动画

  • in Android
    • 我们可以通过通过xml文件或者调用View.animate()方法创建一个动画。
  • in Flutter
    • 我们将需要做动画的Widget包裹到一个Transition中来实现。

像Android一样,在Flutter中我们也有AnimationController和Interpolator,Interpolator通过继承Animation类实现,比如下面例子中用到的CurvedAnimation。我们传递AnimationController和Animation到一个Widget中,然后通过AnimationController来启动动画。 下面的例子展示了使用FadeTransition来实现当按下按钮时将展示Logo的FlutterLogo Widget淡出的效果:

import 'package:flutter/material.dart';

void main() {
  runApp(new FadeAppTest());
}

class FadeAppTest extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Fade Demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new MyFadeTest(title: 'Fade Demo'),
    );
  }
}

class MyFadeTest extends StatefulWidget {
  MyFadeTest({Key key, this.title}) : super(key: key);
  final String title;
  @override
  _MyFadeTest createState() => new _MyFadeTest();
}

class _MyFadeTest extends State<MyFadeTest> with TickerProviderStateMixin {
  AnimationController controller;
  CurvedAnimation curve;

  @override
  void initState() {
    controller = new AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
    curve = new CurvedAnimation(parent: controller, curve: Curves.easeIn);
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new Center(
          child: new Container(
              child: new FadeTransition(
                  opacity: curve,
                  child: new FlutterLogo(
                    size: 100.0,
                  )))),
      floatingActionButton: new FloatingActionButton(
        tooltip: 'Fade',
        child: new Icon(Icons.brush),
        onPressed: () {
          controller.forward();
        },
      ),
    );
  }
}

整个Widget Tree还是跟之前类似,MaterialApp的构造函数中home参数传入的依然是我们自定义的一个继承自StatefulWidget的MyFadeTest,_MyFadeTest是其对应的State,其中定义了AnimationController和CurvedAnimation,AnimationController用于控制动画,CurvedAnimation是一个插值器实现。接着在build方法中通过将我们需要动画的FlutterLogo Widget包裹在一个FadeTransition中来让FlutterLogo Widget产生动画,最后在按下FloatingActionButton的回调中使用AnimationController.forward()方法来触发动画。 这里或者那里查看更多关于动画的具体细节。

小结: 在Flutter中也有AnimationController和插值器,通过AnimationController来控制动画的播放,插值器改变动画播放的加速度。 使用时先构造AnimationController,然后将构造好的AnimationController作为参数构造插值器,最后将构造好的插值器作为参数构造Transition。之后就可以通过AnimationController来控制Transition中包含的Widget的动画执行。

怎样使用Canvas去画内容

  • in Android
    • 我们可以用Canvas去画一些自定义的图形在屏幕上。
  • in Flutter
    • CustomPaint和CustomPainter这两个类可以帮助我们在Canvas上作画。

下面的代码实现一个可自由签名的Widget:

import 'package:flutter/material.dart';
class SignaturePainter extends CustomPainter {
  SignaturePainter(this.points);
  final List<Offset> points;
  void paint(Canvas canvas, Size size) {
    Paint paint = new Paint()
      ..color = Colors.black
      ..strokeCap = StrokeCap.round
      ..strokeWidth = 5.0;
    for (int i = 0; i < points.length - 1; i++) {
      if (points[i] != null && points[i + 1] != null)
        canvas.drawLine(points[i], points[i + 1], paint);
    }
  }
  bool shouldRepaint(SignaturePainter other) => other.points != points;
}
class Signature extends StatefulWidget {
  SignatureState createState() => new SignatureState();
}
class SignatureState extends State<Signature> {
  List<Offset> _points = <Offset>[];
  Widget build(BuildContext context) {
    return new GestureDetector(
      onPanUpdate: (DragUpdateDetails details) {
        setState(() {
          RenderBox referenceBox = context.findRenderObject();
          Offset localPosition =
          referenceBox.globalToLocal(details.globalPosition);
          _points = new List.from(_points)..add(localPosition);
        });
      },
      onPanEnd: (DragEndDetails details) => _points.add(null),
      child: new CustomPaint(painter: new SignaturePainter(_points)),
    );
  }
}
class DemoApp extends StatelessWidget {
  Widget build(BuildContext context) => new Scaffold(body: new Signature());
}
void main() => runApp(new MaterialApp(home: new DemoApp()));

可以看到CustomPaint和CustomPainter搭配使用来实现了向Canvas上绘制自定义内容的目的。首先自定义SignaturePainter继承于CustomPainter,并重写paint方法,在本例中paint方法首先采用链式写法构造一个Paint实例paint,接着遍历points列表的内容来画线。points列表是构造SignaturePainter时传入的,里面保存了触摸屏幕的事件点的信息。其他部分其实与之前的结构都差不多。关键在SignatureState的build方法中返回的是一个GestureDetector,这是一个能够帮助我们捕获手势信息的Widget,在它的构造函数中child参数传入的是一个CustomPaint,CustomPaint的构造函数又传入了一个我们自定义的SignaturePainter,并且将手势捕获事件时捕获到的_points列表在这个时候传递给SignaturePainter。于是CustomPaint就和CustomPainter产生化学反应,相互配合完成在Canvas上作画的效果。

效果如下:

小结: 在Flutter中实现在Canvas上作画需要CustomPaint和CustomPainter相互配合,首先继承CustomPainter自定义一个Painter并重写paint方法实现绘制逻辑。然后将自定义的CustomPainter传递给CustomPaint的构造方法,CustomPaint作为Widget Tree中的一个Widget使用自定义的CustomPainter完成绘制。

怎样构建自定义Widget

  • in Android
    • 自定义View一般通过继承View或者已经存在的其他组件并重写一些关键方法来实现。
  • in Flutter
    • 自定义一个Widget不是通过继承而是通过组合其他widgets。

让我们来看一个栗子:

class CustomButton extends StatelessWidget {
  final String label;
  CustomButton(this.label);

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

代码很简单,看到CustomButton一样还是继承于StatelessWidget。构造函数接受一个字符串参数并保存在内部成员变量label中。在build方法中返回的是一个RaisedButton(一个Flutter提供的Widget),巧妙的地方是在RaisedButton的构造函数中传入了一个Text(一个Flutter提供的Widget)作为其child参数。这个Text Widget显示的就是CustomButton的label成员中的内容。

在使用CustomButton的时候可以像使用其他Widget一样直接使用:

override
  Widget build(BuildContext context) {
    return new Center(
      child: new CustomButton("Hello"),
    );
  }
}

小结: 在Flutter中自定义Widget是通过组合不同的Widget来实现的,自定义的Widget只继承于StatelessWidget或者StatefulWidget,通过build方法中组合其他的Widget来实现自定义Widget。

英文原版传送