Flutter之旅:認識Widget(源碼級)

1.Widget的第一印象

1.1:初次的見面

首先咱們來到第一次看到Widget類的場景,那時還對這個世界一無所知,
進入程序的入口時runApp函數中須要傳入一個Widget對象,這即是第一眼。
初始項目中的作法是自定義了一個MyApp類繼承自StatelessWidget。canvas

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的框架層中是比較頂尖的類。bash

你以後就會知道,Widget是Flutter界面的中心,可顯示在頁面上的一切,都和Widget相關。微信


1.3:Widget類的構成

首先,Widget是一個抽象類,擁有一個createElement()的抽象方法返回一個Element對象。
其次,Widget類自己只有一個字段、一個構造方法、一個抽象方法、一個靜態方法和兩個普通方法。app

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狀態的問題:less

/// 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也作了簡要的描述ide

///  * [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方法。post

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,期待與你的交流與切磋。

相關文章
相關標籤/搜索