//RenderObjectWidget 的源码
abstract class RenderObjectWidget extends Widget {
@override
RenderObjectElement createElement();
@protected
RenderObject createRenderObject(BuildContext context);
@protected
void updateRenderObject(BuildContext context, covariant RenderObject renderObject) {
}
...
}
abstract class RenderObjectElement extends Element {
RenderObject _renderObject;
@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_renderObject = widget.createRenderObject(this);
attachRenderObject(newSlot);
_dirty = false;
}
@override
void update(covariant RenderObjectWidget newWidget) {
super.update(newWidget);
widget.updateRenderObject(this, renderObject);
_dirty = false;
}
...
}
abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget {
...
void layout(Constraints constraints, {
bool parentUsesSize = false }) {
...}
void paint(PaintingContext context, Offset offset) {
}
}
// Android设置某文本控件展示文案为Hello World
TextView textView = (TextView) findViewById(R.id.txt);
textView.setText("Hello World");
// iOS设置某文本控件展示文案为Hello World
UILabel *label = (UILabel *)[self.view viewWithTag:1234];
label.text = @"Hello World";
// 原生JavaScript设置某文本控件展示文案为Hello World
document.querySelector("#demo").innerHTML = "Hello World!";
在 Flutter 中,Widget 采用由父到子、自顶向下的方式进行构建,父 Widget 控制着子 Widget 的显示样式,其样式配置由父 Widget 在构建时提供。
用这种方式构建出的 Widget,有些(比如 Text、Container、Row、Column 等)在创建时,除了这些配置参数之外不依赖于任何其他信息,换句话说,它们一旦创建成功就不再关心、也不响应任何数据变化进行重绘。在 Flutter 中,这样的 Widget 被称为 StatelessWidget(无状态组件)。
以 Text 的部分源码为例,说明 StatelessWidget 的构建过程。
class Text extends StatelessWidget {
//构造方法及属性声明部分
const Text(this.data, {
Key key,
this.textAlign,
this.textDirection,
//其他参数
...
}) : assert(data != null),
textSpan = null,
super(key: key);
final String data;
final TextAlign textAlign;
final TextDirection textDirection;
//其他属性
...
@override
Widget build(BuildContext context) {
...
Widget result = RichText(
//初始化配置
...
)
);
...
return result;
}
}
class Image extends StatefulWidget {
//构造方法及属性声明部分
const Image({
Key key,
@required this.image,
//其他参数
}) : assert(image != null),
super(key: key);
final ImageProvider image;
//其他属性
...
@override
_ImageState createState() => _ImageState();
...
}
class _ImageState extends State<Image> {
ImageInfo _imageInfo;
//其他属性
...
void _handleImageChanged(ImageInfo imageInfo, bool synchronousCall) {
setState(() {
_imageInfo = imageInfo;
});
}
...
@override
Widget build(BuildContext context) {
final RawImage image = RawImage(
image: _imageInfo?.image,
//其他初始化配置
...
);
return image;
}
...
}
Widget 是不可变的,更新则意味着销毁 + 重建(build)。StatelessWidget 是静态的,一旦创建则无需更新;而对于 StatefulWidget 来说,在 State 类中调用 setState 方法更新数据,会触发视图的销毁和重建,也将间接地触发其每个子 Widget 的销毁和重建。
需要注意的是,除了我们主动地通过 State 刷新 UI 之外,在一些特殊场景下,Widget 的 build 方法有可能会执行多次。因此,我们不应该在这个方法内部,放置太多有耗时的操作。
反思:build执行多次,通过接口获取表单数据,不要在build里写耗时方法,外部处理传入一个变量即可!
组件销毁相对比较简单。比如组件被移除,或是页面销毁的时候,系统会调用 deactivate 和 dispose 这两个方法,来移除或销毁组件。
左边部分展示了当父 Widget 状态发生变化时,父子双方共同的生命周期;而中间和右边部分则描述了页面切换时,两个关联的 Widget 的生命周期函数是如何响应的。State生命周期中的方法调用对比如图。
abstract class WidgetsBindingObserver {
//页面pop
Future<bool> didPopRoute() => Future<bool>.value(false);
//页面push
Future<bool> didPushRoute(String route) => Future<bool>.value(false);
//系统窗口相关改变回调,如旋转
void didChangeMetrics() {
}
//文本缩放系数变化
void didChangeTextScaleFactor() {
}
//系统亮度变化
void didChangePlatformBrightness() {
}
//本地化语言变化
void didChangeLocales(List<Locale> locale) {
}
//App生命周期变化
void didChangeAppLifecycleState(AppLifecycleState state) {
}
//内存警告回调
void didHaveMemoryPressure() {
}
//Accessibility相关特性回调
void didChangeAccessibilityFeatures() {
}
}
didChangeAppLifecycleState 回调函数中,有一个参数类型为 AppLifecycleState 的枚举类,这个枚举类是 Flutter 对 App 生命周期状态的封装。它的常用状态包括 resumed、inactive、paused 这三个。
class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver{
...
@override
@mustCallSuper
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);//注册监听器
}
@override
@mustCallSuper
void dispose(){
super.dispose();
WidgetsBinding.instance.removeObserver(this);//移除监听器
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) async {
print("$state");
if (state == AppLifecycleState.resumed) {
//do sth
}
}
}
试着切换一下前、后台,观察控制台输出的 App 状态,可以发现:
除了需要监听 App 的生命周期回调做相应的处理之外,有时候我们还需要在组件渲染之后做一些与显示安全相关的操作。在 Android 开发中,我们可以通过 View.post() 插入消息队列,来保证在组件渲染后进行相关操作。
在 Flutter 中实现同样的需求会更简单:依然使用万能的 WidgetsBinding 来实现。
WidgetsBinding 提供了单次 Frame 绘制回调,以及实时 Frame 绘制回调两种机制,来分别满足不同的需求。
WidgetsBinding.instance.addPostFrameCallback((_){
print("单次Frame绘制回调");//只回调一次
});
WidgetsBinding.instance.addPersistentFrameCallback((_){
print("实时Frame绘制回调");//每帧都回调
});
Text(
'文本是视图系统中的常见控件,用来显示一段特定样式的字符串,就比如Android里的TextView,或是iOS中的UILabel。',
textAlign: TextAlign.center,//居中显示
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20, color: Colors.red),//20号红色粗体展示
);
TextStyle blackStyle = TextStyle(fontWeight: FontWeight.normal, fontSize: 20, color: Colors.black); //黑色样式
TextStyle redStyle = TextStyle(fontWeight: FontWeight.bold, fontSize: 20, color: Colors.red); //红色样式
Text.rich(
TextSpan(
children: [
TextSpan(text:'文本是视图系统中常见的控件,它用来显示一段特定样式的字符串,类似', style: redStyle), //第1个片段,红色样式
TextSpan(text:'Android', style: blackStyle), //第1个片段,黑色样式
TextSpan(text:'中的', style:redStyle), //第1个片段,红色样式
TextSpan(text:'TextView', style: blackStyle) //第1个片段,黑色样式
]),
textAlign: TextAlign.center,
);
FadeInImage.assetNetwork(
placeholder: 'assets/loading.gif', //gif占位
image: 'https://xxx/xxx/xxx.jpg',
fit: BoxFit.cover, //图片拉伸模式
width: 200,
height: 200,
)
ImageCache 使用 LRU(Least Recently Used,最近最少使用)算法进行缓存更新策略,并且默认最多存储 1000 张图片,最大缓存限制为 100MB,当限定的空间已存满数据时,把最久没有被访问到的图片清除。图片缓存只会在运行期间生效,也就是只缓存在内存中。如果想要支持缓存到文件系统,可以使用第三方的CachedNetworkImage控件。
CachedNetworkImage 的使用方法与 Image 类似,除了支持图片缓存外,还提供了比 FadeInImage 更为强大的加载过程占位与加载错误占位,可以支持比用图片占位更灵活的自定义控件占位。
CachedNetworkImage(
imageUrl: "http://xxx/xxx/jpg",
placeholder: (context, url) => CircularProgressIndicator(),
//错误图兜底,以备图片加载出错
errorWidget: (context, url, error) => Icon(Icons.error),
)
FloatingActionButton(onPressed: () => print('FloatingActionButton pressed'),child: Text('Btn'),);
FlatButton(onPressed: () => print('FlatButton pressed'),child: Text('Btn'),);
RaisedButton(onPressed: () => print('RaisedButton pressed'),child: Text('Btn'),);
FlatButton(
color: Colors.yellow, //设置背景色为黄色
shape:BeveledRectangleBorder(borderRadius: BorderRadius.circular(20.0)), //设置斜角矩形边框
colorBrightness: Brightness.light, //确保文字按钮为深色
onPressed: () => print('FlatButton pressed'),
child: Row(children: <Widget>[Icon(Icons.add), Text("Add")],)
);
ListView(
children: <Widget>[
//设置ListTile组件的标题与图标
ListTile(leading: Icon(Icons.map), title: Text('Map')),
ListTile(leading: Icon(Icons.mail), title: Text('Mail')),
ListTile(leading: Icon(Icons.message), title: Text('Message')),
]);
备注:ListTile 是 Flutter 提供的用于快速构建列表项元素的一个小组件单元,用于 1~3 行(leading、title、subtitle)展示文本、图标等视图元素的场景,通常与 ListView 配合使用。上面这段代码中用到 ListTile,是为了演示 ListView 的能力。
ListView(
scrollDirection: Axis.horizontal,
itemExtent: 140, //item延展尺寸(宽度)
children: <Widget>[
Container(color: Colors.black),
Container(color: Colors.red),
Container(color: Colors.blue),
Container(color: Colors.green),
Container(color: Colors.yellow),
Container(color: Colors.orange),
]);
考虑到创建子 Widget 产生的性能问题,更好的方法是抽象出创建子 Widget 的方法,交由 ListView 统一管理,在真正需要展示该子 Widget 时再去创建。
ListView 的另一个构造函数 ListView.builder,则适用于子 Widget 比较多的场景。这个构造函数有两个关键参数:
ListView.builder(
itemCount: 100, //元素个数
itemExtent: 50.0, //列表项高度
itemBuilder: (BuildContext context, int index) => ListTile(title: Text("title $index"), subtitle: Text("body $index"))
);
//使用ListView.separated设置分割线
ListView.separated(
itemCount: 100,
separatorBuilder: (BuildContext context, int index) => index %2 ==0? Divider(color: Colors.green) : Divider(color: Colors.red),//index为偶数,创建绿色分割线;index为奇数,则创建红色分割线
itemBuilder: (BuildContext context, int index) => ListTile(title: Text("title $index"), subtitle: Text("body $index"))//创建子Widget
)
CustomScrollView(
slivers: <Widget>[
SliverAppBar(//SliverAppBar作为头图控件
title: Text('CustomScrollView Demo'),//标题
floating: true,//设置悬浮样式
flexibleSpace: Image.network("https://xx.jpg",fit:BoxFit.cover),//设置悬浮头图背景
expandedHeight: 300,//头图控件高度
),
SliverList(//SliverList作为列表控件
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(title: Text('Item #$index')),//列表项创建方法
childCount: 100,//列表元素个数
),
),
]);
/**
* 声明了一个有着 100 个元素的列表项,当滚动视图到特定位置后,用户可以点击按钮返回列表顶部:
*/
class MyAPPState extends State<MyApp> {
ScrollController _controller;//ListView控制器
bool isToTop = false;//标示目前是否需要启用"Top"按钮
@override
void initState() {
_controller = ScrollController();
_controller.addListener(() {
//为控制器注册滚动监听方法
if(_controller.offset > 1000) {
//如果ListView已经向下滚动了1000,则启用Top按钮
setState(() {
isToTop = true;});
} else if(_controller.offset < 300) {
//如果ListView向下滚动距离不足300,则禁用Top按钮
setState(() {
isToTop = false;});
}
});
super.initState();
}
Widget build(BuildContext context) {
return MaterialApp(
...
//顶部Top按钮,根据isToTop变量判断是否需要注册滚动到顶部的方法
RaisedButton(onPressed: (isToTop ? () {
if(isToTop) {
_controller.animateTo(.0,
duration: Duration(milliseconds: 200),
curve: Curves.ease
);//做一个滚动到顶部的动画
}
}:null),child: Text("Top"),)
...
ListView.builder(
controller: _controller,//初始化传入控制器
itemCount: 100,//列表元素总数
itemBuilder: (context, index) => ListTile(title: Text("Index : $index")),//列表项构造方法
)
...
);
@override
void dispose() {
_controller.dispose(); //销毁控制器
super.dispose();
}
}
Widget build(BuildContext context) {
return MaterialApp(
title: 'ScrollController Demo',
home: Scaffold(
appBar: AppBar(title: Text('ScrollController Demo')),
body: NotificationListener<ScrollNotification>(//添加NotificationListener作为父容器
onNotification: (scrollNotification) {
//注册通知回调
ScrollMetrics metrics = notification.metrics;
print(metrics.pixels);// 当前位置
print(metrics.atEdge);//是否在顶部或底部
print(metrics.axis);//垂直或水平滚动
print(metrics.axisDirection);// 滚动方向是down还是up
print(metrics.extentAfter);//视口底部距离列表底部有多大
print(metrics.extentBefore);//视口顶部距离列表顶部有多大
print(metrics.extentInside);//视口范围内的列表长度
print(metrics.maxScrollExtent);//最大滚动距离,列表长度-视口长度
print(metrics.minScrollExtent);//最小滚动距离
print(metrics.viewportDimension);//视口长度
print(metrics.outOfRange);//是否越过边界
return true;
},
child: ListView.builder(
itemCount: 30,//列表元素个数
itemBuilder: (context, index) => ListTile(title: Text("Index : $index")),//列表项创建方法
),
)
)
);
}
在ListView中,如何提前缓存子元素?
答:ListView构造函数中有一个cacheExtent参数,即预渲染区域长度,ListView会在其可视化区域的两边留一个cacheExtent长度的区域作为预渲染区域,相当于提前缓存些元素,这样当滑动时迅速呈现。
Container(
child: Text('Container(容器)在UI框架中是一个很常见的概念,Flutter也不例外。'),
padding: EdgeInsets.all(18.0), // 内边距
margin: EdgeInsets.all(44.0), // 外边距
width: 180.0,
height:240,
alignment: Alignment.center, // 子Widget居中对齐
decoration: BoxDecoration( //Container样式
color: Colors.red, // 背景色
borderRadius: BorderRadius.circular(10.0), // 圆角边框
),
)
Padding(
padding: EdgeInsets.all(44.0),
child: Text('Container(容器)在UI框架中是一个很常见的概念,Flutter也不例外。'),
);
Scaffold(
body: Center(child: Text("Hello")) // This trailing comma makes auto-formatting nicer for build methods.
);
Container(
child: Center(child: Text('Container(容器)在UI框架中是一个很常见的概念,Flutter也不例外。')),
padding: EdgeInsets.all(18.0), // 内边距
margin: EdgeInsets.all(44.0), // 外边距
width: 180.0,
height:240,
decoration: BoxDecoration( //Container样式
color: Colors.red, // 背景色
borderRadius: BorderRadius.circular(10.0), // 圆角边框
),
);
//Row的用法示范
Row(
children: <Widget>[
Container(color: Colors.yellow, width: 60, height: 80,),
Container(color: Colors.red, width: 100, height: 180,),
Container(color: Colors.black, width: 60, height: 80,),
Container(color: Colors.green, width: 60, height: 80,),
],
);
//Column的用法示范
Column(
children: <Widget>[
Container(color: Colors.yellow, width: 60, height: 80,),
Container(color: Colors.red, width: 100, height: 180,),
Container(color: Colors.black, width: 60, height: 80,),
Container(color: Colors.green, width: 60, height: 80,),
],
);
Row(
children: <Widget>[
Expanded(flex: 1, child: Container(color: Colors.yellow, height: 60)), //设置了flex=1,因此宽度由Expanded来分配
Container(color: Colors.red, width: 100, height: 180,),
Container(color: Colors.black, width: 60, height: 80,),
Expanded(flex: 1, child: Container(color: Colors.green,height: 60),)/设置了flex=1,因此宽度由Expanded来分配
],
);
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, //由于容器与子Widget一样宽,因此这行设置排列间距的代码并未起作用
mainAxisSize: MainAxisSize.min, //让容器宽度与所有子Widget的宽度一致
children: <Widget>[
Container(color: Colors.yellow, width: 60, height: 80,),
Container(color: Colors.red, width: 100, height: 180,),
Container(color: Colors.black, width: 60, height: 80,),
Container(color: Colors.green, width: 60, height: 80,),
],
)
Stack(
children: <Widget>[
Container(color: Colors.yellow, width: 300, height: 300),//黄色容器
Positioned(
left: 18.0,
top: 18.0,
child: Container(color: Colors.green, width: 50, height: 50),//叠加在黄色容器之上的绿色控件
),
Positioned(
left: 18.0,
top:70.0,
child: Text("Stack提供了层叠布局的容器"),//叠加在黄色容器之上的文本
)
],
)
class UpdateItemModel {
String appIcon;//App图标
String appName;//App名称
String appSize;//App大小
String appDate;//App更新日期
String appDescription;//App更新文案
String appVersion;//App版本
//构造函数语法糖,为属性赋值
UpdateItemModel({
this.appIcon, this.appName, this.appSize, this.appDate, this.appDescription, this.appVersion});
}
Widget buildTopRow(BuildContext context) {
return Row(//Row控件,用来水平摆放子Widget
children: <Widget>[
Padding(//Paddng控件,用来设置Image控件边距
padding: EdgeInsets.all(10),//上下左右边距均为10
child: ClipRRect(//圆角矩形裁剪控件
borderRadius: BorderRadius.circular(8.0),//圆角半径为8
child: Image.asset(model.appIcon, width: 80,height:80)图片控件//
)
),
Expanded(//Expanded控件,用来拉伸中间区域
child: Column(//Column控件,用来垂直摆放子Widget
mainAxisAlignment: MainAxisAlignment.center,//垂直方向居中对齐
crossAxisAlignment: CrossAxisAlignment.start,//水平方向居左对齐
children: <Widget>[
Text(model.appName,maxLines: 1),//App名字
Text(model.appDate,maxLines: 1),//App更新日期
],
),
),
Padding(//Paddng控件,用来设置Widget间边距
padding: EdgeInsets.fromLTRB(0,0,10,0),//右边距为10,其余均为0
child: FlatButton(//按钮控件
child: Text("OPEN"),
onPressed: onPressed,//点击回调
)
)
]);
}
Widget buildBottomRow(BuildContext context) {
return Padding(//Padding控件用来设置整体边距
padding: EdgeInsets.fromLTRB(15,0,15,0),//左边距和右边距为15
child: Column(//Column控件用来垂直摆放子Widget
crossAxisAlignment: CrossAxisAlignment.start,//水平方向距左对齐
children: <Widget>[
Text(model.appDescription),//更新文案
Padding(//Padding控件用来设置边距
padding: EdgeInsets.fromLTRB(0,10,0,0),//上边距为10
child: Text("${model.appVersion} • ${model.appSize} MB")
)
]
));
}
class UpdatedItem extends StatelessWidget {
final UpdatedItemModel model;//数据模型
//构造函数语法糖,用来给model赋值
UpdatedItem({
Key key,this.model, this.onPressed}) : super(key: key);
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return Column(//用Column将上下两部分合体
children: <Widget>[
buildTopRow(context),//上半部分
buildBottomRow(context)//下半部分
]);
}
Widget buildBottomRow(BuildContext context) {
...}
Widget buildTopRow(BuildContext context) {
...}
}
//用 6 种不同颜色的画笔依次画了 6 个 1/6 圆弧,拼成了一张饼图。
class WheelPainter extends CustomPainter {
// 设置画笔颜色
Paint getColoredPaint(Color color) {
//根据颜色返回不同的画笔
Paint paint = Paint();//生成画笔
paint.color = color;//设置画笔颜色
return paint;
}
@override
void paint(Canvas canvas, Size size) {
//绘制逻辑
double wheelSize = min(size.width,size.height)/2;//饼图的尺寸
double nbElem = 6;//分成6份
double radius = (2 * pi) / nbElem;//1/6圆
//包裹饼图这个圆形的矩形框
Rect boundingRect = Rect.fromCircle(center: Offset(wheelSize, wheelSize), radius: wheelSize);
// 每次画1/6个圆弧
canvas.drawArc(boundingRect, 0, radius, true, getColoredPaint(Colors.orange));
canvas.drawArc(boundingRect, radius, radius, true, getColoredPaint(Colors.black38));
canvas.drawArc(boundingRect, radius * 2, radius, true, getColoredPaint(Colors.green));
canvas.drawArc(boundingRect, radius * 3, radius, true, getColoredPaint(Colors.red));
canvas.drawArc(boundingRect, radius * 4, radius, true, getColoredPaint(Colors.blue));
canvas.drawArc(boundingRect, radius * 5, radius, true, getColoredPaint(Colors.pink));
}
// 判断是否需要重绘,这里我们简单的做下比较即可
@override
bool shouldRepaint(CustomPainter oldDelegate) => oldDelegate != this;
}
//将饼图包装成一个新的控件
class Cake extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CustomPaint(
size: Size(200, 200),
painter: WheelPainter(),
);
}
}
MaterialApp(
title: 'Flutter Demo',//标题
theme: ThemeData(//设置主题
brightness: Brightness.dark,//明暗模式为暗色
primaryColor: Colors.cyan,//主色调为青色
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
MaterialApp(
title: 'Flutter Demo',//标题
theme: ThemeData(//设置主题
brightness: Brightness.dark,//设置明暗模式为暗色
accentColor: Colors.black,//(按钮)Widget前景色为黑色
primaryColor: Colors.cyan,//主色调为青色
iconTheme:IconThemeData(color: Colors.yellow),//设置icon主题色为黄色
textTheme: TextTheme(body1: TextStyle(color: Colors.red))//设置文本颜色为红色
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
// 新建主题
Theme(
data: ThemeData(iconTheme: IconThemeData(color: Colors.red)),
child: Icon(Icons.favorite)
);
// 继承主题
Theme(
data: Theme.of(context).copyWith(iconTheme: IconThemeData(color: Colors.green)),
child: Icon(Icons.feedback)
);
Container(
color: Theme.of(context).primaryColor,//容器背景色复用应用主题色
child: Text(
'Text with a background color',
style: Theme.of(context).textTheme.title,//Text组件文本样式复用应用文本样式
));
// iOS浅色主题
final ThemeData kIOSTheme = ThemeData(
brightness: Brightness.light,//亮色主题
accentColor: Colors.white,//(按钮)Widget前景色为白色
primaryColor: Colors.blue,//主题色为蓝色
iconTheme:IconThemeData(color: Colors.grey),//icon主题为灰色
textTheme: TextTheme(body1: TextStyle(color: Colors.black))//文本主题为黑色
);
// Android深色主题
final ThemeData kAndroidTheme = ThemeData(
brightness: Brightness.dark,//深色主题
accentColor: Colors.black,//(按钮)Widget前景色为黑色
primaryColor: Colors.cyan,//主题色Wie青色
iconTheme:IconThemeData(color: Colors.blue),//icon主题色为蓝色
textTheme: TextTheme(body1: TextStyle(color: Colors.red))//文本主题色为红色
);
// 应用初始化
MaterialApp(
title: 'Flutter Demo',
theme: defaultTargetPlatform == TargetPlatform.iOS ? kIOSTheme : kAndroidTheme,//根据平台选择不同主题
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
assets
├── background.jpg
├── icons
│ └── food_icon.jpg
├── loading.gif
└── result.json
flutter:
assets:
- assets/background.jpg #挨个指定资源路径
- assets/loading.gif #挨个指定资源路径
- assets/result.json #挨个指定资源路径
- assets/icons/ #子目录批量指定
- assets/ #根目录也是可以批量指定的
rootBundle.loadString('assets/result.json').then((msg)=>print(msg));
assets
├── background.jpg //1.0x图
├── 2.0x
│ └── background.jpg //2.0x图
└── 3.0x
└── background.jpg //3.0x图
flutter:
assets:
- assets/background.jpg #1.0x图资源
fonts:
- family: RobotoCondensed #字体名字
fonts:
- asset: assets/fonts/RobotoCondensed-Regular.ttf #普通字体
- asset: assets/fonts/RobotoCondensed-Italic.ttf
style: italic #斜体
- asset: assets/fonts/RobotoCondensed-Bold.ttf
weight: 700 #粗体
Text("This is RobotoCondensed", style: TextStyle(
fontFamily: 'RobotoCondensed',//普通字体
));
Text("This is RobotoCondensed", style: TextStyle(
fontFamily: 'RobotoCondensed',
fontWeight: FontWeight.w700, //粗体
));
Text("This is RobotoCondensed italic", style: TextStyle(
fontFamily: 'RobotoCondensed',
fontStyle: FontStyle.italic, //斜体
));
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 白色背景 -->
<item android:drawable="@android:color/white" />
<item>
<!-- 内嵌一张居中展示的图片 -->
<bitmap
android:gravity="center"
android:src="@mipmap/bitmap_launcher" />
</item>
</layer-list>
name: flutter_app_example #应用名称
description: A new Flutter application. #应用描述
version: 1.0.0
#Dart运行环境区间
environment:
sdk: ">=2.1.0 <3.0.0"
#Flutter依赖库
dependencies:
flutter:
sdk: flutter
cupertino_icons: ">0.1.1"
environment:
sdk: 2.3.0
flutter: 1.2.1
dependencies:
package1:
path: ../package1/ #路径依赖
date_format:
git:
url: https://github.com/xxx/package2.git #git依赖
pubspec.yaml
└──assets
├──2.0x
│ └── placeholder.png
└──3.0x
└── placeholder.png
Image.asset('assets/placeholder.png', package: 'package4');
AssetImage('assets/placeholder.png', package: 'package4');
dependencies:
date_format: 1.0.6
print(formatDate(DateTime.now(), [mm, '月', dd, '日', hh, ':', n]));
//输出2019年06月30日01:56
print(formatDate(DateTime.now(), [m, '月第', w, '周']));
//输出6月第5周
Listener(
child: Container(
color: Colors.red,//背景色红色
width: 300,
height: 300,
),
onPointerDown: (event) => print("down $event"),//手势按下回调
onPointerMove: (event) => print("move $event"),//手势移动回调
onPointerUp: (event) => print("up $event"),//手势抬起回调
);
//红色container坐标
double _top = 0.0;
double _left = 0.0;
Stack(//使用Stack组件去叠加视图,便于直接控制视图坐标
children: <Widget>[
Positioned(
top: _top,
left: _left,
child: GestureDetector(//手势识别
child: Container(color: Colors.red,width: 50,height: 50),//红色子视图
onTap: ()=>print("Tap"),//点击回调
onDoubleTap: ()=>print("Double Tap"),//双击回调
onLongPress: ()=>print("Long Press"),//长按回调
onPanUpdate: (e) {
//拖动回调
setState(() {
//更新位置
_left += e.delta.dx;
_top += e.delta.dy;
});
},
),
)
],
);
GestureDetector(
onTap: () => print('Parent tapped'),//父视图的点击回调
child: Container(
color: Colors.pinkAccent,
child: Center(
child: GestureDetector(
onTap: () => print('Child tapped'),//子视图的点击回调
child: Container(
color: Colors.blueAccent,
width: 200.0,
height: 200.0,
),
),
),
),
);
class MultipleTapGestureRecognizer extends TapGestureRecognizer {
@override
void rejectGesture(int pointer) {
acceptGesture(pointer);
}
}
RawGestureDetector(//自己构造父Widget的手势识别映射关系
gestures: {
//建立多手势识别器与手势识别工厂类的映射关系,从而返回可以响应该手势的recognizer
MultipleTapGestureRecognizer: GestureRecognizerFactoryWithHandlers<
MultipleTapGestureRecognizer>(
() => MultipleTapGestureRecognizer(),
(MultipleTapGestureRecognizer instance) {
instance.onTap = () => print('parent tapped ');//点击回调
},
)
},
child: Container(
color: Colors.pinkAccent,
child: Center(
child: GestureDetector(//子视图可以继续使用GestureDetector
onTap: () => print('Child tapped'),
child: Container(...),
),
),
),
);
通过组合嵌套的方式,利用数据对基础 Widget 的样式进行视觉属性定制,我们已经实现了多种界面布局,在 Flutter 中实现跨组件数据传递的标准方式是通过属性传值。
对于稍微复杂一点的、尤其视图层级比较深的 UI 样式,一个属性可能需要跨越很多层才能传递给子组件,这种传递方式就会导致中间很多并不需要这个属性的组件也需要接收其子 Widget 的数据,不仅繁琐而且冗余。
所以,对于数据的跨层传递,Flutter 还提供了三种方案:InheritedWidget、Notification 和 EventBus。接下来,我将依次为你讲解这三种方案。
class CountContainer extends InheritedWidget {
//方便其子Widget在Widget树中找到它
static CountContainer of(BuildContext context) => context.inheritFromWidgetOfExactType(CountContainer) as CountContainer;
final int count;
CountContainer({
Key key,
@required this.count,
@required Widget child,
}): super(key: key, child: child);
// 判断是否需要更新
@override
bool updateShouldNotify(CountContainer oldWidget) => count != oldWidget.count;
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
//将CountContainer作为根节点,并使用0作为初始化count
return CountContainer(
count: 0,
child: Counter()
);
}
}
class Counter extends StatelessWidget {
@override
Widget build(BuildContext context) {
//获取InheritedWidget节点
CountContainer state = CountContainer.of(context);
return Scaffold(
appBar: AppBar(title: Text("InheritedWidget demo")),
body: Text(
'You have pushed the button this many times: ${state.count}',
),
);
}
class CountContainer extends InheritedWidget {
...
final _MyHomePageState model;//直接使用MyHomePage中的State获取数据
final Function() increment;
CountContainer({
Key key,
@required this.model,
@required this.increment,
@required Widget child,
}): super(key: key, child: child);
...
}
class _MyHomePageState extends State<MyHomePage> {
int count = 0;
void _incrementCounter() => setState(() {
count++;});//修改计数器
@override
Widget build(BuildContext context) {
return CountContainer(
model: this,//将自身作为model交给CountContainer
increment: _incrementCounter,//提供修改数据的方法
child:Counter()
);
}
}
class Counter extends StatelessWidget {
@override
Widget build(BuildContext context) {
//获取InheritedWidget节点
CountContainer state = CountContainer.of(context);
return Scaffold(
...
body: Text(
'You have pushed the button this many times: ${state.model.count}', //关联数据读方法
),
floatingActionButton: FloatingActionButton(onPressed: state.increment), //关联数据修改方法
);
}
}
class CustomNotification extends Notification {
CustomNotification(this.msg);
final String msg;
}
//抽离出一个子Widget用来发通知
class CustomChild extends StatelessWidget {
@override
Widget build(BuildContext context) {
return RaisedButton(
//按钮点击时分发通知
onPressed: () => CustomNotification("Hi").dispatch(context),
child: Text("Fire Notification"),
);
}
}
class _MyHomePageState extends State<MyHomePage> {
String _msg = "通知:";
@override
Widget build(BuildContext context) {
//监听通知
return NotificationListener<CustomNotification>(
onNotification: (notification) {
setState(() {
_msg += notification.msg+" ";});//收到子Widget通知,更新msg
},
child:Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[Text(_msg),CustomChild()],//将子Widget加入到视图树中
)
);
}
}
dependencies:
event_bus: 1.1.0
class CustomEvent {
String msg;
CustomEvent(this.msg);
}
//建立公共的event bus
EventBus eventBus = new EventBus();
//第一个页面
class _FirstScreenState extends State<FirstScreen> {
String msg = "通知:";
StreamSubscription subscription;
@override
initState() {
//监听CustomEvent事件,刷新UI
subscription = eventBus.on<CustomEvent>().listen((event) {
setState(() {
msg+= event.msg;});//更新msg
});
super.initState();
}
dispose() {
subscription.cancel();//State销毁时,清理注册
super.dispose();
}
@override
Widget build(BuildContext context) {
return new Scaffold(
body:Text(msg),
...
);
}
}
class SecondScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new Scaffold(
...
body: RaisedButton(
child: Text('Fire Event'),
// 触发CustomEvent事件
onPressed: ()=> eventBus.fire(CustomEvent("hello"))
),
);
}
}
class FirstScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return RaisedButton(
//打开页面
onPressed: ()=> Navigator.push(context, MaterialPageRoute(builder: (context) => SecondScreen()));
);
}
}
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return RaisedButton(
// 回退页面
onPressed: ()=> Navigator.pop(context)
);
}
}
MaterialApp(
...
//注册路由
routes:{
"second_page":(context)=>SecondPage(),
},
);
//使用名字打开页面
Navigator.pushNamed(context,"second_page");
MaterialApp(
...
//注册路由
routes:{
"second_page":(context)=>SecondPage(),
},
//错误路由处理,统一返回UnknownPage
onUnknownRoute: (RouteSettings setting) => MaterialPageRoute(builder: (context) => UnknownPage()),
);
//使用错误名字打开页面
Navigator.pushNamed(context,"unknown_page");
//打开页面时传递字符串参数
Navigator.of(context).pushNamed("second_page", arguments: "Hey");
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
//取出路由参数
String msg = ModalRoute.of(context).settings.arguments as String;
return Text(msg);
}
}
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: <Widget>[
Text('Message from first screen: $msg'),
RaisedButton(
child: Text('back'),
//页面关闭时传递参数
onPressed: ()=> Navigator.pop(context,"Hi")
)
]
));
}
}
class _FirstPageState extends State<FirstPage> {
String _msg='';
@override
Widget build(BuildContext context) {
return new Scaffold(
body: Column(children: <Widget>[
RaisedButton(
child: Text('命名路由(参数&回调)'),
//打开页面,并监听页面关闭时传递的参数
onPressed: ()=> Navigator.pushNamed(context, "third_page",arguments: "Hey").then((msg)=>setState(()=>_msg=msg)),
),
Text('Message from Second screen: $_msg'),
],),
);
}
}
问题:Navigator.pushA->B->C->D,请问如何 D页面 pop 到 B 呢?
答:Navigator.popUntil(context,ModalRoute.withName(‘B’));