目录导航
UI界面的编写方式
在Android中,通常使用xml编写UI,然后通过LayoutInflater解析为一个树状结构的UI tree,同时也支持代码方式编写UI。
Flutter只能通过代码的方式构造UI tree,代表一个界面。
UI界面的组成元素
在Android中,UI界面的组成元素是View,一切界面元素都继承View类,由View衍生而来。而Flutter UI界面的组成元素是Widget,任何界面元素均继承Widget。
UI界面元素的可变性
Android中View是可变的,当用户交互或数据更新时,可直接调用View的invalidate方法重绘,达到更新UI的目的。
Flutter中Widget本身是不可变的(immutable)。那么Flutter如何做到更新界面呢?
Flutter将Widget分为两种:
- stateless widget
无状态的Widget,基类为StatelessWidget,整个生命周期外观不发生变化,例如,显示app图标的元素不会在运行期间发生改变。
stateless widget的build函数负责构建该Widget,可以是单个Widget或复杂的Widget tree。
abstract class StatelessWidget extends Widget {
...
@protected
Widget build(BuildContext context);
...
}
- stateful widget
拥有状态的Widget,基类为StatefulWidget,自身关联一个State对象,保存着该Widget的状态信息,如当前是否选中等。状态信息也可以保存在Widget中,然后State通过Widget获取状态信息。stateful widget的createState函数负责创建自身关联的State对象。
abstract class StatefulWidget extends Widget {
...
@protected
State createState();
...
}
stateful widget将自身的构建委托给State对象,State对象的build函数负责构建该Widget,当用户交互或数据发生变化时,Widget状态发生改变,调用State的setState方法通知它,而后State根据当前的状态信息,重新构建Widget tree。
abstract class State<T extends StatefulWidget> extends Diagnosticable {
...
@protected
Widget build(BuildContext context);
...
}
假设现在我们自定义一个选择框SampleCheckBox,SampleCheckBox根据用户选择而展现“选中”和“取消”两种外观。
第一步,
SampleCheckBox继承StatefulWidget,并在createState函数中创建自己的State对象SampleCheckBoxState。
class SampleCheckBox extends StatefulWidget{
@override
SampleCheckBoxState createState() {
return SampleCheckBoxState();
}
}
第二步,
实现SampleCheckBoxState,SampleCheckBoxState根据当前的状态值_isChecked决定如何构造SampleCheckBox。为了降低复杂性、便于理解大意,我们假设CheckedBox和UncheckedBox是已存在的两个StatelessWidget,他们的外观分别展示“选中”和“取消”。CheckBoxContainer是支持点击事件的Widget。
class SampleCheckBoxState extends State<SampleCheckBox> {
bool _isChecked = false;
@override
Widget build(BuildContext context) {
return CheckBoxContainer(
child: _isChecked ? CheckedBox() : UncheckedBox(),
//设置点击事件的回调代码
onTap: () {
setState(() {
_isChecked = !_isChecked;
});
});
}
}
在上面的例子中,当我们触发点击事件时,会调用SampleCheckBoxState的setState函数通知它,setState函数接受一个回调函数为参数,该回调函数为我们提供了修改状态数据的时机,之后SampleCheckBoxState根据_isChecked的最新值重新构建SampleCheckBox。
通过上面的介绍,你应该对Flutter Widget的构建有所了解,但有可能对此依然“头懵”,无法透彻地明白Flutter Widget与Android View的区别。我给大家做一个类比:
我们把Android View和Flutter Widget都比作画板,假设现在我们要用画板来展示个人肖像,轮流展示四个人的肖像:朱志强(笔者)、刘备、关羽、张飞。
对于Android View来说,只需准备一张画板,先展示朱志强的肖像,轮到刘备时,直接擦除当前内容,绘制刘备的肖像,以此类推。
对于Flutter Widget来说,由于Widget是不可变的,所以我们要准备四张画板,分别画有四人的肖像,并定义一个超级画板,超级画板并不直接绘制肖像,仅仅是其余四张画板的组织者。当需要展示这四个人中某个人的肖像时,拿出对应的画板来展示。超级画板就相当于Stateful Widget。
自定义Widget
在Android中,我们通过继承已存在的某个View来实现自定义View,例如,当我想定制一个特殊的文本显示View时,我可以继承TextView。
Flutter与之不同,通常自定义Widget其实是对其他已存在的Widget的组装。自定义Widget需直接继承StatelessWidget或StatefulWidget,然后重写build函数,实现该Widget的构造。
例如,当我想定制一个特殊的文本显示Widget时,不能继承Text,而是要继承StatelessWidget或StatefulWidget,在build函数中返回一个特殊的Text,如文本字体变粗。
class CustomText extends StatelessWidget{
final String content;
CustomText(this.content);
@override
Widget build(BuildContext context) {
return Text(
content,
style: TextStyle(fontWeight: FontWeight.bold),
textDirection: TextDirection.ltr
);
}
}
动态添加child widget
在Android中,ViewGroup绘制出来后,可随时通过addView/removeView来添加/删除child view。
在Flutter中,由于Widget是不可变的,如果想动态添加一个child widget如Text,父widget只能准备两套widget tree,一套添加了Text,一套没有添加,然后根据状态返回不同的widget tree。
class SampleWidget extends StatefulWidget{
@override
SampleWidgetState createState()=>SampleWidgetState();
}
class SampleWidgetState extends State<SampleWidget>{
bool _needAddTextChild = false;
@override
Widget build(BuildContext context) {
return _needAddTextChild ? Container(child: Text("添加文本")) : Container();
}
}
canvas 绘图
在Android 中,你可以继承View并重写它的onDraw方法,通过canvas自定义绘图。
在Flutter中,自定义绘图要使用CustomPaint这个Widget,并为其提供一个CustomPainter,CustomPainter负责实现绘制逻辑。
下面这个例子,在CustomPaint的正中间绘制一个蓝色、半径为10的实心圆:
- 继承CustomPainter,重写paint方法,实现绘制圆的逻辑
class CirclePainter extends CustomPainter {
var paintStyle = Paint()..color = Colors.blue;
@override
void paint(Canvas canvas, Size size) {
//绘制一个实心圆
canvas.drawCircle(Offset(size.width / 2, size.height / 2), 10, paintStyle);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
//该方法用于判断是否重绘,由于这里不是动态绘制,所以直接返回false
return false;
}
}
- 创建一个CustomPaint,将CirclePainter传递给它
void main() => runApp(CustomPaint(painter: CirclePainter()));
手势监测
在Android中,两种方式进行手势监测:
- 某些View本身支持设置监听器,处理手势操作,例如,CheckBox可设置用户“选中/取消”的监听器
checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
...
}
});
2.本身不支持特定事件的监听器或想进行复杂的手势处理时,重写View的onTouchEvent方法
public class MyView extends View {
...
@Override
public boolean onTouchEvent(MotionEvent event) {
...
}
...
}
在Flutter中,手势监测也有两种方式:
- 该Widget本身支持触碰事件的监听,我们只需设置监听代码即可。例如,为RaisedButton设置点击事件
RaisedButton(
onPressed: ()=>print("callback when the button was click"),
child: Text("Button")
);
上面,我们为onPressed参数传递了一个回调函数,当RaisedButton被点击时将会回调该函数。
- 该Widget本身不支持触碰事件的监听或无法满足复杂的手势处理,那么需要用GestureDetector包裹该Widget。GestureDetector是一个用来手势监测的特殊Widget。被GestureDetector包裹后,该Widget的手势监测交给GestureDetector处理。
GestureDetector(
child: Text("可响应点击的文本",textDirection: TextDirection.ltr),
onTap: ()=>print("文本被点击了"),
);
GestureDetector包含丰富的触碰监听,这里不再详细列出。
界面管理及界面跳转
在Android中,Activity代表一个界面,每个Activity都有对应的View tree。界面之间的跳转就是Activity之间的跳转。Activity还可以将UI“分块管理”——分成若干Fragment。
Flutter中,一个界面就是一个widget,该widget可以添加child widget,构成一个复杂的widget tree。界面之间的跳转就是widget之间的跳转。
跳转到新界面
Android中,通过startActivity/startActivityForResult跳转到新的actiivty界面。
Flutter中,通过Navigator的push函数进入新界面。Navigator把不同的界面称之为“路由”(route),
push函数接受一个“路由”参数。我们可以使用MaterialPageRoute,并传递WidgetBuilder函数给它,告诉它如何构建要跳转到的界面(也就是一个widget)。
Navigator.of(context)
.push(MaterialPageRoute(builder: (context) => NextPageWidget()));
传递数据到新界面
Android中,我们通过Intent传递数据至新启动的activity。
Flutter中,向新界面传递数据,直接在表示新界面的widget的构造函数中定义参数来接受该数据。
Navigator.of(context)
.push(MaterialPageRoute(builder: (context) => NextPageWidget("new data")));
返回到上一个界面
Android中,当我们想通过代码的方式返回上一个activity时,可以调用当前activity的finish方法。
Flutter中,通过Navigator的pop函数返回至上一个界面。
Navigator.of(context).pop();
将数据回传到上一个界面
Android中,当前activity调用setResult方法向上一个界面回传数据。并在上一个activity的onActivityResult方法里接收数据。
Flutter中,将数据传递给pop函数进行回传。
Navigator.of(context).pop("returned data");
启动新界面时,设置接收回传的数据。
var returnedData = await Navigator.of(context)
.push(MaterialPageRoute(builder: (context) => NextPageWidget()));
await关键字涉及到Flutter的异步编程,我们留到异步编程相关篇章,本篇不再讲述。