Flutter开发实战初级(2)布局详解github
若是不知道怎么搭建开发环境,能够参考这篇博客:Flutter开发实战初级(0)Mac下Fullter环境搭建数组
源码下载点击这里:flutter_listview_demo 先来看一下效果图,下面是运行在iphone11上面的: 缓存
在Flutter中,用ListView来显示列表项,支持垂直和水平方向展现,经过一个属性咱们就能够控制其方向 1.水平的列表 2.垂直的列表 3.数据量很是大的列表 4.内置的ListTile(挺好用的)bash
class Car {
const Car({
this.name,
this.imageUrl,
});
final String name;
final String imageUrl;
}
复制代码
//模型数组
final List<Car> datas = [
Car(
name: '保时捷918 Spyder',
imageUrl:
'https://upload-images.jianshu.io/upload_images/2990730-7d8be6ebc4c7c95b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240',
),
Car(
name: '兰博基尼Aventador',
imageUrl:
'https://upload-images.jianshu.io/upload_images/2990730-e3bfd824f30afaac?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240',
),
Car(
name: '法拉利Enzo',
imageUrl:
'https://upload-images.jianshu.io/upload_images/2990730-a1d64cf5da2d9d99?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240',
),
Car(
name: 'Zenvo ST1',
imageUrl:
'https://upload-images.jianshu.io/upload_images/2990730-bf883b46690f93ce?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240',
),
Car(
name: '迈凯伦F1',
imageUrl:
'https://upload-images.jianshu.io/upload_images/2990730-5a7b5550a19b8342?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240',
),
Car(
name: '萨林S7',
imageUrl:
'https://upload-images.jianshu.io/upload_images/2990730-2e128d18144ad5b8?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240',
),
Car(
name: '科尼赛克CCR',
imageUrl:
'https://upload-images.jianshu.io/upload_images/2990730-01ced8f6f95219ec?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240',
),
Car(
name: '布加迪Chiron',
imageUrl:
'https://upload-images.jianshu.io/upload_images/2990730-7fc8359eb61adac0?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240',
),
Car(
name: '轩尼诗Venom GT',
imageUrl:
'https://upload-images.jianshu.io/upload_images/2990730-d332bf510d61bbc2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240',
),
Car(
name: '西贝尔Tuatara',
imageUrl:
'https://upload-images.jianshu.io/upload_images/2990730-3dd9a70b25ae6bc9?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240',
)
];
复制代码
@override
Widget build(BuildContext context) {
// TODO: implement build
return ListView.builder(
//控制方向 默认是垂直的
// scrollDirection: Axis.horizontal, //控制水平方向显示
/* children: <Widget>[ _getContainer('Maps', Icons.map), _getContainer('phone', Icons.phone), _getContainer('Maps', Icons.map), ], */
itemCount: datas.length, //告诉ListView总共有多少个cell
itemBuilder: _cellForRow //使用_cellForRow回调返回每一个cell
);
}
复制代码
Widget _cellForRow(BuildContext context, int index) {
return Container(
color: Colors.white,
margin: EdgeInsets.all(10),
child: Column(
children: <Widget>[
Image.network(
datas[index].imageUrl
),
SizedBox(
height: 10,
),
Text(
datas[index].name,
style: TextStyle(
fontWeight: FontWeight.w800,
fontSize: 18.0,
fontStyle: FontStyle.values[1]
),
),
Container(height: 20,),
],
), //每人一辆跑车
);
}
复制代码
import 'package:flutter/material.dart';
import 'model/carlistview.dart';
//若是只有一行代码,能够是 => 代替 {}
void main() => runApp(KYLApp());
class KYLApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Home(),
theme: ThemeData(
primaryColor: Colors.yellow
),
);
}
}
class Home extends StatelessWidget{
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
backgroundColor: Colors.grey[100],
appBar: AppBar(
title: Text('kongyulu first app'),
),
body: ListViewDemo(),
);
}
}
复制代码
咱们处理手势可使用GestureDetector组件,它是能够添加手势的一个widget,观察它的源码:微信
class GestureDetector extends StatelessWidget {
GestureDetector({
Key key,
this.child,
this.onTapDown,
this.onTapUp,
this.onTap,
this.onTapCancel,
this.onDoubleTap,
this.onLongPress,
this.onLongPressUp,
this.onVerticalDragDown,
this.onVerticalDragStart,
this.onVerticalDragUpdate,
this.onVerticalDragEnd,
this.onVerticalDragCancel,
this.onHorizontalDragDown,
this.onHorizontalDragStart,
this.onHorizontalDragUpdate,
this.onHorizontalDragEnd,
this.onHorizontalDragCancel,
this.onPanDown,
this.onPanStart,
this.onPanUpdate,
this.onPanEnd,
this.onPanCancel,
this.onScaleStart,
this.onScaleUpdate,
this.onScaleEnd,
this.behavior,
this.excludeFromSemantics = false
})
复制代码
能够看到GestureDetector的本质就是一个普通的widget,它拥有不少的手势onTapDown(点下),onTapUp(抬起),onTap(点击)...等,同时也拥有child属性,咱们能够利用child绘制界面,利用手势处理点击事件。app
首先咱们须要明白,Widget 是什么?这里有一个 “总所周知” 的答就是:Widget并不真正的渲染对象 。是的,事实上在 Flutter 中渲染是经历了从 Widget 到 Element 再到 RenderObject 的过程。框架
咱们都知道 Widget 是不可变的,那么 Widget 是如何在不可变中去构建画面的?上面咱们知道,Widget 是须要转化为 Element 去渲染的,而从下图注释能够看到,事实上 Widget 只是 Element 的一个配置描述 ,告诉 Element 这个实例如何去渲染。
那么RenderObject 又是什么?它和上述两个的关系是什么?从源码注释写着 An object in the render tree 能够看出到 RenderObject 才是实际的渲染对象,而经过 Element 源码咱们能够看出:Element 持有 RenderObject 和 Widget。
再结合下图,能够大体总结出三者的关系是:配置文件 Widget 生成了 Element,然后建立 RenderObject 关联到 Element 的内部 renderObject 对象上,最后Flutter 经过 RenderObject 数据来布局和绘制。 理论上你也能够认为 RenderObject 是最终给 Flutter 的渲染数据,它保存了大小和位置等信息,Flutter 经过它去绘制出画面。
RenderBox 避免了直接使用 RenderObject 的麻烦场景,其中 RenderBox 的布局和计算大小是在 performLayout() 和 performResize() 这两个方法中去处理,不少时候咱们更多的是选择继承 RenderBox 去实现自定义。
由此可知:Widget 从新建立,Element 树和 RenderObject 树并不会彻底从新建立。
看到这,说个题外话:那通常咱们能够怎么获取布局的大小和位置呢?
首先这里须要用到咱们前文中提过的 GlobalKey ,经过 key 去获取到控件对象的 BuildContext,而咱们也知道 BuildContext 的实现实际上是 Element,而Element持有 RenderObject 。So,咱们知道的 RenderObject ,实际上获取到的就是 RenderBox ,那么经过 RenderBox 咱们就只大小和位置了。
showSizes() {
RenderBox renderBoxRed = fileListKey.currentContext.findRenderObject();
print(renderBoxRed.size);
}
showPositions() {
RenderBox renderBoxRed = fileListKey.currentContext.findRenderObject();
print(renderBoxRed.localToGlobal(Offset.zero));
}
复制代码
通俗点讲就是:
stateful组件就是和用户交互后会有状态变化,例如滚动条Slider。
stateless组件就是交互后没有状态变化,例如显示的一个文本Text。
复制代码
Widget实现者的责任就是 在状态改变时经过 State.setState. 当即通知状态
当您描述的用户界面部分不依赖于对象自己中的配置信息和其中构件被夸大的BuildContext时,无状态小部件颇有用。对于能够动态改变的组合,例如因为具备内部时钟驱动状态,或取决于某些系统状态,请考虑使用StatefulWidget。
StatefulWidget实例自己是不可变的,并将其可变状态存储在由createState方法建立的独立状态对象中 ,或者存储在该状态订阅的对象中,例如Stream或ChangeNotifier对象,其引用存储在StatefulWidget的最终字段中自己。
该框架只要调用一个StatefulWidget就 调用createState,这意味着若是该小部件已经插入到多个位置的树中,那么多个State对象可能与同一个StatefulWidget关联。一样,若是StatefulWidget从树中移除,后来在树再次插入时,框架将调用createState再建立一个新的国家目标,简化的生命周期状态的对象。
不须要可变状态的小部件。
无状态小部件是一个小部件,它经过构建一系列其余小部件来更加具体地描述用户界面,从而描述用户界面的一部分。构建过程以递归方式继续进行,直到用户界面的描述彻底具体(例如,彻底由RenderObjectWidget组成,它描述具体的RenderObject)。
当您描述的用户界面部分不依赖于对象自己中的配置信息和其中构件被夸大的BuildContext时,无状态小部件颇有用。对于能够动态改变的组合,例如因为具备内部时钟驱动状态,或取决于某些系统状态,请考虑使用StatefulWidget。
无状态小部件的构建方法一般只在如下三种状况下调用:第一次将小部件插入树中,第一次在小部件的父级更改其配置时以及第二次使用InheritedWidget时,它依赖于更改。
若是一个小部件的父节点会按期更改小部件的配置,或者若是它依赖于频繁更改的继承小部件,那么优化构建方法的性能以保持流畅的渲染性能很是重要。
有几种技术能够用来最小化重建无状态小部件的影响:
最小化构建方法及其建立的任何小部件传递建立的节点数量。例如,能够考虑只使用一个Align或一个 CustomSingleChildLayout,而不是精心安排Row s,Column s,Padding s和SizedBox es来定位一个单独的孩子。您能够考虑使用单个CustomPaint小部件,而不是使用多个Container的复杂分层和装饰 s来绘制恰当的图形效果。 const尽量使用小部件,并为小部件提供const构造函数,以便小部件的用户也能够这样作。 考虑将无状态小部件重构为有状态的小部件,以便它可使用StatefulWidget中描述的一些技术,例如缓存子树的公共部分,并在更改树结构时使用GlobalKey。 若是因为使用了InheritedWidget,小部件可能会常常重建 ,请考虑将无状态小部件重构为多个小部件,并将更改后的树部分推送到树叶。例如,不是构建一个具备四个小部件的树,最内部的小部件取决于主题,而是考虑将构建最内部小部件的构建函数的部分分解到其本身的小部件中,以便只有最内部的小部件当主题改变时须要重建。
Flutter的Widget有StatelessWidget和StatefulWidget两个子类(固然还有其余子类,此处暂且不谈),两者的的使用方式大体模板代码以下:
//StatelessWidget的使用模板代码
class StatelessWidgetDemo extends StatelessWidget{
@override
Widget build(BuildContext context) {
return null;///返回建立的页面
}
}
//StatefulWidget的使用方式模板代码
class StatefulWidgetDemo extends StatefulWidget{
@override
State<StatefulWidget> createState() {
//建立state对象
return _State();
}
}
class _State extends State<StatefulWidgetDemo>{
//建立页面
@override
Widget build(BuildContext context) {
return null;
}
}
复制代码
这是典型的模板设计模式的应用,咱们只须要依葫芦画瓢就能够建立所需的UI页 阅读上面的代码,能够跑出一下问题: 1) build方法须要一个BuildContext参数,那么这个BuildContext是什么? 2)build方法是模板方法,那么何时调用的呢? 带着这两个问题,后面简单的梳理下Widget的结构,之因此说是简单的梳理,由于可贵我也不会,还没研究到。
StatelessWidget和StatefulWidget都继承于Widget,其定义以下:
abstract class Widget extends DiagnosticableTree {
const Widget({ this.key });
final Key key;
@protected
Element createElement();
}
复制代码
Widget继承于DiagnosticableTree,且提供了一个createElement抽象方法返回了一个Element对象,该对象查看源码可知其继承解构是Element extends DiagnosticableTree implements BuildContext.因此其Widget 和Element的总体解构能够用以下图表示:
abstract class StatelessWidget extends Widget {
@override
StatelessElement createElement() => StatelessElement(this);
@protected
Widget build(BuildContext context);
}
复制代码
StatelessWidget实现了createElement方法返回了一个StatelessElement对象,且提供了一个build方法,注意build方法的参数是BuildContext,那么这个BuildContext是否是就是StatelessElement这个对象了呢?预知答案如何先看看build是在那儿调用的,在StatelessElement这个类里能够找到答案,其源码以下:
class StatelessElement extends ComponentElement {
//在element中调用了widget.build方法,并将本身传入了进去
//因此BuildContext就是StatelessElement
@override
Widget build() => widget.build(this);
}
复制代码
经过其源码能够知道StatelessElement继承了ComponentElement,且重写了build方法,其调用了widget的build方法。这个build就是StatelessWidget对象(或者其子对象),而且能够肯定StatelessWidget的build方法的参数就是StatelessElement这个对象。
因此能够判定想要知道StatelessWidget的build(BuildContext)方法何时调用,就须要知道StatelessElement的build()何时调用。在StatelessElement的父类ComponentElement的perfromReBuild方法能够获得解答:
@override
void performRebuild() {
//省略了部分代码
Widget built = build();
//省略部分代码
}
复制代码
因此概述下来就是StatelessWidget经过build(BuildContext)方法构建Widget是经过StatelessElement的build()方法来完成的。想要调用build(BuildContext)一定先经过createElement方法建立一个StatelessElement对象。那么有一个此处就有一个问题了:Widget的createElement方法是神马时候调用的呢?
上面粗略的分了StatelessWidget,下来再来简略的看下StatefullWidget这个类。
abstract class StatefulWidget extends Widget {
@override
StatefulElement createElement() => StatefulElement(this);
@protected
State createState();
}
复制代码
StatefulWidget的createElement方法返回了SatefulElement,且提供了一个createState()方法,大胆猜想一下createState就是在StatefulElement里面调用的,果不其然,证据以下:
StatefulElement 的构造器:
StatefulElement(StatefulWidget widget)
///调用了createState方法
: _state = widget.createState(), super(widget) {
}
复制代码
StatefulWidget须要经过createState方法建立一个State,State也提供了build(BuildContext)方法。另外查看StatefulElement的能够该类也实现了ComponentElement的build方法:
@override
Widget build() => state.build(this);
复制代码
分析到这儿StatelessWidget ,StatefulWidget和Element的关系能够用以下图来表示:
build(BuildContext)方法就须要先调用具体子类的createElement方法建立对应的ComponentElement对象,然后重写Component的build方法。performRebuild方法又是什么时机调用的的呢?performRebuild方法在ComponentElment的mount方法和rebuild方法()方法里面都有调用,而ComponentElement的mount方法又是Flutter造成渲染树的入口:
//mount方法造成了解析Widget,构建渲染树
@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_firstBuild();
}
void _firstBuild() {
//rebuild方法内部调用了performRebuild方法。
rebuild();
}
复制代码