1. Dart 基礎語法
1.1 hello world
先來看個hello world,入口依然是main方法.
printInteger(int a) {
print('Hello world, this is $a.');
}
main() {
var number = 2019;
printInteger(number);
}
1.2 變量與類型
- 定義變量使用var或者具體類型來定義
- 未初始化的變量的值都是null
- 所有東西都是對象,都是繼承自Object.包括數字、布爾值、函數和 null
- 內置了一些基本數據類型num、bool、String、List 和 Map
- int和double都繼承自num
- 常見的運算符也是繼承自num
- Dart 的 String 由 UTF-16 的字符串組成,可以使用
$xx
或${xx}
來把表達式放入字符串中 - List和Map,可以顯式指定元素類型,也可以推斷
- 常量定義使用final(運行時常量)或const(編譯期常量)
- 儘量爲變量指定類型,這樣編輯器額編譯器能更好的理解你的意圖
var a = 1;
int b = 1;
num c = 3;
int x = 1;
int hex = 0xEEADBEEF;
double y = 1.1;
double exponents = 1.13e5;
int roundY = y.round();
//List
var arr1 = ["Tom", "Andy", "Jack"];
var arr3 = <String>["Tom", "Andy", "Jack"];
var arr2 = List.of([1, 2, 3]);
var arr4 = List<int>.of([1, 2, 3]);
arr2.add(499);
arr2.forEach((v) => print(v));
//Map
var map1 = {"name": "Tom", "sex": "male"};
var map2 = new Map();
//添加或修改
map2["name"] = "Tom";
map2["name"] = "Tom22";
map2["sex"] = "male";
print(map2);
map2.forEach((k, v) => print('k = $k v=$v'));
1.3 函數
- 函數是一個對象,類型Function,可以定義爲變量
- 函數可以只寫一句代碼,不要
{}
- 提供了可選命名參數和可選參數
- 返回值類型可以省略
void main() {
Function f = isZero;
int x = 10;
int y = 10;
printInfo(x, f);
printInfo(y, f);
enable1Flags(bold: true);
}
bool isZero(int a) {
return a == 0;
}
bool isNull(var a) => a == null;
void printInfo(int number, Function check) {
print('$number is zero: ${check(number)}');
}
//可選命名參數 Flutter 中大量使用
void enable1Flags({bool bold, bool hidden}) => print("$bold $hidden");
//可選命名參數 加默認值
void enable2Flags({bool bold = true, bool hidden = false}) =>
print('$bold $hidden');
//可忽略參數,也可以加默認值
void enable3Flags(bool bold, [bool hidden]) => print("$bold $hidden");
//返回值類型 省略
price() {
double sum = 0.0;
for (var i in booking) {
sum += i.price;
}
return sum;
}
1.4 類
- 沒有public、protected、private這些修飾符,可以在變量和方法前面加
_
,加_
的限制範圍並不是類級別的,而是庫訪問級別. - 對象調用方法時可以加
?
,加了之後如果對象是空,則跳過 - 類A可以implements另一個類B,這時相當於implements B的方法和字段.
- 當類A需要複用類B的方法時,可以使用混入(Mixin).
class A with B{}
a ?? b
a不爲null,返回a.否則返回b.
void main() {
//類 都是繼承自Object
//無修飾符關鍵字 變量與方法前面加"_"則表示private,不加則爲public
//加"_"的限制範圍並不是類訪問級別的,而是庫訪問級別的
Test test = Test();
print(test.b);
var p = Point(1, 2);
p.printInfo();
Point.factor = 10;
Point.printZValue();
//爲空時跳過執行
Point?.printZValue();
var p2 = Point2.test(1);
}
class Test {
int _a = 1;
int b = 2;
}
class Point {
num x, y;
static num factor = 0;
//語法糖,等同於在函數體內:this.x = x;this.y = y;
Point(this.x, this.y);
void printInfo() => print('($x,$y)');
static void printZValue() => print('factor=$factor');
}
class Point2 {
num x, y, z;
//z也得到了初始化
Point2(this.x, this.y) : z = 0;
//重定向構造函數
Point2.test(num x) : this(x, 0);
}
class Point3 {
num x = 0, y = 0;
void printInfo() => print('($x,$y)');
}
class Vector extends Point3 {
num z = 0;
//覆寫了父類的方法
@override
void printInfo() => print('x = $x,y=$y');
}
class Coordinate implements Point3 {
//成員變量需要重新聲明
num x = 0, y = 0;
//成員函數需要重新實現
@override
void printInfo() {}
}
//混入Mixin 可以視爲具有實現方法的接口
//非繼承的方法,使用其他類中的變量與方法
class Coordinate2 with Point3 {}
2. Flutter區別於其他方案的關鍵技術
- Flutter 使用 Native 引擎渲染視圖
- React Native 之類的框架,只是通過 JavaScript 虛擬機擴展調用系統組件,由 Android 和 iOS 系統進行組件的渲染;Flutter 則是自己完成了組件渲染的閉環。
- UI線程使用Dart來構建視圖結構數據,這些數據會在GPU線程進行圖層合成,隨後交給Skia引擎加工成GPU數據,而這些數據會通過OpenGL最終提供給GPU渲染.
- 使用Dart語言,同時支持JIT(動態編譯,需要用的時候編譯,開發的時候)和AOT(靜態編譯,先編譯好,正式包的時候)
3. Widget的設計思路和基本原理
- 核心設計思想: 一切皆Widget
- Widget是不可變的,當視圖渲染的配置信息發生變化時,Flutter會選擇重建Widget樹的方法進行數據更新.
- 數據驅動UI構建
- Widget本身不涉及實際渲染位圖,它是輕量級的數據結構,重建成本低.
- Element是Widget的一個實例化對象,它承載了視圖構建的上下文數據,是連接結構化的配置信息到完成最終渲染的橋樑
- Element同時持有Widget和RenderObject,最後負責渲染的是RenderObject
- Flutter展示過程: 佈局,繪製,合成,渲染
4. State的選擇
- Widget有StatelessWidget和StatefulWidget
- StatefulWidget對應有交互,需要動態變化視覺效果的場景,StatelessWidget則用於處理靜態的,無狀態的視圖展示
- Flutter的視圖開發是聲明式的,其核心設計思想就將視圖和數據分離
- Widget生命週期內,State中的任何更改都將強制Widget重新構建
- StatelessWidget,如Text,Container,Row,Column等.它們一旦創建成功就不再關心,也不相應任何數據變化進行重繪
- 避免無謂的StatefulWidget使用,可以提高Flutter應用渲染性能.
5. 生命週期
5.1 Widget 視圖生命週期
- 生命週期其實是State的
- State生命週期分爲3個階段
- 創建(插入視圖樹)
- 更新(在視圖樹中存在)
- 銷燬(從視圖樹中移除)
- 創建
- 構造方法
- initState
- didChangeDependencies
- build
- 更新
- setState->build
- disUpdateWidget->build
- didChangeDependencies->build
- 銷燬
- deactivate
- dispose
- 構造方法: 接收父Widget傳遞的初始化UI配置數據
- initState: State對象被插入視圖樹的時候被調用,在這裏做初始化工作
- didChangeDependencies: 處理State對象依賴關係變化,initState()調用結束後會被調用
- build: 構建視圖,在這裏根據父Widget傳遞過來的初始化配置數據,以及State狀態,創建一個Widget返回
- setState: 當狀態數據發生變化時,調用這個方法,告訴Flutter,數據變了,根據更新後的數據重建UI
- didChangeDependencies: State對象的依賴關係發生變化時(系統語言Locale或應用主題更改),系統會通知State調用此方法
- didUpdateWidget: 當Widget的配置發生變化時,如父Widget觸發重建,熱重載時,會被調用.
- deactivate: 組件的可見狀態發生變化,State會被暫時從視圖樹中移除. 頁面切換時,上一個頁面的State對象在視圖樹中的位置發生了變化,會先調用deactivate,再調用build.
- dispose: 當State被永久地從視圖樹中移除,比如關閉頁面.到這裏時,組件就要銷燬了,這裏做最終的資源釋放,移除監聽,清理環境.
方法名 | 功能 | 調用時機 | 調用次數 |
---|---|---|---|
構造方法 | 接收父Widget傳遞的初始化UI配置數據 | 創建State時 | 1 |
initState | 與渲染相關的初始化工作 | 在State被插入視圖樹時 | 1 |
didChangeDependencies | 處理State對象依賴關係變化 | initState後及State對象依賴關係變化時 | >=1 |
build | 構建視圖 | State準備好數據需要渲染時 | >=1 |
setState | 觸發視圖重建 | 需要刷新UI時 | >=1 |
didUpdateWidget | 處理Widget的配置變化 | 父Widget setState觸發子Widget重建時 | >=1 |
deactivate | 組件被移除 | 組件不可視 | >=1 |
dispose | 組件被銷燬 | 組件被永久移除 | 1 |
5.2 App(也是Widget) 生命週期
- 利用WidgetsBindingObserver類
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: 不可見並不能響應用戶的輸入,但是在後臺繼續活動中
- 在initState中註冊監聽器,在dispose中移除監聽器
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
}
}
}
- 後臺(paused)切入前臺: AppLifecycleState.inactive->AppLifecycleState.resumed
- 前臺(resumed)退回到後臺: AppLifecycleState.inactive->AppLifecycleState.paused
- WidgetsBingding提供了單次Frame繪製回調,以及實時Frame繪製回調兩種機制.
- 單次
WidgetsBinding.instance.addPostFrameCallback((_){
print("單次Frame繪製回調");//只回調一次
});
- 實時繪製
WidgetsBinding.instance.addPersistentFrameCallback((_){
print("實時Frame繪製回調");//每幀都回調
});
6. 文本
- Text,單一樣式. 構造參數分爲2類
- 控制整體文本佈局的參數: 對齊方式textAlign,文本排版方向textDirection,文本顯示最大行數 maxLines、文本截斷規則 overflow 等
- 控制文本展示樣式的參數: 統一封裝到style參數中,字體名稱fontFamily,字體大小fontSize,文本顏色color,文本陰影shadows等
Text(
'文本是視圖系統中的常見控件,用來顯示一段特定樣式的字符串,就比如Android裏的TextView,或是iOS中的UILabel。',
textAlign: TextAlign.center,//居中顯示
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20, color: Colors.red),//20號紅色粗體展示
);
- TextSpan,可展示混合樣式.(類似SpannableString)
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>[
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,
);
7. 圖片
- Image
- 加載本地資源圖片,如 Image.asset(‘images/logo.png’);
- 加載本地(File 文件)圖片,如 Image.file(new File(’/storage/xxx/xxx/test.jpg’));
- 加載網絡圖片,如 Image.network(‘http://xxx/xxx/test.gif’)
- 填充模式fit,拉伸 centerSlice,重複模式repeat
- Image通過內部ImageProvider根據緩存狀態,觸發異步加載流程,通知
_imageState
(Image這種控件肯定不是靜態的撒,得需要一個State)刷新UI. - FadeInImage,可以提供佔位圖,加載動畫等.
FadeInImage.assetNetwork(
placeholder: 'assets/loading.gif', //gif佔位
image: 'https://xxx/xxx/xxx.jpg',
fit: BoxFit.cover, //圖片拉伸模式
width: 200,
height: 200,
)
- 圖片默認緩存到內存,LRU(最近最少使用),如需緩存到本地則需要使用第三方的CachedNetworkImage(還提供了錯誤展示圖片)控件
8. 按鈕
- FloatingActionButton 圓形按鈕
- RaisedButton,凸起的按鈕,和Android默認的Button長得一樣醜
- FlatButton,扁平的按鈕,默認透明背景,被點擊後呈現灰色背景
FloatingActionButton(onPressed: () => print('FloatingActionButton pressed'),child: Text('Btn'),);
FlatButton(onPressed: () => print('FlatButton pressed'),child: Text('Btn'),);
RaisedButton(onPressed: () => print('RaisedButton pressed'),child: Text('Btn'),);
- onPressed參數用於設置回調,如果參數爲空,則按鈕會被禁用
- child參數用於控制控件長什麼樣子
- 其他豐富api
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")],)
);
- Button都是由RawMaterialButton承載視覺,Image都是RawImage,Text是RichText。它們都繼承自RenderObjectWidget,而RenderObjectWidget的父類就是Widget。
9. ListView
9.1 ListView
- 同時支持垂直方向和水平方向滾動
- 創建子視圖方式
構造函數名 | 特點 | 適用場景 | 適用頻次 |
---|---|---|---|
ListView | 一次性創建好全部子Widget | 適用於展示少量連續子Widget的場景 | 中 |
ListView.builder | 提供子Widget創建方法,僅在需要展示的時候才創建 | 適用於子Widget較多,且視覺效果呈現某種規律性的場景 | 高 |
ListView.separated | 與ListView.builder類似,並提供了自定義分割線的功能 | 與ListView.builder場景類似 | 中 |
- 第一種 ListView 直接構建
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')),
]);
- 第二種 ListView.builder.itemExtent 並不是一個必填參數。但,對於定高的列表項元素,我強烈建議你提前設置好這個參數的值。
ListView.builder(
//itemCount,表示列表項的數量,如果爲空,則表示 ListView 爲無限列表
itemCount: 100, //元素個數
itemExtent: 50.0, //列表項高度
itemBuilder: (BuildContext context, int index) => ListTile(title: Text("title $index"), subtitle: Text("body $index"))
);
- 第三種 ListView.separated
//使用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
)
9.2 CustomScrollView
- CustomScrollView是用來處理多個需要自定義滑動效果的Widget.在CustomScrollView中,這些彼此獨立的,可滑動的Widget被統稱爲Sliver.
- 比如ListView 的 Sliver 實現爲 SliverList,AppBar 的 Sliver 實現爲 SliverAppBar
- 這些Sliver不再維護各自的滾動狀態,交由CustomScrollView統一管理,最終實現滑動效果的一致性
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,//列表元素個數
),
),
]);
9.3 ScrollController
- ScrollController用於對ListView進行滾動信息的監聽,以及相應的滾動控制.
class MyControllerAppState extends State<MyControllerApp> {
//ListView控制器
ScrollController _controller;
//標識目前是否需要啓用top按鈕
bool isToTop = false;
@override
void initState() {
_controller = ScrollController();
_controller.addListener(() {
//ListView向下滾動1000 則啓用top按鈕
if (_controller.offset > 1000) {
setState(() {
isToTop = true;
});
} else if (_controller.offset < 300) {
//向下滾動不足300,則禁用按鈕
setState(() {
isToTop = false;
});
}
});
super.initState();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: ListView.builder(
//將控制器傳入
controller: _controller,
itemCount: 100,
itemExtent: 100,
itemBuilder: (context, index) =>
ListTile(title: Text('index $index'))),
floatingActionButton: RaisedButton(
//如果isToTop是true則滑動到頂部,否則禁用按鈕
onPressed: isToTop
? () {
//滑動到頂部
_controller.animateTo(0.0,
duration: Duration(microseconds: 200),
curve: Curves.ease);
}
: null,
child: Text('top'),
),
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
9.4 NotificationListener
- NotificationListener是一個Widget,需要將ListView添加到NotificationListener中
class MyListenerApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: NotificationListener<ScrollNotification>(
//添加NotificationListener作爲父容器
//註冊通知回調
onNotification: (scrollNotification) {
//開始滑動
if (scrollNotification is ScrollStartNotification) {
//scrollNotification.metrics.pixels 滑動的位置
print('scroll start ${scrollNotification.metrics.pixels}');
} else if (scrollNotification is ScrollUpdateNotification) {
//滑動中
print('scroll update');
} else if (scrollNotification is ScrollEndNotification) {
//滑動結束
print('scroll end');
}
return null;
},
child: ListView.builder(
itemCount: 100,
itemExtent: 70,
itemBuilder: (context, index) => ListTile(
title: Text('index $index'),
)),
),
),
);
}
}
10. 佈局容器
10.1 Container,Padding,Center
- Container內部提供了間距,背景樣式,圓角邊框等基礎屬性,可以控制子Widget的擺放方式(居中,左,右)
- Padding 設置間距,將Widget放裏面
- Center 設置居中,將Widget放裏面
getContainer() {
return Container(
child: Center(
child: Text('Container(容器)在UI框架中是一個很常見的概念,Flutter也不例外。'),
),
//內邊距
padding: EdgeInsets.all(18.0),
//外邊距
margin: EdgeInsets.all(44.0),
width: 180.0,
height: 240,
//子Widget居中對齊
/* alignment: Alignment.center,*/
//Container樣式
decoration: BoxDecoration(
//背景色
color: Colors.red,
//圓角邊框
borderRadius: BorderRadius.circular(10.0),
),
);
}
getPadding() {
//只需要設置邊距 可以使用Padding
return Padding(
padding: EdgeInsets.all(44.0),
child: Text('我是Padding'),
);
}
getCenter() {
//直接居中
return Center(
child: Text('center text'),
);
}
10.2 Row,Column,Expanded
- Row是水平佈局
- Column是垂直佈局
- Expanded表示將剩餘的空間,如何分配
- Row 與 Column 自身的大小由父widget的大小、子widget的大小、以及mainSize設置共同決定(mainAxisSize和crossAxisSize)
- 主軸(縱軸)值爲max:主軸(縱軸)大小等於屏幕主軸(縱軸)方向大小或者父widget主軸(縱軸)方向大小
- 主軸(縱軸)值爲min: 所有子widget組合在一起的主軸(縱軸)大小
//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來分配
],
);
對齊方式
- 根據主軸與縱軸,設置子Widget在這兩個方向上的對齊規則mainAxisAlignment與crossAxisAlignment.比如主軸方向start表示靠左對齊,center表示橫向居中對齊,end表示靠右對齊,spaceEvenly表示按固定間距對齊;而縱軸方向start則表示靠上對齊,center表示縱向居中對齊,end表示靠下對齊.
控制大小
- 如果想讓容器與子Widget在主軸上完全匹配,需要通過設置Row的mainAxisSize參數爲MainAxisSize.min,由所有子Widget來決定主軸方向的容器長度,即主軸方向的長度儘可能小.類似wrap_content.
mainAxisSize: MainAxisSize.min, //讓容器寬度與所有子Widget的寬度一致
10.3 Stack,Positioned
- Stack,類似FrameLayout.
- Stack提供了層疊佈局的容器,而Positioned則提供了設置子Widget位置的能力.
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提供了層疊佈局的容器"),//疊加在黃色容器之上的文本
)
],
)
- Positioned只能在Stack中使用.
11. 自定義控件
11.1 組合控件
- 將多個控件組合在一起
11.2 自定義控件
- CustomPaint是用來承接自繪控件的容器,並不負責真正的繪製.
- 畫布是canvas,畫筆是Paint.
- 畫成什麼樣子由CustomPainter來控制,將CustomPainter設置給容器CustomPaint的painter屬性,我們就完成了一個自繪組件的封裝
- Paint,其實和Android中的差不多,可以配置它的各種屬性,比如顏色、樣式、粗細等;而畫布 Canvas,則提供了各種常見的繪製方法,比如畫線 drawLine、畫矩形 drawRect、畫點 DrawPoint、畫路徑 drawPath、畫圓 drawCircle、畫圓弧 drawArc 等。
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;
//分成6份
double nbElem = 6;
//角度
double radius = (2 * pi) / nbElem;
//包裹餅圖的矩形框 center:相對於原點的偏移量
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.green));
canvas.drawArc(
boundingRect, radius * 2, radius, true, getColoredPaint(Colors.red));
canvas.drawArc(
boundingRect, radius * 3, radius, true, getColoredPaint(Colors.blue));
canvas.drawArc(
boundingRect, radius * 4, radius, true, getColoredPaint(Colors.pink));
canvas.drawArc(boundingRect, radius * 5, radius, true,
getColoredPaint(Colors.deepOrange));
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
//判斷是否需要重繪,簡單做下比較
return oldDelegate != this;
}
}
class Cake extends StatelessWidget {
@override
Widget build(BuildContext context) {
//CustomPaint是用來承載自定義View的容器,需要自定義一個畫筆,得繼承自CustomPainter
return CustomPaint(
size: Size(200, 200),
painter: WheelPainter(),
);
}
}
12. 主題定製
- 視覺效果是易變的,我們將這些變化的部分抽離出來,把提供不同視覺效果的資源和配置按照主題進行歸類,整合到一個統一的中間層去管理,這樣我們就能實現主題的管理和切換.
- Flutter中由ThemeData來統一管理主題的配置信息
- ThemeData中涵蓋了Material Design規範的可自定義部分樣式,比如應用明暗模式 brightness、應用主色調 primaryColor、應用次級色調 accentColor、文本字體 fontFamily、輸入框光標顏色 cursorColor 等。
- 全局統一的視覺風格:
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來對App的主題進行局部覆蓋,Theme是一個單子Widget容器,將控件放裏面就可以控制主題了.
- 局部新建主題: 如果不想繼承任何App全局的顏色或字體樣式,可以直接新建一個ThemeData實例,依次設置對應的樣式.
// 新建主題
Theme(
data: ThemeData(iconTheme: IconThemeData(color: Colors.red)),
child: Icon(Icons.favorite)
);
- 繼承主題: 如果不想在局部重寫所有的樣式,則可以繼承App的主題,使用copyWith方法,只更新部分樣式
// 繼承主題
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組件文本樣式複用應用文本樣式
));
13. 依賴管理
- 可以把資源放任意目錄,只需要使用根目錄下的pubspec.yaml文件,對這些資源的所在位置進行顯示聲明就行.
13.1 圖片
flutter:
assets:
- assets/background.jpg #挨個指定資源路徑
- assets/loading.gif #挨個指定資源路徑
- assets/result.json #挨個指定資源路徑
- assets/icons/ #子目錄批量指定
- assets/ #根目錄也是可以批量指定的
- Flutter遵循了基於像素密度的管理方式,如1.0x,2.0x,3.0x.Flutter會根據當前設備分辨率加載最接近設備像素比例的圖片資源
- 想讓Flutter適配不同的分辨率,只需要將其他分辨率的圖片放到對應的分辨率子目錄中.
目錄如下
assets
├── background.jpg //1.0x圖
├── 2.0x
│ └── background.jpg //2.0x圖
└── 3.0x
在pubspec.yaml文件聲明:
flutter:
assets:
- assets/background.jpg #1.0x圖資源
13.2 字體
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 #粗體
13.3 三方庫 三方組件庫
- Dart提供包管理工具: Pub,管理代碼和資源
- 對於包,通常是指定版本區間,而很少直接指定特定版本.
- 多人協作時,建議將Dart和Flutter的SDK環境寫死,統一團隊的開發環境.避免因爲跨SDK版本出現的API差異而導致工程問題.
dependencies:
//1. #路徑依賴
package1:
path: ../package1/
//2. github
date_format:
git:
url: https://github.com/xxx/package2.git #git依賴
//3. pub上面的
date_format: 1.0.6
14. 手勢識別
- 底層原始指針事件: 用戶的觸摸數據,如手指接觸屏幕 PointerDownEvent、手指在屏幕上移動 PointerMoveEvent、手指擡起 PointerUpEvent,以及觸摸取消 PointerCancelEvent.
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"),//手勢擡起回調
);
- 冒泡分發機制: 將觸摸事件交給最內層的組件去響應,事件會從這個最內層的組件開始,沿着組件樹向根節點向上冒泡分發.
- 封裝了底層指針事件手勢語義的Gesture,平常一般使用GestureDetector.如點擊 onTap、雙擊 onDoubleTap、長按 onLongPress、拖拽 onPanUpdate、縮放 onScaleUpdate 等。
//紅色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;
});
},
),
)
],
);
- 事件處理機制: Flutter會使用手勢競技場來進行各個手勢的PK,以保證最後只有一個手勢能夠響應用戶行爲.
- 手勢衝突只是手勢的語義化識別過程,對於底層指針事件是不會衝突的.
- 父子都有點擊事件的情況 因爲子視圖在父視圖的上面,所以如果點擊區域在子視圖區域,子視圖響應事件.
15. 跨組件共享數據
視圖層級比較深的UI樣式,直接通過屬性傳值會導致很多中間層增加冗餘屬性.
- 代碼太多,見demo https://github.com/xfhy/FlutterBasic/tree/master/lib/data
15.1 InheritedWidget
- 共享父Widget的屬性
15.2 Notification
- 從下往上的數據傳遞,在父Widget中監聽來自子Widget的事件
15.3 EventBus
- EventBus 不依賴Widget樹 這是事件總線,666
- 遵循發佈訂閱 模式
15.4 對比
方式 | 數據流動方式 | 使用場景 |
---|---|---|
屬性傳值 | 父到子 | 簡單數據傳遞 |
InheritedWidget | 父到子 | 跨層數據傳遞 |
Notification | 子到父 | 狀態通知 |
EventBus | 發佈訂閱 | 消息批量同步 |
16. 路由管理
- Route是頁面的抽象,主要負責創建對應的界面,接收參數,響應Navigator打開和關閉
- Navigator則會維護一個路由棧管理Route,Route打開即入棧,Route關閉即出棧.
- 基本路由: 創建一個MaterialPageRoute實例,調用Navigator.push方法將新頁面壓到堆棧的頂部
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");
- 錯誤路由處理,統一返回UnknownPage
MaterialApp(
...
//註冊路由
routes:{
"second_page":(context)=>SecondPage(),
},
//錯誤路由處理,統一返回UnknownPage
onUnknownRoute: (RouteSettings setting) => MaterialPageRoute(builder: (context) => UnknownPage()),
);
//使用錯誤名字打開頁面
Navigator.pushNamed(context,"unknown_page");
- 頁面參數: Flutter提供了路由參數的機制,可以在打開路由時傳遞相關參數,在目標頁面通過RouteSettings來獲取頁面參數
//打開頁面時傳遞字符串參數
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);
}
}
- 返回參數(類似startActivityForResult): 在push目標頁面時,可以設置目標頁面關閉時監聽函數,以獲取返回參數.而目標頁面可以在關閉路由時傳遞相關參數.
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.push
A->B->C->D,如何從 D頁面 pop 到 B 呢?Navigator.popUntil(context,ModalRoute.withName('B'));