带你快速掌握Flutter的视图(Widgets)

Posted by CrazyCodeBoy的技术博客官网 on March 16, 2019

为大家倾力打造的课程《Flutter从入门到进阶-实战携程网App》上线了,点我Get!!!

带你快速掌握Flutter的视图(Widgets)

在这篇文章中,将向大家分享Flutter开发中的一些视图(Widgets)相关的一些知识和经验,主要包含:

  • 谁是Flutter中View?
  • 如何更新Widgets?
  • 如何布局?
  • 如何在布局中添加或删除组件?
  • 如何对 Widget 做动画?
  • 如何绘图(Canvas draw/paint)?
  • 如何构建自定义Widgets?
  • 如何设置Widget的透明度?

通过这篇文章的学习,将为你揭开这些答案。

谁是Flutter中View?

  • 在Android中,View是屏幕上显示的所有内容的基础, 按钮、工具栏、输入框等一切都是View。
  • 在 iOS 中,构建 UI 的过程中将大量使用 view 对象。这些对象都是 UIView 的实例。它们可以用作容器来承载其他的 UIView,最终构成你的界面布局。
  • 在React Native中,View是一个支持Flexbox布局的容器,样式,触摸处理和辅助控制。

Android中的View与iOS中的UIView在下文中统称为:View,React Native统称为RN。

那么,在Flutter中我们可以将Widget当做是Android、iOS、RN中的View,但他们并不完全等价,但当我们试图去理解 Flutter 是如何工作的时候,我们可以认为它是“声明和构建 UI 的方法”。

但是,Widget与View有一些区别。 首先,Widget具有不同的生命周期:它们是不可变的,它们会存在于状态被改变之前。 每当Widget或其状态发生变化时,Flutter的框架都会创建一个新的Widget实例树。 相比之下,Android/iOS视图被绘制一次,并且在调用invalidate/setNeedsDisplay之前不会重绘。

此外,与View不同,FlutterWidget很轻巧,部分原因在于它的不变性。 因为它本身不是视图,并且不是直接绘制任何东西,而是对UI及其语义的描述。

Flutter 包含了 Material 组件库。这些 Widgets 遵循了 Material 设计规范。材料设计是一个灵活的设计系统,并且为包括 iOS 在内的所有系统进行了优化。

但是用 Flutter 实现任何的设计语言都非常的灵活和富有表现力。在 iOS 平台,你可以使用 Cupertino widgets 来构建遵循了 Apple’s iOS design language 的界面。

Flutter中,您可以使用Widgets库中的核心布局小部件 如 Container, Column, Row, 和 Center,关于Widget的更多内容可参考:Layout Widgets目录。

如何更新Widgets?

在Android/iOS中要更新视图,我们可以直接通过对应的方法来操作更改。 在Flutter中,Widget是不可变的,不会直接更新。 相反,我们可以通过操纵Widget的状态来更新它们。

这就是有状态和无状态Widget的概念来源。 StatelessWidget听起来就像是一个没有状态信息的Widget

StatelessWidgets适用于当我们描述的用户界面不依赖于对象中的配置信息时。

例如,在Android/iOS中,我们需要用ImageView/UIImageView来显示logo。 logo在运行时不会改变,因此在Flutter中使用StatelessWidget是最好不过了。

如果要根据HTTP网络请求或用户交互后收到的数据动态更改UI,则必须使用StatefulWidget并告诉Flutter框架Widget的状态已更新,以便更新该Widget

无状态Widget和有状态Widget之间的重要区别在于StatefulWidgets具有一个State对象,该对象存储状态数据并将其传递到树重建中,因此状态不会丢失。

请记住以下规则:如果Widgetbuild之外更改(例如,由于运行时用户交互),则它是有状态的。 如果Widget永远不会改变,一旦构建,它就是无状态的。 但是,即使Widget是有状态的,如果包含它的父窗口小部件本身不对这些更改(或其他输入)做出反应,父Widget仍然可以是无状态的。

接下来,我们来看看你如何使用一个StatelessWidget。Text就是一个常见的StatelessWidget。如果你查看Text Widget的实现,你会发现它是一个StatelessWidget的子类:

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

以上代码片段的完整部分可以在课程源码中查找。

正如你所看到的,Text 没有与之关联的状态信息,它呈现了构造函数中传递的内容,仅此而已。

但是,如果你想让“I Like Flutter”动态变化,例如点击一个FloatingActionButton?可以通过将Text包装在StatefulWidget中并在点击按钮时更新它来实现,如:

import 'package:flutter/material.dart';

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

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

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

  @override
  _SampleAppPageState createState() => _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 Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: Center(child: Text(textToShow)),
      floatingActionButton: FloatingActionButton(
        onPressed: _updateText,
        tooltip: 'Update Text',
        child: Icon(Icons.update),
      ),
    );
  }
}

以上代码片段的完整部分可以在课程源码中查找。

如何布局?

  • 在Android中,我们通过XML编写布局;
  • 在iOS 中,我们会用 Storyboard 文件来组织 views,并对它们设置约束,或在 view controller 中使用代码来设置约束;

Flutter中,我们通过编写一个 Widget 树来声明布局。

下面这个例子展示了如何展示一个带有 padding 的简单 Widget

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

以上代码片段的完整部分可以在课程源码中查找。

另外推荐大家在widget catalog中查看 Flutter提供的布局。

如何在布局中添加或删除组件?

  • 在Android中,我们可以调用父级控件的addChild或removeChild方法以动态添加或删除View。
  • 在 iOS 中,我们可以调用父view的addSubview() 或在子view的removeFromSuperview()来动态地添加或移除子 view。

Flutter中,因为Widget是不可变的,所以没有类似的方法。相反,我们可以传入一个函数或表达式,该函数或表达式返回一个Widget给父项,并通过布尔值控制该Widget的创建。

例如,当点击一个FloatingActionButton时,如何在两个Widget之间切换:

import 'package:flutter/material.dart';

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

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

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

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

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

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

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

以上代码片段的完整部分可以在课程源码中查找。

如何对Widget做动画?

  • 在Android中,我们可以通过XML创建动画或调用view.animate()。
  • 在 iOS 中,你通过调用 animate(withDuration:animations:) 方法来给一个 view 创建动画。

Flutter 中,使用动画库来包裹 Widgets,而不是创建一个动画 Widget

Flutter 中,使用 AnimationController,这是一个可以暂停、寻找、停止、反转动画的 Animation 类型。它需要一个 Ticker 当 vsync 发生时来发送信号,并且在每帧运行时创建一个介于 0 和 1 之间的线性插值(interpolation)。我们可以创建一个或多个的 Animation 并附加给一个 controller。

例如,我们可能会用 CurvedAnimation 来实现一个 interpolated 曲线。在这个场景中,controller 是动画过程的“主人”,而 CurvedAnimation 计算曲线,并替代 controller 默认的线性模式。

当构建 Widget 树时,你会把 Animation 指定给一个 Widget 的动画属性,比如 FadeTransition 的 opacity,并告诉控制器开始动画。

下面这个例子展示了在点击 FloatingActionButton 之后,如何使用 FadeTransition 来让 Widget 淡出到 logo 图标:

import 'package:flutter/material.dart';

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

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

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

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

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

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

以上代码片段的完整部分可以在课程源码中查找。

推荐大家通过:Animation & Motion widgets, Animations tutorial, Animations overview查看更多信息。

如何绘图(Canvas draw/paint)?

  • 在Android中,可以使用CanvasDrawable 在屏幕上绘制出自定义形状和图片;
  • 在 iOS 上,可以通过 CoreGraphics 来在屏幕上绘制线条和形状;
  • 在RN中我们通常是由react-native-canvas插件来进行绘图;

Flutter也有类似的Canvas API,因为它基于相同的底层渲染引擎Skia。 因此,对于Android开发人员来说,在Flutter中绘制到画布是一项非常熟悉的任务。Flutter有两个类可以帮助我们绘制画布,CustomPaintCustomPainter,它们实现您的算法以绘制到画布。

要了解如何在Flutter中实现签名Painter,可参阅Collin在StackOverflow上的答案。

CustomPainter

import 'package:flutter/material.dart';

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

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

class Signature extends StatefulWidget {
  SignatureState createState() => SignatureState();
}

class SignatureState extends State<Signature> {
  List<Offset> _points = <Offset>[];
  Widget build(BuildContext context) {
    return GestureDetector(
      onPanUpdate: (DragUpdateDetails details) {
        setState(() {
          RenderBox referenceBox = context.findRenderObject();
          Offset localPosition =
          referenceBox.globalToLocal(details.globalPosition);
          _points = List.from(_points)..add(localPosition);
        });
      },
      onPanEnd: (DragEndDetails details) => _points.add(null),
      child: CustomPaint(painter: SignaturePainter(_points), size: Size.infinite),
    );
  }
}

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

以上代码片段的完整部分可以在课程源码中查找。

绘制圆形和方形

Flutter中,你可以使用 CustomPaintCustomPainter 类去绘制到画布。

以下示例显示如何使用CustomPaint widget在绘制阶段绘制。 它实现了抽象类CustomPainter,并将其传递给CustomPaintpainter属性。 CustomPaint子类必须实现paintshouldRepaint方法:

CustomPainter

import 'package:flutter/material.dart';
import 'package:flutter_app/navigator/tab_navigator.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter bottomNavigationBar',
      theme: new ThemeData.fallback(),
      home: _MyCanvas(),
    );
  }
}
// Flutter
class MyCanvasPainter extends CustomPainter {

  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint();
    paint.color = Colors.amber;
    canvas.drawCircle(Offset(100.0, 200.0), 40.0, paint);
    Paint paintRect = Paint();
    paintRect.color = Colors.lightBlue;
    Rect rect = Rect.fromPoints(Offset(150.0, 300.0), Offset(300.0, 400.0));
    canvas.drawRect(rect, paintRect);
  }

  bool shouldRepaint(MyCanvasPainter oldDelegate) => false;
  bool shouldRebuildSemantics(MyCanvasPainter oldDelegate) => false;
}
class _MyCanvas extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomPaint(
        painter: MyCanvasPainter(),
      ),
    );
  }
}

以上代码片段的完整部分可以在课程源码中查找。

如何构建自定义Widgets?

  • 在Android中,可以通过继承View或已经存在的某个控件,然后覆盖其绘制方法来实现自定义View;
  • 在iOS中,可以通过编写 UIView 的子类,或使用已经存在的 view 来重载并实现方法,以达到特定的功能;

Flutter 中,推荐组合多个小的 Widgets 来构建一个自定义的 Widget(而不是扩展它)。

举个例子,如果你要构建一个 CustomButton ,并在构造器中传入它的 label?那就组合 RaisedButton 和 label,而不是扩展 RaisedButton。

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

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

//使用自定义组件
@override
  Widget build(BuildContext context) {
    return new Center(
      child: new CustomButton("Hello"),
    );
  }
}

以上代码片段的完整部分可以在课程源码中查找。

如何设置Widget的透明度?

  • 在 iOS 中,什么东西都会有一个 .opacity 或是 .alpha 的属性;
  • 在Android中View有setAlpha方法;

Flutter中如果要改变透明度,我们可以给Widget 包裹一个 Opacity Widget 来做到这一点。

Opacity(
      opacity: 0.5,
      child: Text('透明度50%')
      )

以上代码片段的完整部分可以在课程源码中查找。

未完待续

参考资料