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();
}
複製代碼