视图

视图

在 Flutter 中页面(Page)与元素(Element)的相同的是什么?

类 react 的风格,或者说是 声明式 编程风格,与传统的命令式编程风格有何不同?作为比较,可以参考 声明式 UI 介绍。

一个 ContentPage、TabbedPage、MasterDetailPage就是你可以在 Xamarin.Forms 应用程序中使用的全部页面类型。这些页面会控制元素(Element)来显示各种控件。在 Xamarin.Forms 中,Entry 或者 Button 就是一个 元素 的示例。

在 Flutter 中,几乎所有东西都是 widget,一个页面在 Flutter 中被称作路由(Route),也是一个 widget。按钮、进度条、动画控制器都是 widget。当构建一个路由时,就会创建一棵 widget 树。

Flutter 包含 Material 组件 库。这些都是实现了 Material Design 指南 的 widget。Material Design 是一个灵活的 针对所有平台 的设计系统,包括 iOS。

不过, Flutter 有足够灵活和自描述性(expressive)去实现任何设计语言。举个例子,在 iOS 上,你可以用 Cupertino widget来生成一个看起来像 苹果 iOS 设计语言 的接口。

如何更新 widget?

在 Xamarin.Forms 中,每一个页面或者元素都是一个有状态的类,拥有一些属性和方法。通过更新一个属性来更新你的元素,而且这会传递到原生控件。

在 Flutter 中,widget是不可变的,你不可以直接地通过修改一个属性来更新它们,而是应该使用 widget 的状态。

有状态 widget 和无状态 widget 的概念就是出自这里,无状态 widget(StatelessWidget)顾名思义,就是一个没有状态信息的 widget。

当你在描绘用户界面的一个不依赖除对象中的配置信息之外任何东西的部分时,StatelessWidgets 是有用的。

举个例子,在 Xamarin.Forms 中,可以轻而易举地用你的logo替换一张图片。这个logo将不会在运行过程中修改,所以在 Flutter 会使用StatelessWidget。

如果你想动态地基于进行了 HTTP 调用或者用户交互后接收到的数据来修改 UI,你需要使用 StatefulWidget 并告诉 Flutter 框架这个 widget 的 状态(State)已经被更新了所以它可以更新那个 widget。

这里要记下的重要内容是有状态和无状态 widget 的核心行为都是一样的。他们重建每个结构,不同的是StatefulWidget拥有一个状态(State)对象来跨结构储存状态数据和恢复它。

如果你有疑惑,那么就记住这个规则:如果一个 widget 改变了(例如是因为用户交互),它就是有状态的。相反,如果一个 widget 对修改作出反应,包含它的父 widget 如果本身没有对修改作出反应,仍然可以是无状态的。

接下来的示例展示了如何使用一个StatelessWidget。一个公共的StatelessWidget是Text widget。如果你查阅 Text widget 的实现,你会发现他是 StatelessWidget 的子类。

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

如你所见,文本 widget 没有状态信息与它关联,它只渲染在它的构造函数中呈现的内容,没有更多。

但是,如果你想动态地作出 “I Like Flutter”的修改呢?例如在点击一个FloatingActionButton时。

为了实现这个目标,需要将 Text widget 封装到一个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),
      ),
    );
  }
}

该如何布局我的 widget 呢?什么东西可以等价于一个 XAML 文件?

在 Xamarin.Forms 中,大部分开发者用 XAML 写布局,尽管有时用 C#。在 Flutter 中编码一棵 widget 树来编写布局。

接下来的示例展示如何显示一个简单的带填充(padding)的 widget:

@overrideWidget 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 在 widget 目录 中提供的布局。

如何从布局中添加或移除一个元素?

在 Xamarin.Forms 中,你需要在代码中移除或添加一个 元素(Element)。如果它说一个列表,这将会涉及设置 Content 属性或者调用 Add() 或者 Remove() 方法。

在 Flutter 中,因为 widget 都是不可变的,所以没有直接对等的东西。相反,你可以将一个返回一个 widget 的函数传递给父级,并用布尔标控制它的子 widget 的创建。

下面的示例展示当用户点击 FloatingActionButton 时,如何在两个 widget 之间切换。

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 CupertinoButton(
        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),
      ),
    );
  }
}

如何让一个 widget 动起来?

在 Xamarin.Forms 中,你可以利用包括例如 FadeTo 和 TranslateTo 等方法的视图扩展(ViewExtensions)来创建简单的动画。你会在一个视图中使用这些方法来执行需要的动画。

<Image Source="{Binding MyImage}" x:Name="myImage" />

然后再后面的代码或一个动作中,这个会在1秒内淡入这张图像。

myImage.FadeTo(0, 1000);

在 Flutter 中,通过封装 widget 到一个动画 widget 中,可以使用动画类库来让 widget 动起来。使用一个 AnimationController ,即一个可以暂停、寻找、停止和倒退动画的 Animation<double> 。它需要一个滴答器(Ticker),当垂直同步(vsync)发生时,会发出信号,并在运行时的每一帧都会产生0和1之间的线性插值。然后你可以创建一个或多个动画并把它们附加到控制器上。

举个例子,你可以使用 CurvedAnimation 来实现一个沿着插值曲线的动画。在这个场景中,控制器说一个动画进展的“大师”源,而 CurvedAnimation 计算用来替代控制器默认线性运动的曲线。跟 widget 一样,Flutter 中的动画与组成一起工作。

当你在构建一个 widget 树,赋值一个动画(Animation)给一个 widget 的一个动画属性时,比如 渐退(FadeTransition)的不透明度,会告诉控制器开始执行动画。

下面的实例展示如何去写一个 渐退(FadeTransition),当你按下 FloatingActionButton 时,它会把 widget 渐变到一个logo。

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, 动画教程,以及动画概述。

如何在屏幕上绘图?

Xamarin.Forms 从来没有任何内置的方法来直接在屏幕上绘图。如果他们需要一个自定义图像绘制,大多数使用 SkiaSharp。在 Flutter中,你可以直接访问 Skia 画布(Skia Canvas)方便地在屏幕上绘图。

Flutter 拥有两个类来帮助你在画布上绘图:CustomPaint 和 CustomPainter,后者实现了你在画布上绘图的算法。

如果想学习如何在 Flutter 中实现一个签名画手,请阅读 Collin 在 StackOverflow 的回答。

import 'package:flutter/material.dart';

void main() => runApp(new MaterialApp(home: new DemoApp()));

class DemoApp extends StatelessWidget {
  Widget build(BuildContext context) => new Scaffold(body: new Signature());
}

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), size: Size.infinite),
    );
  }
}

class SignaturePainter extends CustomPainter {
  SignaturePainter(this.points);
  final List<Offset> points;
  void paint(Canvas canvas, Size size) {
    var 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;
}

widget 的不透明度在哪里?

Xamarin.Forms 上,所有 虚拟元素(VisualElement)都拥有一个不透明度。在 Flutter 中,你需要封装一个 widget 到一个 不透明度 widget 来实现它。

如何构建一个自定义 widget ?

在 Xamarin.Forms 中,通常派生 VisualElement 或使用一个已有的 VisualElement ,来重写和实现所需行为的方法。

在 Flutter 中,通过组合(composing)更小的 widget(而不是扩展它们)来构建一个自定义 widget。这有点类似于基于 Grid 实现自定义控件,其中添加了大量 VisualElement,同时使用自定义逻辑进行扩展。

举个例子,如何构建一个在构造器接受一个标签的自定义按钮?创建一个组合了一个带有标签的RaisedButton的自定义按钮,而不是扩展 RaisedButton 。

class CustomButton extends StatelessWidget {
  final String label;

  CustomButton(this.label);

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

然后就可以像使用其他 Flutter widget 一样使用这个自定义按钮。

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