1. 视图在 Flutter 中的对应概念是什么?
Android 中的 View 是显示在屏幕上的一切的基础。按钮、工具栏、输入框以及一切内容都是 View。而 Flutter 中 View 的大致对应物是 Widget。 Widget 并非完全对应于 Android 中的 View,但是在你熟悉 Flutter 的工作原理的过程中可以把它们看做“声明和构建 UI 的方式”。
然而,widget 和 View 还是有一些差异。首先,widget 有着不一样的生命周期:它们是不可变的,一旦需要变化则生命周期终止。任何时候 widget 或它们的状态变化时, Flutter 框架都会创建一个新的 widget 树的实例。对比来看,一个 Android View 只会绘制一次,除非调用 invalidate 才会重绘。
Flutter 的 widget 很轻量,部分原因在于它们的不可变性。因为它们本身既非视图,也不会直接绘制任何内容,而是 UI 及其底层创建真正视图对象的语义的描述。
Flutter 支持 Material Components 库。它提供实现了 Material Design 设计规范 的 widgets。 Material Design 是一套为所有平台优化 (包括 iOS)的灵活的设计系统。
Flutter 非常灵活、有表达能力,它可以实现任何设计语言。例如,在 iOS 平台上,你可以使用 Cupertino widgets 创建 Apple 的 iOS 设计语言风格的界面。
2. 如何更新 widgets?
在 Android 中,你可以直接操作更新 View。然而在 Flutter 中, Widget 是不可变的,无法被直接更新,你需要操作 Widget 的状态。
这就是有状态 (Stateful) 和无状态 (Stateless) Widget 概念的来源。 StatelessWidget 如其字面意思—没有状态信息的 Widget。
StatelessWidget 用于你描述的用户界面的一部分不依赖于除了对象中的配置信息以外的任何东西的场景。
例如在 Android 中,这就像显示一个展示图标的 ImageView。这个图标在运行过程中不会改变,所以在 Flutter 中就使用 StatelessWidget。
如果你想要根据 HTTP 请求返回的数据或者用户的交互来动态地更新界面,那么你就必须使用 StatefulWidget,并告诉 Flutter 框架 Widget 的状态 (State) 更新了,以便 Flutter 可以更新这个 Widget。
这里需要着重注意的是,无状态和有状态的 Widget 本质上是行为一致的。它们每一帧都会重建,不同之处在于 StatefulWidget 有一个跨帧存储和恢复状态数据的 State 对象。
如果你有疑问,那么记住这条规则:如果一个 Widget 会变化(例如由于用户交互),它是有状态的。然而,如果一个 Widget 响应变化,它的父 Widget 只要本身不响应变化,就依然是无状态的。
下面的例子展示了如何使用 StatelessWidget。Text Widget 是一个普通的 StatelessWidget。如果你查看 Text Widget 的实现,你会发现它继承自 StatelessWidget。
Text(
'I like Flutter!',
style: TextStyle(fontWeight: FontWeight.bold),
);
如上所示,这个 Text Widget 没有相关联的状态信息,它只渲染传入构造器的信息,仅此而已。
但是,假如你想要动态地改变 “I Like Flutter”,例如当你点击一个 FloatingActionButton 的时候,该怎么办呢?
为了实现这个效果,将 Text Widget 嵌入一个 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),
),
);
}
}
3. 如何布局 Widget?我的 XML 布局文件在哪里?
在 Android 中,你通过 XML 文件定义布局,但是在 Flutter 中,你是通过一个 widget 树来定义布局的。
以下示例展示了如何显示一个带有填充 (padding) 的简单 Widget:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: Center(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
padding: EdgeInsets.only(left: 20.0, right: 30.0),
),
onPressed: () {},
child: Text('Hello'),
),
),
);
}
你可以在 widget 目录 中查看 Flutter 提供的布局。
关于 Flutter 布局的更多详细资料可以看这篇文章 Flutter 入门:Flutter 中的布局
4. 如何在布局中添加或删除一个组件?
在 Android 中,你通过调用父 View 的 addChild() 或 removeChild() 方法动态地添加或者删除子 View。在 Flutter 中,由于 Widget 是不可变的,所以没有 addChild() 的直接对应的方法。不过,你可以给返回一个 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 ElevatedButton(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),
),
);
}
}
5. Widget 如何实现动画?
在 Android 中,你既可以通过 XML 文件定义动画,也可以调用 View 对象的 animate() 方法。在 Flutter 里,则使用动画库,通过将 Widget 嵌入一个动画 Widget 的方式实现 Widget 的动画效果。
Flutter 通过 Animation 的子类 AnimationController 来暂停、播放、停止以及逆向播放动画。它需要一个 Ticker 在垂直同步 (vsync) 的时候发出信号,并且在运行的时候创建一个介于 0 和 1 之间的线性插值。然后你就可以创建一个或多个 Animation,并将它们绑定到控制器上。
例如,你可以使用 CurvedAnimation 来实现一个曲线插值的动画。在这种情况下,控制器决定了动画进度,CurvedAnimation 计算用于替换控制器默认线性动画的曲线值。和 Widget 一样,Flutter 中的动画效果也可以组合使用。
在构建 Widget 树的时候,你需要将 Animation 对象赋值给某个 Widget 的动画属性,例如 FadeTransition 的不透明度属性,并让控制器开始动画。
下面的例子展示了如何实现一个点击 FloatingActionButton 的时候将一个 Widget 渐变为一个图标的 FadeTransition:
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();
},
),
);
}
}
获取更多内容,请查看 动画 Widget,动画指南 以及 动画概览。
6. 如何使用 Canvas 进行绘制?
在 Android 中,你可以使用 Canvas 和 Drawable 将图片和形状绘制到屏幕上。Flutter 也有一个类似于 Canvas 的 API,因为它基于相同的底层渲染引擎 Skia。因此,在 Flutter 中用画布 (canvas) 进行绘制对于 Android 开发者来说是一件非常熟悉的工作。
Flutter 有两个帮助你用画布 (canvas) 进行绘制的类:CustomPaint 和 CustomPainter,后者可以实现自定义的绘制算法。
如果想学习在 Flutter 中如何实现一个签名功能,可以查看 Collin 在 Custom Paint 上的回答。
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;
}
7. 如何创建自定义 Widget?
在 Android 中,一般通过继承 View 类,或者使用已有的视图类,再覆写或实现可以达到特定效果的方法。
在 Flutter 中,通过 组合 更小的 Widget 来创建自定义 Widget(而不是继承它们)。这和 Android 中实现一个自定义的 ViewGroup 有些类似,所有的构建 UI 的模块代码都在手边,不过由你提供不同的行为—例如,自定义布局 (layout) 逻辑。
举例来说,你该如何创建一个在构造器接收标签参数的 CustomButton?你要组合 RaisedButton 和一个标签来创建自定义按钮,而不是继承 RaisedButton:
class CustomButton extends StatelessWidget {
final String label;
CustomButton(this.label);
@override
Widget build(BuildContext context) {
return ElevatedButton(onPressed: () {}, child: Text(label));
}
}
然后就像使用其它 Flutter Widget 一样使用 CustomButton:
@override
Widget build(BuildContext context) {
return Center(
child: CustomButton("Hello"),
);
}