flutter 基础控件

471 阅读29分钟

App的页面框架

Material Design是由Google推出的全新设计语言,这种设计语言旨在为手机、平板电脑等平台提供更一致、更广泛的外观和感觉。Material Design是一种有质感的设计风格,还会提供一些默认的交互动画。

对于没有相关基础的人,在正式学习App的UI之前,建议先了解Material Design相关的知识

关于Material Design设计风格的资料

MaterialApp

MaterialApp代表使用Material设计风格的应用,里面包含了其所需要的基本控件。一个完整的Flutter项目是由这个主组件开始的。

MaterialApp属性详解

属性类型简述
homeWidget主页。用于指定当前App打开时显示的页面
routesMap<String, WidgetBuilder>路由表,定义页面跳转规则
initialRouteString初始路由名称
onGenerateRouteRouteFactory通过pushNamed跳转路由页面时,在routes查找不到时回调
onUnknownRouteRouteFactoryonGenerateRoute 无法生成路由时调用
navigatorObserversList<NavigatorObserver>导航的监听器列表
builderTransitionBuilder构建Widget前调用, 一般做字体大小,方向,主题颜色等配置
titleString应用标题。出现在Android任务管理器的程序快照之上 ,或iOS的程序切换管理器中
onGenerateTitleGenerateAppTitletitle一样,但含有一个context参数用于做本地化
themeThemeData应用程序的主题,各种的定制颜色都可以设置,用于程序主题切换
darkThemeThemeData深色模式下的主题
themeModeThemeMode用于设定主题模式
colorColor应用的主颜色值
localeLocale用于本地化。如果为null则使用当前系统区域
localizationsDelegatesIterable<LocalizationsDelegate<dynamic>>本地化委托,用于更改Widget默认的提示语,按钮text等
localeListResolutionCallbackLocaleListResolutionCallback该回调负责在应用启动时以及用户更改设备的区域设置时选择应用的区域设置
localeResolutionCallbackLocaleResolutionCallback当传入的是不支持的语种,可通过该回调做相应处理
supportedLocalesIterable<Locale>传入支持的语种列表
showPerformanceOverlaybool用于性能测试。
checkerboardRasterCacheImagesbooltrue时,打开光栅缓存图像的棋盘格
checkerboardOffscreenLayersbool为true时,打开棋盘格层
showSemanticsDebuggerbool为true时,打开Widget边框,显示布局边界
debugShowCheckedModeBannerbool为true时,在debug模式下显示右上角的debug横幅
debugShowMaterialGridbooldebug模式下是否显示Material网格
// 在构建UI前,设置一些属性
MaterialApp(
      builder: (BuildContext context, Widget child) {
        return MediaQuery(
          data: MediaQuery.of(context).copyWith(
                textScaleFactor: 1.4,
              ),
          child: child,
        );
      },
    );

Scaffold

Scaffold是App的页面框架,将整个页面分为如下的几个部分

img

Scaffold属性

属性类型简述
appBarPreferredSizeWidget界面顶部的一栏控件,相当于 Android 中的 ActionBar
bodyWidget当前页面所显示的主要内容
floatingActionButtonWidgetMaterial中所定义的FAB,是一个悬浮的功能按钮
floatingActionButtonLocationFloatingActionButtonLocation设定悬浮按钮的位置
floatingActionButtonAnimatorFloatingActionButtonAnimator悬浮按钮动画
persistentFooterButtonsList<Widget>在底部显示的一组按钮
drawerWidget开始部分的(左边)抽屉菜单
endDrawerWidget结束部分的(右边)抽屉菜单
drawerScrimColorColor打开侧滑菜单时遮盖在主要内容区的蒙层颜色
backgroundColorColor内容的背景颜色。默认为 ThemeData.scaffoldBackgroundColor
bottomNavigationBarWidget显示在底部的导航栏
bottomSheetWidget底部永久性显示的提示框
resizeToAvoidBottomInsetbool页面浮动控件部分自动调整,以避免被弹出键盘所遮盖,默认为true
primarybool是否填充顶部栏,默认为true
drawerDragStartBehaviorDragStartBehavior处理拖动开始行为的方式
drawerEdgeDragWidthdouble水平滑动将要打开侧滑菜单的区域的宽度
extendBodybool若为true且指定了bottomNavigationBar或者persistentFooterButtonsbody将延伸到Scaffold的底部
extendBodyBehindAppBarbool作用类似extendBody,但延伸的位置是AppBar

AppBar

AppBar可以显示顶部leading、title和actions等内容。底部通常为选项卡TabBar。flexibleSpace显示在AppBar的下方,高度和AppBar高度一样,可以实现一些特殊的效果

img

文档地址

示例

BottomAppBar

BottomAppBar 是一个不规则底部工具栏,它比BottomNavigationBar 灵活,可以放置文字和图标等等控件。

img

文档地址

示例

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text('Sample Code'),
    ),
    body: Center(
      child: Text('You have pressed the button'),
    ),
    bottomNavigationBar: BottomAppBar(
      shape: const CircularNotchedRectangle(),
      child: Container(height: 50.0,),
    ),
    floatingActionButton: FloatingActionButton(
      child: Icon(Icons.add),
    ),
    floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
  );
}

基础控件

在Flutter中,UI小控件有两种设计风格,一种是Material设计,这是安卓的官方设计风格,另一种则是Cupertino风格,是iOS的官方设计风格。因此,当遇到带有这两个单词开头的控件时,我们应该明确他们表达的意思。

控件简述
Text文本控件
Image图片控件
Icon图标控件
SelectableText可选文本控件
TextField文本输入框
MaterialButtonMaterial(安卓)风格按钮
RaisedButton凸起的按钮
FlatButton扁平的按钮
IconButton图标按钮
CupertinoButtonCupertino(iOS)风格按钮
OutlineButton线框按钮
Radio单选框
Checkbox多选框
Chip碎片控件
Slider滑块控件
CupertinoSlideriOS 风格滑块控件
Switch开关控件
CupertinoSwitchiOS 风格开关控件
Placeholder占位控件

Text

属性名类型简述
dataString需要显示的文本字符串
styleTextStyle文本显示的样式
textAlignTextAlign文本对齐方式
textDirectionTextDirection文本显示方向
softWrapbool是否自动换行
overflowTextOverflow溢出处理。clip:剪辑溢出的文本;fade:将溢出的文本淡化为透明;ellipsis:用省略号表示溢出;visible:在容器之外显示溢出的文本
textScaleFactordouble每个逻辑像素的字体像素值。简单说就是字体缩放系数
maxLinesint文本最多可显示的行数。如果文本超过给定的行数,则根据溢出规则截断
textSpanTextSpanTextSpan方式显示文本。需使用Text.rich构造方法创建

Image

构造方法

  • Image : 从ImageProvider中获取图片
  • Image.asset :加载资源目录中的图片
  • Image.network:加载网络图片
  • Image.file:加载本地图片文件
  • Image.memory:加载Uint8List资源图片
属性名类型简述
imageImageProvider用于自定义图片控件的情况
width/heightdouble设置Image控件自身的宽高
fitBoxFit图片的填充模式
colorColor图片颜色
colorBlendModeBlendMode对图片进行混合颜色处理,有多种值可选
alignmentAlignment设置图片的对齐位置
repeatImageRepeat设置图片的重复填充方式
centerSliceRect类似与Android中的点9处理,在图片上定义某个矩形区域用于拉伸,这9个点其实就是八个方向加上正中
gaplessPlaybackboolImageProvider发生变化时,显示新图片的过程中,如果值为true则保留旧图片直至显示出新图片为止;如果false,则不保留旧图片,直接空白等待下一张图片的加载
// 直接构造
Image(
  image: NetworkImage('https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg'),
)
​
// 调用相应的命名构造方法
Image.network('https://flutter.github.io/assets-for-api-docs/assets/widgets/owl-2.jpg')

BoxFit 填充模式

文档地址

取值简述
BoxFit.contain显示整张图片,按照原始比例缩放显示
BoxFit.fill显示整张图片,拉伸填充全部可显示区域
BoxFit.cover按照原始比例缩放,可能裁剪,填满可显示区域
BoxFit.fitHeight按照原始比例缩放,可能裁剪,高度优先填满
BoxFit.fitWidth按照原始比例缩放,可能裁剪宽度优先填满
BoxFit.none图片居中显示,不缩放原图
BoxFit.scaleDown显示整张图片,只能缩小或者原图显示

TextField

属性名类型简述
controllerTextEditingController输入框的控制器,通常用于获取输入的内容
focusNodeFocusNode用于输入框的焦点管理和监听
decorationInputDecoration输入框的装饰器,用于修改外观
keyboardTypeTextInputType设置输入类型,不同的输入类型键盘会不一样
textInputActionTextInputAction用于设置键盘动作(一般位于右下角,默认是完成)
textCapitalizationTextCapitalization配置平台键盘如何选择大写或小写键盘。
styleTextStyle文本样式
textAlignTextAlign文本位置
textDirectionTextDirection文本显示方向
autofocusbool是否自动获取焦点
obscureTextbool是否隐藏输入的文字,通常用于密码框
autocorrectbool是否自动校验
maxLinesint最大行数
maxLengthint输入的最大字符数
maxLengthEnforcedbool配合maxLength使用,达到最大长度时是否阻止输入
onChangedValueChanged<String>输入文本发生变化时回调
onEditingCompleteVoidCallback点击键盘完成按钮时触发的回调,无参数
onSubmittedValueChanged<String>点击完成按钮时触发的回调,该回调有参数,参数即为输入的值
inputFormattersList<TextInputFormatter>对输入文本的校验
cursorWidthdouble光标的宽度
cursorRadiusRadius光标的圆角
cursorColorColor光标的颜色
keyboardAppearanceBrightness键盘的外观,仅在iOS设备上支持
onTapGestureTapCallback点击输入框时的回调
enabledbool输入框是否可用
readOnlybool是否只读

装饰器 InputDecoration

属性名类型简述
iconWidget设置位于输入框前的图标
labelTextString设置描述输入框的标签
labelStyleTextStyle设置labelText的样式
helperTextString帮助文本,位于输入框下方,如果errorText为空则不会显示
helperStyleTextStyle设置helperText的样式
hintTextString提示文本,位于输入框内部
hintStyleTextStylehintText的样式
hintMaxLinesint提示文本最大行数
errorTextString错误文本信息提示
errorStyleTextStyleerrorText的样式
hasFloatingPlaceholderboollabelText是否浮动,默认为true
isDensebool输入框是否为密集型,默认为false,为true时,图标及间距会变小
contentPaddingEdgeInsetsGeometry内间距
isCollapsedbool是否装饰的大小与输入字段的大小相同。
prefixIconWidget位于输入框内部起始位置的图标
prefixWidget预先填充的Widget,跟prefixText只能同时出现一个
prefixTextString预填充的文本,例如手机号前面预先加上区号等
prefixStyleTextStyleprefixText的样式
suffixIconWidget位于输入框尾部的图标
suffixWidget位于输入框尾部的控件
suffixTextString位于尾部的填充文字
suffixStyleTextStylesuffixText的样式
counterWidget输入框右下方的计数小控件,不能和counterText同时使用
counterTextString右下方显示的文本,常用于显示输入的字符数量
counterStyleTextStylecounterText的样式
filledbool如果为true,则使用fillColor指定的颜色填充
fillColorColor输入框的背景颜色
errorBorderInputBordererrorText不为空,且输入框没有焦点时要显示的边框
focusedBorderInputBorder输入框有焦点时的边框,errorText必须为空
focusedErrorBorderInputBordererrorText不为空时,输入框有焦点时的边框
disabledBorderInputBorder输入框禁用时显示的边框,errorText必须为空
enabledBorderInputBorder输入框可用时显示的边框,errorText必须为空
borderInputBorder正常情况下的边框
enabledbool输入框是否可用

border的三种值

  • InputBorder.none 没有边框
  • OutlineInputBorder 线框
  • UnderlineInputBorder 底边线,默认的
TextField(
      decoration: InputDecoration(
        border: OutlineInputBorder(
          borderRadius: BorderRadius.all(
            Radius.circular(30), // 圆角
          ),
          borderSide: BorderSide(
            color: Colors.amber, //线框颜色为黄色
            width: 2,            //线框宽度为2
          ),
        ),
​
        labelText: "labelText",
        helperText: "helperText",
        hintText: "hintText",
        prefixIcon: Icon(Icons.perm_identity),
      ),
    );

小技巧

当输入框的默认线框无法满足时,可以使用Container容器自定义边框。这时候可以将装饰器设置为InputDecoration.collapsed(hintText: 'hint')表示禁用装饰线

输入校验 TextInputFormatter

inputFormatters属性需要我们提供一个TextInputFormatter 类型的列表,该类有三个子类提供我们使用

  • WhitelistingTextInputFormatter 白名单校验,只允许输入符合规则的字符
  • BlacklistingTextInputFormatter 黑名单校验,除了限定的字符其他的都可以输入
  • LengthLimitingTextInputFormatter 长度限制,与maxLength作用类似

前两个在实际使用时,其实是使用的Dart中正则表达式

/// 黑名单校验 + 长度限制
inputFormatters: [
        BlacklistingTextInputFormatter(RegExp("[a-z]")),
        LengthLimitingTextInputFormatter(11)
      ],
​
/// 白名单校验
inputFormatters: [BlacklistingTextInputFormatter(RegExp("[0-9]"))],

Button

Button的通用属性

属性名类型简述
onPressedVoidCallback点击事件监听
onLongPressVoidCallback长按事件监听
onHighlightChangedValueChanged<bool>水波纹高亮变化回调,按下返回true,抬起返回false
textThemeButtonTextTheme定义按钮主题
textColorColor按钮文字颜色
disabledTextColorColor禁用按钮时文字颜色
colorColor按钮颜色
disabledColorColor禁用按钮时颜色
focusColorColor获取焦点时按钮颜色
splashColorColor水波纹效果的初始化颜色
hoverColorColor当指针悬停在按钮上时的填充颜色
highlightColorColor水波纹的高亮颜色
elevationdouble阴影高度
hoverElevationdouble指针悬停在按钮上时的阴影
focusElevationdouble获取焦点时的阴影
highlightElevationdouble高亮时的阴影
disabledElevationdouble禁用时的阴影
colorBrightnessBrightness用于此按钮的主题亮度
childWidget子控件
enabledbool是否禁用按钮
paddingEdgeInsetsGeometry内边距
shapeShapeBorder设置形状
clipBehaviorClip剪裁
focusNodeFocusNode用于焦点管理和监听
autofocusbool是否自动获取焦点
animationDurationDuration设置按钮形状和阴影变化的持续时间
materialTapTargetSizeMaterialTapTargetSize配置点击目标的最小大小
minWidthdouble按钮最小宽度
heightdouble按钮高度
enableFeedbackbool是否开启按钮触觉反馈
    RaisedButton(
      child: Text('凸起按钮'),
      onPressed: (){},
      color: Colors.blue[200],
      splashColor:Colors.yellow[100],
    ),
​
    FlatButton(
      child: Text('扁平按钮'),
      onPressed: (){},
      color: Colors.blue[200],
    ),
​
    OutlineButton(
      child: Text('线框按钮'),
      onPressed: (){},
      textColor: Colors.red,
      borderSide: BorderSide(color: Colors.red,),
    ),

Radio 与 Checkbox

Radio

属性名类型简述
value动态类型此单选按钮表示的值
groupValue动态类型该组单选按钮当前选定的值
onChangedValueChanged<T>状态变化回调
activeColorColor选中时的颜色
materialTapTargetSizeMaterialTapTargetSize配置点击目标的最小大小。
focusNodeFocusNode用于焦点管理和监听
autofocusbool是否自动获得焦点
    Row(
      children: <Widget>[
        Radio(
          value: 1,
          activeColor: Colors.pink,
          groupValue: this._sex,
          onChanged: (value) {
            setState(() {
              this._sex = value;
            });
          },
        ),
        Text('男'),
        Radio(
          value: 2,
          activeColor: Colors.pink,
          groupValue: this._sex,
          onChanged: (value) {
            setState(() {
              this._sex = value;
            });
          },
        ),
        Text('女'),
      ],
    ),

Checkbox

属性名类型简述
valuebool是否选中此复选框
onChangedValueChanged<bool>该组单选按钮当前选定的值
tristatebool默认false,如果为true,复选框的值可以为true、false或null
activeColorColor选中时的颜色
checkColorColor选中时复选框图标的颜色
materialTapTargetSizeMaterialTapTargetSize配置点击目标的最小大小
focusNodeFocusNode用于焦点管理和监听
autofocusbool是否自动获得焦点
    Row(
      children: <Widget>[
        Checkbox(
          activeColor: Colors.pink,
          checkColor: Colors.blue,
          value: this._flag1,
          onChanged: (value) {
            setState(() {
              this._flag1 = value;
            });
          },
        ),
        Checkbox(
          activeColor: Colors.pink,
          checkColor: Colors.blue,
          value: this._flag2,
          onChanged: (value) {
            setState(() {
              this._flag2 = value;
            });
          },
        ),
      ],
    )

Chip

属性名类型简述
avatarWidget在碎片标签之前显示的小控件
labelWidget碎片的标签
labelStyleTextStyle标签文字样式
labelPaddingEdgeInsetsGeometry标签文字内间距
shapeShapeBorder形状
clipBehaviorClip裁剪。 默认Clip.none(不裁剪)
backgroundColorColor背景颜色
paddingEdgeInsetsGeometry内间距
deleteIconWidget添加图标按钮, 必须与onDeleted 配合使用
onDeletedVoidCallback图标按钮监听
deleteIconColorColordeleteIcon的颜色
deleteButtonTooltipMessageStringdeleteIcon长按文字提示
materialTapTargetSizeMaterialTapTargetSize配置点击目标的最小大小
elevationdouble阴影高度
shadowColorColor阴影颜色
    Chip(
      avatar: Icon(Icons.add_alert),
      label: Text('我是一个标签'),
      deleteIcon: Icon(Icons.close),
      deleteIconColor:Colors.red,
      deleteButtonTooltipMessage:'点你妹',
      labelStyle: TextStyle(color: Colors.white),
      backgroundColor: Colors.blue,
      elevation:20,
      shadowColor:Colors.grey,
      onDeleted: (){
        print('onTap');
      },
    ),

Slider 与 CupertinoSlider

Slider

属性名类型简述
valuedouble当前值 默认 0 -- 1 之间
onChangedValueChanged<double>滑动过程中调用
onChangeStartValueChanged<double>开始滑动时调用
onChangeEndValueChanged<double>滑动完成时调用
mindouble最小值 默认 0
maxdouble最大值 默认 1
divisionsint分段个数
labelString滑动时 显示的文字 (必须与divisions配合使用)
activeColorColor用于滑块轨道的活动部分的颜色
inactiveColorColor滑块轨道的非活动部分的颜色

CupertinoSlider 控件属性与Slider 基本相同。

    Slider(
        label:'current ${valuec.round()}',
        max: 100,
        min: 0,
        divisions: 5,
        activeColor:Colors.blue,
        inactiveColor: Colors.yellow,
​
        value:this.valuec,
        onChanged: (double v) {
          setState(() {
            this.valuec = v;
          });
        },
        onChangeStart: (startValue) {
          print('Started at $startValue');
        },
        onChangeEnd: (newValue) {
          print('Ended on $newValue');
        },
    ),

Switch 和 CupertinoSwitch

Switch

属性名类型简述
valuebool当前开关状态
onChangedValueChanged<bool>开关状态变化回调
activeColorColor打开状态的颜色
activeTrackColorColor打开状态时轨道上的颜色。
inactiveThumbColorColor关闭状态按钮的颜色
inactiveTrackColorColor关闭状态轨道颜色
activeThumbImageImageProvider打开状态下按钮图片
inactiveThumbImageImageProvider关闭状态下按钮图片
materialTapTargetSizeMaterialTapTargetSize配置点击目标的最小大小
dragStartBehaviorDragStartBehavior确定处理拖动启动行为的方式
focusNodeFocusNode用于焦点管理和监听
autofocusbool是否自动获得焦点

CupertinoSwitch 的属性较少

属性名类型简述
valuebool当前开关状态
onChangedValueChanged<bool>开关状态变化回调
activeColorColor打开状态的颜色
    Switch(
      activeColor:Colors.red,
      activeTrackColor:Colors.yellow,
      inactiveThumbColor:Colors.pink[200],
      inactiveTrackColor:Colors.black,
      value: this.valuea,
      onChanged: (v) {
        setState(() {
          this.valuea = v;
        });
      },
    ),

容器与布局

通常的,把只包含单个子Widget的Widget称为容器,把可以包含多个子Widget的Widget称为布局,实际上容器本身也是一种布局,因为布局本质上是指的对控件的位置的管理或控制。

万能容器

Container

它可以为 Widget 添加大小、背景等各种参数,其 child 属性用于设置它的子控件。由于它较为复杂,这里简述一下它的绘制顺序

  1. 首先会绘制transform效果
  2. 接着绘制decoration
  3. 然后绘制child
  4. 最后绘制foregroundDecoration
属性类型简述
alignmentAlignmentGeometry容器内 child 的对齐方式
paddingEdgeInsetsGeometry容器内边距
colorColor容器的背景色
decorationDecoration容器的背景装饰
foregroundDecorationDecoration容器的前景装饰
widthdouble容器的宽度
heightdouble容器的高度
constraintsBoxConstraints容器的大小限制
marginEdgeInsetsGeometry容器外边距
transformMatrix4容器的变化
childWidget容器里显示的 Widget

Container 本身是一个盒子模型

img

Container(
          width: 300.0,
          height: 500.0,
          margin: EdgeInsets.all(16.0),
          padding: EdgeInsets.all(16.0),
          alignment: Alignment.center,
          decoration: BoxDecoration(
            //背景填充颜色
            color: Colors.amberAccent,
            //背景边框
            border: Border.all(
                //边框颜色
                color: Colors.black12,
                //边框宽度
                width: 5),
            //边框圆角
            borderRadius: BorderRadius.only(
                topLeft: Radius.circular(5.0),
                topRight: Radius.circular(10.0),
                bottomLeft: Radius.circular(15.0),
                bottomRight: Radius.circular(20.0)),
            //渐变效果,会覆盖 color
            gradient: LinearGradient(
              colors: [Colors.redAccent, Colors.greenAccent, Colors.blueAccent],
            ),
            //阴影效果
            boxShadow: [BoxShadow(color: Colors.grey, blurRadius: 5.0)],
          ),
          //前景装饰,绘制在 child 之上
          foregroundDecoration: BoxDecoration(
              image: DecorationImage(image: NetworkImage('https://gitee.com/arcticfox1919/ImageHosting/raw/master/img/emm.jpg'))
          ))

Container 简单总结

  1. 未设置约束和固定宽高时,若存在子控件,则原则上Container 和子控件一样大;不存在子控件,则原则上占满全屏。
  2. 若未设置约束和宽高,但设置了alignment属性,且存在子控件,那么Container 原则上也会占满全屏
  3. 设置了固定宽高,则无论是否存在子控件,宽高都是固定的。

关于几个属性的类型:

alignment可使用三种类型的值

  • Alignment
  • FractionalOffset
  • AlignmentDirectional

其中Alignment和FractionalOffset比较类似,而AlignmentDirectional主要用于国际化中左右顺序的支持。

marginpadding都使用相同类型的值,即EdgeInsets,该类型共用四中构造方法

  • EdgeInsets.all()
  • EdgeInsets.symmetric()
  • EdgeInsets.fromLTRB()
  • EdgeInsets.only()

decoration有四种类型,都是Decoration的子类,其中三种在开发中会用到

  • BoxDecoration
  • ShapeDecoration
  • UnderlineTabIndicator

ShapeDecoration 通常是用于单独为四条边框绘制不同的效果,它的属性与BoxDecoration大致相同,其中shape属性是和BoxDecoration中不同的,类型为ShapeBorder,它的取值如下

  • Border 绘制四边
  • UnderlineInputBorder 绘制底边线
  • RoundedRectangleBorder 绘制矩形边框
  • CircleBorder 绘制圆形边框
  • StadiumBorder 绘制竖向椭圆边框
  • BeveledRectangleBorder 绘制八角边框

边距容器

Padding

主要用于设置控件之间的边距,用法参见Container

属性类型简述
paddingEdgeInsetsGeometry容器内边距
childWidget容器里显示的 Widget

基础布局

弹性布局

Flex

弹性布局,类似于 CSS 的 Flexbox。

属性类型简述
directionAxis主轴的方向
mainAxisAlignmentMainAxisAlignment子Widget 在主轴的对齐方式
mainAxisSizeMainAxisSize主轴应该占用多大的空间
crossAxisAlignmentCrossAxisAlignment子Widget 在交叉轴的对齐方式
textDirectionTextDirection子Widget 在主轴方向上的布局顺序
verticalDirectionVerticalDirection子Widget 在交叉轴方向上的布局顺序
textBaselineTextBaseline子Widget 时使用哪个基线
childrenList< Widget>Flex布局里排列的子控件

direction 的取值:主轴的方向

Axis 值简述
Axis.horizontal主轴为水平方向,子Widget 就会沿水平方向排列,则交叉轴为垂直方向。
Axis.vertical主轴为垂直方向,子Widget 就会沿垂直方向排列,则交叉轴为水平方向。

mainAxisAlignment 的取值:子控件在主轴的对齐方式

MainAxisAlignment 值简述
MainAxisAlignment.start沿着主轴的起点对齐 textDirection 必须有值,以确定是从左边开始的还是从右边开始的
MainAxisAlignment.end沿着主轴的终点对齐 textDirection 必须有值,以确定是在左边结束的还是在右边结束的
MainAxisAlignment.center在主轴上居中对齐
MainAxisAlignment.spaceBetween在主轴上,两端对齐,子控件之间的间隔都相等
MainAxisAlignment.spaceAround在主轴上,将多余的控件均匀分布给 子控件之间,而且第一个 子Widget 和 最后一个子Widget 距边框的距离是 两个 子Widget 距离的一半
MainAxisAlignment.spaceEvenly在主轴上,将多余的控件均匀分布给子控件之间,而且第一个 子Widget 和 最后一个子Widget 距边框的距离和 子控件之间的距离一样

其中最后三个属性不太好理解,这里给出图示:

MainAxisAlignment.spaceBetween

两端顶格,中间均分

img

MainAxisAlignment.spaceAround

该效果我称为拉手布局,相当于小朋友伸开手,且相互间手拉手。

img

MainAxisAlignment.spaceEvenly

均分间距

img

mainAxisSize 的取值 : 表示主轴应该占用多大的空间

MainAxisSize 值简述
MainAxisSize.min主轴的大小是能显示完 子Widget 的最小大小,主轴的大小就是 子Widget 的大小
MainAxisSize.max主轴能显示的最大的大小,根据约束来判断

crossAxisAlignment 的取值:子控件在交叉轴的对齐方式

CrossAxisAlignment 值简述
CrossAxisAlignment.start沿着交叉轴的起点对齐 verticalDirection 必须有值,以确定是从左边开始的还是从右边开始的
CrossAxisAlignment.end沿着主轴的终点对齐 verticalDirection 必须有值,以确定是在左边结束的还是在右边结束的
CrossAxisAlignment.center在交叉轴上居中对齐
CrossAxisAlignment.stretch要求 子Widget 在交叉轴上填满
CrossAxisAlignment.baseline要求 子Widget 的基线在交叉轴上对齐

textDirection 的取值:子控件在主轴方向上的布局顺序

TextDirection 值简述
TextDirection.rtl表示从右到左
TextDirection.ltr表示从左到右

verticalDirection 的取值 :子控件在交叉轴方向上的布局顺序

VerticalDirection 值简述
VerticalDirection.up表示从下到上
VerticalDirection.down表示从上到下

线性布局

Row

水平方向的线性布局

属性类型简述
mainAxisAlignmentMainAxisAlignment子Widget 在主轴的对齐方式
mainAxisSizeMainAxisSize表示主轴应该占用多大的空间
crossAxisAlignmentCrossAxisAlignment子Widget 在交叉轴的对齐方式
textDirectionTextDirection子Widget 在主轴方向上的布局顺序
verticalDirectionVerticalDirection子Widget 在交叉轴方向上的布局顺序
textBaselineTextBaseline设置 子Widget 基线
childrenList< Widget>用于排列的子控件列表

Column

垂直方向的线性布局,其属性可直接参照Row

流式布局

Wrap

属性类型简述
directionAxis主轴的方向。默认是 Axis.horizontal
alignmentWrapAlignment子Widget 在主轴上的对齐方式,默认值为WrapAlignment.start
runAlignmentWrapAlignment纵轴对齐方式,默认值为WrapAlignment.start
runSpacingdouble纵轴间距,默认是0.0
crossAxisAlignmentWrapCrossAlignment交叉轴上的对齐方式
textDirectionTextDirection子Widget 在主轴方向上的排列顺序
verticalDirectionVerticalDirection子Widget 在交叉轴方向上的排列顺序
childrenList< Widget>子控件列表

层叠布局

Stack

属性类型简述
alignmentAlignmentDirectional决定子Widget如何对齐 ,默认值为 AlignmentDirectional.topStart
textDirectionTextDirection用于确定 alignment的对齐方向
fitStackFit决定非positioned子Widget 如何去适应Stack的大小
overflowOverflow如何显示超出 Stack空间的 子widget
childrenList< Widget>排列的子控件

在Stack布局中,通常会与另外两个控件配合使用,它们是AlignPositioned,前者用于相对定位,后者用于绝对定位。

Align比较简单,这里列一下Positioned的属性

属性类型简述
leftdouble离 Stack 左边的距离
topdouble离 Stack 上边的距离
rightdouble离 Stack 右边的距离
bottomdouble离 Stack 底边的距离
widthdouble设定子控件的宽度
heightdouble设定子控件的高度

辅助布局

Center

水平垂直居中布局。类似Container设置alignment

SizedBox

固定宽高布局,类似Container设置了宽高

AspectRatio

宽高比布局。

FractionallySizedBox

百分比布局。

Card

卡片布局。

高级布局

列表 ListView

它是一种可滚动的列表,共四种构造方法。其中最常用的是ListView.builder构造方法,因为它适用于大量的列表项的情形,甚至可以是无限数量的项。

  • ListView()
  • ListView.builder()
  • ListView.separated()
  • ListView custom()

网格 GridView

用于创建二维网格列表。

  • GridView() 默认构造
  • GridView.count 用于快速的创建固定横轴数量的网格
  • GridView.extent 用于创建交叉轴子最大可容纳的网格
  • GridView.builder 同ListView的builder
  • GridView.custom 用于构建自定义子Widget

GridView.count 是在交叉轴上创建固定个数的网格,crossAxisCount为必须的属性,表示交叉轴网格的个数,而GridView.extent是在交叉轴上创建最大可容纳的网格,maxCrossAxisExtent是必须的属性,表示交叉轴上网格的最大的宽度。

表格 Table/TableRow

表格布局和线性布局比较相似,只是使用起来更简洁一些。

属性类型简述
columnWidthsMap<int, TableColumnWidth>设置每一列的宽度
defaultColumnWidthTableColumnWidth默认的每一列宽度值,默认情况下均分
textDirectionTextDirection文字方向
borderTableBorder表格边框
defaultVerticalAlignmentTableCellVerticalAlignment每一个cell的垂直方向的alignment
childrenList<TableRow>子控件列表

示例

Container(
      child: Table(
        columnWidths: const {
          //列宽
          0: FixedColumnWidth(100.0),
          1: FixedColumnWidth(200.0),
          2: FixedColumnWidth(50.0),
        },
​
        border: TableBorder.all(
          color: Colors.green,
          width: 2.0,
          style: BorderStyle.solid,
        ),
        children: [
          TableRow(
              decoration: BoxDecoration(
                color: Colors.grey,
              ),
              children: [
                SizedBox(
                  height: 30.0,
                  child: Text('姓名'),
                ),
                Text('性别'),
                Text('年龄'),
              ]
          ),
          TableRow(
              children: [
                Text('张三'),
                Text('男'),
                Text('20'),
              ]
          ),
          TableRow(
              children: [
                Text('李四'),
                Text('女'),
                Text('28'),
              ]
          ),
        ],
      ),
    );

栈索引

IndexedStack 继承自Stack,用于显示第index个child,而其他child则是不可见的。所以IndexedStack的尺寸永远是跟最大的子控件尺寸一致。与Stack相比,只是多了index的设置。

class _HomePageState extends State<HomePage> {
  int _pageIndex = 0;
​
  @override
  Widget build(BuildContext context) {
    print("_HomePageState build ...");
    const bgColor = const [      Colors.red,      Colors.green,      Colors.blue,      Colors.yellow    ];
    return Scaffold(
      appBar: AppBar(
        title: Text("Flutter Widget"),
      ),
      body: Container(
        child: Column(
          children: <Widget>[
            Expanded(
              child: IndexedStack(
                index: _pageIndex,
                children: <Widget>[
                  Container(
                    color: Colors.red,
                  ),
                  Container(
                    color: Colors.green,
                  ),
                  Container(
                    color: Colors.blue,
                  ),
                ],
              ),
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                FlatButton(child: Text("1"),onPressed: ()=>onClick(0),),
                FlatButton(child: Text("2"),onPressed: ()=>onClick(1)),
                FlatButton(child: Text("3"),onPressed: ()=>onClick(2)),
              ],
            )
          ],
        ),
      ),
    );
  }
​
  void onClick(int index){
    setState(() {
      _pageIndex = index;
    });
  }
}

事件与通知

Flutter中的触摸事件可分为两层来看待。第一层是触摸原事件(指针),有相应的四种事件类型:PointerDownEvent:用户与屏幕接触产生了联系。PointerMoveEvent:手指已从屏幕上的一个位置移动到另一个位置。PointerUpEvent:用户已停止接触屏幕。PointerCancelEvent:此指针的输入不再指向此应用程序。第二层就是检测到的手势,主要分为三大类,包括轻击、拖动和缩放

GestureDetector

GestureDetector 可以进行手势检测,共有7种类型事件。比如点击一次、双击、长按、垂直滑动及水平滑动等

img

属性类型简述
onTapDownGestureTapDownCallback手指按下时触发
onTapUpGestureTapUpCallback手指抬起时触发
onTapGestureTapCallback单击屏幕时触发
onTapCancelGestureTapCancelCallback没有完成Tap的动作时触发
onDoubleTapGestureTapCallback快速双击屏幕时触发
onLongPressGestureLongPressCallback长按屏幕时触发
onLongPressStartGestureLongPressStartCallback监听长按事件的开始
onLongPressMoveUpdateGestureLongPressMoveUpdateCallback长按屏幕且移动手指时触发
onLongPressUpGestureLongPressUpCallback手指完全离开屏幕时触发
onLongPressEndGestureLongPressEndCallback手指开始离开屏幕时触发
onVerticalDragDownGestureDragDownCallback手指按下并在垂直方向上移动时触发
onVerticalDragStartGestureDragStartCallback触摸点开始在垂直方向上移动时触发(在onVerticalDragDown之后)
onVerticalDragUpdateGestureDragUpdateCallback触摸点在垂直方向上移动时触发
onVerticalDragEndGestureDragEndCallback停止移动,且该拖拽操作就被认为是完成了时触发
onVerticalDragCancelGestureDragCancelCallback突然停止拖拽时触发
onHorizontalDragDownGestureDragDownCallback参见onVerticalDragDown,为水平方向
onHorizontalDragStartGestureDragStartCallback参见onVerticalDragDown,为水平方向
onHorizontalDragUpdateGestureDragUpdateCallback参见onVerticalDragDown,为水平方向
onHorizontalDragEndGestureDragEndCallback参见onVerticalDragDown,为水平方向
onHorizontalDragCancelGestureDragCancelCallback参见onVerticalDragDown,为水平方向
onPanDownGestureDragDownCallback手指接触屏幕,并且可能开始移动时触发
onPanStartGestureDragStartCallback触摸点开始移动时触发
onPanUpdateGestureDragUpdateCallback触摸点不断移动时触发
onPanEndGestureDragEndCallback操作完成手指离开屏幕时触发
onPanCancelGestureDragCancelCallback先前触发onPanDown的指针未完成时触发
onScaleStartGestureScaleStartCallback触摸屏幕开始缩放操作时触发
onScaleUpdateGestureScaleUpdateCallback缩放过程中的监听
onScaleEndGestureScaleEndCallback手指离开屏幕,缩放完成时触发
onForcePressStartGestureForcePressStartCallback触摸屏幕且有足够压力时触发(仅在具有压力检测的屏幕设备支持)
onForcePressPeakGestureForcePressPeakCallback触摸屏幕压力达到最大时触发(需设备支持)
onForcePressUpdateGestureForcePressUpdateCallback有足够的压力并在屏幕上移动时触发(需设备支持)
onForcePressEndGestureForcePressEndCallback离开屏幕时触发(需设备支持)
behaviorHitTestBehavior在命中测试期间,此手势检测器应如何表现
excludeFromSemanticsbool是否从语义树中排除这些手势。 例如,用于显示工具提示的长按手势被排除在外,因为工具提示本身直接包含在语义树中,因此具有显示该工具提示的手势将导致信息重复
dragStartBehaviorDragStartBehavior设定处理拖动开始行为的方式

需要注意,GestureDetector并不会监听所有的手势,只有传入的回调非空时,才会监听。所以,如果想要禁用某个手势时,可将对应的回调函数设置为null

另外,GestureDetector的某些事件是互斥的,不能同时存在,例如onVerticalUpdateonHorizontalUpdateonPanUpdate这三个事件不能同时存在,onPanUpdateonScaleUpdate也不能同时存在。

测试手势处理回调

GestureDetector(
        child: Container(
          width: 300.0,
          height: 500.0,
          color: Colors.amber,
        ),
        onTapDown: (_) => debugPrint("onTapDown"),
        onTapUp: (_) => debugPrint("onTapUp"),
        onTap: () => debugPrint("onTap"),
        onTapCancel: () => debugPrint("onTapCancel"),
        onDoubleTap: () => debugPrint("onDoubleTap"),
        onLongPress: () => debugPrint("onLongPress"),
        onLongPressUp: () => debugPrint("onLongPressUp"),
        onVerticalDragDown: (_) => debugPrint("onVerticalDragDown"),
        onVerticalDragStart: (_) => debugPrint("onVerticalDragStart"),
        onVerticalDragUpdate: (_) => debugPrint("onVerticalDragUpdate"),
        onVerticalDragEnd: (_) => debugPrint("onVerticalDragEnd"),
        onVerticalDragCancel: () => debugPrint("onVerticalDragCancel"),
        onHorizontalDragDown: (_) => debugPrint("onHorizontalDragDown"),
        onHorizontalDragStart: (_) => debugPrint("onHorizontalDragStart"),
        onHorizontalDragUpdate: (_) => debugPrint("onHorizontalDragUpdate"),
        onHorizontalDragEnd: (_) => debugPrint("onHorizontalDragEnd"),
        onHorizontalDragCancel: () => debugPrint("onHorizontalDragCancel"),
        onPanDown: (_) => debugPrint("onPanDown"),
        onPanStart: (_) => debugPrint("onPanStart"),
        onPanUpdate: (_) => debugPrint("onPanUpdate"),
        onPanEnd: (_) => debugPrint("onPanEnd"),
        onPanCancel: () => debugPrint("onPanCancel"),
        onScaleStart: (_) => debugPrint("onScaleStart"),
        onScaleUpdate: (_) => debugPrint("onScaleUpdate"),
        onScaleEnd: (_) => debugPrint("onScaleEnd"),
      ),

通过手势处理实现一个拖动示例

class _HomePageState extends State<HomePage> {
  double _left = 0.0;
  double _top = 0.0;
​
  GlobalKey _gKey = GlobalKey();
​
  double stackWidth = 0.0;
  double stackHeight = 0.0;
​
  @override
  void initState() {
    super.initState();
​
    // 注册一个回调,当屏幕渲染第一帧的时候回调
    WidgetsBinding.instance.addPostFrameCallback((_){
        stackWidth = _gKey.currentContext.size.width;
        stackHeight = _gKey.currentContext.size.height;
    });
​
  }
​
  @override
  Widget build(BuildContext context) {
    print("_HomePageState build ...");
​
    return Scaffold(
      appBar: AppBar(
        title: Text("Flutter Widget"),
      ),
      body: GestureDetector(
        onPanUpdate: (DragUpdateDetails details){
          setState(() {
            // 边界检查
            if(_left + 200 > stackWidth){
              _left = stackWidth- 200;
            }else if(_left < 0){
              _left = 0;
            } else{
              _left += details.delta.dx;
            }
​
            if(_top + 200 > stackHeight){
              _top = stackHeight- 200;
            }else if(_top < 0){
              _top = 0;
            }else{
              _top += details.delta.dy;
            }
          });
        },
        child: Stack(
          key: _gKey,
          children: <Widget>[
            Positioned(
              left: _left,
              top: _top,
              width: 200.0,
              height: 200.0,
              child: Container(
                color: Colors.amber,
              ),
            )
          ],
        ),
      ),
    );
  }
}

缩放示例

class _ScaleWidgetState extends State<ScaleWidget>{
  double _width = 300.0;
  double _height = 200.0;
  @override
  Widget build(BuildContext context) {
    return Center(
        child: GestureDetector(
          child: Container(
            width: _width,
            height: _height,
            color: Colors.pink,
          ),
          onScaleUpdate: (e) {
            print(e);
            setState(() {
              // 限制缩放比例在0.7-1.2之间,超过范围则变为原大小
              _width = 300* e.scale.clamp(0.7, 1.2);
              _height = 200*e.scale.clamp(0.7, 1.2);
            });
          },
        )
    );
  }
}

InkWell

具有水波纹效果(或称为溅墨效果)的点击事件控件。能处理的触摸事件很少,如需处理复杂的手势事件,应使用GestureDetector。

需要注意,当InkWell的父控件设置了背景色时,是看不到溅墨效果效果的,此时需要进行特殊处理, 当使用InkWell包裹image时,也无法显示出溅墨效果,这时建议使用Ink.Image控件。

// 需使用Matetial 以及 Ink控件包裹
Material(
    child: Ink(
        decoration: BoxDecoration(
            color: Colors.blue,
            borderRadius: BorderRadius.circular(10)
        ),
        child: InkWell(
            onTap: (){
                print("onTap...");
            },
            child: Container(
                padding: EdgeInsets.symmetric(horizontal: 16,vertical: 6),
                child: Text("这是按钮",style: TextStyle(color: Colors.white),),
            ),
        ),
    ),
)

如需取消溅墨效果,可进行如下设置

// 设置highlightColor和radius取消溅墨效果
InkWell(
    highlightColor: Colors.transparent,
    radius: 0.0,
    child: Text(
        "click me",
      ),
    onTap: () {
      print("onTap");
    },
  ),

Snackbar

它是Material Design设计中的一个轻量级消息通知控件。

属性类型简述
contentWidget展示的内容
backgroundColorColor背景色
elevationdouble阴影高度
shapeShapeBorder形状
behaviorSnackBarBehavior位置,SnackBarBehavior.fixed 固定在底部;SnackBarBehavior.floating显示在底部导航栏上方
actionSnackBarAction执行的动作(相当于按钮)
durationDuration停留时间
animationanimation显示或隐藏的动画效果
onVisibleVoidCallback显示时的回调
    Scaffold.of(context).showSnackBar(SnackBar(
      content: Text('这是一个 SnackBar'),
      backgroundColor: Colors.black26,
      duration: Duration(seconds: 1),
      behavior: SnackBarBehavior.floating,
      action: SnackBarAction(
          label: '我是按钮',
          onPressed: () {
            print('点击了按钮');
          }),
    ));

小结:

  • 显示SnackbarScaffold.of(context).showSnackBar(snackBar)
  • 隐藏当前的SnackBarScaffold.of(context).hideCurrentSnackBar()

布局小案例

案例一

img

            Container(
              margin: const EdgeInsets.all(8.0),
              padding: const EdgeInsets.all(8.0),
              child: AspectRatio(
                  aspectRatio: 4 / 3,
                  child: Stack(
                    fit: StackFit.expand,
                    children: <Widget>[
                      Image.network(
                        'https://c-ssl.duitang.com/uploads/item/201810/07/20181007131933_qhjkl.thumb.1000_0.jpg',
                        fit: BoxFit.cover,
                      ),
                      Align(
                        alignment: Alignment.bottomCenter,
                        child: Container(
                          padding: const EdgeInsets.only(left: 16),
                          alignment: Alignment.centerLeft,
                          height: 60,
                          // width: double.infinity,
                          color: Color.fromRGBO(0, 0, 0, 0.5),
                          child: Text("日本万年一遇美女——桥本环奈",style: TextStyle(color: Colors.white,fontSize: 18),),
                        ),
                      )
                    ],
                  )),
              decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(8),
                  border: Border.all(color: Colors.grey, width: 1),
                  color: Colors.white,
                  boxShadow: [
                    BoxShadow(
                        color: Colors.grey[400],
                        offset: Offset(2, 4),
                        blurRadius: 2)
                  ]),
            ),

案例二

img

Container(
            color: Colors.grey[200],
            child: Column(
              children: <Widget>[
                Container(
                  padding: const EdgeInsets.symmetric(horizontal: 16),
                  height: 65,
                  color: Colors.white,
                  child: Row(
                    children: <Widget>[
                      Padding(
                        padding: const EdgeInsets.only(right: 16.0),
                        child: Icon(
                          Icons.wifi,
                          color: Colors.blueAccent,
                        ),
                      ),
                      Expanded(
                        child: Column(
                          mainAxisAlignment: MainAxisAlignment.center,
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: <Widget>[
                            Text(
                              "网络和互联网",
                              style: TextStyle(fontSize: 18),
                            ),
                            Text(
                              "WLAN、移动网络、流量使用",
//                              maxLines: 1,
//                              overflow: TextOverflow.ellipsis,
                              style: TextStyle(color: Colors.grey[600]),
                            )
                          ],
                        ),
                      ),
                      Icon(Icons.arrow_forward_ios,color: Colors.grey,)
                    ],
                  ),
                ),
                Container(
                  height: 65,
                  margin: const EdgeInsets.only(top: 16),
                  padding: const EdgeInsets.symmetric(horizontal: 16),
                  color: Colors.white,
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: <Widget>[
                      Text("设置项1",style: TextStyle(fontSize: 18),),
                      Checkbox(
                        value: isCheck,
                        onChanged: (val){
​
                        },
                      ),
//                      Checkbox(
//                        value: isCheck,
//                        onChanged: (val){
//
//                        },
//                      )
                    ],
                  ),
                ),
                Container(
                  height: 65,
                  margin: const EdgeInsets.only(top: 16),
                  padding: const EdgeInsets.symmetric(horizontal: 16),
                  color: Colors.white,
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: <Widget>[
                      Text("是否开启蓝牙",style: TextStyle(fontSize: 18),),
                      Switch(value: isCheck,onChanged: (val){
​
                      },),
                      CupertinoSwitch(value: isCheck,)
​
                    ],
                  ),
                ),
                Container(
                  height: 65,
                  margin: const EdgeInsets.only(top: 16),
                  padding: const EdgeInsets.symmetric(horizontal: 16),
                  color: Colors.white,
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: <Widget>[
                      Text("音量",style: TextStyle(fontSize: 18),),
                      Slider(
                        value: 50,
                        min: 0,
                        max: 100,
                          divisions:10,
                        onChanged: (val){
​
                        },
                      )
​
                    ],
                  ),
                )
              ],
            ),
          )

案例三

img

class _HomePageState extends State<HomePage> {
  bool _isShow = false;
​
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        alignment: Alignment.topLeft,
        decoration: BoxDecoration(
            image: DecorationImage(
                fit: BoxFit.fill,
                image: NetworkImage(
                    "http://8.pic.pc6.com/thumb/up/2011-12/20111221111636431530_600_0.png"))),
        child: Container(
          width: double.infinity,
          decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(12),
              color: Color.fromRGBO(0, 0, 0, 0.5)),
          margin: EdgeInsets.only(top: 60),
          // 线性布局的主轴默认占满屏幕
          child: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Padding(
                padding: const EdgeInsets.all(8.0),
                child: Text(
                  "这是标题",
                  style: TextStyle(color: Colors.white, fontSize: 20),
                ),
              ),
​
              // 用于控制Widget 是否显示的控件
              Visibility(
                visible: _isShow,
                child: Text("这是一段文本内容这是一段文本内容这这是一段文本内容是一段文本内容这是一段文本内容这是一段文本内容这是一段文本内容",
                    style: TextStyle(color: Colors.white, fontSize: 18)),
              ),
​
              // onPressed 按钮点击事件回调
              IconButton(icon:Icon(_isShow?Icons.keyboard_arrow_up:Icons.keyboard_arrow_down,size: 40,color: Colors.white),onPressed:(){
                print("onPressed !");
                // 更新屏幕
                setState(() {
                  _isShow = !_isShow;
                });
              })
            ],
          ),
        ),
      ),
    );
  }
}

布局综合案例练习

img

import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
​
import 'view.dart';
​
class LoginPage2 extends StatefulWidget {
  @override
  _LoginPage2State createState() => _LoginPage2State();
}
​
class _LoginPage2State extends State<LoginPage2> {
  TextEditingController _phoneNum = TextEditingController();
  TextEditingController _password = TextEditingController();
​
  TapGestureRecognizer _tapGestureRecognizer;
​
  GlobalKey<ScaffoldState> _gk = GlobalKey<ScaffoldState>();
​
  @override
  void initState() {
    super.initState();
    _tapGestureRecognizer = TapGestureRecognizer();
    _tapGestureRecognizer.onTap = myTap;
  }
​
  void myTap(){
    print("myTap run 。。。。");
  }
​
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      resizeToAvoidBottomPadding: false,
      key: _gk,
      body: Stack(
        children: <Widget>[
          CurveBgWidget(color: const Color(0xfff5f6f7),),
​
          Align(
            alignment: Alignment.topCenter,
            child: Container(
              width: 100,
              height: 100,
              margin: EdgeInsets.only(top: 150),
              decoration: BoxDecoration(
                  color: Colors.white,
                  shape: BoxShape.circle
              ),
            ),
          ),
​
          Container(
            padding: EdgeInsets.only(top: 100,left: 16,right: 16),
            margin: EdgeInsets.only(top: 180,right: 16,left: 16),
            decoration: BoxDecoration(
              color: Colors.white,
              borderRadius: BorderRadius.circular(8)
            ),
            child: Column(
              children: <Widget>[
                TextField(
                  keyboardType: TextInputType.phone,
                  controller: _phoneNum,
                  maxLines: 1,
                  maxLength: 30,
                  decoration: InputDecoration(
                      prefixIcon: Icon(Icons.phone_iphone),
                      hintText: "请输入手机号",
                      contentPadding: EdgeInsets.symmetric(
                          vertical: 10)),
                ),
                Padding(
                  padding: EdgeInsets.only(
                      top: 10, bottom: 20),
                  child: TextField(
                    controller: _password,
                    maxLines: 1,
                    maxLength: 32,
                    obscureText: true,
                    keyboardType: TextInputType.emailAddress,
                    decoration: InputDecoration(
                        prefixIcon: Icon(Icons.lock_outline),
                        hintText: "请输入登录密码",
                        contentPadding: EdgeInsets.symmetric(
                            vertical: 10),
                        suffixIcon: Icon(
                          Icons.remove_red_eye,
                        )),
                  ),
                ),
                Row(
                    mainAxisAlignment: MainAxisAlignment.end,
                    children: <Widget>[
                      Text(
                        '忘记密码?',
                        style: TextStyle(fontSize: 16,color: Color(0xff333333)),
                      )
                    ]),
​
                InkWell(
                  child: Container(
                    alignment: Alignment.center,
                    margin: EdgeInsets.symmetric(vertical: 12),
                    padding: EdgeInsets.symmetric(vertical: 6),
                    child: Text(
                      '登录',
                      style: TextStyle(
                          fontSize: 20, color: Colors.white),
                    ),
                    decoration: BoxDecoration(
                        gradient: LinearGradient(colors: [
                          const Color(0xFFEA3D87),
                          const Color(0xFFFF7095)
                        ]),
                        borderRadius: BorderRadius.circular(20),
                        boxShadow: [
                          BoxShadow(
                            offset: Offset(1.0, 5.0),
                            color: Color.fromRGBO(234, 61, 135, 0.4),
                            blurRadius: 5.0,
                          )
                        ]),
                  ),
                ),
                SizedBox(height: 20,),
​
                Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    Text('验证码登录',style: TextStyle(fontSize: 16,color: Color(0xff333333))),
​
                    Container(
                        height: 12,
                        padding: EdgeInsets.symmetric(horizontal: 8),
                        child: VerticalDivider(width: 0.0,color: Color(0xFFDEDEDE),thickness: 1,)),
​
                    InkWell(
                      onTap: (){
                      },
                      child: Text('新用户注册',style: TextStyle(
                          fontSize: 16,
                          color: Color(0xFF02A9FF)),),
                    ),
                  ],
                ),
                SizedBox(height: 20,),
​
                Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    Checkbox(
                      value: true,
                      onChanged: (v){},
                    ),
​
                    Text.rich(
                        TextSpan(
                            text:'我已阅读并同意遵守',
                            style: TextStyle(
                                fontSize: 16,
                                color: const Color(0xff999999)
                            ),
                            children: [
                              TextSpan(
                                text: '《服务许可协议》',
                                style: TextStyle(
                                    fontSize: 16,
                                    decoration: TextDecoration.underline,
                                    color: const Color(0xff333333)
                                ),
                                recognizer: _tapGestureRecognizer
                              )
                            ]
                        )),
                  ],
                )
              ],
            ),
          ),
          Container(
            alignment: Alignment.topCenter,
            margin: EdgeInsets.only(top: 160),
            child: Column(
              children: <Widget>[
                Container(
                  width: 80,
                  height: 80,
                  decoration: BoxDecoration(
                    shape: BoxShape.circle,
                    color: Colors.pinkAccent
                  ),
                ),
                SizedBox(height: 16,),
                Text("登录")
              ],
            ),
          )
        ],
      ),
    );
  }
}

\