Flutter 学习

Flutter 学习

参照:https://book.flutterchina.club/
参照:https://flutter.cn/docs/development/platform-integration/platform-channels?tab=type-mappings-java-tab
目前进度:https://book.flutterchina.club/chapter9/animated_widgets.html(https://book.flutterchina.club/chapter9/hero.html#_9-4-1-%E8%87%AA%E5%AE%9E%E7%8E%B0hero%E5%8A%A8%E7%94%BB)
Material Design所有图标可以在其官网查看:https://material.io/tools/icons/

一、基础

基础显示模块为:StatelessWidget 相当于–安卓的Activity
基础渲染布局的位置:StatelessWidget-build 返回 new MaterialApp 相当于–安卓的setContentView()
页面标题:MaterialApp-title 相当于–安卓的manifist的label
主题:MaterialApp-theme
标题栏:MaterialApp-home-Scaffold-appBar;
标题栏左侧:MaterialApp-home-Scaffold-appBar-leading;
标题栏右侧:MaterialApp-home-Scaffold-appBar- actions: [];
内容主体:MaterialApp-home-Scaffold-body;
悬浮按钮:MaterialApp-home-Scaffold-floatingActionButton
内容区域:Text控件(不可变)或者StatefulWidget(可变);
私有变量:以“_”下划线为前缀的变量;
绑定State状态控制:StatefulWidget-createState
点击事件:Widget-onTap
触发页面刷新:setState((){})
跳转到下一个页面: Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) {}),); 或者Navigator.of(context).pushNamed(RouterPath.pagePathLogin);
页面路由注册:MaterialApp-routes: {RouterPath.pagePathLogin: (BuildContext context) => Login(),}
调试:debugger(when: offset > 30.0);
切换焦点:FocusScope.of(context).requestFocus(FocusNode());
隐藏软键盘:FocusNode().unfocus();FocusScope.of(context).unfocus();
焦点改变监听:FocusNode().addListener((){print(focusNode.hasFocus);});
屏幕宽度:double.infinity
获取当前RenderObject大小:context.size
获取当前Widget主题:
获取当前Widget的坐标: GlobalKey.currentContext?.position(offset: offset)
获取当前Widget的宽高: GlobalKey.currentContext?.size(存在异常可能,慎重使用)
获取当前Widget的RenderBox: GlobalKey.currentContext?.renderBox()
extension BuildContextExt on BuildContext {
  /// 获取当前组件的 RenderBox
  RenderBox? renderBox() {
    return findRenderObject() is RenderBox ? (findRenderObject() as RenderBox) : null;
  }

  /// 获取当前组件的 position
  Offset? position({Offset offset = Offset.zero}) {
    return renderBox()?.localToGlobal(offset);
  }
}

获取文本宽度:
  ///value: 文本内容;
  ///fontSize : 文字的大小;
  ///fontWeight:文字权重;
  ///maxWidth:文本框的最大宽度;
  ///maxLines:文本支持最大多少行
  static double getTextWidth(BuildContext context, String value, FontWeight? fontWeight, double? fontSize, double? maxWidth, int? maxLines) {
    if (value.isEmpty) {
      return 0.0;
    }
    TextPainter painter = TextPainter(
      locale: Localizations.localeOf(context),
      maxLines: maxLines,
      textDirection: TextDirection.ltr,
      text: TextSpan(
        text: value,
        style: TextStyle(
          fontSize: fontSize,
          fontWeight: fontWeight,
        ),
      ),
    );
    painter.layout(maxWidth: maxWidth ?? double.infinity);
    return painter.width;
  }

二、控件

1.基础

文本—Text()

该组件可让您创建一个带格式的文本。

  • pair:文字文本
  • textAlign:文本的对齐方式
  • maxLines:指定文本显示的最大行数
  • overflow:如果有多余的文本,可以通过overflow来指定截断方式,默认是直接截断,本例中指定的截断方式TextOverflow.ellipsis,它会将多余文本截断后以省略符“…”表示
  • textScaleFactor:文本相对于当前字体大小的缩放因子,默认值将为1.0;
  • TextStyle.color:文本颜色;
  • TextStyle.fontSize:文本字体大小;
  • TextStyle.height:文本行高,但它并不是一个绝对值,而是一个因子,具体的行高等于fontSize*height;
  • TextStyle.fontFamily:文本字体;
  • TextStyle.textScaleFactor:跟随系统的字体大小;
  • TextStyle.background:文本背景;
  • TextStyle.decoration:文本装饰,下划线等;
  • TextStyle.decorationStyle:文本装饰,样式;
  • TextStyle.inherit:是否继承默认样式;

不同样式显示文本—TextSpan

  • style:默认样式;
  • text:显示文本;
  • children:是一个TextSpan的数组,也就是说TextSpan可以包括其他TextSpan;
  • recognizer:用于对该文本片段上用于手势进行识别处理;
    – 点击处理:recognizer: TapGestureRecognizer()…onTap = () {}

图标按钮—IconButton()

是一个可点击的Icon,不包括文字,默认没有背景,点击后会出现背景

漂浮按钮—ElevatedButton()

默认带有阴影和灰色背景。按下后,阴影会变大

文本按钮—TextButton()

默认背景透明并不带阴影。按下后,会有背景色

边框按钮—OutlineButton()

默认有一个边框,不带阴影且背景透明。按下后,边框颜色会变亮、同时出现背景和阴影(较弱)

图片加载—Image()

数据源可以是asset、文件、内存以及网络。
ImageProvider:是一个抽象类,主要定义了图片数据获取的接口load(),从不同的数据源获取图片需要实现不同的ImageProvider ,如AssetImage是实现了从Asset中加载图片的 ImageProvider,而NetworkImage 实现了从网络加载图片的 ImageProvider。
一个必选的image参数,它对应一个 ImageProvider。

  • AssetImage:资源图片加载;
  • NetworkImage:网络图片加载

属性

  • width:图片宽度;
  • height:图片高度;
  • color:图片的混合色值,相当于tint;
  • colorBlendMode:混合模式,相当于tint渲染模式;
  • fit:缩放模式,模式是在BoxFit中定义,它是一个枚举类型;
    – fill:会拉伸填充满显示空间,图片本身长宽比会发生变化,图片会变形。
    – cover:会按图片的长宽比放大后居中填满显示空间,图片不会变形,超出显示空间部分会被剪裁。
    – contain:这是图片的默认适应规则,图片会在保证图片本身长宽比不变的情况下缩放以适应当前显示空间,图片不会变形。
    – fitWidth:图片的宽度会缩放到显示空间的宽度,高度会按比例缩放,然后居中显示,图片不会变形,超出显示空间部分会被剪裁。
    – fitHeight:图片的高度会缩放到显示空间的高度,宽度会按比例缩放,然后居中显示,图片不会变形,超出显示空间部分会被剪裁。
    – none:图片没有适应策略,会在显示空间内显示图片,如果图片比显示空间大,则显示空间只会显示图片中间部分。
  • alignment:对齐方式;
  • repeat:重复方式;

圆形图片—CircleAvatar()

  • backgroundColor:需要显示的背景图片provide
  • radius:图片大小

字体图标—Text.style.fontFamily

所有图标
开启逻辑:

flutter:
  uses-material-design: true

开关按钮—Switch()

只能定义宽度,高度也是固定的,不保存状态;

  • onChanged:状态改变监听;
  • activeColor:选中状态颜色;

选择按钮—Checkbox()

大小是固定的,无法自定义,不保存状态;

  • onChanged:状态改变监听;
  • activeColor:选中状态颜色;
  • tristate:表示是否为三态,其默认值为false ,这时 Checkbox 有两种状态即“选中”和“不选中”,对应的 value 值为true和false ;如果tristate值为true时,value 的值会增加一个状态null,读者可以自行测试;

文本输入框—TextField()

  • controller:(TextEditingController)编辑框的控制器,通过它可以设置/获取编辑框的内容、选择编辑内容、监听编辑文本改变事件。大多数情况下我们都需要显式提供一个controller来与文本框交互。如果没有提供controller,则TextField内部会自动创建一个。
  • focusNode:(FocusNode)用于控制TextField是否占有当前键盘的输入焦点。它是我们和键盘交互的一个句柄(handle)。
  • decoration:(InputDecoration)用于控制TextField的外观显示,如提示文本、背景颜色、边框等。
  • keyboardType:(TextInputType)用于设置该输入框默认的键盘输入类型,取值如下:
    – text 文本输入键盘
    – multiline 多行文本,需和maxLines配合使用(设为null或大于1)
    – number 数字;会弹出数字键盘
    – phone 优化后的电话号码输入键盘;会弹出数字键盘并显示“* #”
    – datetime 优化后的日期输入键盘;Android上会显示“: -”
    – emailAddress 优化后的电子邮件地址;会显示“@ .”
    – url 优化后的url输入键盘; 会显示“/ .”
  • textInputAction:(TextInputAction)键盘动作按钮图标(即回车键位图标),它是一个枚举值,有多个可选值,例如->TextInputAction.search
  • style:(TextStyle)正在编辑的文本样式。
  • textAlign:(TextAlign)输入框内编辑文本在水平方向的对齐方式。
  • autofocus:是否自动获取焦点。
  • obscureText:是否隐藏正在编辑的文本,如用于输入密码的场景等,文本内容会用“•”替换。
  • maxLines:输入框的最大行数,默认为1;如果为null,则无行数限制。
  • maxLength:代表输入框文本的最大长度,设置后输入框右下角会显示输入的文本计数;
  • maxLengthEnforcement:决定当输入文本长度超过maxLength时如何处理,如截断、超出等。
  • toolbarOptions:长按或鼠标右击时出现的菜单,包括 copy、cut、paste 以及 selectAll。
  • onChange:输入框内容改变时的回调函数;注:内容改变事件也可以通过controller来监听
  • onEditingComplete:输入框输入完成时触发,函数无参数;
  • onSubmitted:回调是ValueChanged类型,它接收当前输入内容做为参数;
  • inputFormatters:用于指定输入格式;当用户输入内容改变时,会根据指定的格式来校验。
  • enable:如果为false,则输入框会被禁用,禁用状态不接收输入和事件,同时显示禁用态样式(在其decoration中定义)。
  • cursorWidth:输入框光标宽度;
  • cursorRadius:输入框光标圆角;
  • cursorColor:输入框光标颜色;
  • decoration.labelText:输入框文本;
  • decoration.prefixIcon:输入框图标;
  • decoration.enabledBorder:未获得焦点样式;
  • decoration.focusedBorder:获得焦点样式;
  • decoration.hintStyle:提示颜色;
  • decoration.hintText:提示文本;
  • decoration.border:(InputBorder)下划线或者说是边框;

表单—Form()

实际业务中,在正式向服务器提交数据前,都会对各个输入框数据进行合法性校验,但是对每一个TextField都分别进行校验将会是一件很麻烦的事。还有,如果用户想清除一组TextField的内容,除了一个一个清除有没有什么更好的办法呢?为此,Flutter提供了一个Form 组件,它可以对输入框进行分组,然后进行一些统一操作,如输入内容校验、输入框重置以及输入内容保存。
FormState:FormState为Form的State类,可以通过Form.of()或GlobalKey获得。我们可以通过它来对Form的子孙FormField进行统一操作。我们看看其常用的三个方法:

  • FormState.validate():调用此方法后,会调用Form子孙FormField的validate回调,如果有一个校验失败,则返回false,所有校验失败项都会返回用户返回的错误提示。
  • FormState.save():调用此方法后,会调用Form子孙FormField的save回调,用于保存表单内容
  • FormState.reset():调用此方法后,会将子孙FormField的内容清空。

登录按钮的onPressed方法中不能通过Form.of(context)来获取FormState,原因是,此处的context为FormTestRoute的context,而Form.of(context)是根据所指定context向根去查找,而FormState是在FormTestRoute的子树中,所以不行。正确的做法是通过Builder来构建登录按钮,Builder会将widget节点的context作为回调参数:

属性:

  • autovalidate:是否自动校验输入内容;当为true时,每一个子 FormField 内容发生变化时都会自动校验合法性,并直接显示错误信息。否则,需要通过调用FormState.validate()来手动校验。
  • onWillPop:决定Form所在的路由是否可以直接返回(如点击返回按钮),该回调返回一个Future对象,如果 Future 的最终结果是false,则当前路由不会返回;如果为true,则会返回到上一个路由。此属性通常用于拦截返回按钮。
  • onChanged:Form的任意一个子FormField内容发生变化时会触发此回调。

表单子元素—FormField()

Form的子孙元素必须是FormField类型,FormField是一个抽象类,定义几个属性,FormState内部通过它们来完成操作

  • onSaved:(FormFieldSetter )保存回调;
  • validator:(FormFieldValidator )保存回调;
  • initialValue:初始值;
  • autovalidate:是否自动校验;

表单输入控件—TextFormField()

继承自FormField类,也是TextField的一个包装类,所以除了FormField定义的属性之外,它还包括TextField的属性

线性进度条----LinearProgressIndicator()

  • value:value表示当前的进度,取值范围为[0,1];如果value为null时则指示器会执行一个循环动画(模糊进度);当value不为null时,指示器为一个具体进度的进度条;
  • backgroundColor:指示器的背景色;
  • valueColor:指示器的进度条颜色;值得注意的是,该值类型是Animation,这允许我们对进度条的颜色也可以指定动画。如果我们不需要对进度条颜色执行动画,换言之,我们想对进度条应用一种固定的颜色,此时我们可以通过AlwaysStoppedAnimation来指定;

圆形进度条—CircularProgressIndicator()

  • value:value表示当前的进度,取值范围为[0,1];如果value为null时则指示器会执行一个循环动画(模糊进度);当value不为null时,指示器为一个具体进度的进度条;
  • backgroundColor:指示器的背景色;
  • valueColor:指示器的进度条颜色;值得注意的是,该值类型是Animation,这允许我们对进度条的颜色也可以指定动画。如果我们不需要对进度条颜色执行动画,换言之,我们想对进度条应用一种固定的颜色,此时我们可以通过AlwaysStoppedAnimation来指定;
  • strokeWidth:圆形进度条的粗细;

下拉菜单选择按钮—DropdownButton()

  • items:下拉选项列表
  • selectedItemBuilder:选项 item 构造器
  • value:选中内容
  • hint:启动状态下默认内容
  • disabledHint:禁用状态下默认内容
  • onChanged:选择 item 回调
  • elevation = 8:阴影高度
  • style:选项列表 item 样式
  • underline:按钮下划线
  • icon:下拉按钮图标
  • iconDisabledColor:禁用状态下图标颜色
  • iconEnabledColor:启动时图标颜色
  • iconSize = 24.0:图标尺寸
  • isDense = false:是否降低按钮高度
  • isExpanded = false:是否将下拉列表内容设置水平填充

下拉菜单选择按钮Item—DropdownMenuItem()

  • value:对应选中状态内容
  • child:下拉列表 item 内容

2.约束

盒模型配置—BoxConstraints(最大最小宽高)

盒模型额外约束—ConstrainedBox()

盒模型固定宽高—SizedBox()

拦截父级约束—UnconstrainedBox()

指定子组件的长宽比—AspectRatio()

指定最大宽高—LimitedBox()

可以根据父容器宽高的百分比来设置子组件宽高—FractionallySizedBox()

3.布局

弹性布局—Flex()

Flex组件可以沿着水平或垂直方向排列子组件,如果你知道主轴方向,使用Row或Column会方便一些,因为Row和Column都继承自Flex,参数基本相同,所以能使用Flex的地方基本上都可以使用Row或Column。Flex本身功能是很强大的,它也可以和Expanded组件配合实现弹性布局。接下来我们只讨论Flex和弹性布局相关的属性(其他属性已经在介绍Row和Column时介绍过了)。

弹性布局比例扩展—Expanded()相对于安卓等比布局

Expanded 只能作为 Flex 的孩子(否则会报错),它可以按比例“扩伸”Flex子组件所占用的空间。因为 Row和Column 都继承自 Flex,所以 Expanded 也可以作为它们的孩子。
flex参数为弹性系数,如果为 0 或null,则child是没有弹性的,即不会被扩伸占用的空间。如果大于0,所有的Expanded按照其 flex 的比例来分割主轴的全部空闲空间。
Spacer的功能是占用指定比例的空间,实际上它只是Expanded的一个包装类,相当于空白占位

流式布局—Wrap、Flow

溢出部分则会自动折行

  • spacing:主轴方向子widget的间距
  • runSpacing:纵轴方向的间距
  • runAlignment:纵轴方向的对齐方式

层叠布局—(Stack、Positioned)

层叠布局和 Web 中的绝对定位、Android 中的 Frame 布局是相似的,子组件可以根据距父容器四个角的位置来确定自身的位置。层叠布局允许子组件按照代码中声明的顺序堆叠起来。Flutter中使用Stack和Positioned这两个组件来配合实现绝对定位。Stack允许子组件堆叠,而Positioned用于根据Stack的四个角来确定子组件的位置。

  • Positioned:left、top 、right、 bottom分别代表离Stack左、上、右、底四边的距离。width和height用于指定需要定位元素的宽度和高度。注意,Positioned的width、height 和其他地方的意义稍微有点区别,此处用于配合left、top 、right、 bottom来定位组件,举个例子,在水平方向时,你只能指定left、right、width三个属性中的两个,如指定left和width后,right会自动算出(left+width),如果同时指定三个属性则会报错,垂直方向同理。
  • alignment:此参数决定如何去对齐没有定位(没有使用Positioned)或部分定位的子组件。所谓部分定位,在这里特指没有在某一个轴上定位:left、right为横轴,top、bottom为纵轴,只要包含某个轴上的一个定位属性就算在该轴上有定位。默认居中
  • textDirection:和Row、Wrap的textDirection功能一样,都用于确定alignment对齐的参考系,即:textDirection的值为TextDirection.ltr,则alignment的start代表左,end代表右,即从左往右的顺序;textDirection的值为TextDirection.rtl,则alignment的start代表右,end代表左,即从右往左的顺序。
  • fit:此参数用于确定没有定位的子组件如何去适应Stack的大小。StackFit.loose表示使用子组件的大小,StackFit.expand表示扩伸到Stack的大小。
  • clipBehavior:此属性决定对超出Stack显示空间的部分如何剪裁,Clip枚举类中定义了剪裁的方式,Clip.hardEdge 表示直接剪裁,不应用抗锯齿,更多信息可以查看源码注释。

对齐与相对定位—Align()

简单的调整一个子元素在父元素中的位置的话,使用Align组件会更简单一些。

  • alignment:表示子组件在父组件中的起始位置。
  • widthFactor、heightFactor:用于确定Align 组件本身宽高的属性;它们是两个缩放因子,会分别乘以子元素的宽、高,最终的结果就是Align 组件的宽高。如果值为null,则组件的宽高将会占用尽可能多的空间。

父布局信息获取—LayoutBuilder()

通过 LayoutBuilder,我们可以在布局过程中拿到父组件传递的约束信息,然后我们可以根据约束信息动态的构建不同的布局。

布局结束后布局信息回调—AfterLayout()

可以在子组件布局完成后执行一个回调,并同时将 RenderObject 对象作为参数传递。

Row、 Column

这些具有弹性空间的布局类Widget可让您在水平(Row)和垂直(Column)方向上创建灵活的布局。其设计是基于web开发中的Flexbox布局模型。

  • children: [] 子控件列表
  • textDirection: 表示水平方向子组件的布局顺序(是从左往右还是从右往左),默认为系统当前Locale环境的文本方向(如中文、英语都是从左往右,而阿拉伯语是从右往左)。即开始布局顺序,是从左侧开始布局第一个还是右侧开始布局第一个
  • mainAxisSize: 表示Row在主轴(水平)方向占用的空间,默认是MainAxisSize.max,表示尽可能多的占用水平方向的空间,此时无论子 widgets 实际占用多少水平空间,Row的宽度始终等于水平方向的最大宽度;而MainAxisSize.min表示尽可能少的占用水平空间,当子组件没有占满水平剩余空间,则Row的实际宽度等于所有子组件占用的的水平空间;相当于是安卓的match还是wrap_content
  • mainAxisAlignment: 表示子组件在Row所占用的水平空间内对齐方式,如果mainAxisSize值为MainAxisSize.min,则此属性无意义,因为子组件的宽度等于Row的宽度。只有当mainAxisSize的值为MainAxisSize.max时,此属性才有意义,MainAxisAlignment.start表示沿textDirection的初始方向对齐,如textDirection取值为TextDirection.ltr时,则MainAxisAlignment.start表示左对齐,textDirection取值为TextDirection.rtl时表示从右对齐。而MainAxisAlignment.end和MainAxisAlignment.start正好相反;MainAxisAlignment.center表示居中对齐。读者可以这么理解:textDirection是mainAxisAlignment的参考系。相当于安卓的layout_gravity(水平方向)
  • verticalDirection: 表示Row纵轴(垂直)的对齐方向,默认是VerticalDirection.down,表示从上到下。即开始布局顺序,是从底下开始第一个还是从顶部开始第一个
    容器(Row、Column等).crossAxisAlignment: 表示子组件在纵轴方向的对齐方式,Row的高度等于子组件中最高的子元素高度,它的取值和MainAxisAlignment一样(包含start、end、 center三个值),不同的是crossAxisAlignment的参考系是verticalDirection,即verticalDirection值为VerticalDirection.down时crossAxisAlignment.start指顶部对齐,verticalDirection值为VerticalDirection.up时,crossAxisAlignment.start指底部对齐;而crossAxisAlignment.end和crossAxisAlignment.start正好相反;相当于安卓的layout_gravity(垂直方向)

Stack

取代线性布局 (译者语:和Android中的LinearLayout相似),Stack允许子 widget 堆叠, 你可以使用 Positioned 来定位他们相对于Stack的上下左右四条边的位置。Stacks是基于Web开发中的绝度定位(absolute positioning )布局模型设计的。

3.容器

布局填充—Padding()

可以给其子节点添加填充(留白),和边距效果类似。

  • EdgeInsets.fromLTRB:分别指定四个方向的填充。
  • EdgeInsets.all:所有方向均使用相同数值的填充。
  • EdgeInsets.only:可以设置具体某个方向的填充(可以同时指定多个方向)。
  • EdgeInsets.symmetric:用于设置对称方向的填充,vertical指top和bottom,horizontal指left和right。

装饰容器—DecoratedBox()

可以在其子组件绘制前(或后)绘制一些装饰(Decoration),如背景、边框、渐变等。

  • decoration:代表将要绘制的装饰,它的类型为Decoration。Decoration是一个抽象类,它定义了一个接口 createBoxPainter(),子类的主要职责是需要通过实现它来创建一个画笔,该画笔用于绘制装饰。
  • position:此属性决定在哪里绘制Decoration,它接收DecorationPosition的枚举类型,该枚举类有两个值:background:在子组件之后绘制,即背景装饰。foreground:在子组件之上绘制,即前景。

装饰容器常用—DecoratedBox.decoration(BoxDecoration())

  • color:颜色
  • image:图片
  • border:边框
  • borderRadius:圆角
  • boxShadow:阴影,可以指定多个
  • gradient:渐变
  • backgroundBlendMode:背景混合模式
  • shape:形状

变换—Transform()

可以在其子组件绘制时对其应用一些矩阵变换来实现一些特效。
Transform.translate接收一个offset参数,可以在绘制时沿x、y轴对子组件平移指定的距离。
Transform.rotate可以对子组件进行旋转变换
Transform.scale可以对子组件进行缩小或放大

变换—RotatedBox()

变换是在layout阶段,会影响在子组件的位置和大小。

组件—Container()

Container 可让您创建矩形视觉元素。container 可以装饰为一个BoxDecoration, 如 background、一个边框、或者一个阴影。 Container 也可以具有边距(margins)、填充(padding)和应用于其大小的约束(constraints)。另外, Container可以使用矩阵在三维空间中对其进行变换。
是一个组合类容器,它本身不对应具体的RenderObject,它是DecoratedBox、ConstrainedBox、Transform、Padding、Align等组件组合的一个多功能容器,所以我们只需通过一个Container组件可以实现同时需要装饰、变换、限制的场景。

  • padding:容器内补白,属于decoration的装饰范围
  • margin:容器外补白,不属于decoration的装饰范围
  • color:背景色
  • decoration:背景装饰
  • foregroundDecoration:前景装饰
  • width:容器的宽度
  • height:容器的高度
  • constraints:容器大小的限制条件
  • transform:变换

剪裁

Clip()

ClipOval()

沿宽高进行圆形或椭圆裁剪

ClipRRect()

圆角矩形剪裁

ClipRect()

默认剪裁掉子组件布局空间之外的绘制内容(溢出部分剪裁)

ClipPath()

按照自定义的路径剪裁

继承CustomClipper()

getClip() 是用于获取剪裁区域的接口,由于图片大小是60×60,我们返回剪裁区域为Rect.fromLTWH(10.0, 15.0, 40.0, 30.0),即图片中部40×30像素的范围。
shouldReclip() 接口决定是否重新剪裁。如果在应用中,剪裁区域始终不会发生变化时应该返回false,这样就不会触发重新剪裁,避免不必要的性能开销。如果剪裁区域会发生变化(比如在对剪裁区域执行一个动画),那么变化后应该返回true来重新执行剪裁。

空间适配—FittedBox()

单独的一个控件超出父容器限制处理

  • fit:适配方式
  • alignment:对齐方式
  • clipBehavior:是否剪裁

页面骨架—Scaffold()

包含:一个导航栏、导航栏右边有一个分享按钮、有一个抽屉菜单、有一个底部导航、右下角有一个悬浮的动作按钮

  • appBar:导航栏
  • appBar.leading:导航栏最左侧Widget,常见为抽屉菜单按钮或返回按钮。
  • appBar.automaticallyImplyLeading:如果leading为null,是否自动实现默认的leading按钮
  • appBar.title:页面标题
  • appBar.actions:导航栏右侧菜单
  • appBar.bottom:导航栏底部菜单,通常为Tab按钮组
  • appBar.elevation:导航栏阴影
  • appBar.centerTitle:标题是否居中
  • appBar.backgroundColor:背景颜色
  • drawer:抽屉
  • bottomNavigationBar:底部导航
  • floatingActionButton:悬浮按钮

4.可滚动组件

布局模型

主要由三个角色组成:Scrollable、Viewport 和 Sliver:

  • Scrollable :用于处理滑动手势,确定滑动偏移,滑动偏移变化时构建 Viewport 。
  • Viewport:显示的视窗,即列表的可视区域;
  • Sliver:视窗里显示的元素。

具体布局过程:

  • Scrollable 监听到用户滑动行为后,根据最新的滑动偏移构建 Viewport 。
  • Viewport 将当前视口信息和配置信息通过 SliverConstraints 传递给 Sliver。
  • Sliver 中对子组件(RenderBox)按需进行构建和布局,然后确认自身的位置、绘制等信息,保存在 geometry 中(一个 SliverGeometry 类型的对象)。
Scrollable()

用于处理滑动手势,确定滑动偏移,滑动偏移变化时构建 Viewport

Viewport()

用于渲染当前视口中需要显示 Sliver

  • offset:用户的滚动偏移
  • cacheExtent、cacheExtentStyle:CacheExtentStyle 是一个枚举,有 pixel 和 viewport 两个取值。当 cacheExtentStyle 值为 pixel 时,cacheExtent 的值为预渲染区域的具体像素长度;当值为 viewport 时,cacheExtent 的值是一个乘数,表示有几个 viewport 的长度,最终的预渲染区域的像素长度为:cacheExtent * viewport 的积, 这在每一个列表项都占满整个 Viewport 时比较实用,这时 cacheExtent 的值就表示前后各缓存几个页面。
Sliver()

Sliver 主要作用是对子组件进行构建和布局,比如 ListView 的 Sliver 需要实现子组件(列表项)按需加载功能,只有当列表项进入预渲染区域时才会去对它进行构建和布局、渲染。
Sliver 对应的渲染对象类型是 RenderSliver,RenderSliver 和 RenderBox 的相同点是都继承自 RenderObject 类,不同点是在布局的时候约束信息不同。

常用的 Sliver

Sliver名称 功能 对应的可滚动组件
SliverList 列表 ListView
SliverFixedExtentList 高度固定的列表 ListView,指定itemExtent时
SliverAnimatedList 添加/删除列表项可以执行动画 AnimatedList
SliverGrid 网格 GridView
SliverPrototypeExtentList 根据原型生成高度固定的列表 ListView,指定prototypeItem 时
SliverFillViewport 包含多个子组件,每个都可以填满屏幕 PageView
SliverAppBar 对应 AppBar,主要是为了在 CustomScrollView 中使用。
SliverToBoxAdapter 一个适配器,可以将 RenderBox 适配为 Sliver,后面介绍。
SliverPersistentHeader 滑动到顶部时可以固定住,后面介绍。

对 Sliver 进行布局、装饰的组件,它们的子组件必须是 Sliver

SliverPadding Padding
SliverVisibility、SliverOpacity Visibility、Opacity
SliverFadeTransition FadeTransition
SliverLayoutBuilder LayoutBuilder
SliverAppBar

它最常见的使用场景是在作为 NestedScrollView 的 header

  • collapsedHeight:收缩起来的高度
  • expandedHeight:展开时的高度
  • pinned:是否固定
  • floating:是否漂浮
  • snap:漂浮时,此参数才有
  • forceElevated:导航栏下面是否一直显示阴影
Scrollbar()

是一个Material风格的滚动指示器(滚动条),如果要给可滚动组件添加滚动条,只需将Scrollbar作为可滚动组件的任意一个父级组件即可

  • axisDirection:滚动方向
  • physics:它决定可滚动组件如何响应用户操作,比如用户滑动完抬起手指后,继续执行动画;或者滑动到边界时,如何显示。
  • controller:控制滚动位置和监听滚动事件
  • viewportBuilder:构建 Viewport 的回调。当用户滑动时,Scrollable 会调用此回调构建新的 Viewport,同时传递一个 ViewportOffset 类型的 offset 参数,该参数描述 Viewport 应该显示那一部分内容。
CupertinoScrollbar()

是 iOS 风格的滚动条,如果你使用的是Scrollbar,那么在iOS平台它会自动切换为CupertinoScrollbar。

SingleChildScrollView()

类似于Android中的ScrollView,它只能接收一个子组件

ListView()

ListView是最常用的可滚动组件之一,它可以沿一个方向线性排布所有子组件,并且它也支持列表项懒加载(在需要时才会创建)。

  • itemExtent:该参数如果不为null,则会强制children的“长度”为itemExtent的值;这里的“长度”是指滚动方向上子组件的长度,也就是说如果滚动方向是垂直方向,则itemExtent代表子组件的高度;如果滚动方向为水平方向,则itemExtent就代表子组件的宽度。
  • prototypeItem:如果我们知道列表中的所有列表项长度都相同但不知道具体是多少,这时我们可以指定一个列表项,该列表项被称为 prototypeItem(列表项原型)。
  • shrinkWrap:该属性表示是否根据子组件的总长度来设置ListView的长度,默认值为false 。默认情况下,ListView会在滚动方向尽可能多的占用空间。当ListView在一个无边界(滚动方向上)的容器中时,shrinkWrap必须为true。
  • addAutomaticKeepAlives:该属性我们将在介绍 PageView 组件时详细解释。
  • addRepaintBoundaries:该属性表示是否将列表项(子组件)包裹在RepaintBoundary组件中。RepaintBoundary 读者可以先简单理解为它是一个”绘制边界“,将列表项包裹在RepaintBoundary中可以避免列表项不必要的重绘,但是当列表项重绘的开销非常小(如一个颜色块,或者一个较短的文本)时,不添加RepaintBoundary反而会更高效(具体原因会在本书后面 Flutter 绘制原理相关章节中介绍)。如果列表项自身来维护是否需要添加绘制边界组件,则此参数应该指定为 false。

ListView.builder适合列表项比较多或者列表项不确定的情况

  • itemBuilder:它是列表项的构建器,类型为IndexedWidgetBuilder,返回值为一个widget。当列表滚动到具体的index位置时,会调用该构建器构建列表项。
  • itemCount:列表项的数量,如果为null,则为无限列表。

ListView.separated可以在生成的列表项之间添加一个分割组件,它比ListView.builder多了一个separatorBuilder参数,该参数是一个分割组件生成器。

ps:ListView 中的列表项组件都是 RenderBox,并不是 Sliver, 这个一定要注意。
一个 ListView 中只有一个Sliver,对列表项进行按需加载的逻辑是 Sliver 中实现的。
ListView 的 Sliver 默认是 SliverList,如果指定了 itemExtent ,则会使用 SliverFixedExtentList;如果 prototypeItem 属性不为空,则会使用 SliverPrototypeExtentList,无论是是哪个,都实现了子组件的按需加载模型。

多滑动控件容器—CustomScrollView()

  • slivers:滑动控件列表

原理:

  • CustomScrollView 组合 Sliver 的原理是为所有子 Sliver 提供一个共享的 Scrollable,然后统一处理指定滑动方向的滑动事件。
  • CustomScrollView 和 ListView、GridView、PageView 一样,都是完整的可滚动组件(同时拥有 Scrollable、Viewport、Sliver)。
  • CustomScrollView 只能组合 Sliver,如果有孩子也是一个完整的可滚动组件(通过 SliverToBoxAdapter 嵌入)且它们的滑动方向一致时便不能正常工作
将 RenderBox 适配为 Sliver—SliverToBoxAdapter()
滑动到 CustomScrollView 的顶部时固定—SliverPersistentHeader()
  • pinned:header 滑动到可视区域顶部时是否固定在顶部
  • floating:pinned 为 false 时 ,则 header 可以滑出可视区域(CustomScrollView 的 Viewport)(不会固定到顶部),当用户再次向下滑动时,此时不管 header 已经被滑出了多远,它都会立即出现在可视区域顶部并固定住,直到继续下滑到 header 在列表中原来的位置时,header 才会重新回到原来的位置(不再固定在顶部)。

嵌套可滚动组件—NestedScrollView()

  • headerSliverBuilder:header,sliver构造器,外部可滚动组件(outer scroll view),可以认为这个可滚动组件就是 CustomScrollView ,所以它只能接收 Sliver
  • body:可以接受任意的可滚动组件,可以接收任意的可滚动组件,该可滚动组件称为内部可滚动组件 (inner scroll view)
    Flutter 学习_第1张图片
    注意:
  • 要确认内部的可滚动组件(body)的 physics 是否需要设置为 ClampingScrollPhysics。比如上面的示例运行在 iOS 中时,ListView 如果没有设置为 ClampingScrollPhysics,则用户快速滑动到顶部时,会执行一个弹性效果,此时 ListView 就会与 header 显得割裂(滑动效果不统一),所以需要设置。但是,如果 header 中只有一个 SliverAppBar 则不应该加,因为 SliverAppBar 是固定在顶部的,ListView 滑动到顶部时上面已经没有要继续往下滑动的元素了,所以此时出现弹性效果是符合预期的。
  • 内部的可滚动组件(body的)不能设置 controller 和 primary,这是因为 NestedScrollView 的协调器中已经指定了它的 controller,如果重新设定则协调器将会失效。

滚动监听

ScrollController

  • initialScrollOffset:初始滚动位置
  • keepScrollOffset:是否保存滚动位置
  • offset:可滚动组件当前的滚动位置。
  • jumpTo(double offset)、animateTo(double offset,…):这两个方法用于跳转到指定的位置,它们不同之处在于,后者在跳转时会执行一个动画,而前者不会。
  • addListener(()=>print(controller.offset)):添加滚动监听;
  • ListView(key: PageStorageKey(1), …):通过不同的key的PageStorageKey来存储各自列表的状态数据,用户恢复;
  • controller.positions.elementAt(0):读取记录不同列表的ScrollPosition数据;

原理: 当ScrollController和可滚动组件关联时,可滚动组件首先会调用ScrollController的createScrollPosition()方法来创建一个ScrollPosition来存储滚动位置信息,接着,可滚动组件会调用attach()方法,将创建的ScrollPosition添加到ScrollController的positions属性中,这一步称为“注册位置”,只有注册后animateTo() 和 jumpTo()才可以被调用。
当可滚动组件销毁时,会调用ScrollController的detach()方法,将其ScrollPosition对象从ScrollController的positions属性中移除,这一步称为“注销位置”,注销后animateTo() 和 jumpTo() 将不能再被调用。
需要注意的是,ScrollController的animateTo() 和 jumpTo()内部会调用所有ScrollPosition的animateTo() 和 jumpTo(),以实现所有和该ScrollController关联的可滚动组件都滚动到指定的位置。

NotificationListener

滚动监听的类
接收到滚动事件时,参数类型为ScrollNotification,它包括一个metrics属性,它的类型是ScrollMetrics,该属性包含当前ViewPort及滚动位置等信息:

  • pixels:当前滚动位置。
  • maxScrollExtent:最大可滚动长度。
  • extentBefore:滑出ViewPort顶部的长度;此示例中相当于顶部滑出屏幕上方的列表长度。
  • extentInside:ViewPort内部长度;此示例中屏幕显示的列表部分的长度。
  • extentAfter:列表中未滑入ViewPort部分的长度;此示例中列表底部未显示到屏幕范围部分的长度。
  • atEdge:是否滑到了可滚动组件的边界(此示例中相当于列表顶或底部)

带有操作动画的列表—AnimatedList()

AnimatedList 和 ListView 的功能大体相似,不同的是, AnimatedList 可以在列表中插入或删除节点时执行一个动画,在需要添加或删除列表项的场景中会提高用户体验。
AnimatedList 是一个 StatefulWidget,它对应的 State 类型为 AnimatedListState,添加和删除元素的方法位于 AnimatedListState 中:
例如:

 // 添加一个列表项
data.add('${++counter}');
// 告诉列表项有新添加的列表项
globalKey.currentState!.insertItem(data.length - 1);
// 移除一个列表项
globalKey.currentState!.removeItem(
    index,
    (context, animation) {
      // 删除过程执行的是反向动画,animation.value 会从1变为0
      var item = buildItem(context, index);
      print('删除 ${data[index]}');
      data.removeAt(index);
      // 删除动画是一个合成动画:渐隐 + 缩小列表项告诉
      return FadeTransition(
        opacity: CurvedAnimation(
          parent: animation,
          //让透明度变化的更快一些
          curve: const Interval(0.5, 1.0),
        ),
        // 不断缩小列表项的高度
        child: SizeTransition(
          sizeFactor: animation,
          axisAlignment: 0.0,
          child: item,
        ),
      );
    },
    duration: Duration(milliseconds: 200), // 动画时间为 200 ms
  );

二维网格列表(宫格列表)—GridView()

  • gridDelegate:类型是SliverGridDelegate,它的作用是控制GridView子组件如何排列(layout)。Flutter中提供了两个SliverGridDelegate的子类SliverGridDelegateWithFixedCrossAxisCount和SliverGridDelegateWithMaxCrossAxisExtent

SliverGridDelegateWithFixedCrossAxisCount
该子类实现了一个横轴为固定数量子元素的layout算法,子元素的大小是通过crossAxisCount和childAspectRatio两个参数共同决定的。注意,这里的子元素指的是子组件的最大显示空间,注意确保子组件的实际大小不要超出子元素的空间。

  • crossAxisCount:横轴子元素的数量。此属性值确定后子元素在横轴的长度就确定了,即ViewPort横轴长度除以crossAxisCount的商。
  • mainAxisSpacing:主轴方向的间距。
  • crossAxisSpacing:横轴方向子元素的间距。
  • childAspectRatio:子元素在横轴长度和主轴长度的比例。由于crossAxisCount指定后,子元素横轴长度就确定了,然后通过此参数值就可以确定子元素在主轴的长度。

SliverGridDelegateWithMaxCrossAxisExtent
该子类实现了一个横轴子元素为固定最大长度的layout算法

  • maxCrossAxisExtent为子元素在横轴上的最大长度,之所以是“最大”长度,是因为横轴方向每个子元素的长度仍然是等分的,举个例子,如果ViewPort的横轴长度是450,那么当maxCrossAxisExtent的值在区间[450/4,450/3)内的话,子元素最终实际长度都为112.5,而childAspectRatio所指的子元素横轴和主轴的长度比为最终的长度比。其他参数和SliverGridDelegateWithFixedCrossAxisCount相同。

固定数量子元素的GridView—GridView.count()

横轴子元素为固定最大长度的的GridView—GridView.extent()

动态创建子widget----GridView.builder()

轮播或左右上下页面切换控件—PageView()

如果要实现页面切换和 Tab 布局,我们可以使用 PageView 组件。

  • scrollDirection:滑动方向Axis.horizontal,
  • pageSnapping:每次滑动是否强制切换整个页面,如果为false,则会根据实际的滑动距离显示页面
  • allowImplicitScrolling:是否进行左右各一个页面缓存功能;

可滚动组件子项缓存(侵入性高)—AutomaticKeepAlive()

虽然 PageView 的默认构造函数和 PageView.builder 构造函数中没有该参数,但它们最终都会生成一个 SliverChildDelegate 来负责列表项的按需加载,而在 SliverChildDelegate 中每当列表项构建完成后,SliverChildDelegate 都会为其添加一个 AutomaticKeepAlive 父组件。
AutomaticKeepAlive 的组件的主要作用是将列表项的根 RenderObject 的 keepAlive 按需自动标记 为 true 或 false。为了方便叙述,我们可以认为根 RenderObject 对应的组件就是列表项的根 Widget,代表整个列表项组件,同时我们将列表组件的 Viewport区域 + cacheExtent(预渲染区域)称为加载区域

  • 当 keepAlive 标记为 false 时,如果列表项滑出加载区域时,列表组件将会被销毁。
  • 当 keepAlive 标记为 true 时,当列表项滑出加载区域后,Viewport 会将列表组件缓存起来;当列表项进入加载区域时,Viewport 从先从缓存中查找是否已经缓存,如果有则直接复用,如果没有则重新创建列表项。

想要缓存在页面page中处理
让Page 页变成一个 AutomaticKeepAlive Client 即可。为了便于开发者实现,Flutter 提供了一个 AutomaticKeepAliveClientMixin ,我们只需要让 PageState 混入这个 mixin,且同时添加一些必要操作即可:

class _PageState extends State with AutomaticKeepAliveClientMixin {

  @override
  Widget build(BuildContext context) {
    super.build(context); // 必须调用
    return Center(child: Text("${widget.text}", textScaleFactor: 5));
  }

  @override
  bool get wantKeepAlive => true; // 是否需要缓存
}

可滚动组件子项缓存(侵入性低)—KeepAliveWrapper()

@override
Widget build(BuildContext context) {
  var children = [];
  for (int i = 0; i < 6; ++i) {
    //只需要用 KeepAliveWrapper 包装一下即可
    children.add(KeepAliveWrapper(child:Page( text: '$i'));
  }
  return PageView(children: children);
}

横向切换控件相关即tab相关控件

TabBar 通常位于 AppBar 的底部,它也可以接收一个 TabController ,如果需要和 TabBarView 联动, TabBar 和 TabBarView 使用同一个 TabController 即可,注意,联动时 TabBar 和 TabBarView 的孩子数量需要一致。如果没有指定 controller,则会在组件树中向上查找并使用最近的一个 DefaultTabController 。另外我们需要创建需要的 tab 并通过 tabs 传给 TabBar, tab 可以是任何 Widget,不过Material 组件库中已经实现了一个 Tab 组件,我们一般都会直接使用它。
实战中,如果需要 TabBar 和 TabBarView 联动,通常会创建一个 DefaultTabController 作为它们共同的父级组件,这样它们在执行时就会从组件树向上查找,都会使用我们指定的这个 DefaultTabController。

仿横向页面滑动控件—TabBarView()

  • children:tab 页
  • controller:TabController
  • physics
  • dragStartBehavior

TabController 用于监听和控制 TabBarView 的页面切换,通常和 TabBar 联动。如果没有指定,则会在组件树中向上查找并使用最近的一个 DefaultTabController 。

仿tablayout控件—TabBar()

  • tabs: 具体的 Tabs,需要我们创建
  • isScrollable:是否可以滑动
  • indicatorColor:指示器颜色,默认是高度为2的一条下划线
  • indicatorWeight:指示器高度
  • indicatorPadding:指示器padding
  • indicator:指示器
  • indicatorSize:指示器长度,有两个可选值,一个tab的长度,一个是label长度

TabBar子控件—Tab()

  • text:文本
  • icon:图标
  • child:自定义 widget

ps: ext 和 child 是互斥的,不能同时制定

自定义Sliver

Sliver 的布局协议如下:

  • Viewport 将当前布局和配置信息通过 SliverConstraints 传递给 Sliver。
  • Sliver 确定自身的位置、绘制等信息,保存在 geometry 中(一个 SliverGeometry 类型的对象)。
  • Viewport 读取 geometry 中的信息来对 Sliver 进行布局和绘制。

Sliver布局模型和盒布局模型
两者布局流程基本相同:父组件告诉子组件约束信息 > 子组件根据父组件的约束确定自生大小 > 父组件获得子组件大小调整其位置。不同是:

  • 父组件传递给子组件的约束信息不同。盒模型传递的是 BoxConstraints,而 Sliver 传递的是 SliverConstraints。
  • 描述子组件布局信息的对象不同。盒模型的布局信息通过 Size 和 offset描述 ,而 Sliver的是通过 SliverGeometry 描述。
  • 布局的起点不同。Sliver布局的起点一般是Viewport ,而盒模型布局的起点可以是任意的组件。

SliverConstraints()

  • axisDirection:主轴方向
  • growthDirection:Sliver 沿着主轴从列表的哪个方向插入?枚举类型,正向或反向
  • userScrollDirection:用户滑动方向
  • scrollOffset:当前Sliver理论上(可能会固定在顶部)已经滑出可视区域的总偏移
  • precedingScrollExtent:当前Sliver之前的Sliver占据的总高度,因为列表是懒加载,如果不能预估时,该值为double.infinity
  • overlap:上一个 sliver 覆盖当前 sliver 的大小,通常在 sliver 是 pinned/floating,或者处于列表头尾时有效,我们在后面的小节中会有相关的例子。
  • remainingPaintExtent:当前Sliver在Viewport中的最大可以绘制的区域。绘制如果超过该区域会比较低效(因为不会显示)
  • crossAxisExtent:纵轴的长度;如果列表滚动方向是垂直方向,则表示列表宽度。
  • crossAxisDirection:纵轴方向
  • viewportMainAxisExtent:Viewport在主轴方向的长度;如果列表滚动方向是垂直方向,则表示列表高度。
  • cacheOrigin:Viewport 预渲染区域的起点[-Viewport.cacheExtent, 0]
  • remainingCacheExtent:Viewport加载区域的长度,范围 [viewportMainAxisExtent,viewportMainAxisExtent + Viewport.cacheExtent*2]

SliverGeometry()

  • scrollExtent:Sliver在主轴方向预估长度,大多数情况是固定值,用于计算sliverConstraints.scrollOffset
  • paintExtent:可视区域中的绘制长度
  • paintOrigin:绘制的坐标原点,相对于自身布局位置
  • layoutExtent:在 Viewport中占用的长度;如果列表滚动方向是垂直方向,则表示列表高度。范围[0,paintExtent]
  • maxPaintExtent:最大绘制长度
  • hitTestExtent:点击测试的范围
  • visible:是否显示
  • hasVisualOverflow:是否会溢出Viewport,如果为true,Viewport便会裁剪
  • scrollOffsetCorrection:scrollExtent的修正值:layoutExtent变化后,为了防止sliver突然跳动(应用新的layoutExtent),可以先进行修正,具体的作用在后面 SliverFlexibleHeader 示例中会介绍。
  • cacheExtent:在预渲染区域中占据的长度

5.功能型组件

按需rebuild—ValueListenableBuilder()

它的功能是监听一个数据源,如果数据源发生变化,则会重新执行其 builder,
ValueListenableBuilder 和数据流向是无关的,只要数据源发生变化它就会重新构建子组件树,因此可以实现任意流向的数据共享。

ps: 关于 ValueListenableBuilder 有两点需要牢记:
1.和数据流向无关,可以实现任意流向的数据共享。
2.实践中,ValueListenableBuilder 的拆分粒度应该尽可能细,可以提高性能。

  • valueListenable:数据源,类型为ValueListenable
  • builder:数据源发生变化通知时,会重新调用 builder 重新 build 子组件树。
  • child:builder 中每次都会重新构建整个子组件树,如果子组件树中有一些不变的部分,可以传递给child,child 会作为builder的第三个参数传递给 builder,通过这种方式就可以实现组件缓存,原理和AnimatedBuilder 第三个 child 相同。

异步UI更新—FutureBuilder()

  • future:FutureBuilder依赖的Future,通常是一个异步耗时任务。
  • initialData:初始数据,用户设置默认数据。
  • builder:Widget构建器;该构建器会在Future执行的不同阶段被多次调用,返回参数:Function (BuildContext context, AsyncSnapshot snapshot)

异步UI更新—StreamBuilder()

  • stream:通常是一个异步耗时任务。
  • initialData:初始数据,用户设置默认数据。
  • builder:Widget构建器;该构建器会在Future执行的不同阶段被多次调用,返回参数:Function (BuildContext context, AsyncSnapshot snapshot)

触摸监听—Listener()

  • onPointerDown:手指按下回调
  • onPointerMove:手指移动回调
  • onPointerUp:手指抬起回调
  • onPointerCancel:触摸事件取消回调
  • behavior:HitTestBehavior.deferToChild

触摸事件—PointerEvent()

  • position:它是指针相对于当对于全局坐标的偏移。
  • localPosition:它是指针相对于当对于本身布局坐标的偏移。
  • delta:两次指针移动事件(PointerMoveEvent)的距离。
  • pressure:按压力度,如果手机屏幕支持压力传感器(如iPhone的3D Touch),此属性会更有意义,如果手机不支持,则始终为1。
  • orientation:指针移动方向,是一个角度值。

忽略触摸事件—IgnorePointer(不会参与命中测试也就是触摸处理)

忽略触摸事件—AbsorbPointer(会参与命中测试也就是触摸处理)

手势识别—GestureDetector()

ps: 当同时监听onTap和onDoubleTap事件时,当用户触发tap事件时,会有200毫秒左右的延时,这是因为当用户点击完之后很可能会再次点击以触发双击事件,所以GestureDetector会等一段时间来确定是否为双击事件。如果用户只监听了onTap(没有监听onDoubleTap)事件时,则没有延时。

  • onTap:点击
  • onDoubleTap:双击
  • onLongPress:长按
  • onPanDown:手指按下时会触发此回调,onPanDown: (DragDownDetails e)
  • onPanUpdate:手指滑动时会触发此回调, onPanUpdate: (DragUpdateDetails e)
  • onPanEnd:手指拿起后会触发此回单,onPanEnd: (DragEndDetails e)
  • onVerticalDragUpdate:垂直方向拖动事件,onVerticalDragUpdate: (DragUpdateDetails details)
  • onScaleUpdate:缩放事件,onScaleUpdate: (ScaleUpdateDetails details)

手势函数属性说明:

  • DragDownDetails.globalPosition:当用户按下时,此属性为用户按下的位置相对于屏幕(而非父组件)原点(左上角)的偏移。
  • DragUpdateDetails.delta:当用户在屏幕上滑动时,会触发多次Update事件,delta指一次Update事件的滑动的偏移量。
  • DragEndDetails.velocity:该属性代表用户抬起手指时的滑动速度(包含x、y两个轴的),示例中并没有处理手指抬起时的速度,常见的效果是根据用户抬起手指时的速度做一个减速动画。

手势识别—GestureRecognizer()

GestureDetector内部是使用一个或多个GestureRecognizer来识别各种手势的,而GestureRecognizer的作用就是通过Listener来将原始指针事件转换为语义手势,GestureDetector直接可以接收一个子widget。GestureRecognizer是一个抽象类,一种手势的识别器对应一个GestureRecognizer的子类,Flutter实现了丰富的手势识别器,我们可以直接使用。
PS: 使用GestureRecognizer后一定要调用其dispose()方法来释放资源(主要是取消内部的计时器)。

6.特殊组件

Interval:CurvedAnimation内部动画时间设置

//高度动画
    height = Tween(
      begin: .0,
      end: 300.0,
    ).animate(
      CurvedAnimation(
        parent: controller,
        curve: const Interval(
          0.0, 0.6, //间隔,前60%的动画时间
          curve: Curves.ease,
        ),
      ),
    );

动画切换组件-AnimatedSwitcher()

  • duration:新child显示动画时长
  • reverseDuration:旧child隐藏的动画时长
  • switchInCurve:Curves.linear, // 新child显示的动画曲线
  • switchOutCurve:Curves.linear,// 旧child隐藏的动画曲线
  • transitionBuilder:AnimatedSwitcher.defaultTransitionBuilder, // 动画构建器
  • layoutBuilder:AnimatedSwitcher.defaultLayoutBuilder, //布局构建器

当AnimatedSwitcher的 child 发生变化时(类型或 Key 不同),旧 child 会执行隐藏动画,新 child 会执行执行显示动画。究竟执行何种动画效果则由transitionBuilder参数决定,该参数接受一个AnimatedSwitcherTransitionBuilder类型的 builder,定义如下:

AnimatedSwitcher(
             duration: const Duration(milliseconds: 500),
             transitionBuilder: (Widget child, Animation animation) {
               //执行缩放动画
               return ScaleTransition(child: child, scale: animation);
             },
             child: Text(
               '$_count',
               //显示指定key,不同的key会被认为是不同的Text,这样才能执行动画
               key: ValueKey(_count),
               style: Theme.of(context).textTheme.headline4,
             ),
           )

三、控件通用属性

DefaultTextStyle:当前widget节点内部所有文本控件的默认样式
padding:内边距
package:指定报名下的资源
onPressed:点击事件;
*.color:渲染颜色
*.icon:图标,按钮的话是默认在左侧添加一个图标;
可滚动组件.scrollDirection:滑动的主轴
可滚动组件.reverse:滑动方向是否反向
可滚动组件.controller:这些属性最终会透传给对应的 Scrollable 和 Viewport
可滚动组件.physics:这些属性最终会透传给对应的 Scrollable 和 Viewport
可滚动组件.cacheExtent:这些属性最终会透传给对应的 Scrollable 和 Viewport

四、函数

Int.isOdd:此整数是否是基数
Widget.xxx:获取指定Widget下的参数数据

五、手势、事件、触摸

1.事件分发

当指针按下时,Flutter会对应用程序执行命中测试(Hit Test),以确定指针与屏幕接触的位置存在哪些组件(widget), 指针按下事件(以及该指针的后续事件)然后被分发到由命中测试发现的最内部的组件,然后从那里开始,事件会在组件树中向上冒泡,这些事件会从最内部的组件被分发到组件树根的路径上的所有组件,这和Web开发中浏览器的事件冒泡机制相似, 但是Flutter中没有机制取消或停止“冒泡”过程,而浏览器的冒泡是可以停止的。注意,只有通过命中测试的组件才能触发事件。

  • 命中测试:当手指按下时,触发 PointerDownEvent 事件,按照深度优先遍历当前渲染(render object)树,对每一个渲染对象进行“命中测试”(hit test),如果命中测试通过,则该渲染对象会被添加到一个 HitTestResult 列表当中。
  • 事件分发:命中测试完毕后,会遍历 HitTestResult 列表,调用每一个渲染对象的事件处理方法(handleEvent)来处理 PointerDownEvent 事件,该过程称为“事件分发”(event dispatch)。随后当手指移动时,便会分发 PointerMoveEvent 事件。
  • 事件清理:当手指抬( PointerUpEvent )起或事件取消时(PointerCancelEvent),会先对相应的事件进行分发,分发完毕后会清空 HitTestResult 列表。

命中测试是在 PointerDownEvent 事件触发时进行的,一个完成的事件流是 down > move > up (cancle)。
如果父子组件都监听了同一个事件,则子组件会比父组件先响应事件。这是因为命中测试过程是按照深度优先规则遍历的,所以子渲染对象会比父渲染对象先加入 HitTestResult 列表,又因为在事件分发时是从前到后遍历 HitTestResult 列表的,所以子组件比父组件会更先被调用 handleEvent 。
如果想要跳过某个组件的命中测试可以使用IgnorePointer 包裹组件

ps:

  • 组件只有通过命中测试才能响应事件。
  • 一个组件是否通过命中测试取决于 hitTestChildren(…) || hitTestSelf(…) 的值。
  • 组件树中组件的命中测试顺序是深度优先的。
  • 组件子节点命中测试的循序是倒序的,并且一旦有一个子节点的 hitTest 返回了 true,就会终止遍历,后续子节点将没有机会参与命中测试。这个原则可以结合 Stack 组件来理解。
  • 大多数情况下 Listener 的 HitTestBehavior 为 opaque 或 translucent 效果是相同的,只有当其子节点的 hitTest 返回为 false 时才会有区别。
  • HitTestBlocker 是一个很灵活的组件,我们可以通过它干涉命中测试的各个阶段。

2.触摸监听—Listener()

参考控件中的触摸监听

3.手势识别

参考控件中的手势识别

4.手势冲突以及解决

冲突处理:是因为在拖动时,刚开始按下手指且没有移动时,拖动手势还没有完整的语义,此时TapDown手势胜出(win),此时打印"down",而拖动时,拖动手势会胜出,当手指抬起时,onHorizontalDragEnd 和 onTapUp发生了冲突,但是因为是在拖动的语义中,所以onHorizontalDragEnd胜出,所以就会打印 “onHorizontalDragEnd”。
Listener监听原始指针事件
通过 Listener 解决手势冲突的原因是竞争只是针对手势的,而 Listener 是监听原始指针事件,原始指针事件并非语义话的手势,所以根本不会走手势竞争的逻辑,所以也就不会相互影响。

Positioned(
  top:80.0,
  left: _leftB,
  child: Listener(
    onPointerDown: (details) {
      print("down");
    },
    onPointerUp: (details) {
      //会触发
      print("up");
    },
    child: GestureDetector(
      child: CircleAvatar(child: Text("B")),
      onHorizontalDragUpdate: (DragUpdateDetails details) {
        setState(() {
          _leftB += details.delta.dx;
        });
      },
      onHorizontalDragEnd: (details) {
        print("onHorizontalDragEnd");
      },
    ),
  ),
)

自定义 Recognizer 解决手势冲突
自定义手势识别器的方式比较麻烦,原理时当确定手势竞争胜出者时,会调用胜出者的acceptGesture 方法,表示“宣布成功”,然后会调用其他手势识别其的rejectGesture 方法,表示“宣布失败”。既然如此,我们可以自定义手势识别器(Recognizer),然后去重写它的rejectGesture 方法:在里面调用acceptGesture 方法,这就相当于它失败是强制将它也变成竞争的成功者了,这样它的回调也就会执行。

class CustomTapGestureRecognizer extends TapGestureRecognizer {
  @override
  void rejectGesture(int pointer) {
    //不,我不要失败,我要成功
    //super.rejectGesture(pointer);
    //宣布成功
    super.acceptGesture(pointer);
  }
}

//创建一个新的GestureDetector,用我们自定义的 CustomTapGestureRecognizer 替换默认的
RawGestureDetector customGestureDetector({
  GestureTapCallback? onTap,
  GestureTapDownCallback? onTapDown,
  Widget? child,
}) {
  return RawGestureDetector(
    child: child,
    gestures: {
      CustomTapGestureRecognizer:
          GestureRecognizerFactoryWithHandlers(
        () => CustomTapGestureRecognizer(),
        (detector) {
          detector.onTap = onTap;
        },
      )
    },
  );
}


customGestureDetector( // 替换 GestureDetector
  onTap: () => print("2"),
  child: Container(
    width: 200,
    height: 200,
    color: Colors.red,
    alignment: Alignment.center,
    child: GestureDetector(
      onTap: () => print("1"),
      child: Container(
        width: 50,
        height: 50,
        color: Colors.grey,
      ),
    ),
  ),
);

六、生命周期

1.State生命周期

  • createState----------只调用一次,通常在该回调中做一些一次性的操作,如状态初始化、订阅子树的事件通知等
  • initState----------只调用一次,此时View没有渲染,但是StatefulWidget 已经被加载到渲染树里了
  • didChangeDependencies----------当StatefulWidget依赖的InheritedWidget发生变化之后,才会调用
  • build----------调用场景:在调用initState()之后、在调用didUpdateWidget()之后、在调用setState()之后、在调用didChangeDependencies()之后、State对象从树中一个位置移除后(会调用deactivate)又重新插入到树的其他位置之后。
  • reassemble----------此回调是专门为了开发调试而提供的,在热重载(hot reload)时会被调用,此回调在Release模式下永远不会被调用。
  • addPostFrameCallback ----------StatefulWidget渲染结束之后的回调,只会调用一次
  • didUpdateWidget ----------组件状态改变时候调用
  • deactivate----------当 State 对象从树中被移除时,会调用此回调。在一些场景下,Flutter 框架会将 State 对象重新插到树中,如包含此 State 对象的子树在树的一个位置移动到另一个位置时(可以通过GlobalKey 来实现)。如果移除后没有重新插入到树中则紧接着会调用dispose()方法。
  • dispose----------当 State 对象从树中被永久移除时调用;通常在此回调中释放资源。

2.App生命周期

通过WidgetsBindingObserver的didChangeAppLifecycleState 来获取。通过该接口可以获取是生命周期在AppLifecycleState类中。

1、resumed
可见并能响应用户的输入,同安卓的onResume

2、inactive
处在并不活动状态,无法处理用户响应,同安卓的onPause

3、paused
不可见并不能响应用户的输入,但是在后台继续活动中,同安卓的onStop

下面是生命周期:

初次打开widget时,不执行AppLifecycleState的回调;
按home键或Power键, AppLifecycleState inactive---->AppLifecycleState pause
从后台到前台:AppLifecycleState inactive—>ApplifecycleState resumed
back键退出应用: AppLifecycleState inactive—>AppLifecycleState paused

3.页面切换相关

后退刷新上个页面

参考:
https://blog.csdn.net/qq_25529689/article/details/125406500

后退刷新的关键:
从HomePage到SecondPage 使用
// 第一部分需要设置的地方
void navigateSecondPage() {
Route route = MaterialPageRoute(builder: (context) => SecondPage());
Navigator.push(context, route).then(onGoBack);
}
关键也在这里,就是有.then中的刷新方法。
onGoBack(dynamic value) {
refreshData();
setState(() {});
};

从SecondPage后退到HomePage时,使用
//第二部分要设置的地方
Navigator.pop(context);
也就是说,不需要额外的传递参数,后退到HomePage时会根据第一次跳转过来的 Navigator.push中的.then方法自动刷新。

七、Flutter 四棵树

既然 Widget 只是描述一个UI元素的配置信息,那么真正的布局、绘制是由谁来完成的呢?Flutter 框架的的处理流程是这样的:
Widget->Element->Render->Layer

  • 根据 Widget 树生成一个 Element 树,Element 树中的节点都继承自 Element 类。
  • 根据 Element 树生成 Render 树(渲染树),渲染树中的节点都继承自RenderObject 类。
  • 根据渲染树生成 Layer 树,然后上屏显示,Layer 树中的节点都继承自 Layer 类。

八、定义对象以及使用

/// 定义
class Echo {
  const Echo({
    Key? key,  
    required this.text,
    this.backgroundColor = Colors.grey, //默认为灰色
  }):super(key:key);
  }
/// 使用
Echo(text: "hello world");

九、Context

build方法有一个context参数,它是BuildContext类的一个实例,表示当前 widget 在 widget 树中的上下文,每一个 widget 都会对应一个 context 对象(因为每一个 widget 都是 widget 树上的一个节点)。实际上,context是当前 widget 在 widget 树中位置中执行”相关操作“的一个句柄(handle),比如它提供了从当前 widget 开始向上遍历 widget 树以及按照 widget 类型查找父级 widget 的方法。

1.函数

// 在 widget 树中向上查找最近的父级Scaffold widget
context.findAncestorWidgetOfExactType();
// 直接通过of静态方法来获取ScaffoldState
ScaffoldState _state=Scaffold.of(context);
//定义一个globalKey, 由于GlobalKey要保持全局唯一性,我们使用静态变量存储
static GlobalKey _globalKey= GlobalKey();
Scaffold(
key: _globalKey )

十、自定义 Widget

1、实际上Flutter 最原始的定义组件的方式就是通过定义RenderObject 来实现,而StatelessWidget 和 StatefulWidget 只是提供的两个帮助类。
2、如果组件不会包含子组件,则我们可以直接继承自 LeafRenderObjectWidget ,它是 RenderObjectWidget 的子类,而 RenderObjectWidget 继承自 Widget
3、如果自定义的 widget 可以包含子组件,则可以根据子组件的数量来选择继承SingleChildRenderObjectWidget 或 MultiChildRenderObjectWidget,它们也实现了createElement() 方法,返回不同类型的 Element 对象。

class CustomWidget extends LeafRenderObjectWidget{
  @override
  RenderObject createRenderObject(BuildContext context) {
    // 创建 RenderObject
    return RenderCustomObject();
  }
  @override
  void updateRenderObject(BuildContext context, RenderCustomObject  renderObject) {
    // 更新 RenderObject
    super.updateRenderObject(context, renderObject);
  }
}

class RenderCustomObject extends RenderBox{

  @override
  void performLayout() {
    // 实现布局逻辑
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    // 实现绘制
  }
}

1.控制台输入显示树结构

debugDumpApp();
debugDumpRenderTree()
debugDumpSemanticsTree()

2.要找出相对于帧的开始/结束事件发生的位置

可以切换debugPrintBeginFrameBanner (opens new window)和debugPrintEndFrameBanner (opens new window)布尔值以将帧的开始和结束打印到控制台。

十一、路由

实体后退按键拦截

   return WillPopScope(
        onWillPop: () async {
          后退按钮处理
          return false;
        },
        child: Widget);

跳转到下一个页面

Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) {}),); 或者Navigator.of(context).pushNamed(RouterPath.pagePathLogin,{Object arguments});

返回上一个页面

Navigator.of(context).pop(BuildContext context, [ result ];(result 为页面关闭时返回给上一个页面的数据)

页面路由注册

MaterialApp-routes: {RouterPath.pagePathLogin: (BuildContext context) => Login(),}

页面路由钩子

MaterialApp-onGenerateRoute: {RouterPath.pagePathLogin: (BuildContext context) => Login(),}

获取路由参数

var args=ModalRoute.of(context).settings.arguments;

注册表配置

MaterialApp(
  ... //省略无关代码
  routes: {
   "tip2": (context){
     return TipRoute(text: ModalRoute.of(context)!.settings.arguments);
   },
 }, 
);

MaterialPageRoute继承自PageRoute类,PageRoute类是一个抽象类,表示占有整个屏幕空间的一个模态路由页面,它还定义了路由构建及切换时过渡动画的相关接口及属性。MaterialPageRoute 是 Material组件库提供的组件,它可以针对不同平台,实现与平台页面切换动画风格一致的路由切换动画

  • 对于 Android,当打开新页面时,新的页面会从屏幕底部滑动到屏幕顶部;当关闭页面时,当前页面会从屏幕顶部滑动到屏幕底部后消失,同时上一个页面会显示到屏幕上。
  • 对于 iOS,当打开页面时,新的页面会从屏幕右侧边缘一直滑动到屏幕左边,直到新页面全部显示到屏幕上,而上一个页面则会从当前屏幕滑动到屏幕左侧而消失;当关闭页面时,正好相反,当前页面会从屏幕右侧滑出,同时上一个页面会从屏幕左侧滑入。

MaterialPageRoute 构造函数的各个参数的意义:

  • builder(WidgetBuilder) 是一个WidgetBuilder类型的回调函数,它的作用是构建路由页面的具体内容,返回值是一个widget。我们通常要实现此回调,返回新路由的实例。
  • settings(RouteSettings) 包含路由的配置信息,如路由名称、是否初始路由(首页)。
  • maintainState(bool):默认情况下,当入栈一个新路由时,原来的路由仍然会被保存在内存中,如果想在路由没用的时候释放其所占用的所有资源,可以设置maintainState为 false。
  • **fullscreenDialog(bool)**表示新的路由页面是否是一个全屏的模态对话框,在 iOS 中,如果- fullscreenDialog为true,新页面将会从屏幕底部滑入(而不是水平方向)。

路由钩子生成
注意,onGenerateRoute 只会对命名路由生效。通过name打开路由前的判断处理。

MaterialApp(
  ... //省略无关代码
  onGenerateRoute:(RouteSettings settings){
	  return MaterialPageRoute(builder: (context){
		   String routeName = settings.name;
       // 如果访问的路由页需要登录,但当前未登录,则直接返回登录页路由,
       // 引导用户登录;其他情况则正常打开路由。
     }
   );
  }
);

页面切换动画

Navigator.push(context, CupertinoPageRoute(  
   builder: (context)=>PageB(),
 ));
//以渐隐渐入动画来实现路由过渡,实现代码如下:
Navigator.push(
  context,
  PageRouteBuilder(
    transitionDuration: Duration(milliseconds: 500), //动画时间为500毫秒
    pageBuilder: (BuildContext context, Animation animation,
        Animation secondaryAnimation) {
      return FadeTransition(
        //使用渐隐渐入过渡,
        opacity: animation,
        child: PageB(), //路由B
      );
    },
  ),
);

无论是MaterialPageRoute、CupertinoPageRoute,还是PageRouteBuilder,它们都继承自PageRoute类,而PageRouteBuilder其实只是PageRoute的一个包装,我们可以直接继承PageRoute类来实现自定义路由,

class FadeRoute extends PageRoute {
  FadeRoute({
    required this.builder,
    this.transitionDuration = const Duration(milliseconds: 300),
    this.opaque = true,
    this.barrierDismissible = false,
    this.barrierColor,
    this.barrierLabel,
    this.maintainState = true,
  });

  final WidgetBuilder builder;

  @override
  final Duration transitionDuration;

  @override
  final bool opaque;

  @override
  final bool barrierDismissible;

  @override
  final Color barrierColor;

  @override
  final String barrierLabel;

  @override
  final bool maintainState;

  @override
  Widget buildPage(BuildContext context, Animation animation,
      Animation secondaryAnimation) => builder(context);

  @override
  Widget buildTransitions(BuildContext context, Animation animation,
      Animation secondaryAnimation, Widget child) {
     return FadeTransition( 
       opacity: animation,
       child: builder(context),
     );
  }
}
//退出无动画实现
@override
Widget buildTransitions(BuildContext context, Animation animation,
    Animation secondaryAnimation, Widget child) {
 //当前路由被激活,是打开新路由
 if(isActive) {
   return FadeTransition(
     opacity: animation,
     child: builder(context),
   );
 }else{
   //是返回,则不应用过渡动画
   return Padding(padding: EdgeInsets.zero);
 }
}

Navigator.push(context, FadeRoute(builder: (context) {
  return PageB();
}));

十二、参数配置

  • name:应用或包名称。
  • description: 应用或包的描述、简介。
  • version:应用或包的版本号。
  • dependencies:应用或包依赖的其他包或插件。
  • dev_dependencies:开发环境依赖的工具包(而不是flutter应用本身依赖的包)。
  • flutter:flutter相关的配置选项。

1.本地包依赖

dependencies:
	pkg1:
        path: ../../code/pkg1

2.git依赖

dependencies:
  pkg1:
    git:
      url: git://github.com/xxx/pkg1.git
      path: packages/package1(根目录是可以不写这个字段)

3.颜色

将颜色字符串转成 Color 对象

Color(0xffdc380d); //如果颜色固定可以直接使用整数值
//颜色是一个字符串变量
var c = "dc380d";
Color(int.parse(c,radix:16)|0xFF000000) //通过位运算符将Alpha设置为FF
Color(int.parse(c,radix:16)).withAlpha(255)  //通过方法将Alpha设置为FF

获取颜色亮度值

Color 类中提供了一个computeLuminance()方法,它可以返回一个[0-1]的一个值,数字越大颜色就越浅

级别渐变色列表—MaterialColor()

是实现Material Design中的颜色的类,它包含一种颜色的10个级别的渐变色。MaterialColor通过"[]"运算符的索引值来代表颜色的深度,有效的索引有:50,100,200,…,900,数字越大,颜色越深。MaterialColor的默认值为索引等于500的颜色。

4.主题

ThemeData

用于保存是Material 组件库的主题数据,Material组件需要遵守相应的设计规范,而这些规范可自定义部分都定义在ThemeData中了,所以我们可以通过ThemeData来自定义应用主题。在子组件中,我们可以通过Theme.of方法来获取当前的ThemeData。

  • brightness:深色还是浅色
  • primarySwatch:主题颜色样本,见下面介绍
  • primaryColor:主色,决定导航栏颜色
  • cardColor:卡片颜色
  • dividerColor:分割线颜色
  • buttonTheme:按钮主题
  • dialogBackgroundColor:对话框背景颜色
  • fontFamily:文字字体
  • textTheme:字体主题,包括标题、body等文字样式
  • iconTheme:Icon的默认样式
  • platform:指定平台,应用特定平台控件风格

十三、资源管理

1.指定 assets(会打包到程序中)

flutter:
  fonts:
    - family: myIcon  #指定一个字体名
      fonts:
        - asset: fonts/iconfont.ttf
    - family: Raleway
      fonts:
        - asset: assets/fonts/Raleway-Regular.ttf
        - asset: assets/fonts/Raleway-Medium.ttf
          weight: 500
        - asset: assets/fonts/Raleway-SemiBold.ttf
          weight: 600
    - family: AbrilFatface
      fonts:
        - asset: assets/fonts/abrilfatface/AbrilFatface-Regular.ttf
  assets:
    - assets/my_icon.png
    - assets/background.png

变体(variant)

…/pubspec.yaml
…/graphics/my_icon.png
…/graphics/background.png
…/graphics/dark/background.png
…etc.
flutter:
  assets:
    - graphics/background.png

2.加载 assets

  • 通过rootBundle (opens new window)对象加载:每个Flutter应用程序都有一个rootBundle (opens new window)对象, 通过它可以轻松访问主资源包,直接使用package:flutter/services.dart中全局静态的rootBundle对象来加载asset即可。
  • 通过 DefaultAssetBundle (opens new window)加载:建议使用 DefaultAssetBundle (opens new window)来获取当前 BuildContext 的AssetBundle。 这种方法不是使用应用程序构建的默认 asset bundle,而是使父级 widget 在运行时动态替换的不同的 AssetBundle,这对于本地化或测试场景很有用。

3.不同分辨率图片处理

加载可以使用: AssetImage(‘graphics/background.png’, package: ‘my_icons’)(其他包的图片资源需指定package,无改参数默认为当前)

…/image.png
…/Mx/image.png
…/Nx/image.png
…etc.
…/my_icon.png
…/2.0x/my_icon.png
…/3.0x/my_icon.png

4.不同平台处理

Android:…/android/app/src/main/res 指南
iOS:…/ios/Runner。该目录中Assets.xcassets/AppIcon.appiconset已经包含占位符图片(见图2-16), 只需将它们替换为适当大小的图片,保留原始文件名称。

启动图
Android:res/drawable/launch_background.xml,通过自定义drawable来实现自定义启动界面(你也可以直接换一张图片)
iOS:Assets.xcassets/LaunchImage.imageset, 拖入图片,并命名为LaunchImage.png、[email protected][email protected]。 如果你使用不同的文件名,那您还必须更新同一目录中的Contents.json文件,图片的具体尺寸可以查看苹果官方的标准。

十四、调试相关

1.可视化调试

  • 我们也可以通过设置debugPaintSizeEnabled为true以可视方式调试布局问题。 这是来自rendering库的布尔值。它可以在任何时候启用,并在为true时影响绘制。 设置它的最简单方法是在void main()的顶部设置。当它被启用时,所有的盒子都会得到一个明亮的深青色边框,padding(来自widget如Padding)显示为浅蓝色,子widget周围有一个深蓝色框, 对齐方式(来自widget如Center和Align)显示为黄色箭头. 空白(如没有任何子节点的Container)以灰色显示。
  • debugPaintBaselinesEnabled (opens new window)做了类似的事情,但对于具有基线的对象,文字基线以绿色显示,表意(ideographic)基线以橙色显示。
  • debugPaintPointersEnabled (opens new window)标志打开一个特殊模式,任何正在点击的对象都会以深青色突出显示。 这可以帮助我们确定某个对象是否以某种不正确的方式进行hit测试(Flutter检测点击的位置是否有能响应用户操作的widget),例如,如果它实际上超出了其父项的范围,首先不会考虑通过hit测试。
  • 如果我们尝试调试合成图层,例如以确定是否以及在何处添加RepaintBoundary widget,则可以使用debugPaintLayerBordersEnabled (opens new window)标志, 该标志用橙色或轮廓线标出每个层的边界,或者使用debugRepaintRainbowEnabled (opens new window)标志, 只要他们重绘时,这会使该层被一组旋转色所覆盖。
  • debugDumpApp() 控制台打印树结构
  • debugDumpRenderTree() 控制台打印树结构
  • debugDumpSemanticsTree() 控制台打印树结构

所有这些标志只能在调试模式下工作。通常,Flutter框架中以“debug…” 开头的任何内容都只能在调试模式下工作。

2.调试动画

调试动画最简单的方法是减慢它们的速度。为此,请将timeDilation (opens new window)变量(在scheduler库中)设置为大于1.0的数字,例如50.0。 最好在应用程序启动时只设置一次。如果我们在运行中更改它,尤其是在动画运行时将其值改小,则在观察时可能会出现倒退,这可能会导致断言命中,并且这通常会干扰我们的开发工作。

3.调试性能问题

要了解我们的应用程序导致重新布局或重新绘制的原因,我们可以分别设置debugPrintMarkNeedsLayoutStacks (opens new window)和 debugPrintMarkNeedsPaintStacks (opens new window)标志。 每当渲染盒被要求重新布局和重新绘制时,这些都会将堆栈跟踪记录到控制台。如果这种方法对我们有用,我们可以使用services库中的debugPrintStack()方法按需打印堆栈痕迹。

4.统计应用启动时间

要收集有关Flutter应用程序启动所需时间的详细信息,可以在运行flutter run时使用trace-startup和profile选项。
flutter run --trace-startup --profile
跟踪输出保存为start_up_info.json,在Flutter工程目录在build目录下。输出列出了从应用程序启动到这些跟踪事件(以微秒捕获)所用的时间:

  • 进入Flutter引擎时.
  • 展示应用第一帧时.
  • 初始化Flutter框架时.
  • 完成Flutter框架初始化时.

5.跟踪Dart代码性能

要执行自定义性能跟踪和测量Dart任意代码段的wall/CPU时间(类似于在Android上使用systrace (opens new window))。 使用dart:developer的Timeline (opens new window)工具来包含你想测试的代码块,例如:

Timeline.startSync('interesting function');
// iWonderHowLongThisTakes();
Timeline.finishSync();

然后打开你应用程序的Observatory timeline页面,在“Recorded Streams”中选择‘Dart’复选框,并执行你想测量的功能。
刷新页面将在Chrome的跟踪工具 (opens new window)中显示应用按时间顺序排列的timeline记录。
请确保运行flutter run时带有–profile标志,以确保运行时性能特征与我们的最终产品差异最小。

十五、布局

布局类组件都会包含一个或多个子组件,不同的布局类组件对子组件排列(layout)方式不同,

  • LeafRenderObjectWidget—非容器类组件基类—Widget树的叶子节点,用于没有子节点的widget,通常基础组件都属于这一类,如Image。
  • SingleChildRenderObjectWidget—单子组件基类—包含一个子Widget,如:ConstrainedBox、DecoratedBox等
  • MultiChildRenderObjectWidget —多子组件基类—包含多个子Widget,一般都有一个children参数,接受一个Widget数组。如Row、Column、Stack等

Flutter 中有两种布局模型
1.基于 RenderBox 的盒模型布局。
2.基于 Sliver ( RenderSliver ) 按需加载列表布局。
布局流程如下
1.上层组件向下层组件传递约束(constraints)条件。
2.下层组件确定自己的大小,然后告诉上层组件。注意下层组件的大小必须符合父组件的约束。
3.上层组件确定下层组件相对于自身的偏移和确定自身的大小(大多数情况下会根据子组件的大小来确定自身的大小)。
盒模型布局组件有两个特点
1.组件对应的渲染对象都继承自 RenderBox 类。
2.在布局过程中父级传递给子级的约束信息由 BoxConstraints 描述。

十六、纯 Dart Package

用 Dart 编写的传统 package,比如 path。其中一些可能包含 Flutter 的特定功能,因此依赖于 Flutter 框架,其使用范围仅限于 Flutter,比如 fluro。
对于纯 Dart 库的 package,只要在 lib/.dart 文件中添加功能实现,或在 lib 目录中的多个文件中添加功能实现。
如果要对 package 进行测试,在 test 目录下添加 单元测试。

十七、原生插件 (Plugin packages)

使用 Dart 编写的,按需使用 Java 或 Kotlin、Objective-C 或 Swift 分别在 Android 和/或 iOS 平台实现的 package。

  • 面向应用的 package
    该 package 是用户使用插件的的直接依赖。它指定了 Flutter 应用使用的 API。
  • 平台 package
    一个或多个包含特定平台代码的 package。面向应用的 package 会调用这些平台 package—— 除非它们带有一些终端用户需要的特殊平台功能,否则它们不会包含在应用中。
  • 平台接口 package
    将面向应用的 package 与平台 package 进行整合的 package。该 package 会声明平台 package 需要实现的接口,供面向应用的 package 使用。使用单一的平台接口 package 可以确保所有平台 package 都按照各自的方法实现了统一要求的功能。

1.目录结构

  • lib/hello.dart 文件
    Dart 插件 API 实现。
  • android/src/main/java/com/example/hello/HelloPlugin.kt 文件
    Android 平台原生插件 API 实现(使用 Kotlin 编程语言)。
  • ios/Classes/HelloPlugin.m 文件
    iOS 平台原生插件 API 实现(使用 Objective-C 编程语言)。
  • example/ 文件
    一个依赖于该插件并说明了如何使用它的 Flutter 应用。

2.定义 package API(.dart)

原生插件类型 package 的 API 在 Dart 代码中要首先定义好,使用你钟爱的 Flutter 编辑器,打开 hello 主目录,并找到 lib/hello.dart 文件。

3.添加 Android 平台代码(.kt/.java)

在菜单中选择 File > Open,然后选择 hello/example/android/build.gradle 文件;
在Gradle Sync 对话框中,选择 OK;
在“Android Gradle Plugin Update”对话框中,选择“Don’t remind me again for this project”。
插件中与 Android 系统徐相关的代码在 hello/java/com.example.hello/HelloPlugin 这个文件里。
你可以在 Android Studio 中点击运行 ▶ 按钮来运行示例程序。

4.添加 iOS 平台代码(.swift/.h+.m)

使用 Xcode 编辑 iOS 平台代码之前,首先确保代码至少被构建过一次(即从 IDE/编辑器执行示例程序,或在终端中执行以下命令: cd hello/example; flutter build ios --no-codesign)。

接下来执行下面步骤:
启动 Xcode
选择“File > Open”,然后选择 hello/example/ios/Runner.xcworkspace 文件。
插件的 iOS 平台代码位于项目导航中的这个位置: Pods/Development Pods/hello/…/…/example/ios/.symlinks/plugins/hello/ios/Classes。
你可以点击运行 ▶ 按钮来运行示例程序。

5.关联 API 和平台代码

最后,你需要将 Dart 编写的 API 代码与特定平台的实现相互关联。这是通过 平台通道 完成的。

6.添加文档

建议将下列文档添加到所有 package 中:
README.md 文件用来对 package 进行介绍
CHANGELOG.md 文件用来记录每个版本的更改
LICENSE 文件用来阐述 package 的许可条款
API 文档包含所有的公共 API(详情参见下文)

API 文档
当你提交一个 package 时,会自动生成 API 文档并将其提交到 pub.flutter-io.cn/documentation,示例请参见 device_info 文档。
如果你希望在本地开发环境中生成 API 文档,可以使用以下命令:

  • 将当前工作目录切换到 package 所在目录:
cd ~/dev/mypackage

-告知文档工具 Flutter SDK 所在位置(请自行更改 Flutter SDK 该在的位置)

   export FLUTTER_ROOT=~/dev/flutter  # on macOS or Linux (适用于 macOS 或 Linux 操作系统)
   set FLUTTER_ROOT=~/dev/flutter     # on Windows (适用于 Windows 操作系统)

-运行 dartdoc 工具(已经包含到 Flutter SDK 了):

   $FLUTTER_ROOT/bin/cache/dart-sdk/bin/dartdoc   # on macOS or Linux (适用于 macOS 或 Linux 操作系统)
   %FLUTTER_ROOT%\bin\cache\dart-sdk\bin\dartdoc  # on Windows (适用于 Windows 操作系统)

十八、插件原生之间交互处理

安卓端获取全局上下文
context = flutterPluginBinding.applicationContext;

文件结构

  • android:安卓插件
  • ios:苹果插件
  • example:插件样例运行文件
  • lib:插件入口

编译调试构建目录

  • 安卓插件目录:
    flutter项目中:/android/src/main/kotlin/com/moments_of_life/plugin/mobile/mobile_plugin
    AS项目中:/src/main/kotlin/com/moments_of_life/plugin/mobile/mobile_plugin
  • iOS插件目录
    flutter项目中:/ios/Classes
    xcode项目中:/Pods/Developments Pods/mobile_plugins/…/…/example/ios/.symlinks/plugins/mobile_plugins/ios/Classes

交互通信方式

  • MethodChannel:Flutter 与 Native 端相互调用,调用后可以返回结果,可以 Native 端主动调用,也可以Flutter主动调用,属于双向通信。此方式为最常用的方式, Native 端调用需要在主线程中执行。
  • BasicMessageChannel:用于使用指定的编解码器对消息进行编码和解码,属于双向通信,可以 Native 端主动调用,也可以Flutter主动调用。
  • EventChannel:用于数据流(event streams)的通信, Native 端主动发送数据给 Flutter,通常用于状态的监听,比如网络变化、传感器数据等

1.功能方面交互

代码处理
ps: 需要统一的通道名称:CHANNEL = “指定功能的通道名称”

  • Dart文件(不是类型安全的)
    声明渠道变量:const platform = MethodChannel(CHANNEL);
    函数调用:
Future _getBatteryLevel() async {
    String batteryLevel;
    try {
      final int result = await platform.invokeMethod('getBatteryLevel');
      batteryLevel = 'Battery level at $result % .';
    } on PlatformException catch (e) {
      batteryLevel = "Failed to get battery level: '${e.message}'.";
    }
    setState(() {
      _batteryLevel = batteryLevel;
    });
  }
  • Dart文件(类型安全的)
    Pigeon 声明:
import 'package:pigeon/pigeon.dart';
class SearchRequest {
  String query = '';
}
class SearchReply {
  String result = '';
}
@HostApi()
abstract class Api {
  Future search(SearchRequest request);
}

Dart使用:

import 'generated_pigeon.dart';
Future onClick() async {
  SearchRequest request = SearchRequest()..query = 'test';
  Api api = SomeApi();
  SearchReply reply = await api.search(request);
  print('reply: ${reply.result}');
}
  • Android文件
    通道监听:
 override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
            // This method is invoked on the main thread.
                call, result ->
            if (call.method == "getBatteryLevel") {
                val batteryLevel = getBatteryLevel()

                if (batteryLevel != -1) {
                    result.success(batteryLevel -40)
                } else {
                    result.error("UNAVAILABLE", "Battery level not available.", null)
                }
            } else {
                result.notImplemented()
            }
        }
    }
  • iOS文件
    通道监听:
    override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {

    let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
    let batteryChannel = FlutterMethodChannel(name: "samples.flutter.dev/battery",
                                              binaryMessenger: controller.binaryMessenger)
    batteryChannel.setMethodCallHandler({
      (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
        guard call.method == "getBatteryLevel" else {
          result(FlutterMethodNotImplemented)
          return
        }
        self.receiveBatteryLevel(result: result)
    
    })
    
    GeneratedPluginRegistrant.register(with: self)
    

    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }

2.UI方面交互

Flutter 支持两种集成模式:虚拟显示模式 (Virtual displays) 和混合集成模式 (Hybrid composition) 。

  • 虚拟显示模式会将 android.view.View 实例渲染为纹理,因此它不会嵌入到 Android Activity 的视图层次结构中。某些平台交互(例如键盘处理和辅助功能)可能无法正常工作。
  • 混合集成模式会将原生的 android.view.View 附加到视图层次结构中。因此,键盘处理和无障碍功能是开箱即用的。在 Android 10 之前,此模式可能会大大降低 Flutter UI 的帧吞吐量 (FPS)。

ps: 当前发现集成暂时只能是静态控件,而且是使用通过代码实例化的,如果在各个平台中筛选控件状态则不会生效,只有通过flutter刷新才可以

安卓端平台代码
安卓端实现托管控件必须要实现一个view的实例化逻辑,并且继承PlatformView,同时要声明一个工厂类用来建设控件,工厂类继承 PlatformViewFactory,二者缺一不可

// view的实现类,用来生成相应的view视图
internal class NativeView(context: Context, id: Int, creationParams: Map?) : PlatformView {
    private val textView: TextView
    override fun getView(): View {
        return textView
    }
    override fun dispose() {}
    init {
        textView = TextView(context)
        textView.textSize = 72f
        textView.setBackgroundColor(Color.rgb(255, 255, 255))
        textView.text = "Rendered on a native Android view (id: $id)"
    }
}
// view工厂类
class NativeViewFactory : PlatformViewFactory(StandardMessageCodec.INSTANCE) {
    override fun create(context: Context, viewId: Int, args: Any?): PlatformView {
        val creationParams = args as Map?
        return NativeView(context, viewId, creationParams)
    }
}

// 插件对外入口注册
class PlatformViewPlugin : FlutterPlugin {
    override fun onAttachedToEngine(binding: FlutterPluginBinding) {
        binding
                .platformViewRegistry
                .registerViewFactory("", NativeViewFactory())
    }
    override fun onDetachedFromEngine(binding: FlutterPluginBinding) {}
}

// 插件内部样例应用入口注册
class MainActivity : FlutterActivity() {
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        flutterEngine
                .platformViewsController
                .registry
                .registerViewFactory("", NativeViewFactory())
    }
}

iOS端平台代码
和安卓差不多,都需要一个view的实现(继承FlutterPlatformView)以及工厂创建类(集成FlutterPlatformViewFactory)

//view实现
class FLNativeView: NSObject, FlutterPlatformView {
    private var _view: UIView
    init(
        frame: CGRect,
        viewIdentifier viewId: Int64,
        arguments args: Any?,
        binaryMessenger messenger: FlutterBinaryMessenger?
    ) {
        _view = UIView()
        super.init()
        // iOS views can be created here
        createNativeView(view: _view)
    }
    func view() -> UIView {
        return _view
    }
    func createNativeView(view _view: UIView){
        _view.backgroundColor = UIColor.blue
        let nativeLabel = UILabel()
        nativeLabel.text = "Native text from iOS"
        nativeLabel.textColor = UIColor.white
        nativeLabel.textAlignment = .center
        nativeLabel.frame = CGRect(x: 0, y: 0, width: 180, height: 48.0)
        _view.addSubview(nativeLabel)
    }
}
// 工厂实现
class FLNativeViewFactory: NSObject, FlutterPlatformViewFactory {
    private var messenger: FlutterBinaryMessenger
    init(messenger: FlutterBinaryMessenger) {
        self.messenger = messenger
        super.init()
    }
    func create(
        withFrame frame: CGRect,
        viewIdentifier viewId: Int64,
        arguments args: Any?
    ) -> FlutterPlatformView {
        return FLNativeView(
            frame: frame,
            viewIdentifier: viewId,
            arguments: args,
            binaryMessenger: messenger)
    }
}

//插件对外入口注册
class FLPlugin: NSObject, FlutterPlugin {
    public static func register(with registrar: FlutterPluginRegistrar) {
        let factory = FLNativeViewFactory(messenger: registrar.messenger)
        registrar.register(factory, withId: "")
    }
}

// 插件内部样例应用入口注册
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]?
    ) -> Bool {
        GeneratedPluginRegistrant.register(with: self)
        weak var registrar = self.registrar(forPlugin: "plugin-name")
        let factory = FLNativeViewFactory(messenger: registrar!.messenger())
        self.registrar(forPlugin: "")!.register(
            factory,
            withId: "")
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
}

Dart端代码
因为安卓和iOS视图不是一种,所以实现的时候需要区分安卓和iOS

Widget build(BuildContext context) {
  // This is used in the platform side to register the view.
  const String viewType = '';
  // Pass parameters to the platform side.
  final Map creationParams = {};
  switch (defaultTargetPlatform) {
    case TargetPlatform.android:
    return getAndroid(context,viewType,creationParams);
    case TargetPlatform.iOS:
    return getIOS(context,viewType,creationParams);
    default:
      throw UnsupportedError('Unsupported platform view');
  }
}
Widget getIOS(BuildContext context,viewType,creationParams) {
  return UiKitView(
    viewType: viewType,
    layoutDirection: TextDirection.ltr,
    creationParams: creationParams,
    creationParamsCodec: const StandardMessageCodec(),
  );
}
Widget getAndroid(BuildContext context,viewType,creationParams) {
  return PlatformViewLink(
    viewType: viewType,
    surfaceFactory:
        (context, controller) {
      return AndroidViewSurface(
        controller: controller as AndroidViewController,
        gestureRecognizers: const >{},
        hitTestBehavior: PlatformViewHitTestBehavior.opaque,
      );
    },
    onCreatePlatformView: (params) {
      return PlatformViewsService.initSurfaceAndroidView(
        id: params.id,
        viewType: viewType,
        layoutDirection: TextDirection.ltr,
        creationParams: creationParams,
        creationParamsCodec: const StandardMessageCodec(),
        onFocus: () {
          params.onFocusChanged(true);
        },
      )
        ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated)
        ..create();
    },
  );
}

十九、数据存储

1.共享存储—InheritedWidget()

提供了一种在 widget 树中从上到下共享数据的方式,比如我们在应用的根 widget 中通过InheritedWidget共享了一个数据,那么我们便可以在任意子widget 中来获取该共享的数据!这个特性在一些需要在整个 widget 树中共享数据的场景中非常方便!如Flutter SDK中正是通过 InheritedWidget 来共享应用主题(Theme)和 Locale (当前语言环境)信息的。

//会产生didChangeDependencies() 通知的实例化
 //定义一个便捷方法,方便子树中的widget获取共享数据
  static ShareDataWidget? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType();
  }

//不会产生didChangeDependencies() 通知的实例化
static ShareDataWidget of(BuildContext context) {
  //return context.dependOnInheritedWidgetOfExactType();
  return context.getElementForInheritedWidgetOfExactType().widget;
}

//两者区别:dependOnInheritedWidgetOfExactType() 比 getElementForInheritedWidgetOfExactType()多调了dependOnInheritedElement方法,dependOnInheritedElement方法中主要是注册了依赖关系!看到这里也就清晰了,调用dependOnInheritedWidgetOfExactType() 和 getElementForInheritedWidgetOfExactType()的区别就是前者会注册依赖关系,而后者不会,

二十、组件状态管理

1.全局事件总线EventBus

//订阅者回调签名
typedef void EventCallback(arg);

class EventBus {
  //私有构造函数
  EventBus._internal();

  //保存单例
  static EventBus _singleton = EventBus._internal();

  //工厂构造函数
  factory EventBus()=> _singleton;

  //保存事件订阅者队列,key:事件名(id),value: 对应事件的订阅者队列
  final _emap = Map?>();

  //添加订阅者
  void on(eventName, EventCallback f) {
    _emap[eventName] ??=  [];
    _emap[eventName]!.add(f);
  }

  //移除订阅者
  void off(eventName, [EventCallback? f]) {
    var list = _emap[eventName];
    if (eventName == null || list == null) return;
    if (f == null) {
      _emap[eventName] = null;
    } else {
      list.remove(f);
    }
  }

  //触发事件,事件触发后该事件所有订阅者会被调用
  void emit(eventName, [arg]) {
    var list = _emap[eventName];
    if (list == null) return;
    int len = list.length - 1;
    //反向遍历,防止订阅者在回调中移除自身带来的下标错位
    for (var i = len; i > -1; --i) {
      list[i](arg);
    }
  }
}


//定义一个top-level(全局)变量,页面引入该文件后可以直接使用bus
var bus = EventBus();


//定义事件
enum Event{
  login,
  ... //省略其他事件
}

//发送事件
bus.emit(Event.login);

//订阅事件
@override
void initState() {
  //订阅登录状态改变事件
  bus.on(Event.login,onLogin);
  super.initState();
}

//取消订阅事件
@override
void dispose() {
  //取消订阅
  bus.off(Event.login,onLogin);
  super.dispose();
}

2.Provider

InheritedProvider 相当于最小的一个Provider。

3.其他状态管理包

包名 介绍
Provider (opens new window)& Scoped Model(opens new window) 这两个包都是基于InheritedWidget的,原理相似
Redux(opens new window) 是Web开发中React生态链中Redux包的Flutter实现
MobX(opens new window) 是Web开发中React生态链中MobX包的Flutter实现
BLoC(opens new window) 是BLoC模式的Flutter实现

4.通知 Notification

通知(Notification)是Flutter中一个重要的机制,在widget树中,每一个节点都可以分发通知,通知会沿着当前节点向上传递,所有父节点都可以通过NotificationListener来监听通知。Flutter中将这种由子向父的传递通知的机制称为通知冒泡(Notification Bubbling)。通知冒泡和用户触摸事件冒泡是相似的,但有一点不同:通知冒泡可以中止,但用户触摸事件不行。

样例

除了在可滚动组件在滚动过程中会发出ScrollNotification之外,还有一些其他的通知,如SizeChangedLayoutNotification、KeepAliveNotification 、LayoutChangedNotification等

NotificationListener(
  onNotification: (notification){
    switch (notification.runtimeType){
      case ScrollStartNotification: print("开始滚动"); break;
      case ScrollUpdateNotification: print("正在滚动"); break;
      case ScrollEndNotification: print("滚动停止"); break;
      case OverscrollNotification: print("滚动到边界"); break;
    }
  },
  child: ListView.builder(
    itemCount: 100,
    itemBuilder: (context, index) {
      return ListTile(title: Text("$index"),);
    }
  ),
);

//指定监听通知的类型为滚动结束通知(ScrollEndNotification)
NotificationListener(
  onNotification: (notification){
    //只会在滚动结束时才会触发此回调
    print(notification);
  },
  child: ListView.builder(
    itemCount: 100,
    itemBuilder: (context, index) {
      return ListTile(title: Text("$index"),);
    }
  ),
);

自定义通知

1、定义一个通知类,要继承自Notification类;
class MyNotification extends Notification {
  MyNotification(this.msg);
  final String msg;
}

2、分发通知。
Notification有一个dispatch(context)方法,它是用于分发通知的,我们说过context实际上就是操作Element的一个接口,它与Element树上的节点是对应的,通知会从context对应的Element节点向上冒泡。

阻止通知冒泡

 onNotification: (notification){
        print(notification.msg); //打印通知
        return false;
      }

二十一、对话框

函数:
取消对话框:Navigator.of(context).pop()
展示对话框:(Material风格)

Future showDialog({
  required BuildContext context,
  required WidgetBuilder builder, // 对话框UI的builder
  bool barrierDismissible = true, //点击对话框barrier(遮罩)时是否关闭它
})

展示对话框:(普通风格的对话框呢(非Material风格))

Future showGeneralDialog({
  required BuildContext context,
  required RoutePageBuilder pageBuilder, //构建对话框内部UI
  bool barrierDismissible = false, //点击遮罩是否关闭对话框
  String? barrierLabel, // 语义化标签(用于读屏软件)
  Color barrierColor = const Color(0x80000000), // 遮罩颜色
  Duration transitionDuration = const Duration(milliseconds: 200), // 对话框打开/关闭的动画时长
  RouteTransitionsBuilder? transitionBuilder, // 对话框打开/关闭的动画
  ...
})

展示对话框:(底部菜单列表)

// 弹出底部菜单列表模态对话框
Future _showModalBottomSheet() {
  return showModalBottomSheet(
    context: context,
    builder: (BuildContext context) {
      return ListView.builder(
        itemCount: 30,
        itemBuilder: (BuildContext context, int index) {
          return ListTile(
            title: Text("$index"),
            onTap: () => Navigator.of(context).pop(index),
          );
        },
      );
    },
  );
}

展示对话框:(日历选择器)

Future _showDatePicker1() {
  var date = DateTime.now();
  return showDatePicker(
    context: context,
    initialDate: date,
    firstDate: date,
    lastDate: date.add( //未来30天可选
      Duration(days: 30),
    ),
  );
}

Future _showDatePicker2() {
  var date = DateTime.now();
  return showCupertinoModalPopup(
    context: context,
    builder: (ctx) {
      return SizedBox(
        height: 200,
        child: CupertinoDatePicker(
          mode: CupertinoDatePickerMode.dateAndTime,
          minimumDate: date,
          maximumDate: date.add(
            Duration(days: 30),
          ),
          maximumYear: date.year + 1,
          onDateTimeChanged: (DateTime value) {
            print(value);
          },
        ),
      );
    },
  );
}

操作弹窗内容方式

  • 单独抽离出StatefulWidget
  • 使用StatefulBuilder方法
class StatefulBuilder extends StatefulWidget {
  const StatefulBuilder({
    Key? key,
    required this.builder,
  }) : assert(builder != null),
       super(key: key);

  final StatefulWidgetBuilder builder;

  @override
  _StatefulBuilderState createState() => _StatefulBuilderState();
}

class _StatefulBuilderState extends State {
  @override
  Widget build(BuildContext context) => widget.builder(context, setState);
}
  • (context as Element).markNeedsBuild();

1.选择对话框—AlertDialog()

  • title:对话框标题组件
  • titlePadding:标题填充
  • titleTextStyle:标题文本样式
  • content:对话框内容组件
  • contentPadding:内容的填充
  • contentTextStyle:内容文本样式
  • actions:对话框操作按钮组
  • backgroundColor:对话框背景色
  • elevation:对话框的阴影
  • semanticLabel:对话框语义化标签(用于读屏软件)
  • shape:对话框外形

2.列表选择对话框—SimpleDialog()

3.基类—Dialog()

4.提示对话框—PopupRoute()

使用时需使用以下代码:

    Navigator.push(
      context,
      PopupViewShow(//继承PopupRoute的类
        child: widget,//子页面
        showViewKey: key,//使用弹窗的控件key,因为对话框是全屏的,所以需要使用这个做定位使用
      ),
    );

class PopupViewShow extends PopupRoute {
  final Duration _duration = Duration(milliseconds: 300);
  Widget child;
  GlobalKey showViewKey;
  final EdgeInsetsGeometry padding;
  PopupViewShow({required this.child, required this.showViewKey, padding}) : padding = padding ?? EdgeInsets.zero;
  @override
  Color? get barrierColor => null;
  @override
  bool get barrierDismissible => true;
  @override
  String? get barrierLabel => null;
  @override
  Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) {
    return _Popup(child: child, showViewKey: showViewKey, padding: padding);
  }
  @override
  Duration get transitionDuration => _duration;
}

class _Popup extends StatelessWidget {
  final Widget child;
  final EdgeInsetsGeometry padding;
  GlobalKey showViewKey;

  _Popup({required this.child, required this.showViewKey, required this.padding});

  @override
  Widget build(BuildContext context) {
    Offset offset = showViewKey.position();
    Rect rect = showViewKey.getRect();//宽高属性获取,需使用插件“rect_getter: ^1.1.0”,并调用弹窗的Widget使用“RectGetter.createGlobalKey()”这个key
    return Material(
      color: Colors.transparent,
      child: GestureDetector(
        child: Stack(
          children: [
            Container(//位置为全屏
              width: MediaQuery.of(context).size.width,
              height: MediaQuery.of(context).size.height,
              color: Colors.transparent,
            ),
            Padding(
              child: child,
              padding: EdgeInsets.fromLTRB(offset.dx, offset.dy + rect.height, 0, 0),//内容位置定位使用
            ),
          ],
        ),
        onTap: () {
          //点击空白处
          Navigator.of(context).pop();
        },
      ),
    );
  }
}

二十二、动画

Animation

Animation是一个抽象类,它本身和UI渲染没有任何关系,而它主要的功能是保存动画的插值和状态;其中一个比较常用的Animation类是Animation。Animation对象是一个在一段时间内依次生成一个区间(Tween)之间值的类。Animation对象在整个动画执行过程中输出的值可以是线性的、曲线的、一个步进函数或者任何其他曲线函数等等,这由Curve来决定。 根据Animation对象的控制方式,动画可以正向运行(从起始状态开始,到终止状态结束),也可以反向运行,甚至可以在中间切换方向。Animation还可以生成除double之外的其他类型值,如:Animation 或Animation。在动画的每一帧中,我们可以通过Animation对象的value属性获取动画的当前状态值。

动画通知
我们可以通过Animation来监听动画每一帧以及执行状态的变化,Animation有如下两个方法:

  • addListener();它可以用于给Animation添加帧监听器,在每一帧都会被调用。帧监听器中最常见的行为是改变状态后调用setState()来触发UI重建。
  • addStatusListener();它可以给Animation添加“动画状态改变”监听器;动画开始、结束、正向或反向(见AnimationStatus定义)时会调用状态改变的监听器。

Curv

动画过程可以是匀速的、匀加速的或者先加速后减速等。Flutter中通过Curve(曲线)来描述动画过程,我们把匀速动画称为线性的(Curves.linear),而非匀速动画称为非线性的。
我们可以通过CurvedAnimation来指定动画的曲线,如:final CurvedAnimation curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);
当然我们也可以创建自己Curve,例如我们定义一个正弦曲线:

class ShakeCurve extends Curve {
  @override
  double transform(double t) {
    return math.sin(t * math.PI * 2);
  }
}

AnimationController

AnimationController用于控制动画,它包含动画的启动forward()、停止stop() 、反向播放 reverse()等方法。AnimationController会在动画的每一帧,就会生成一个新的值。默认情况下,AnimationController在给定的时间段内线性的生成从 0.0 到1.0(默认区间)的数字。

  • AnimationController生成数字的区间可以通过lowerBound和upperBound来指定
  • forward()方法可以启动正向动画,reverse()可以启动反向动画
  • vsync参数,它接收一个TickerProvider类型的对象,它的主要职责是创建Ticker,而Ticker就是通过SchedulerBinding来添加屏幕刷新回调,这样一来,每次屏幕刷新都会调用TickerCallback,同时会防止屏幕外动画(动画的UI不在当前屏幕时,如锁屏时)消耗不必要的资源

Tween

要使用 Tween 对象,需要调用其animate()方法,然后传入一个控制器对象。例如,以下代码在 500 毫秒内生成从 0 到 255 的整数值。

final AnimationController controller = AnimationController(
  duration: const Duration(milliseconds: 500), 
  vsync: this,
);
Animation alpha = IntTween(begin: 0, end: 255).animate(controller);
//注意animate()返回的是一个Animation,而不是一个Animatable。

线性插值lerp函数

动画的原理其实就是每一帧绘制不同的内容,一般都是指定起始和结束状态,然后在一段时间内从起始状态逐渐变为结束状态,而具体某一帧的状态值会根据动画的进度来算出,因此,Flutter 中给有可能会做动画的一些状态属性都定义了静态的 lerp 方法(线性插值),比如:

 //a 为起始颜色,b为终止颜色,t为当前动画的进度[0,1]
Color.lerp(a, b, t);
// Size.lerp(a, b, t)
// Rect.lerp(a, b, t)
// Offset.lerp(a, b, t)
// Decoration.lerp(a, b, t)
// Tween.lerp(t) //起始状态和终止状态在构建 Tween 的时候已经指定了

需要注意,lerp 是线性插值,意思是返回值和动画进度t是成一次函数(y = kx + b)关系,因为一次函数的图像是一条直线,所以叫线性插值。如果我们想让动画按照一个曲线来执行,我们可以对 t 进行映射,比如要实现匀加速效果,则 t’ = at²+bt+c,然后指定加速度 a 和 b 即可(大多数情况下需保证 t’ 的取值范围在[0,1],当然也有一些情况可能会超出该取值范围,比如弹簧(bounce)效果),而不同 Curve 可以按照不同曲线执行动画的的原理本质上就是对 t 按照不同映射公式进行映射。

使用

基本方式:

//线性
class ScaleAnimationRoute extends StatefulWidget {
  const ScaleAnimationRoute({Key? key}) : super(key: key);
  
  @override
  _ScaleAnimationRouteState createState() => _ScaleAnimationRouteState();
}

//需要继承TickerProvider,如果有多个AnimationController,则应该使用TickerProviderStateMixin。
class _ScaleAnimationRouteState extends State
    with SingleTickerProviderStateMixin {
  late Animation animation;
  late AnimationController controller;
  
  @override
  initState() {
    super.initState();
    controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    );

    //匀速
    //图片宽高从0变到300
    animation = Tween(begin: 0.0, end: 300.0).animate(controller)
      ..addListener(() {
        setState(() => {});
      });

    //启动动画(正向执行)
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Image.asset(
        "imgs/avatar.png",
        width: animation.value,
        height: animation.value,
      ),
    );
  }
  
  @override
  dispose() {
    //路由销毁时需要释放动画资源
    controller.dispose();
    super.dispose();
  }
}

//弹性
@override
initState() {
    super.initState();
    controller = AnimationController(
        duration: const Duration(seconds: 3), vsync: this);
    //使用弹性曲线
    animation=CurvedAnimation(parent: controller, curve: Curves.bounceIn);
    //图片宽高从0变到300
    animation = Tween(begin: 0.0, end: 300.0).animate(animation)
      ..addListener(() {
        setState(() => {});
      });
    //启动动画
    controller.forward();
  }

AnimatedWidget简化:

class AnimatedImage extends AnimatedWidget {
  const AnimatedImage({
    Key? key,
    required Animation animation,
  }) : super(key: key, listenable: animation);

  @override
  Widget build(BuildContext context) {
    final animation = listenable as Animation;
    return  Center(
      child: Image.asset(
        "imgs/avatar.png",
        width: animation.value,
        height: animation.value,
      ),
    );
  }
}

class ScaleAnimationRoute1 extends StatefulWidget {
  const ScaleAnimationRoute1({Key? key}) : super(key: key);

  @override
  _ScaleAnimationRouteState createState() =>  _ScaleAnimationRouteState();
}

class _ScaleAnimationRouteState extends State
    with SingleTickerProviderStateMixin {
  late Animation animation;
  late AnimationController controller;

  @override
  initState() {
    super.initState();
    controller =  AnimationController(
        duration: const Duration(seconds: 2), vsync: this);
    //图片宽高从0变到300
    animation =  Tween(begin: 0.0, end: 300.0).animate(controller);
    //启动动画
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedImage(
      animation: animation,
    );
  }

  @override
  dispose() {
    //路由销毁时需要释放动画资源
    controller.dispose();
    super.dispose();
  }
}

AnimatedBuilder重构:

@override
Widget build(BuildContext context) {
  //return AnimatedImage(animation: animation,);
    return AnimatedBuilder(
      animation: animation,
      child: Image.asset("imgs/avatar.png"),
      builder: (BuildContext ctx, child) {
        return  Center(
          child: SizedBox(
            height: animation.value,
            width: animation.value,
            child: child,
          ),
        );
      },
    );
}

好处:
1、不用显式的去添加帧监听器,然后再调用setState() 了,这个好处和AnimatedWidget是一样的。
2、更好的性能:因为动画每一帧需要构建的 widget 的范围缩小了,如果没有builder,setState()将会在父组件上下文中调用,这将会导致父组件的build方法重新调用;而有了builder之后,只会导致动画widget自身的build重新调用,避免不必要的rebuild。
3、通过AnimatedBuilder可以封装常见的过渡效果来复用动画。

动画状态监听

我们可以通过Animation的addStatusListener()方法来添加动画状态改变监听器。Flutter中,有四种动画状态,在AnimationStatus枚举类中定义,下面我们逐个说明:

枚举值 含义
dismissed 动画在起始点停止
forward 动画正在正向执行
reverse 动画正在反向执行
completed 动画在终点停止
animation.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        //动画执行结束时反向执行动画
        controller.reverse();
      } else if (status == AnimationStatus.dismissed) {
        //动画恢复到初始状态时执行动画(正向)
        controller.forward();
      }
    });

Hero动画

Hero 指的是可以在路由(页面)之间“飞行”的 widget,简单来说 Hero 动画就是在路由切换时,有一个共享的widget 可以在新旧路由间切换。由于共享的 widget 在新旧路由页面上的位置、外观可能有所差异,所以在路由切换时会从旧路逐渐过渡到新路由中的指定位置,这样就会产生一个 Hero 动画。

Hero(
              tag: "avatar", //唯一标记,前后两个路由页Hero的tag必须相同
              child: ClipOval(
                child: Image.asset(
                  "imgs/avatar.png",
                  width: 50.0,
                ),
              ),
            )

交织动画

有些时候我们可能会需要一些复杂的动画,这些动画可能由一个动画序列或重叠的动画组成,比如:有一个柱状图,需要在高度增长的同时改变颜色,等到增长到最大高度后,我们需要在X轴上平移一段距离。可以发现上述场景在不同阶段包含了多种动画,要实现这种效果,使用交织动画(Stagger Animation)会非常简单。交织动画需要注意以下几点:

  • 要创建交织动画,需要使用多个动画对象(Animation)。
  • 一个AnimationController控制所有的动画对象。
  • 给每一个动画对象指定时间间隔(Interval)
 //高度动画
    height = Tween(
      begin: .0,
      end: 300.0,
    ).animate(
      CurvedAnimation(
        parent: controller,
        curve: const Interval(
          0.0, 0.6, //间隔,前60%的动画时间
          curve: Curves.ease,
        ),
      ),
    );

    color = ColorTween(
      begin: Colors.green,
      end: Colors.red,
    ).animate(
      CurvedAnimation(
        parent: controller,
        curve: const Interval(
          0.0, 0.6, //间隔,前60%的动画时间
          curve: Curves.ease,
        ),
      ),
    );

    padding = Tween(
      begin: const EdgeInsets.only(left: .0),
      end: const EdgeInsets.only(left: 100.0),
    ).animate(
      CurvedAnimation(
        parent: controller,
        curve: const Interval(
          0.6, 1.0, //间隔,后40%的动画时间
          curve: Curves.ease,
        ),
      ),
    );

自定义页面切换动画

class SlideTransitionX extends AnimatedWidget {
  SlideTransitionX({
    Key? key,
    required Animation position,
    this.transformHitTests = true,
    this.direction = AxisDirection.down,
    required this.child,
  }) : super(key: key, listenable: position) {
    switch (direction) {
      case AxisDirection.up:
        _tween = Tween(begin: const Offset(0, 1), end: const Offset(0, 0));
        break;
      case AxisDirection.right:
        _tween = Tween(begin: const Offset(-1, 0), end: const Offset(0, 0));
        break;
      case AxisDirection.down:
        _tween = Tween(begin: const Offset(0, -1), end: const Offset(0, 0));
        break;
      case AxisDirection.left:
        _tween = Tween(begin: const Offset(1, 0), end: const Offset(0, 0));
        break;
    }
  }

  final bool transformHitTests;

  final Widget child;

  final AxisDirection direction;

  late final Tween _tween;

  @override
  Widget build(BuildContext context) {
    final position = listenable as Animation;
    Offset offset = _tween.evaluate(position);
    if (position.status == AnimationStatus.reverse) {
      switch (direction) {
        case AxisDirection.up:
          offset = Offset(offset.dx, -offset.dy);
          break;
        case AxisDirection.right:
          offset = Offset(-offset.dx, offset.dy);
          break;
        case AxisDirection.down:
          offset = Offset(offset.dx, -offset.dy);
          break;
        case AxisDirection.left:
          offset = Offset(-offset.dx, offset.dy);
          break;
      }
    }
    return FractionalTranslation(
      translation: offset,
      transformHitTests: transformHitTests,
      child: child,
    );
  }
}

你可能感兴趣的:(开发学习,flutter,学习)