视图
视图
UIView 相当于 Flutter 中的什么?
这里有一份关于响应式编程,或者说 声明式编程 和传统的命令式编程有什么不同之处的文章,你可以浏览 声明式 UI 介绍。
在 iOS 中,你在 UI 中创建的大部分视图都是 UIView 的实例。而在构造布局时,这些视图也可以作为其他视图的容器。
在 Flutter 中,同 UIView 能够进行类比的就是 Widget 了。但 Widget 和 iOS 里的视图并不能同等对待,不过当你想要了解 Flutter 的工作原理时,你可以把它理解为“声明和构造 UI 的方法”。
然而,Widget 和 UIView 还是有着相当一部分区别的。首先,widget 拥有着不同的生命周期:整个生命周期内它是不可变的,且只能够存活到被修改的时候。一旦 widget 实例或者它的状态发生了改变,Flutter 框架就会创建一个新的由 Widget 实例构造而成的树状结构。而在 iOS 里,修改一个视图并不会导致它重新创建实例,它作为一个可变对象,只会绘制一次,只有在发生 setNeedsDisplay() 调用之后才会发生重绘。
还有,和 UIView 不同,Flutter 的 widget 是很轻量的,一部分原因就是源于它的不可变特性。因为它并不是视图,也不直接绘制任何内容,而是作为对 UI 及其特性的一种描述,而被“注入”到视图中去。
Flutter 包含了 Material Components 库。内容都是一些遵循了 Material Design 设计规范 的组件。Material Design 是一种灵活的 支持全平台的设计体系,其中也包括了 iOS。
但是 Flutter 的灵活性和表现力使其能够适配任何的设计语言。在 iOS 中,你可以通过 Cupertino widgets 来构造类似于 Apple iOS 设计语言 的接口。
我该如何更新 widget?
在 iOS 可以直接对视图进行修改。但是在 Flutter 中,widget 都是不可变的,所以也不能够直接对其修改。所以,你必须通过修改 widget 的 state 来达到更新视图的目的。
于是,就引入了 Stateful widget 和 Stateless widget 的概念。和字面意思相同,StatelessWidget 就是一个没有绑定状态的 widget。
当某个 widget 不需要依赖任何别的初始配置来对这个 widget 进行描述时,StatelessWidgets 会是很有用的。
举个例子,在 iOS 中,你需要把 logo 当作 image 并将它放置在 UIImageView 中,如果在运行时这个 logo 不会发生变化,那么对应 Flutter 中你应该使用 StatelessWidget。
但是如果你想要根据 HTTP 请求的返回结果动态的修改 UI,那么你应该使用 StatefulWidget。在 HTTP 请求结束后,通知 Flutter 更新这个 widget 的 State,然后 UI 就会得到更新。
StatefulWidget 和 StatelessWidget 最重要的区别就是,StatefulWidget 中有一个 State 对象,它用来存储一些状态的信息,并在整个生命周期内保持不变。
如果你对此还存有疑虑,记住一点:如果一个 widget 在 build 方法之外(比如运行时下发生用户点击事件)被修改,那么就应该是有状态的。如果一个 widget 一旦生成就不再发生改变,那么它就是无状态的。然而,即使一个 widget 是有状态的,如果不是自身直接响应修改(或别的输入),那么他的父容器也可以是无状态的。
下面是如何使用 StatelessWidget 的示例。Text 是一个常用的 StatelessWidget。如果你看了Text 的源代码,就会发现它继承于 StatelessWidget。
Text(
'I like Flutter!',
style: TextStyle(fontWeight: FontWeight.bold),
);
看了上面的代码,你会注意到 Text 没有携带任何状态。它只会渲染初始化时传进来的内容。
然而,如果你想要动态地修改文本为 “I Like Flutter”,比如说在点击一个 FloatingActionButton 时该怎么做呢?
想要实现这个需求,只需要把 Text 放到 StatefulWidget 中,并在用户点击按钮时更新它即可。
下面是示例代码:
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),
),
);
}
}
如何对 widget 做布局?Storyboard 哪去了?
在 iOS 开发中,你可能会经常使用 Storyboard 来组织你的视图,并直接通过 Storyboard 或者在 ViewController 中通过代码来设置约束。而在 Flutter 中,你要通过代码来对 widget 进行组织来形成一个 widget 树状结构。
下面的例子展示了如何展示一个带有 padding 的 widget:
@overrideWidget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Sample App"), ), body: Center( child: CupertinoButton( onPressed: () { setState(() { _pressedCount += 1; }); }, child: Text('Hello'), padding: EdgeInsets.only(left: 10.0, right: 10.0), ), ), );}
你可以为任何 widget 添加 padding,来达到类似在 iOS 中视图约束的作用。
你可以在 widget 目录 中查看 Flutter 提供的所有 widget 布局方法。
如何增加或者移除一个组件?
在 iOS 中,你可以通过调用父视图的 addSubview() 方法或者removeFromSuperview() 方法来动态的添加或移除视图。在 Flutter 中,因为 widget 是不可变的,所以没有提供直接同 addSubview()作用相同的方法。但是你可以通过向父视图传递一个返回值是 widget 的方法,并通过一个 boolean flag 来控制子视图的存在。
下面的例子中像你展示了如何让用户通过点击 FloatingActionButton 按钮来达到在两个 widget 中切换的目的。
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 CupertinoButton(
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),
),
);
}
}
如何添加动画?
在 iOS 里,你可以使用调用视图的 animate(withDuration:animations:) 方法来创建动画。在 Flutter 里,通过使用动画库将 widget 封装到 animated widget 中来实现带动画效果。
在 Flutter 里,使用 AnimationController,它是一个可以暂停、查找、停止和反转动画的 Animation<double> 类型。它需要一个 Ticker,在屏幕刷新时发出信号量,并在运行时对每一帧都产生一个 0~1 的线性差值。然后你可以创建一个或多个 Animation,并把它们添加到控制器中。
比如,你可以使用 CurvedAnimation 来实现一个曲线翻页动画。这种情况下,控制器就是动画进度的主要数据源,而 CurvedAnimation 计算曲线并替换控制器的默认线性运动。和 widget 一样,在 Flutter 里动画也可以复合嵌套。
当构建一个 widget 树时,可以将 Animation 赋值给 widget 用户表现动画能力的属性,比如 FadeTransition 的 opacity 属性,然后告诉控制器启动动画。
下面的示例描述了当你点击 FloatingActionButton 时,如何实现一个视图渐淡出成 logo 的 FadeTransition 效果。
class SampleApp 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() {
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();
},
),
);
}
@override
dispose() {
controller.dispose();
super.dispose();
}
}
关于更多的内容,可以查看Animation 和 Motion widgets,Animations 教程,以及 Animations 概览。
如何渲染到屏幕上?
在 iOS 里,可以使用 CoreGraphics 绘制线条和图形到屏幕上。Flutter 里有一套基于 Cavans 实现的 API,有两个类可以帮助你进行绘制:CustomPaint 和 CustomPainter,后者实现了绘制图形到 canvas 的算法。
想要学习在 Flutter 里如何实现一个画笔,可以查看 Collin 在 [StackOverflow][] 里的回答。
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;
}
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),
);
}
}
如何设置视图 widget 的透明度?
在 iOS 里,视图都有一个 opacity 或者 alpha 属性。而在 Flutter 里,大部分时候你都需要封装 widget 到一个 Opacity widget 中来实现这一功能。
如何构建自定义 widget?
在 iOS 里,你可以直接继承 UIView 或者使用已经存在的视图,然后重写并实现对应的方法来达到想要的效果。在 Flutter 里,构建自定义 widget 需要通过 组合 一些小的 widget(而不是对它们进行扩展)来实现。
例如,应该如何构建一个初始方法中就包含文本标签的 CustomButton?需要创建一个合成一个 RaisedButton 和一个文本标签的 CustomButton,而不是继承 RaisedButton:
class CustomButton extends StatelessWidget {
final String label;
CustomButton(this.label);
@override
Widget build(BuildContext context) {
return RaisedButton(onPressed: () {}, child: Text(label));
}
}
与其他 Flutter widget 一样的用法,下面我们使用 CustomButton:
@overrideWidget build(BuildContext context) { return Center( child: CustomButton("Hello"), );}
