1.Widget的第一印象
1.1:初次的見面
首先我們來到第一次看到Widget類的場景,那時還對這個世界一無所知,
進入程式的入口時runApp函式中需要傳入一個Widget物件,這便是第一眼。
初始專案中的做法是自定義了一個MyApp類繼承自StatelessWidget。
void main()=>runApp(MyApp());
---->[flutter/lib/src/widgets/binding.dart:778]----
void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized()
..attachRootWidget(app)
..scheduleWarmUpFrame();
}
class MyApp extends StatelessWidget {
//略...
}
複製程式碼
1.2:Widget在原始碼中的位置
位置:
flutterSDK/packages/flutter/lib/src/widgets/framework.dart:369
首先,它在framework包中,可以說至關重要。其次它繼承自DiagnosticableTree
下圖可見Widget類在Flutter的框架層中是比較頂尖的類。
你之後就會知道,Widget是Flutter介面的中心,可顯示在頁面上的一切,都和Widget相關。
1.3:Widget類的構成
首先,Widget是一個抽象類,擁有一個
createElement()
的抽象方法返回一個Element
物件。
其次,Widget類本身只有一個欄位、一個構造方法、一個抽象方法、一個靜態方法和兩個普通方法。
abstract class Widget extends DiagnosticableTree {
const Widget({ this.key });
final Key key;
@protected
Element createElement();
@override
String toStringShort() {
return key == null ? '$runtimeType' : '$runtimeType-$key';
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
}
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
}
複製程式碼
關於Widget的原始碼,這裡暫時不做解讀,隨著瞭解的深入,再去看原始碼,效果會好很,現在火候還未到,有個大概印象就行了。
2.Widget的狀態
2.1:Widget的狀態概述
在Widget原始碼中明顯指出了關於Widget狀態的問題:
/// Widgets themselves have no mutable state (all their fields must be final).
/// If you wish to associate mutable state with a widget, consider using a
/// [StatefulWidget], which creates a [State] object (via
/// [StatefulWidget.createState]) whenever it is inflated into an element and
/// incorporated into the tree.
Widget的本身沒有可變狀態(所有欄位都必須是final)。
如果您希望將一個widget擁有可變狀態,請考慮使用 StatefulWidget,
每當它被載入為元素併合併到渲染樹中時,會建立State物件(通過 StatefulWidget.createState)。
複製程式碼
對StatefulWidget和StatelessWidget也做了簡要的描述
/// * [StatefulWidget] and [State], for widgets that can build differently
/// several times over their lifetime.
/// * [StatelessWidget], for widgets that always build the same way given a
/// particular configuration and ambient state.
StatefulWidget和State,用於可以在其生命週期內多次構建的widget。
StatelessWidget,用於在給定配置和環境的狀態的下始終以相同方式構建的widget。
複製程式碼
2.2: StatelessWidget 無狀態元件
該類的本身非常簡潔,由於Widget有一個createElement抽象方法,
StatelessWidget類中通過StatelessElement物件完成了該抽象方法,
所以StatelessWidget只需要關注build這個抽象方法即可。
abstract class StatelessWidget extends Widget {
const StatelessWidget({ Key key }) : super(key: key);
@override
StatelessElement createElement() => StatelessElement(this);
@protected
Widget build(BuildContext context);
複製程式碼
如初始專案中,MyApp是繼承了StatelessWidget,它的任務在於重寫build方法。
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
複製程式碼
2.3:StatefulWidget有狀態元件
該類本身也比較簡單,繼承自Widget,createElement方法通過StatefulElement實現
所以該類需要注意的只有抽象方法createState(),負責返回一個State狀態物件
abstract class StatefulWidget extends Widget {
const StatefulWidget({ Key key }) : super(key: key);
@override
StatefulElement createElement() => StatefulElement(this);
@protected
State createState();
}
複製程式碼
初始程式碼也是用心良苦,為我們準備了一個簡單的有狀態元件MyHomePage
可以看到,該類的核心是createState方法,返回一個自定義的_MyHomePageState物件
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
複製程式碼
2.4:State 物件
比較引人注目的就是State物件中有一個泛型,從原始碼中來看,
該泛型值接受StatefulWidget,即有狀態元件類。
State作為一個抽象類,存在一個build抽象方法來返回一個Widget物件
abstract class State<T extends StatefulWidget> extends Diagnosticable {
//略...
@protected
Widget build(BuildContext context);
}
複製程式碼
初始程式碼中也為我們做了示例:
這裡將_counter作為可變狀態,通過點按鈕改變狀態,再通過setState重新渲染,
執行build方法,從而達到了點選按鈕數字增加的一個元件,
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
//略...
);
}
}
複製程式碼
這裡需要注意的一點是State類中的widget屬性到底是什麼,這裡通過debug可以看出,就是傳入的泛型類,
至於如何widget屬性何時賦值以及渲染的,先別急,還有一段很長的路要走。
現在回頭看一下,你是否已經對曾經陌生無比的初始專案有了突然熟悉了許多。
驀然回首,希望這會成為你在一段旅行中美麗的一瞬,也是我這個導遊的成功。
3.從Icon原始碼看StatelessWidget元件
趁人打鐵,為了讓大家對Widget有更好的理解,這裡挑選了兩個Widget。
通過原始碼賞析一下:一個Widget是如何構成的。第一個是無狀態家族的Icon元件
3.1:Icon元件的使用
Icon主要有三個屬性,分別控制圖示,顏色,大小
Icon(
Icons.local_shipping,
color: Colors.pink,
size: 30.0,
)
複製程式碼
3.2:Icon原始碼
從原始碼中可以看出,Icon類中主要做了四件事:
建構函式--> 宣告屬性欄位--> 實現build方法,返回Widget物件-->debugFillProperties
class Icon extends StatelessWidget {
const Icon(
this.icon, {
Key key,
this.size,
this.color,
this.semanticLabel,
this.textDirection,
}) : super(key: key);
final IconData icon;
final double size;
final Color color;
final String semanticLabel;
final TextDirection textDirection;
@override
Widget build(BuildContext context) {
//暫略...
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
//暫略...
}
}
複製程式碼
可以看出,建構函式中有一個必須的引數icon,從定義中來看是一個IconData物件
注意:建構函式用const關鍵字修飾,欄位全被修飾為final,這就意味著欄位不可再修改。
這也是被稱為無狀態的原因,一旦物件構建完成,它就樣子就無法再被改變。
3.3:build方法
build方法作為StatelessWidget的抽象方法,子類必須去實現
這個方法也將決定一個Widget在介面上的樣子,所以它至關重要
從原始碼中可以看出Icon主要是通過RichText來實現的,核心是text屬性
@override
Widget build(BuildContext context) {
//略...
Widget iconWidget = RichText(
overflow: TextOverflow.visible, // Never clip.
textDirection: textDirection, // Since we already fetched it for the assert...
text: TextSpan(
text: String.fromCharCode(icon.codePoint),//文字
style: TextStyle(
inherit: false,
color: iconColor,
fontSize: iconSize,
fontFamily: icon.fontFamily,
package: icon.fontPackage,
),
),
);
if (icon.matchTextDirection) {//做文字方向的處理
switch (textDirection) {
case TextDirection.rtl://文字方向從右到左
iconWidget = Transform(
transform: Matrix4.identity()..scale(-1.0, 1.0, 1.0),
alignment: Alignment.center,
transformHitTests: false,
child: iconWidget,
);
break;
case TextDirection.ltr://文字方向從左到右
break;
}
}
// 暫略...
}
複製程式碼
3.4:文字圖示的實現
映入眼簾的是
String.fromCharCode()
方法,它接受一個int值
這個int值是由IconData物件的codePoint屬性提供的,為了方便開發,
Flutter框架給了很多Icon靜態常量,當然你也可以使用自定義的圖示。
---->[flutter/bin/cache/pkg/sky_engine/lib/core/string.dart:134]----
external factory String.fromCharCode(int charCode);
----[flutter/lib/src/widgets/icon_data.dart:22]----
const IconData(
this.codePoint, {
this.fontFamily,
this.fontPackage,
this.matchTextDirection = false,
});
/// The Unicode code point at which this icon is stored in the icon font.
final int codePoint;
----[flutter/lib/src/widgets/icon_data.dart:22]----
static const IconData local_shipping = IconData(0xe558, fontFamily: 'MaterialIcons');
複製程式碼
一個圖示實際上就是一個字型,可以根據一個int值和字型檔名匹配到它。
所以圖示才支援變色和改變大小等方便的功能。
3.5:關於Semantics類
還有一點不知你是否注意,最後返回的是一個包裹了iconWidget的Semantics物件
字面上來看,它是語義化的意思,那他有什麼用處呢?
return Semantics(
label: semanticLabel,
child: ExcludeSemantics(
child: SizedBox(
width: iconSize,
height: iconSize,
child: Center(
child: iconWidget,
),
),
),
);
複製程式碼
Flutter中的MaterialApp有一個
showSemanticsDebugger
的屬性可以用來檢視語義化介面
---->[main.dart:3]----
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
var icon = Icon(
Icons.local_shipping,
color: Colors.pink,
size: 40.0,
semanticLabel: "一個貨車圖示",
);
var scaffold=Scaffold(
appBar: AppBar(
title: Text('Flutter Demo Home Page'),
),
body:icon,
);
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
showSemanticsDebugger: true,
home: scaffold,
);
}
}
複製程式碼
好了,這樣就是對於一個簡單的無狀態元件構成的簡要介紹。
4.從Checkbox看StatefulWidget元件
4.1:CheckBox的使用
有狀態元件很好理解,首先它有一個允許改變的狀態量,不如Checkbox就是選中與否
下面的測試程式碼實現了,點選切換Checkbox選中或未選中的狀態
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
var scaffold=Scaffold(
appBar: AppBar(
title: Text('Flutter Demo Home Page'),
),
body:CheckBoxWidget(),
);
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: scaffold,
);
}
}
class CheckBoxWidget extends StatefulWidget {
@override
_CheckBoxWidgetState createState() => _CheckBoxWidgetState();
}
class _CheckBoxWidgetState extends State<CheckBoxWidget> {
bool _checked=true;//維護CheckBox框狀態
@override
Widget build(BuildContext context) {
return
Checkbox(
value: _checked,
activeColor: Colors.blue, //選中時的顏色
onChanged:(value){
setState(() {
_checked=value;
});
} ,
);
}
}
複製程式碼
4.2:CheckBox原始碼簡析
下面是Checkbox的程式碼,繼承自StatefulWidget,所以需要實現createState方法
這時,原始碼中使用自定義的_CheckboxState類來管理狀態。
class Checkbox extends StatefulWidget {
const Checkbox({
Key key,
@required this.value,
this.tristate = false,
@required this.onChanged,
this.activeColor,
this.checkColor,
this.materialTapTargetSize,
}) : assert(tristate != null),
assert(tristate || value != null),
super(key: key);
final bool value;//是否選中
final ValueChanged<bool> onChanged;//點選回撥
final Color activeColor;//啟用態框顏色
final Color checkColor;//啟用態對勾顏色
final bool tristate;//三態
final MaterialTapTargetSize materialTapTargetSize;
static const double width = 18.0;
@override
_CheckboxState createState() => _CheckboxState();
}
複製程式碼
通過這兩個元件原始碼,可以總結出一些風格特點:
1.建構函式用const修飾,每行寫一個屬性
2.必須的屬性用@required註解
3.非空的屬性用assert斷言
4.欄位全是final型別
複製程式碼
_CheckboxState中的build方法返回_CheckboxRenderObjectWidget物件
CheckBox具體繪製邏輯及狀態改變,在_RenderCheckbox中實現
---->[flutter/packages/flutter/lib/src/material/checkbox.dart:140]----
class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
@override
Widget build(BuildContext context) {
//略...
}
return _CheckboxRenderObjectWidget(
//略...
);
}
}
---->[flutter/packages/flutter/lib/src/material/checkbox.dart:168]----
class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
//略...
@override
_RenderCheckbox createRenderObject(BuildContext context) => _RenderCheckbox(
//略...
);
@override
void updateRenderObject(BuildContext context, _RenderCheckbox renderObject) {
//略...
}
複製程式碼
4.3:Checkbox核心繪製方法
_RenderCheckbox繼承自RenderToggleable,可以重寫paint方法
這邊簡單看一下主要的邊框和對勾的繪製方法
// 可以看出畫筆的顏色是checkColor,以線條的形式
void _initStrokePaint(Paint paint) {
paint
..color = checkColor
..style = PaintingStyle.stroke
..strokeWidth = _kStrokeWidth;
}
//繪製邊線
void _drawBorder(Canvas canvas, RRect outer, double t, Paint paint) {
assert(t >= 0.0 && t <= 0.5);
final double size = outer.width;
// 當t從0.0到1.0時,逐漸填充外部矩形。
final RRect inner = outer.deflate(math.min(size / 2.0, _kStrokeWidth + size * t));
canvas.drawDRRect(outer, inner, paint);
}
//繪製對勾
void _drawCheck(Canvas canvas, Offset origin, double t, Paint paint) {
assert(t >= 0.0 && t <= 1.0);
final Path path = Path();
const Offset start = Offset(_kEdgeSize * 0.15, _kEdgeSize * 0.45);//起始偏移點
const Offset mid = Offset(_kEdgeSize * 0.4, _kEdgeSize * 0.7);//中間偏移點
const Offset end = Offset(_kEdgeSize * 0.85, _kEdgeSize * 0.25);//終止偏移點
if (t < 0.5) {//t<0.5時,繪製短邊
final double strokeT = t * 2.0;
final Offset drawMid = Offset.lerp(start, mid, strokeT);
path.moveTo(origin.dx + start.dx, origin.dy + start.dy);
path.lineTo(origin.dx + drawMid.dx, origin.dy + drawMid.dy);
} else {//t>0.5時,繪製長邊
final double strokeT = (t - 0.5) * 2.0;
final Offset drawEnd = Offset.lerp(mid, end, strokeT);
path.moveTo(origin.dx + start.dx, origin.dy + start.dy);
path.lineTo(origin.dx + mid.dx, origin.dy + mid.dy);
path.lineTo(origin.dx + drawEnd.dx, origin.dy + drawEnd.dy);
}
canvas.drawPath(path, paint);
}
複製程式碼
這樣你一個StatefulWidget元件從定義到狀態,到繪製的流程應該有所瞭解
通過本篇,希望你可以對Widget有一些更深刻的理解,然而這只是開始。之後的還會對Widget做更深入的探索 本文到此接近尾聲了,如果想快速嚐鮮Flutter,《Flutter七日》會是你的必備佳品;如果想細細探究它,那就跟隨我的腳步,完成一次Flutter之旅。
另外本人有一個Flutter微信交流群,歡迎小夥伴加入,共同探討Flutter的問題,本人微訊號:zdl1994328
,期待與你的交流與切磋。