[Flutter]flutter基础之组件基础(五)

一、概述

前面几篇文章介绍了一些基本的Flutter Widget ,现在开始介绍一下前面一直用到但没做过多说明的 MaterialAppScaffold 和一些布局组件,因为后面的很多 Widget 中要使用部分布局 Widget , 所以这里插入先对这些 Widget 做下讲解。

为了方便的创建出特定风格的应用程序,Flutter 包装了创建应用程序的 Widget :MaterialApp 用于创建 Material 设计风格( Android 风格 )的应用程序,CupertinoApp 用于创建 ios 设计风格的应用程序。这里首先介绍下 MaterialApp ,以后在对 CupertinoApp 做说明。

二、MaterialApp

MaterialApp 是一个 Widget ,代表使用 Material 设计风格的应用。在 Flutter 中创建一个Material 设计风格的移动应用程序,应首先在 Widget 树的根插入 MaterialApp Widget 。MaterialApp 继承自 StatefulWidget ,是一个有状态的 Widget ,虽然在其构造函数中没有必传参数,但是有很多命名可选参数是不能为 null 的,必须为他们分配值,这是通过断言的方式的实现的。MaterialApp 主要用来设置应用程序启动时的首界面,主题,路由功能,本地化语言环境,文本的方向等。

其构造函数如下:

  const MaterialApp({
    Key key,
    //GlobalKey<NavigatorState>类型可选命名参数,构建导航器时要使用的键
    this.navigatorKey,
    //Widget类型可选命名参数,应用打开时,要显示的第一个页面(主页)
    this.home,
    //Map<String, WidgetBuilder>类型可选命名参数,路由,用于定义应用中的页面跳转
    this.routes = const <String, WidgetBuilder>{},
    //String类型可选命名参数,初始化路由,用于在构建了导航器的情况下,设置要显示的第一个路由的名称
    this.initialRoute,
    //RouteFactory类型可选命名参数,在应用导航到命名路由时使用的路由生成器回调
    this.onGenerateRoute,
    //RouteFactory类型可选命名参数,当onGenerateRoute未能生成路由(初始路由除外)时调用
    this.onUnknownRoute,
    //List<NavigatorObserver>类型可选命名参数,为此应用程序创建的导航器的观察器列表
    this.navigatorObservers = const <NavigatorObserver>[],
    //TransitionBuilder类型可选命名参数,用于将小部件插入到Navigator上方并且在WidgetApp创建
    //的其他小部件下方的构建器,或用于完全替换Navigator的构建器
    this.builder,
    //String类型可选命名参数,用于标识应用
    this.title = '',
    //GenerateAppTitle类型可选命名参数,如果非空,则调用该回调函数来生成应用程序的标题字符串,否则使用标题
    //onGenerateTitle上下文参数包括WidgetsApp的Localizations小部件,因此该回调可用于生成本地化的标题
    this.onGenerateTitle,
    //Color类型可选命名参数,操作系统界面中用于应用程序的主要颜色
    this.color,
    //ThemeData类型可选命名参数,用于设置应用的主题(外观)
    this.theme,
    //ThemeData类型可选命名参数,设置暗模式主题
    this.darkTheme,
    //ThemeMode类型可选命名参数,用于确定在同时设置了 theme 和 darkTheme 的情况下使用哪个主题
    this.themeMode = ThemeMode.system,
    //Locale类型可选命名参数,用于设置应用程序本地化初始语言
    this.locale,
    //Iterable<LocalizationsDelegate>类型可选命名参数,应用程序本地化代理
    this.localizationsDelegates,
    //LocaleListResolutionCallback类型可选命名参数,该回调负责在应用启动时
    //以及用户更改设备的区域设置时选择应用的区域设置
    this.localeListResolutionCallback,
    //LocaleResolutionCallback类型可选命名参数,这个回调传递给由这个小部件构建的WidgetsApp
    this.localeResolutionCallback,
    //Iterable<Locale>类型可选命名参数,应用支持的本地化语言,
    //不能为空,默认的语言解析算法根据设置顺序进行解析
    this.supportedLocales = const <Locale>[Locale('en', 'US')],
    //bool类型可选命名参数,是否显示网格(可用于调试UI),仅检查模式可用
    this.debugShowMaterialGrid = false,
    //bool类型可选命名参数,是否显示性能提示
    this.showPerformanceOverlay = false,
    //bool类型可选命名参数,是否打开栅格缓存图像
    this.checkerboardRasterCacheImages = false,
    //bool类型可选命名参数,检查渲染到屏幕外的图层
    this.checkerboardOffscreenLayers = false,
    //bool类型可选明敏参数是否显示语义调试
    this.showSemanticsDebugger = false,
    //bool类型可选命名参数,在检查模式下,在屏幕右上角是否显示 DEBUG 横幅,发布模式无效
    this.debugShowCheckedModeBanner = true,
  })

第一个 Key 参数,是作为 WidgetElementSemanticsNode 的标识符,其作用是,当现有元素的键与当前 Widget 的键相同时,会使用新的 Widget 更新现有的元素。前面介绍过的构造参数都有这个参数,这里做下说明。

在这些属性中,homeroutesonGeneratedRoutebuilder 在创建 MaterialApp 时,至少有一个必须进行设置,不能为 null ,否则会报异常。对于指定应用的首界面,如果在 MaterialApp 提供了 home ,则使用 home 指定的 Widget 。如果 homenull ,则寻找 routes 中的 "/" ,如果进行了设置,则使用 "/" 对应的 Widget 作为首界面。如果routes 中没有设置 "/" ,且没有设置 home ,则调用回调函数 onGenerateRoute ,如果提供了,它会作为首界面进行回调。如果没有设置 onGenerateRoute ,也没有提供 builder ,则调用 onUnknownRoute 。如果提供了 builder 则会覆盖以上流程中设置的首界面。

只提供 home 如下:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: FirstPage(),
    );;
  }
}

class FirstPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(child: Text("Page 1"));
  }
}

路由 ( routes ) 用来设置顶级导航的不同界面。如果只提供了 routes ,则必须包含 "/" ,表示应用启动的第一个界面,home"/" 不能同时指定。

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      routes: {
        "/" : (context)=> FirstPage(),
        "/second" : (context)=> SecondPage(),
        "/third" : (context)=> ThirdPage(),
      },
      initialRoute: "/",
    );
  }
}

class FirstPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(child: Text("Page 1"));
  }
}

class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(child: Text("Page 2"));
  }
}

class ThirdPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(child: Text("Page 3"));
  }
}

onGenerateRoute 的类型是 RouteFactory ,其是一个 Function ,原型为 Route<dynamic> Function(RouteSettings settings) 。 使用如下:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {

    return MaterialApp(
      onGenerateRoute: (settings) {
        return MaterialPageRoute(
          builder: (context){
            return Center(
              child: Text("Page 1"),
            );
          },
        );
      },
    );
  }
}

builder 的类型是 TransitionBuilder ,也是一个 Function ,原型为:Widget Function(BuildContext context, Widget child) 。使用如下:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      builder: (context, child) {
        return Center(
          child: Text("Page 1"),
        );
      },
    );
  }
}

如果以上都没有设置就会调用 onUnknownRoute ,其类型也是一个 RouteFactory ,使用方式与 onGenerateRoute 相同。onUnknownRoute 主要用于错误处理,用于描述未找到 Widget ,类似 404 错误页面设置。

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      onUnknownRoute: (settings) {
        return MaterialPageRoute(
          builder: (context){
            return Center(
              child: Text("没有找到路由"),
            );
          },
        );
      },
    );
  }
}

以上正确设置的效果如下图:

2020310627.jpg

现在的界面是没有导航的,在一个完整的应用程序中,一般都会有导航组件,在 Flutter 中,需要使用 Scaffold 来组织界面布局结构。MaterialApp 中的其他属性,在介绍完 Scaffold 后统一说明。

三、Scaffold Widget

Scaffold 称为脚手架 Widget ,用于组织界面布局,其中包含了创建一个应用程序的几乎所有东西。构造方法如下:

const Scaffold({
  Key key,
  //PreferredSizeWidget类型可选命名参数,应用顶部的导航栏
  this.appBar,
  //Widget类型可选命名参数,主界面显示的Widget
  this.body,
  //Widget类型可选命名参数,悬浮在界面上方的按钮
  this.floatingActionButton,
  //FloatingActionButtonLocation类型可选命名参数,设置悬浮按钮的位置
  this.floatingActionButtonLocation,
  //FloatingActionButtonAnimator类型可选命名参数,将悬浮按钮移动到一个新的位置
  this.floatingActionButtonAnimator,
  //List<Widget>类型可选命名参数,在界面底部显示的一组按钮,呈现在bottomNavigationBar上方
  this.persistentFooterButtons,
  //Widget类型可选命名参数,抽屉Widget,显示在侧面的面板,入口在顶部导航左侧
  this.drawer,
  //Widget类型可选命名参数,抽屉Widget,显示在侧面的面板,入口在顶部导航右侧
  this.endDrawer,
  //Widget类型可选命名参数,底部应用栏或导航栏
  this.bottomNavigationBar,
  //Widget类型可选命名参数,要显示的永久性底部工作表
  this.bottomSheet,
  //Color类型可选命名参数,设置整个Scafflod组件的颜色
  this.backgroundColor,
  //bool类型参数,已弃用,使用resizeToAvoidBottomInset
  this.resizeToAvoidBottomPadding,   //已弃用
  //bool类型可选命名参数,指定当键盘出现时,正文是否应该调整大小
  this.resizeToAvoidBottomInset,
  //bool类型可选命名参数,Scaffold是否在屏幕顶部显示,如果为true,
  //则appBar的高度将扩展为屏幕状态栏的高度,即MediaQuery的顶部填充
  this.primary = true,
  //DragStartBehavior类型可选命名参数,确定处理拖动启动行为的方式
  this.drawerDragStartBehavior = DragStartBehavior.start,
  //bool类型可选命名参数,如果为真,并且指定了底部导航栏或持久底托按钮,
  //则主体延伸到脚手架的底部,而不是仅延伸到底部导航栏或持久底托按钮的顶部
  this.extendBody = false,
  //bool类型可选命名参数,如果为true,并且指定了一个应用栏,
  //则主体的高度将被扩展为包括应用栏的高度,并且主体的顶部与应用栏的顶部对齐
  this.extendBodyBehindAppBar = false,
  //Color类型可选命名参数,抽屉打开时,主体遮罩的颜色
  this.drawerScrimColor,
  //double类型可选命名参数,滑动打开抽屉的有效拖动宽度,为0则滑动无法打开抽屉
  this.drawerEdgeDragWidth,
})

其中,appBar 的类型为 PreferredSizeWidget ,是一个抽象类,需要使用其子类 AppBar 来创建顶部导航,body 是界面显示的主体部分,可以使用任何组件,具体使用什么取决于应用程序要实现的功能。

AppBar 构造函数如下:

AppBar({
  Key key,
  //Widget类型可选命名参数,要在标题前显示的小部件(顶部导航左侧的Widget)
  this.leading,
  //bool类型可选命名参数,leading如果为空,是否尝试按时前导Widget
  this.automaticallyImplyLeading = true,
  //Widget类型可选命名参数,用于设置导航栏标题显示的内容
  this.title,
  //List<Widget>类型可选命名参数,要在标题小部件后显示的小部件(顶部导航右侧的Widget)
  this.actions,
  //Widget类型可选命名参数,该小部件堆叠在顶部导航和选项卡栏的后面,通常为FlexibleSpaceBar类型。
  //它的高度将与应用顶部导航栏的整体高度相同
  this.flexibleSpace,
    //PreferredSizeWidget类型可选命名参数,应用程序导航栏底部工具栏,通常为Tabbar  
  this.bottom,
  //double类型可选命名参数,相对于其父应用程序栏放置的z坐标
  this.elevation,
  //ShapeBorder类型可选命名参数,形状设置
  this.shape,
  //Color类型可选命名参数,导航栏颜色
  this.backgroundColor,
  //Brightness类型可选命名参数,导航栏亮度
  this.brightness,
  //IconThemeData类型可选命名参数,导航栏图标颜色、不透明度、大小设置
  this.iconTheme,
  //IconThemeData类型可选命名参数,应用程序栏中出现的图标的颜色、不透明度和大小。
  //只有当动作的主题不同于应用程序栏主窗口中出现的图标时,才应使用此选项
  this.actionsIconTheme,
  //TextTheme类型可选命名参数,应用程序顶部导航栏中用于文本的排版样式。
  //通常,这与亮度背景颜色、图标主题一起设置
  this.textTheme,
  //bool类型可选命名参数,此应用程序栏是否显示在屏幕顶部
  this.primary = true,
  //bool类型可选命名参数,标题是否居中显示
  this.centerTitle,
  //double类型可选命名参数,水平轴上标题内容周围的间距。即使没有前导内容或动作,
  //也会应用此间距。如果希望title占用所有可用空间,请将该值设置为0.0
  this.titleSpacing = NavigationToolbar.kMiddleSpacing,
  //double类型可选命名参数,应用程序顶部导航栏的工具栏部分有多不透明
  this.toolbarOpacity = 1.0,
  //double类型可选命名参数,应用程序栏底部有多不透明
  this.bottomOpacity = 1.0,
})

通过构造函数可以发现,无论 Scaffold 还是 AppBar 都没有必须要设置的必传参数,直接使用就可以。基本的使用方式如下:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(),
      ),
    );;
  }
}

效果如下:

20203101018.jpg

AppBar 布局位置如下图所示:

app_bar.jpg

基本属性设置:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          leading: Icon(Icons.print),   //顶部导航栏左侧Widget
          automaticallyImplyLeading: false,
          title: Text("Page 1"),   //顶部导航栏标题
          actions: <Widget>[   //顶部导航栏右侧Widget List
            IconButton(icon: Icon(Icons.add), onPressed: (){},),
          ],
          backgroundColor: Colors.amber,   //顶部导航背景色
          brightness:Brightness.light,
          iconTheme: IconThemeData(color: Colors.lightGreen, opacity: 1, size: 30), //设置顶部导航栏的Icon图标属性
          textTheme: TextTheme(title: TextStyle(color: Colors.red)),   //顶部导航文本样式
          centerTitle: true,    //居中显示标题
        ),
      ),
    );;
  }
}

效果如下图:

2020311643.jpg

AppBar 中的 bottom 属性,是显示在顶部导航底部的 Widget,通常使用 TabBar ,是一个水平显示的 Widget 。TabBar 通常也与 TabBarView 一起使用。

TabBar 构造方法如下:

const TabBar({
  Key key,
  //List<Widget>类型必传参数,选项卡显示的Widget,通常为Tab。此list长度必须与控制器的
  //TabController.length和TabBarView.children列表的长度匹配
  @required this.tabs,
  //TabController类型可选命名参数,用于控制TabBar选项卡与TabBarView界面切换状态。如果不提供此参数,
  //需要使用DefaultTabController
  this.controller,
  //bool类型可选命名参数,菜单标签是否可以横向滚动
  this.isScrollable = false,
  //Color类型可选命名参数,被选中的选项卡下方线条的颜色
  this.indicatorColor,
  //double类型可选命名参数,被选中的选项卡下方线条的粗细
  this.indicatorWeight = 2.0,
  //EdgeInsetsGeometry类型可选命名参数,被选中的选项卡下方线条的水平填充间距
  this.indicatorPadding = EdgeInsets.zero,
  //Decoration类型可选命名参数,用于定义选项卡指示器的样式
  this.indicator,
  //TabBarIndicatorSize类型可选命名参数,选项卡指示器的大小
  this.indicatorSize,
  //Color类型可选命名参数,选项卡标签颜色
  this.labelColor,
  //TextStyle类型可选命名参数,选项卡标签文本颜色
  this.labelStyle,
  //EdgeInsetsGeometry类型可选命名参数,选项卡每个标签的内边距
  this.labelPadding,
  //Color类型可选命名参数,未选中的选项卡标签的颜色
  this.unselectedLabelColor,
  //TextStyle类型可选命名参数,未选中选项卡标签的文本样式
  this.unselectedLabelStyle,
  //DragStartBehavior类型可选命名参数,用于定义处理拖动开始行为的方式
  this.dragStartBehavior = DragStartBehavior.start,
  //ValueChanged<int>类型可选命名参数,点击选显卡标签时的回调函数
  this.onTap,
})

其中 tabslist 使用的 Tab 是一个无状态 Widget ,构造方法如下:

const Tab({
  Key key,
  //String类型可选命名参数,要显示的文本
  this.text,
  //Widget类型可选命名参数,要显示图标
  this.icon,
  //Widget类型可选命名参数,为用作标签的Widget
  this.child,
})

TabBarcontrollerTabController 类型。其提供的构造方法如下:

TabController({ 
  //int类型可选命名参数,不能为null,用于设置初始化状态选项卡的被选中下标,从0开始
  int initialIndex = 0, 
  //int类型必传参数,选项卡选项的总数
  @required this.length, 
  //TickerProvider类型必传参数,其为抽象类,需要使用其子类SingleTickerProviderStateMixin,
  //下面具体说明
  @required TickerProvider vsync 
}): assert(length != null && length >= 0),
      assert(initialIndex != null && initialIndex >= 0 && (length == 0 || initialIndex < length)),
      _index = initialIndex,
      _previousIndex = initialIndex,
      _animationController = AnimationController.unbounded(
        value: initialIndex.toDouble(),
        vsync: vsync,
      );

这里需要特殊说明的是 vsync 参数,从初始化列表可以看出,其是赋值给 AnimationController.unbounded 的构造方法中的 vsyncAnimationController 是一个动画控制器,是用来控制和生成动画的。vsyncAnimationController.unbounded 构造方法的必传参数。vsyncTickerProvider 类型,其为抽象类,不能直接使用,根据官方文档的说明如下:

If you are creating an AnimationController from a State, then you can use the TickerProviderStateMixin and SingleTickerProviderStateMixin classes to obtain a suitable TickerProvider.

意思是:如果从一个状态类( State )中创建 AnimationController ,可以使用 TickerProviderStateMixinSingleTickerProviderStateMixin 来获取一个适合的 TickerProvider

如果在状态的声明周期中有多个 AnimationController ,则使用 TickerProviderStateMixin 。如果只有一个 AnimationController ,则使用 SingleTickerProviderStateMixin 效率更高。使用方法为混合该类,然后将 this 设置给 vsync 即可。官方说明地址为:https://api.flutter.dev/flutter/widgets/SingleTickerProviderStateMixin-mixin.html

使用 TabController 的目的是关联 TabBarTabBarViewTabBarView 是一个页面视图,用于显示与当前选择的选项卡对应的 Widget 。TabBarView 为一个有状态的 Widget ,其构造方法如下:

const TabBarView({
  Key key,
  //List<Widget>类型必传参数,用于设置每个选项卡标签对应的Widget
  @required this.children,
  //TabController类型可选命名参数,用于关联TabBar和TabBarView,设置动画状态
  this.controller,
  //ScrollPhysics类型可选命名参数,设置页面视图应该如何响应用户输入
  this.physics,
  //DragStartBehavior类型可选命名参数,确定处理拖动开始行为的方式
  this.dragStartBehavior = DragStartBehavior.start,
}) 

TabBar 的使用方式如下:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyAppTabPage(),
    );;
  }
}

class MyAppTabPage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    // TODO: implement createState
    return _MyAppTabPageState();
  }
}

class _MyAppTabPageState extends State<MyAppTabPage> with SingleTickerProviderStateMixin {

  final List _tabMenu = <Tab>[ Tab(text: "菜单1"), Tab(text: "菜单2",)];
  TabController _tabController;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: _tabMenu.length, vsync: this, initialIndex: 0);
  }

  @override
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
      appBar: AppBar(
        title: Text("Page 1"),   //顶部导航栏标题
        bottom: TabBar(
          tabs: _tabMenu,
          controller: _tabController,
          onTap: (value) {print(value);},
        ),
      ),
      body: TabBarView(
        children: <Widget>[
          Center(child: Text("Page 1", style: TextStyle(fontSize: 60, color: Colors.yellow),),),
          Center(child: Text("Page 2", style: TextStyle(fontSize: 60, color: Colors.yellow),),),
        ],
        controller: _tabController,
      ),
    );
  }
}

其中 initState() 方法为当对象插入到 Widget 树中时调用,仅执行一次,做数据的初始化操作,后续会详细介绍。

执行结果如下图:

20203121030.jpg

上面说过,如果在 TabBar 中不提供 controller ,可以使用 DefaultTabController 。因为 TabController 是一个监听状态的控制器,其继承关系如下:

TabController < ChangeNotifier < Object  

其不是一个 Widget ,当共享显示创建 TabController 不太方便时可以使用 DefaultTabController 。其继承自 StatefulWidget ,是一个有状态的 Widget ,构造方法如下:

const DefaultTabController({
  Key key,
  //int类型必传参数,选项卡选项的总数
  @required this.length,
  //int类型可选命名参数,不能为null,用于设置初始化状态选项卡的被选中下标,从0开始
  this.initialIndex = 0,
  //Widget类型必传参数,要显示的Widget
  @required this.child,
})

使用方式如下:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyAppTabPage(),
    );;
  }
}

class MyAppTabPage extends StatelessWidget {

  final List _tabMenu = <Tab>[ Tab(text: "菜单1"), Tab(text: "菜单2",)];

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: _tabMenu.length,
      child: Scaffold(
        appBar: AppBar(
          title: Text("Page 1"),
          bottom: TabBar(
            tabs: _tabMenu,
          ),
        ),
        body: TabBarView(
          children: <Widget>[
            Center(child: Text("Page 1", style: TextStyle(fontSize: 60, color: Colors.yellow),),),
            Center(child: Text("Page 2", style: TextStyle(fontSize: 60, color: Colors.yellow),),),
          ],
        ),
      ),
    );
  }
}

实现效果与上述相同。

关于 floatingActionButton ,其为一个悬浮按钮,通常使用 FloatingActionButton 。前面介绍 Button 的文章做过基础的具体说明,地址为:http://www.mwpush.com/content/aa8d162.html 。在 Scaffold 中的 floatingActionButton 用于特定位置,会悬浮于 body 之上。可以对其做一些属性设置,比如位置等。使用如下:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyFirstPage(),
    );;
  }
}

class MyFirstPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Page 1"),
      ),
      body: Container(
        color: Colors.yellow,
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: (){
          print("点击");
        },
        child: Icon(Icons.add),
      ),
      floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
    );
  }
}

效果如下图:

20203121138.jpg

其中 FloatingActionButtonLocation 用来设置浮动按钮的位置,有以下几种位置可以选择使用:

//在屏幕底部末端浮动,不设置位置参数,默认使用此参数
static const FloatingActionButtonLocation endFloat = _EndFloatFloatingActionButtonLocation();

//在屏幕底部居中浮动
static const FloatingActionButtonLocation centerFloat = _CenterFloatFloatingActionButtonLocation();

//在屏幕底部末端,浮动在Scaffold.bottomNavigationBar上方,以便浮动操作按钮的中心与
//底部导航栏的顶部对齐。
//如果Scaffold.bottomNavigationBar的值为BottomAppBar,则底部应用栏可以设置一个凹口,
//以容纳浮动按钮,没有底部栏不建议使用此位置
static const FloatingActionButtonLocation endDocked = _EndDockedFloatingActionButtonLocation();

//在屏幕底部居中,浮动在Scaffold.bottomNavigationBar上方,以便浮动操作按钮的中心与
//底部导航栏的顶部对齐。
//如果Scaffold.bottomNavigationBar的值为BottomAppBar,则底部应用栏可以设置一个凹口,
//以容纳浮动按钮,没有底部栏不建议使用此位置
static const FloatingActionButtonLocation centerDocked = _CenterDockedFloatingActionButtonLocation();

//在屏幕上方开始位置,Scaffold.appBar和Scaffold.body之间的过渡上浮动
static const FloatingActionButtonLocation startTop = _StartTopFloatingActionButtonLocation();

//在屏幕上方开始位置,Scaffold.appBar和Scaffold.body之间的过渡上浮动,其位置与startTop相同。
//但是,如果FloatingActionButton.mini设置为true,并在Scaffold.body的ListView中的
//ListTile.ListTiles的前导槽的CircleAvatars对齐,请考虑使用miniStartTop
static const FloatingActionButtonLocation miniStartTop = _MiniStartTopFloatingActionButtonLocation();

//在屏幕上方末端位置,Scaffold.appBar和Scaffold.body之间的过渡上浮动
static const FloatingActionButtonLocation endTop = _EndTopFloatingActionButtonLocation();

drawer 为抽屉 Widget ,通常使用 Drawer ,是一个 StatelessWidget 无状态 Widget ,一个由侧边划入主界面的 Widget ,其 child 通常是 ListView ,其第一个子级是 DrawerHeader ,它显示有关当前用户的状态信息。 其余的抽屉式子代通常使用 ListTile 构造,通常使用 AboutListTile 结束。 endDrawerdrawer 一样,只是入口按钮分别分布在顶部导航的右侧和左侧。其构造方法如下:

const Drawer({
  Key key,
  //double类型可选命名参数,相对于其上级放置此抽屉的Z坐标
  this.elevation = 16.0,
  //Widget类型可选命名参数,要显示的Widget
  this.child,
  //String类型可选命名参数,当抽屉打开和关闭时,辅助功能框架用来宣布屏幕转换的对话框的语义标签
  this.semanticLabel,
})

这里引用官方文档的例子,展示下效果。关于 ListView 后面文章会详细介绍,代码如下:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyAppTabPage(),
    );;
  }
}

class MyAppTabPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Page 1"),
      ),
      body: Container(
        color: Colors.yellow,
      ),
      drawer: Drawer(
        child: ListView(
          children: <Widget>[
            DrawerHeader(
              decoration: BoxDecoration(
                color: Colors.blue,
              ),
              child: Text(
                'Drawer Header',
                style: TextStyle(
                  color: Colors.white,
                  fontSize: 24,
                ),
              ),
            ),
            ListTile(
              leading: Icon(Icons.message),
              title: Text('Messages'),
            ),
            ListTile(
              leading: Icon(Icons.account_circle),
              title: Text('Profile'),
            ),
            ListTile(
              leading: Icon(Icons.settings),
              title: Text('Settings'),
            ),
          ],
        ),
      ),
      endDrawer: Drawer(),
      floatingActionButton: FloatingActionButton(
        onPressed: (){
          print("点击");
        },
        child: Icon(Icons.add),
      ),
      floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
    );
  }
}

效果如下:

2020312100.jpg

限于篇幅限制,下一篇文章将继续说明 MaterialAppScaffold

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容