Flutter基础——Widget

Posted by lingjye on July 18, 2019

Widget

在Flutter中,Widget相当于iOS中的UIView,但并不完全一样。

Widgets拥有不同的生命周期,它们一直存在且保持不变,知道需要改变时才会改变。当它们的状态被改变时,Flutter会构建一颗心的widgets树,而iOS中的views改变时并不会重新创建,只是在使用setNeedsDisplay()之后重新绘制。

由于不可变性,Flutter的widgets非常轻量。它们本身并不是什么控件,也不会被直接绘制出什么,而只是UI的描述。

Flutter包含了Material组件库(遵循Material设计规范),MD是一个灵活的设计系统,并且为包括 iOS 在内的所有系统进行了优化。

用Flutter实现任何的设计语言都非常的灵活和富有表现力。在iOS平台,可以使用Cupertino widgets来构建遵循了Apple’s iOS design language的界面。

Material Widget

Flutter提供了许多widgets,可帮助我们构建遵循Material Design的应用程序。Material应用程序以MaterialApp widget开始,该widget在应用程序的根部创建了一些有用的widget,其中包括一个Navigator, 它管理由字符串标识的Widget栈(即页面路由栈)。Navigator可以让您的应用程序在页面之间的平滑的过渡。当然,使用MaterialApp是可选的。

该MaterialApp配置顶级导航搜索路线按以下顺序:

  1. 对于/路由,使用home属性(如果为非null)。

  2. 否则,如果路由表具有路由条目,则使用路由表。

  3. 否则,调用onGenerateRoute(如果提供)。对于未由home和routes处理的任何有效路由,它应返回非null值。

  4. 最后如果所有其他方法都失败,onUnknownRoute被调用。

如果创建了导航,则这些选项中至少有一个必须处理该/路由,因为在启动时指定了无效的initialRoute时使用该路径(例如,通过另一个在Android上启动此项目的应用程序)。

此widget还配置顶级导航器的观察者(如果有)以执行Hero动画。

如果home,routes,onGenerateRoute和onUnknownRoute都为null,并且builder不为null,则不会创建任何Navigator。

构造函数:

1
MaterialApp({Key key, GlobalKey<NavigatorState> navigatorKey, Widget home, Map<String, WidgetBuilder> routes: const {}, String initialRoute, RouteFactory onGenerateRoute, RouteFactory onUnknownRoute, List<NavigatorObserver> navigatorObservers: const [], TransitionBuilder builder, String title: '', GenerateAppTitle onGenerateTitle, Color color, ThemeData theme, ThemeData darkTheme, Locale locale, Iterable<LocalizationsDelegate> localizationsDelegates, LocaleListResolutionCallback localeListResolutionCallback, LocaleResolutionCallback localeResolutionCallback, Iterable<Locale> supportedLocales: const [Locale('en', 'US')], bool debugShowMaterialGrid: false, bool showPerformanceOverlay: false, bool checkerboardRasterCacheImages: false, bool checkerboardOffscreenLayers: false, bool showSemanticsDebugger: false, bool debugShowCheckedModeBanner: true })

参数:

  • builder → TransitionBuilder,构建器,用于插入由其它WidgetsApp widget所创建的导航件,或者更换导航实体。
  • checkerboardOffscreenLayers → bool,打开渲染到屏幕外位图图层的checkerboarding。
  • checkerboardRasterCacheImages → bool,打开光栅缓存图像的检查板。
  • color → Color,应用程序的主要颜色。
  • darkTheme → ThemeData,暗黑主题。
  • debugShowCheckedModeBanner → bool,在选中模式下打开一个小“DEBUG”横幅,表示该应用程序处于检查模式,默认开启。
  • debugShowMaterialGrid → bool,打开绘制基线Material应用的GridPaper叠加层。
  • home → Widget,应用程序默认路由的小部件(Navigator.defaultRouteName,即/)。
  • initialRoute → String,如果构建了导航器,则显示的第一条路径的名称。
  • locale → Locale,此应用程序的“ 本地化”widget的初始区域设置基于此值。
  • localeListResolutionCallback → LocaleListResolutionCallback,此回调负责在应用程序启动时以及更改设备的区域设置时选择应用程序的区域设置。
  • localeResolutionCallback → LocaleResolutionCallback。
  • localizationsDelegates → Iterable,此应用程序的Localizations小部件的代理。
  • navigatorKey → GlobalKey,构建导航器时使用的密钥。
  • navigatorObservers → List,为此应用程序创建 的Navigator的观察者列表。
  • onGenerateRoute → RouteFactory,应用程序导航到命名路由时使用的路由生成器回调。
  • onGenerateTitle → GenerateAppTitle,如果为非null,则调用此回调函数以生成应用程序的标题字符串,否则使用标题。
  • onUnknownRoute → RouteFactory,调用时onGenerateRoute无法生成路由,除了 initialRoute。
  • routes → Map<String, WidgetBuilder>,应用程序的顶级路由表。
  • showPerformanceOverlay → bool,打开性能覆盖。
  • showSemanticsDebugger → bool,打开一个覆盖图,显示框架报告的可访问性信息。
  • supportedLocales → Iterable,此应用已本地化的区域设置列表。
  • theme → ThemeData,默认主题,如颜色字体和形状。
  • title → String,设备用于识别用户应用的单行描述。
  • hashCode → int,哈希码
  • key → Key,控制一个小部件如何替换树中的另一个小部件
  • runtimeType → Type,运行时类型

Scaffold Widget

Material Design布局结构的基本实现。此类提供了用于显示drawer、snackbar和底部sheet的API。

Scaffold被设计为MaterialApp的单顶级容器,实现基本材料设计视觉布局结构,提供AppBar和Drawer等标准app元素。

构造函数:

1
Scaffold({Key key, PreferredSizeWidget appBar, Widget body, Widget floatingActionButton, FloatingActionButtonLocation floatingActionButtonLocation, FloatingActionButtonAnimator floatingActionButtonAnimator, List<Widget> persistentFooterButtons, Widget drawer, Widget endDrawer, Widget bottomNavigationBar, Widget bottomSheet, Color backgroundColor, bool resizeToAvoidBottomPadding, bool resizeToAvoidBottomInset, bool primary: true, DragStartBehavior drawerDragStartBehavior: DragStartBehavior.start, bool extendBody: false, Color drawerScrimColor });

参数:

  • appBar → PreferredSizeWidget,顶部导航栏
  • backgroundColor → Color,整个基类背景颜色
  • body → Widget,主要北荣
  • bottomNavigationBar → Widget,底部导航栏
  • bottomSheet → Widget,底部弹起的sheet
  • drawer → Widget,侧划面板,从左到右(TextDirection.ltr)或从右到左(TextDirection.rtl)滑动
  • drawerDragStartBehavior → DragStartBehavior,确定处理拖动开始行为的方式
  • drawerScrimColor → Color,侧滑遮罩颜色
  • endDrawer → Widget,显示在身body侧面的面板,通常隐藏在移动设备上。从右到左(TextDirection.ltr)或从左到右(TextDirection.rtl)滑动
  • extendBody → bool,如果为true,并且 指定了bottomNavigationBar或persistentFooterButtons,则body将延伸到Scaffold的底部,而不是仅延伸到bottomNavigationBar 或persistentFooterButtons的顶部
  • floatingActionButton → Widget,显示在body上方的按钮,位于右下角
  • floatingActionButtonAnimator → FloatingActionButtonAnimator,Animator将floatingActionButton移动到新的floatingActionButtonLocation
  • floatingActionButtonLocation → FloatingActionButtonLocation,负责确定floatingActionButton的去向
  • persistentFooterButtons → List,一组显示在Scaffold底部的按钮
  • primary → bool,是否显示在屏幕顶部
  • resizeToAvoidBottomInset → bool,如果为true,则body和scaffold的浮动小部件应自行调整大小,以避免屏幕键盘的高度由环境MediaQuery的MediaQueryData.viewInsets bottom属性定义
  • resizeToAvoidBottomPadding → bool,已废弃,使用resizeToAvoidBottomInset指定键盘出现时是否应调整主体的大小

更新Widget

在Flutter中,widgets是不可变的,而且不能被直接更新,需要去操纵widget的state来更新。StatelessWidget(无状态的Widget)和StatefulWidget(有状态的Widget)就是用来描述这种状态的。当页面构建后不需要改变,就是用StatelessWidget,相反可使用StatefulWidget,例如在发起网络请求后动态更新UI操作。

StatefulWidget与SattelssWidget的区别是,有状态的Widget拥有一个State对象来存储它的状态数据,并且会在Widget重建时携带,不会丢失。

如果一个widget在build之后不会改变,那它就是无状态的。如果一个widget在它的build方法之外改变(运行时由于用户的操作而改变),它就是有状态的。一个有状态的Widget,其父类可以是无状态的,只要父Widget本身不响应这些变化。

StatelessWidget使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
void main() {
  runApp(
      Text(
        'Hello world',
        // 排版方向,必须有
        textDirection: TextDirection.ltr,
        style: TextStyle(
          fontWeight: FontWeight.bold,
          fontSize: 100
        ),
      ),
  );
}

StatefulWidget使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
void main() => runApp(
  dart_widget.SampleApp()
);

class SampleApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    // 一个应用只有一个MaterialApp,其他页面使用Scaffold
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
      	 // 可以使用primaryColor,但是primarySwatch不可以设置白色
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({ Key key }) : super(key: key);

  @override
  State<StatefulWidget> createState() {
    // TODO: implement createState
    return _SampleAppPageState();
  }
}

class _SampleAppPageState extends State<SampleAppPage> {
  // 默认占位文字
  String text = 'I like Flutter';
  
  void _updateText() {
    setState(() {
      text = 'Flutter is Awesome!';
    });
  }

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
      appBar: AppBar(
        title: Text('Sample App'),
      ),
      body: Center(
        child: Text(text),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _updateText,
        tooltip: 'Update Text',
        child: Icon(Icons.update),
      ),
    );
  }
}

MaterialApp是一个方便的Widget,它封装了应用程序实现Material Design所需要的一些Widget。

Scaffold组件是Material Design布局结构的基本实现。此类提供了用于显示drawer、snackbar和底部sheet的API。

Widget布局

在Flutter中,通过编写一个widget树来声明布局。

一个带有padding的简单widget:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class LayoutSampleApp extends StatelessWidget {
  const LayoutSampleApp({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: LayoutSampleAppPage(),
    );
  }
}

class LayoutSampleAppPage extends StatefulWidget {
  LayoutSampleAppPage({Key key}) : super(key: key);
  @override
  State<StatefulWidget> createState() {
    // TODO: implement createState
    return _LayoutSampelAppSate();
  }
}

class _LayoutSampelAppSate extends State<LayoutSampleAppPage> {
  var _pressedCount = 0;
  void _updateState() {
    setState(() {
      _pressedCount += 1;
      print(_pressedCount);
    });
  }

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
        appBar: AppBar(
          title: Text('Layout App'),
        ),
        body: Center(
          child: CupertinoButton(
            onPressed: _updateState,
            child: Text(_pressedCount.toString()),
            padding: EdgeInsets.only(left: 40.0, right: 10),
            color: Colors.red,
          ),
        ),
      );
  }
}

可以看到如下效果:

pidding

添加或移除Widget

在Flutter中,由于Widget不可变,所以没有addSubView()类似的方法,但是可以向parent传入一个返回Widget的函数,并用一个布尔值来控制子Widget的创建。

如下,展示了点击FloatingActionButton时动态切换两个widgets:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class SubWidgetSampleApp extends StatelessWidget {
  const SubWidgetSampleApp({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue
      ),
      home: SubWidgetSampleAppPage(),
    );
  }
}

class SubWidgetSampleAppPage extends StatefulWidget {
  SubWidgetSampleAppPage({Key key}) : super(key: key);

  _SubWidgetSampleAppPageState createState() => _SubWidgetSampleAppPageState();
}

class _SubWidgetSampleAppPageState extends State<SubWidgetSampleAppPage> {

  bool toggle = true;
  void _toggle() {
    setState(() {
      toggle = !toggle;
    });
  }
  // 根据toggle来返回不同的widget
  _getToggleChild() {
    if (toggle) {
      return Text('Toggle One');
    } else {
      return CupertinoButton(
        onPressed: (){},
        child: Text('Toggle Two'),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('SubWidget Sample App'),
        ),
        body: Center(
          child: _getToggleChild(),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: _toggle,
          tooltip: 'Update Text',
          child: Icon(Icons.update),
        ),
    );
  }
}

效果请运行Demo查看

Widget动画

在Flutter中使用动画库来包裹widgets,而不是创建一个动画Widget。

在Flutter中使用AnimationController(名字是不是有种熟悉的感觉),这是一个可以暂停、寻找、停止、反转动画的Animation类型。他需要一个`Ticker`当vsync发生时来发送信号,并且在每帧运行时创建一个介于0-1之间的线性插值。可以创建一个或多个Animation并附加给一个controller。

例如,用 CurvedAnimation 来实现一个 interpolated 曲线。在这个场景中,controller 是动画过程的“主人”,而 CurvedAnimation 计算曲线,并替代 controller 默认的线性模式。

当构建 widget 树时,你会把 Animation 指定给一个 widget 的动画属性,比如 FadeTransition 的 opacity,并告诉控制器开始动画。

使用 FadeTransition 来让logo 图标渐变显示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
class AnimationSampleApp extends StatelessWidget {
  const AnimationSampleApp({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Fade Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: AnimationSampleAppPage(title: 'Fade Demo'),
    );
  }
}

class AnimationSampleAppPage extends StatefulWidget {
  AnimationSampleAppPage({Key key, this.title}) : super(key: key);

  final String title;

  _AnimationSampleAppPageState createState() => _AnimationSampleAppPageState();
}

class _AnimationSampleAppPageState extends State<AnimationSampleAppPage> with TickerProviderStateMixin{
  AnimationController controller;
  CurvedAnimation curve;
  
  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    controller = AnimationController(
      duration: const Duration(milliseconds: 2000),
      vsync: this
    );
    curve = CurvedAnimation(
      parent: controller,
      curve: Curves.easeIn,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
       appBar: AppBar(
         title: Text(widget.title),
       ),
       body: Center(
         child: Container(
           child: FadeTransition(
             opacity: curve,
             child: FlutterLogo(
               size: 100.0,
             ),
           ),
         ),
       ),
       floatingActionButton: FloatingActionButton(
         tooltip: 'Fade',
         child: Icon(Icons.update),
         onPressed: (){
           controller.forward();
         },
       ),
    );

    @override
    dispose() {
      controller.dispose();
      super.dispose();
    }
  }
}

效果请运行Demo查看

绘制

Flutter有一套基于Cavas类的不同API,还有 CustomPaint 和 CustomPainter 这两个类来实现在 canvas 上的绘图算法。

示例,参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
class PainterSampleApp extends StatelessWidget {
  const PainterSampleApp({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Painter',
      theme: ThemeData(
        primarySwatch: Colors.blue
      ),
      home: Scaffold(
        body: Signature(),
      ),
    );
  }
}

class SignaturePainter extends CustomPainter {
  SignaturePainter(this.points);
  final List<Offset> points;

  @override
  void paint(Canvas canvas, Size size) {
    // TODO: implement paint
    var paint = Paint()
      ..color = Colors.black
      ..strokeCap = StrokeCap.round
      ..strokeWidth = 5.0;
    for (int i = 0; i <points.length - 1; i++) {
      if (points[i] != null && points[i - 1] != null) {
        canvas.drawLine(points[i], points[i - 1], paint);
      }
    }
  }

  @override
  bool shouldRepaint(SignaturePainter other) => other.points != points;
}

class Signature extends StatefulWidget {
  Signature({Key key}) : super(key: key);

  _SignatureState createState() => _SignatureState();
}

class _SignatureState extends State<Signature> {
  List<Offset> _points = <Offset>[];
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onPanUpdate: (DragUpdateDetails details) {
        setState(() {
          RenderBox referenceBox = context.findRenderObject();
          Offset localPosition = referenceBox.globalToLocal(details.globalPosition);
          _points = List.from(_points)..add(localPosition);
        });
      },
      onPanEnd: (DragEndDetails details) => _points.add(null),
      child: CustomPaint(
        painter: SignaturePainter(_points),
        size: Size.infinite,
      )
    );
  }
}

透明度

在iOS中,有.opacity或.alpha。在Flutter中,需要给Widget包裹一个Opacity widget来实现。

创建自定义的Widget

在Flutter中,使用组合(composing)多个小widgets来构建一个自定义的Widget(而不是扩展它)。

例如,构造一个CustomButton,需要组合RaisedButton和label,而不是扩展RaisedButton:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class CustomButtonSampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'CustonButton',
      theme: ThemeData(
        primarySwatch: Colors.blue
      ),
      home: Center(
        child: CustomButton('Hello'),
      ),
    );
  }
}

class CustomButton extends StatelessWidget {
  final String label; 
  CustomButton(this.label);

  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      onPressed: (){},
      color: Colors.yellow,
      textColor: Colors.red,
      child: Text(label),
    );
  }
}

本文Demo