[译]Flutter for Android Developers - Views

先说两句

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

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

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


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 Androidapp

    • 咱们能够直接修改View的属性来更新它们。
  • in Flutter框架

    • Widget是不可修改的,咱们无法直接去更新它们,取而代之咱们能够用Widget的State来实现更新。

在Flutter中Widget分为两个类型:less

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

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。

英文原版传送