学写Flutter

2021-08-25  本文已影响0人  磊Se

一、Widget

Flutter设计思想,Everything is Widget。

Widget 是一个比较宽泛的概念,无论基本部件、布局、还是手势等都是 Widget。
它是对视图的一种包含配置及状态信息的“描述数据”,用于约束具体的视图元素

Widget树

widget树图

StatelessWidget

StatelessWidget一旦创建就无法进行修改,这意味着它不会因为外部条件变化而重新绘制。

其生命周期:

StatefulWidget

它可以在其生命周期中操作内部持有数据的变化,这些数据被称为State,这样的Widget也叫做StatefulWidget。

例:

class RoutePageA extends StatefulWidget {
  @override
  RoutePageAState createState() => RoutePageAState();
}

class RoutePageAState extends State<RoutePageA> {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Text("abc" );
  }
}

State

一个StatefulWidget类会对应一个State类,State表示与其对应的StatefulWidget要维护的状态,State中的保存的状态信息可以:

State中有两个常用属性:

StatefulWidget生命周期

State作为StatefulWidget的主体,它可以在多个节点对State进行调整。

其生命周期:

Widget构造函数

例:

  const WidgetPage({
    Key? key,
    required this.text,
    this.backgroundColor: Colors.grey,
    this.fontSize: 40,
    this.child,
  }) : super(key: key);
  final String text;
  final Color backgroundColor;
  final double fontSize;
  final Widget? child;
  
调用:
    return WidgetPage(
      text: "Widget Demo",
      fontSize: 100,//非必要参数
    );

BuildContext

Context 仅仅是已创建的所有 Widget 树结构中某个 Widget 的位置引用。

class ContextRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Context测试"),
      ),
      body: Container(
        child: Builder(builder: (context) {
          // 在Widget树中向上查找最近的父级`Scaffold` widget
          Scaffold scaffold = context.findAncestorWidgetOfExactType<Scaffold>();
          // 直接返回 AppBar的title, 此处实际上是Text("Context测试")
          return (scaffold.appBar as AppBar).title;
        }),
      ),
    );
  }
}


在flutter中我们经常会使用到这样的代码

//打开一个新的页面
  Navigator.of(context).push
//打开Scaffold的Drawer
  Scaffold.of(context).openDrawer
//获取display1样式文字主题
  Theme.of(context).textTheme.display1

二、基础组件

flutter官方提供了一些组件,如下官网截图。

文本

Text

Text用于显示简单样式文本

Text(
  "文本",
  textAlign: TextAlign.left,
  overflow: TextOverflow.ellipsis,
  maxLines: 1,
  textScaleFactor: 1.5,
  style: TextStyle(
      color: Colors.cyan,
      fontSize: 20,
  ),
);

TextSpan

如果我们需要对一个Text内容的不同部分按照不同样式显示,这时可以使用TextSpan,它代表文本的一个"片断"
其定义如下:

const TextSpan({
  TextStyle style, 
  Sting text,
  List<TextSpan> children,
  GestureRecognizer recognizer,
});

例:

Text.rich(TextSpan(children: [
  TextSpan(
      text: "Hello",
      style: TextStyle(
        color: Colors.blue,
      )),
  TextSpan(
      text: " World",
      style: TextStyle(
        color: Colors.red,
      ),
      recognizer: _tapSpanText
        ..onTap = () {
          print("SpanTextAction");
        }),
]));

通过Text.rich 方法将TextSpan 添加到Text中,之所以可以这样做,是因为Text其实就是RichText的一个包装,而RichText是可以显示多种样式(富文本)的widget

DefaultTextStyle

在Widget树中,文本的样式默认是可以被继承的,因此,如果在Widget树的某一个节点处设置一个默认的文本样式,那么该节点的子树中所有文本都会默认使用这个样式。
例:

DefaultTextStyle(
  //1.设置文本默认样式
  style: TextStyle(
    color: Colors.red,
    fontSize: 20.0,
  ),
  child: Column(
    children: <Widget>[
      Text("hello world"),
      Text("I am Jack"),
      Text(
        "I am Jack",
        style: TextStyle(
            inherit: false, //2.不继承默认样式
            color: Colors.grey),
      ),
    ],
  ),
);

按钮

Material组件库中的按钮

TextButton

简单的扁平按钮,按下后,会有背景色

TextButton(
  style: TextButton.styleFrom(
    textStyle: const TextStyle(fontSize: 20),
  ),
  child: Text("TextButton"),
  onPressed: () => print("TextButton"),
)

IconButton

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

IconButton(
 onPressed: () => print("IconButton"),
 icon: Icon(Icons.thumb_up),
);

FloatingActionButton

悬浮按钮,圆形、有阴影

FloatingActionButton(
    child: Text("漂"),
    tooltip: "点我",
    onPressed: () => print("FloatingActionButton"));
}

ElevatedButton

漂浮按钮,它默认带有阴影和灰色背景

Column(
  children: [
    ElevatedButton(
        child: Text("漂"), onPressed: () => print("ElevatedButton")),
    ElevatedButton(child: Text("漂"), onPressed: null),
  ],
);

OutlineButton

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

OutlinedButton(
    onPressed: () => print("OutlinedButton"),
    child: Text("OutlinedButton")
);

图片

Flutter中,我们可以通过Image组件来加载并显示图片,Image的数据源可以是asset、文件、内存以及网络

ImageProvider

ImageProvider 是一个抽象类,主要定义了图片数据获取的接口load(),从不同的数据源获取图片需要实现不同的ImageProvider ,如AssetImage是实现了从Asset中加载图片的ImageProvider,而NetworkImage实现了从网络加载图片的ImageProvider

Image

Image widget有一个必选的image参数,它对应一个ImageProvider。下面我们分别演示一下如何从asset和网络加载图片

在asset中加载图片
  assets:
    - images/avatar.png
Image(
  image: AssetImage("images/avatar.png"),
);
在网络中加载图片
Image(
  image: NetworkImage(
      "https://avatars2.githubusercontent.com/u/20411648?s=460&v=4"),
)

Image也提供了一个快捷的构造函数Image.network用于从网络加载、显示图片:

Image.network(
  "https://avatars2.githubusercontent.com/u/20411648?s=460&v=4",
)

参数:

    fill:会拉伸填充满显示空间,图片本身长宽比会发生变化,图片会变形。
    
    cover:会按图片的长宽比放大后居中填满显示空间,图片不会变形,超出显示空间部分会被剪裁。
    
    contain:这是图片的默认适应规则,图片会在保证图片本身长宽比不变的情况下缩放以适应当前显示空间,图片不会变形。
    
    fitWidth:图片的宽度会缩放到显示空间的宽度,高度会按比例缩放,然后居中显示,图片不会变形,超出显示空间部分会被剪裁。
    
    fitHeight:图片的高度会缩放到显示空间的高度,宽度会按比例缩放,然后居中显示,图片不会变形,超出显示空间部分会被剪裁。
    
    none:图片没有适应策略,会在显示空间内显示图片,如果图片比显示空间大,则显示空间只会显示图片中间部分。

输入框

Material组件库中提供了输入框组件TextField和表单组件Form。

TextField

TextField用于文本输入,它提供了很多属性,如下:

const TextField({
  ...
  TextEditingController controller, 
  FocusNode focusNode,
  InputDecoration decoration = const InputDecoration(),
  TextInputType keyboardType,
  TextInputAction textInputAction,
  TextStyle style,
  TextAlign textAlign = TextAlign.start,
  bool autofocus = false,
  bool obscureText = false,
  int maxLines = 1,
  int maxLength,
  bool maxLengthEnforced = true,
  ValueChanged<String> onChanged,
  VoidCallback onEditingComplete,
  ValueChanged<String> onSubmitted,
  List<TextInputFormatter> inputFormatters,
  bool enabled,
  this.cursorWidth = 2.0,
  this.cursorRadius,
  this.cursorColor,
  ...
})

First Header Second Header
TextInputType枚举值 含义
text 文本输入键盘
multiline 多行文本,需和maxLines配合使用(设为null或大于1)
number 数字;会弹出数字键盘
phone 优化后的电话号码输入键盘;会弹出数字键盘并显示“* #”
datetime 优化后的日期输入键盘;Android上会显示“: -”
emailAddress 优化后的电子邮件地址;会显示“@ .”
url 优化后的url输入键盘; 会显示“/ .”
自定义样式

可以通过decoration属性来定义输入框样式

decoration: InputDecoration(
      hintText: "请输入用户名",//提示文字
      prefixIcon: Icon(Icons.lock),//左侧图标
        //边框样式
      border: OutlineInputBorder(
        borderRadius: BorderRadius.all(Radius.circular(10.0)),
      ),
      enabledBorder: OutlineInputBorder(
        borderRadius: BorderRadius.all(Radius.circular(10.0)),
        borderSide: BorderSide(
          color: Colors.grey,
        ),
      ),
      focusedBorder: OutlineInputBorder(
        borderRadius: BorderRadius.all(Radius.circular(10.0)),
        borderSide: BorderSide(
          color: Colors.red,
        ),
      ),
    ),
获取&监听文本变化

获取输入内容有两种方式:

onChanged: (value) => print(value),
TextEditingController _unameController = TextEditingController();

然后设置输入框controller:

TextField(
    autofocus: true,
    controller: _unameController, //设置controller
    ...
)

通过controller获取输入框内容

@override
void initState() {
  //监听输入改变  
  _unameController.addListener((){
    print(_unameController.text);
  });
}


两种方式相比,onChanged是专门用于监听文本变化,而controller的功能却多一些,除了能监听文本变化外,它还可以设置默认值、选择文本等

监听焦点状态改变事件:
...
// 创建 focusNode   
FocusNode focusNode = new FocusNode();
...
// focusNode绑定输入框   
TextField(focusNode: focusNode);
...
// 监听焦点变化    
focusNode.addListener((){
   print(focusNode.hasFocus);
});

此章:Icon、表单Form、进度指示器ProgressIndicator 自己看一下吧

三、布局方式

布局类组件简介

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

根据Widget是否包含子节点将其分为3类

布局类组件就是指直接或间接继承(包含)MultiChildRenderObjectWidget的Widget

线性布局(Row和Column)

所谓线性布局,即指沿水平或垂直方向排布子组件。Flutter中通过Row和Column来实现线性布局,类似于Android中的LinearLayout控件。Row和Column都继承自Flex,我们将在弹性布局一节中详细介绍Flex。

主轴和纵轴

对于线性布局,有主轴和纵轴之分,如果布局是沿水平方向,那么主轴就是指水平方向,而纵轴即垂直方向;如果布局沿垂直方向,那么主轴就是指垂直方向,而纵轴就是水平方向

Row

Row可以在水平方向排列其子widget

Row({
  ...  
  TextDirection textDirection,    
  MainAxisSize mainAxisSize = MainAxisSize.max,    
  MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
  VerticalDirection verticalDirection = VerticalDirection.down,  
  CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
  List<Widget> children = const <Widget>[],
})

Column

Column可以在垂直方向排列其子widget

参数和Row一样,不同的是布局方向为垂直,不同的是布局方向为垂直,主轴纵轴正好相反。

特殊情况

如果Row里面嵌套Row,或者Column里面再嵌套Column,那么只有最外面的Row或Column会占用尽可能大的空间,里面Row或Column所占用的空间为实际大小,若想内部占用尽可能空间可以使用Expanded 组件。

弹性布局(Flex)

弹性布局允许子组件按照一定比例来分配父容器空间。弹性布局的概念在其它UI系统中也都存在,如H5中的弹性盒子布局,Android中的FlexboxLayout等。Flutter中的弹性布局主要通过FlexExpanded来配合实现。

Flex

Flex组件可以沿着水平或垂直方向排列子组件,如果你知道主轴方向,使用Row或Column会方便一些,因为Row和Column都继承自Flex,参数基本相同,所以能使用Flex的地方基本上都可以使用Row或Column。

Flex({
  ...
  @required this.direction, //弹性布局的方向, Row默认为水平方向,Column默认为垂直方向
  List<Widget> children = const <Widget>[],
})

Expanded

可以按比例“扩伸” Row、Column和Flex子组件所占用的空间。

const Expanded({
  int flex = 1, 
  @required Widget child,
})

flex参数为弹性系数,如果为0或null,则child是没有弹性的,即不会被扩伸占用的空间。如果大于0,所有的Expanded按照其flex的比例来分割主轴的全部空闲空间。

Spacer

Spacer的功能是占用指定比例的空间,实际上它只是Expanded的一个包装类

流式布局

在介绍Row和Colum时,如果子widget超出屏幕范围,则会报溢出错误

我们把超出屏幕显示范围会自动折行的布局称为流式布局。Flutter中通过Wrap和Flow来支持流式布局。

Wrap

我们可以看到Wrap的很多属性在Row(包括Flex和Column)中也有,如direction、crossAxisAlignment、textDirection、verticalDirection等,这些参数意义是相同的。Wrap和Flex(包括Row和Column)除了超出显示范围后Wrap会折行外,其它行为基本相同。下面我们看一下Wrap特有的几个属性:

Wrap({
  ...
  this.direction = Axis.horizontal,
  this.alignment = WrapAlignment.start,
  this.spacing = 0.0,
  this.runAlignment = WrapAlignment.start,
  this.runSpacing = 0.0,
  this.crossAxisAlignment = WrapCrossAlignment.start,
  this.textDirection,
  this.verticalDirection = VerticalDirection.down,
  List<Widget> children = const <Widget>[],
})

Flow

我们一般很少会使用Flow,因为其过于复杂,需要自己实现子widget的位置转换,在很多场景下首先要考虑的是Wrap是否满足需求。

总结:复杂,先知道有这么个布局方式

层叠布局Stack、Positioned

层叠布局和Web中的绝对定位、Android中的Frame布局是相似的,子组件可以根据距父容器四个角的位置来确定自身的位置。绝对定位允许子组件堆叠起来(按照代码中声明的顺序)

Flutter中使用StackPositioned两个组件来配合实现绝对定位。Stack允许子组件堆叠,而Positioned用于根据Stack的四个角来确定子组件的位置。

Stack

Stack({
  this.alignment = AlignmentDirectional.topStart,
  this.textDirection,
  this.fit = StackFit.loose,
  this.overflow = Overflow.clip,
  List<Widget> children = const <Widget>[],
})

Positioned

const Positioned({
  Key key,
  this.left, 
  this.top,
  this.right,
  this.bottom,
  this.width,
  this.height,
  @required Widget child,
})

这里和约束布局一样,如确定left、top、height、width或确定left、top 、right、 bottom及可确定一个组件的位置和大小

Stack子元素是堆叠的,所以后添加的widget在最上层

对齐与相对定位

Align

Align 组件可以调整子组件的位置,并且可以根据子组件的宽高来确定自身的的宽高,定义如下:

Align({
  Key key,
  this.alignment = Alignment.center,
  this.widthFactor,
  this.heightFactor,
  Widget child,
})

Alignment

Alignment继承自AlignmentGeometry,表示矩形内的一个点,他有两个属性x、y,分别表示在水平和垂直方向的偏移,Alignment定义如下:

Alignment(this.x, this.y)
(Alignment.x*childWidth/2+childWidth/2, Alignment.y*childHeight/2+childHeight/2)

其中childWidth为子元素的宽度,childHeight为子元素高度。

FractionalOffset

FractionalOffset 继承自 Alignment,它和 Alignment唯一的区别就是坐标原点不同!FractionalOffset 的坐标原点为矩形的左侧顶点,这和布局系统的一致,所以理解起来会比较容易。FractionalOffset的坐标转换公式为:

实际偏移 = (FractionalOffse.x * childWidth, FractionalOffse.y * childHeight)

Align和Stack对比

Align和Stack/Positioned都可以用于指定子元素相对于父元素的偏移,但它们还是有两个主要区别:

Center

Center组件其实是对齐方式确定(Alignment.center)了的Align

四、容器类组件

容器类Widget和布局类Widget都作用于其子Widget,不同的是:

填充(Padding)

Padding可以给其子节点添加填充(留白),和边距效果类似。我们在前面很多示例中都已经使用过它了,现在来看看它的定义:

Padding({
  ...
  EdgeInsetsGeometry padding,
  Widget child,
})

EdgeInsetsGeometry是一个抽象类,开发中,我们一般都使用EdgeInsets类,它是EdgeInsetsGeometry的一个子类,定义了一些设置填充的便捷方法。

EdgeInsets

我们看看EdgeInsets提供的便捷方法:

尺寸限制类容器

尺寸限制类容器用于限制容器大小,Flutter中提供了多种这样的容器,如ConstrainedBoxSizedBoxUnconstrainedBoxAspectRatio等,本节将介绍一些常用的。

ConstrainedBox

ConstrainedBox用于对子组件添加额外的约束。例如,如果你想让子组件的最小高度是80像素,你可以使用const BoxConstraints(minHeight: 80.0)作为子组件的约束。

实现一个最小高度为50,宽度尽可能大的容器

ConstrainedBox(
  constraints: BoxConstraints(
    minWidth: double.infinity, //宽度尽可能大
    minHeight: 50.0 //最小高度为50像素
  ),
  child: Container(
      height: 5.0, 
      color: Colors.red,
  ),
)

BoxConstraints用于设置限制条件,它的定义如下:

const BoxConstraints({
  this.minWidth = 0.0, //最小宽度
  this.maxWidth = double.infinity, //最大宽度
  this.minHeight = 0.0, //最小高度
  this.maxHeight = double.infinity //最大高度
})

BoxConstraints还定义了一些便捷的构造函数,用于快速生成特定限制规则的BoxConstraints,如BoxConstraints.tight(Size size),它可以生成给定大小的限制;const BoxConstraints.expand()可以生成一个尽可能大的用以填充另一个容器的BoxConstraints。除此之外还有一些其它的便捷函数,读者可以查看API文档 (opens new window)。

SizedBox

SizedBox用于给子元素指定固定的宽高,如:

SizedBox(
  width: 80.0,
  height: 80.0,
  child: redBox
)

实际上SizedBox只是ConstrainedBox的一个定制,上面代码等价于:

ConstrainedBox(
  constraints: BoxConstraints.tightFor(width: 80.0,height: 80.0),
  child: redBox, 
)

而BoxConstraints.tightFor(width: 80.0,height: 80.0)等价于:

BoxConstraints(minHeight: 80.0,maxHeight: 80.0,minWidth: 80.0,maxWidth: 80.0)

多重限制

如果某一个组件有多个父级ConstrainedBox限制,那么最终会是哪个生效?我们看一个例子:

ConstrainedBox(
    constraints: BoxConstraints(minWidth: 60.0, minHeight: 60.0), //父
    child: ConstrainedBox(
      constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0),//子
      child: redBox,
    )
)

最终显示效果是宽90,高60,也就是说是子ConstrainedBox的minWidth生效,而minHeight是父ConstrainedBox生效。单凭这个例子,我们还总结不出什么规律,我们将上例中父子限制条件换一下:

ConstrainedBox(
    constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0),
    child: ConstrainedBox(
      constraints: BoxConstraints(minWidth: 60.0, minHeight: 60.0),
      child: redBox,
    )
)

最终的显示效果仍然是90,高60

结论:有多重限制时,对于minWidth和minHeight来说,是取父子中相应数值较大的。

思考题:对于maxWidth和maxHeight,多重限制的策略是什么样的呢?

结论:有多重限制时,对于maxWidth和maxHeight来说,是取父子中相应数值较小的。

UnconstrainedBox

UnconstrainedBox不会对子组件产生任何限制,它允许其子组件按照其本身大小绘制。一般情况下,我们会很少直接使用此组件,但在"去除"多重限制的时候也许会有帮助,我们看下下面的代码:

ConstrainedBox(
    constraints: BoxConstraints(minWidth: 60.0, minHeight: 100.0),  //父
    child: UnconstrainedBox( //“去除”父级限制
      child: ConstrainedBox(
        constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0),//子
        child: redBox,
      ),
    )
)

上面代码中,如果没有中间的UnconstrainedBox,那么根据上面所述的多重限制规则,那么最终将显示一个90×100的红色框。但是由于UnconstrainedBox “去除”了父ConstrainedBox的限制,则最终会按照子ConstrainedBox的限制来绘制redBox,即90×20

装饰容器DecoratedBox

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

const DecoratedBox({
  Decoration decoration,
  DecorationPosition position = DecorationPosition.background,
  Widget child
})

BoxDecoration

我们通常会直接使用BoxDecoration类,它是一个Decoration的子类,实现了常用的装饰元素的绘制。

BoxDecoration({
  Color color, //颜色
  DecorationImage image,//图片
  BoxBorder border, //边框
  BorderRadiusGeometry borderRadius, //圆角
  List<BoxShadow> boxShadow, //阴影,可以指定多个
  Gradient gradient, //渐变
  BlendMode backgroundBlendMode, //背景混合模式
  BoxShape shape = BoxShape.rectangle, //形状
})

变换(Transform)

Transform可以在其子组件绘制时对其应用一些矩阵变换来实现一些特效。

平移

Transform.translate接收一个offset参数,可以在绘制时沿x、y轴对子组件平移指定的距离。

  Widget _createTransform_translate() {
    return Center(
      child: Transform.translate(
        offset: Offset(10, 100),
        child: Text(
          "哈哈哈哈哈哈",
          style: TextStyle(
            backgroundColor: Colors.grey,
          ),
        ),
      ),
    );
  }
旋转

Transform.rotate可以对子组件进行旋转变换,如:

  Widget _createTransform_rotate() {
    return Center(
      child: Transform.rotate(
        angle: pi / 2,
        child: Text(
          "哈哈哈哈哈哈",
          style: TextStyle(
            backgroundColor: Colors.grey,
          ),
        ),
      ),
    );
  }
缩放

Transform.scale可以对子组件进行缩小或放大,如:

  Widget _createTransform_scale() {
    return Row(
      children: [
        Text(
          "哈哈哈哈哈哈",
          style: TextStyle(
            backgroundColor: Colors.grey,
          ),
        ),
        Transform.scale(
          scale: 2,
          child: Text(
            "嘿嘿嘿嘿嘿",
            style: TextStyle(
              backgroundColor: Colors.red,
            ),
          ),
        ),
      ],
    );
  }

由于使用transform只会在绘制时放大,其占用空间依然为原来部分,所以会出现重合现象。

由于矩阵变化只会作用在绘制阶段,所以在某些场景下,在UI需要变化时,可以直接通过矩阵变化来达到视觉上的UI改变,而不需要去重新触发build流程,这样会节省layout的开销,所以性能会比较好。如之前介绍的Flow组件,它内部就是用矩阵变换来更新UI,除此之外,Flutter的动画组件中也大量使用了Transform以提高性能。

RotatedBox

RotatedBox和Transform.rotate功能相似,它们都可以对子组件进行旋转变换,但是有一点不同:RotatedBox的变换是在layout阶段,会影响在子组件的位置和大小。

  Widget _createRotatedBox() {
    return Padding(
      padding: EdgeInsets.symmetric(vertical: 100),
      child: Row(
        children: [
          Text(
            "哈哈哈哈哈哈",
            style: TextStyle(
              backgroundColor: Colors.grey,
            ),
          ),
          RotatedBox(
            quarterTurns: 1,
            child: Text(
              "嘿嘿嘿嘿嘿",
              style: TextStyle(
                backgroundColor: Colors.red,
              ),
            ),
          ),
        ],
      ),
    );
  }

Container

Container是一个组合类容器,它本身不对应具体的RenderObject,它是DecoratedBox、ConstrainedBox、Transform、Padding、Align等组件组合的一个多功能容器,所以我们只需通过一个Container组件可以实现同时需要装饰、变换、限制的场景。下面是Container的定义:

Container({
  this.alignment,
  this.padding, //容器内补白,属于decoration的装饰范围
  Color color, // 背景色
  Decoration decoration, // 背景装饰
  Decoration foregroundDecoration, //前景装饰
  double width,//容器的宽度
  double height, //容器的高度
  BoxConstraints constraints, //容器大小的限制条件
  this.margin,//容器外补白,不属于decoration的装饰范围
  this.transform, //变换
  this.child,
})

Padding和Margin

接下来我们来研究一下Container组件margin和padding属性的区别:

Container(
  margin: EdgeInsets.all(20.0), //容器外补白
  color: Colors.orange,
  child: Text("Hello world!"),
),
Container(
  padding: EdgeInsets.all(20.0), //容器内补白
  color: Colors.orange,
  child: Text("Hello world!"),
),

总结:margin在容器外留白,padding是容器内留白

事实上,Container内margin和padding都是通过Padding 组件来实现的,上面的示例代码实际上等价于:

...
Padding(
  padding: EdgeInsets.all(20.0),
  child: DecoratedBox(
    decoration: BoxDecoration(color: Colors.orange),
    child: Text("Hello world!"),
  ),
),
DecoratedBox(
  decoration: BoxDecoration(color: Colors.orange),
  child: Padding(
    padding: const EdgeInsets.all(20.0),
    child: Text("Hello world!"),
  ),
),
 

Scaffold

一个完整的路由页可能会包含导航栏、抽屉菜单(Drawer)以及底部Tab导航菜单等。如果每个路由页面都需要开发者自己手动去实现这些,这会是一件非常麻烦且无聊的事。幸运的是,Flutter Material组件库提供了一些现成的组件来减少我们的开发任务。Scaffold是一个路由页的骨架,我们使用它可以很容易地拼装出一个完整的页面。

它主要包括:

AppBar

AppBar是一个Material风格的导航栏,通过它可以设置导航栏标题、导航栏菜单、导航栏底部的Tab标题等。下面我们看看AppBar的定义:

AppBar({
  Key key,
  this.leading, //导航栏最左侧Widget,常见为抽屉菜单按钮或返回按钮。
  this.automaticallyImplyLeading = true, //如果leading为null,是否自动实现默认的leading按钮
  this.title,// 页面标题
  this.actions, // 导航栏右侧菜单
  this.bottom, // 导航栏底部菜单,通常为Tab按钮组
  this.elevation = 4.0, // 导航栏阴影
  this.centerTitle, //标题是否居中 
  this.backgroundColor,
  ...   //其它属性见源码注释
})

抽屉菜单Drawer

Scaffold的drawer和endDrawer属性可以分别接受一个Widget来作为页面的左、右抽屉菜单。如果开发者提供了抽屉菜单,那么当用户手指从屏幕左(或右)侧向里滑动时便可打开抽屉菜单。

return Scaffold(
  appBar: createAppBar(),
  endDrawer: Drawer(
    child: Container(
      color: Colors.red,
    ),
  ),
);

底部Tab导航栏

我们可以通过Scaffold的bottomNavigationBar属性来设置底部导航,如本节开始示例所示,我们通过Material组件库提供的BottomNavigationBar和BottomNavigationBarItem两种组件来实现Material风格的底部导航栏。

  bottomNavigationBar: BottomNavigationBar(
    items: [
      BottomNavigationBarItem(
        icon: Icon(Icons.home),
        label: "首页",
      ),
      BottomNavigationBarItem(
        icon: Icon(Icons.history),
        label: "历史",
      ),
      BottomNavigationBarItem(
        icon: Icon(Icons.search),
        label: "搜索",
      ),
    ],
    currentIndex: selectedIndex,
    onTap: _itemTapAction,
  ),

实现中间按钮浮动效果

  floatingActionButton: FloatingActionButton(onPressed: null),
  floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
  bottomNavigationBar: BottomAppBar(
    color: Colors.white,
    shape: CircularNotchedRectangle(), // 底部导航栏打一个圆形的洞
    child: Row(
      children: [
        IconButton(
          onPressed: null,
          icon: Icon(Icons.home),
        ),
        SizedBox(), //中间位置空出
        IconButton(
          onPressed: null,
          icon: Icon(Icons.business),
        ),
      ],
      mainAxisAlignment: MainAxisAlignment.spaceAround, //均分底部导航栏横向空间
    ),
  ),

剪裁(Clip)

Flutter中提供了一些剪裁函数,用于对组件进行剪裁。

  Widget _createClip() {
    const netWorkImage = NetworkImage(
        "https://book.flutterchina.club/assets/img/3-17.a063365a.png");
    return Column(
      children: [
        Padding(
          padding: EdgeInsets.all(10.0),
          //裁剪为圆形
          child: ClipOval(
            child: SizedBox(
              height: 100,
              width: 100,
              child: DecoratedBox(
                decoration: BoxDecoration(color: Colors.blue),
              ),
            ),
          ),
        ),
        Padding(
          padding: EdgeInsets.all(10.0),
          //裁剪为圆角
          child: ClipRRect(
            borderRadius: BorderRadius.circular(10.0),
            child: SizedBox(
              height: 100,
              width: 100,
              child: DecoratedBox(
                decoration: BoxDecoration(color: Colors.blue),
              ),
            ),
          ),
        ),
        Padding(
          padding: EdgeInsets.all(10.0),
          //裁剪为圆角
          child: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              ClipRect(
                //将溢出部分剪裁
                child: Align(
                  alignment: Alignment.topLeft,
                  widthFactor: .5, //宽度设为原来宽度一半
                  child: Image(
                    image: netWorkImage,
                  ),
                ),
              ),
              Text(
                "你好世界",
                style: TextStyle(color: Colors.green),
              )
            ],
          ),
        ),
      ],
    );
  }

CustomClipper

如果我们想剪裁子组件的特定区域,如果我们只想截取图片中部40×30像素的范围应该怎么做?这时我们可以使用CustomClipper来自定义剪裁区域,实现代码如下:
首先,自定义一个CustomClipper:

class MyClipper extends CustomClipper<Rect> {
  @override
  Rect getClip(Size size) => Rect.fromLTWH(10.0, 15.0, 40.0, 30.0);

  @override
  bool shouldReclip(CustomClipper<Rect> oldClipper) => false;
}

然后,我们通过ClipRect来执行剪裁,为了看清图片实际所占用的位置,我们设置一个红色背景:

DecoratedBox(
  decoration: BoxDecoration(
    color: Colors.red
  ),
  child: ClipRect(
      clipper: MyClipper(), //使用自定义的clipper
      child: avatar
  ),
)

可以看到我们的剪裁成功了,但是图片所占用的空间大小仍然是60×60(红色区域),这是因为剪裁是在layout完成后的绘制阶段进行的,所以不会影响组件的大小,这和Transform原理是相似的。

五、路由管理

路由(Route)在移动开发中通常指页面(Page),这跟web开发中单页应用的Route概念意义是相同的,Route在Android中通常指一个Activity,在iOS中指一个ViewController。所谓路由管理,就是管理页面之间如何跳转,通常也可被称为导航管理。Flutter中的路由管理和原生开发类似,无论是Android还是iOS,导航管理都会维护一个路由栈,路由入栈(push)操作对应打开一个新页面,路由出栈(pop)操作对应页面关闭操作,而路由管理主要是指如何来管理路由栈。

MaterialPageRoute

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

MaterialPageRoute 构造函数的各个参数:

  MaterialPageRoute({
    WidgetBuilder builder,
    RouteSettings settings,
    bool maintainState = true,
    bool fullscreenDialog = false,
  })

Navigator

Navigator是一个路由管理的组件,它提供了打开和退出路由页方法。Navigator通过一个栈来管理活动路由集合。通常当前屏幕显示的页面就是栈顶的路由。Navigator提供了一系列方法来管理路由栈,在此我们只介绍其最常用的两个方法:

Future push(BuildContext context, Route route)

将给定的路由入栈(即打开新的页面),返回值是一个Future对象,用以接收新路由出栈(即关闭)时的返回数据。

bool pop(BuildContext context, [ result ])

将栈顶路由出栈,result为页面关闭时返回给上一个页面的数据。

Navigator 还有很多其它方法,如Navigator.replace、Navigator.popUntil等,详情请参考API文档或SDK源码注释。

实例方法

Navigator类中第一个参数为context的静态方法都对应一个Navigator的实例方法, 比如Navigator.push(BuildContext context, Route route)等价于Navigator.of(context).push(Route route)

路由传值

很多时候,在路由跳转时我们需要带一些参数,并且返回上一个路由页面时同时会带上一个返回参数。

例:

_jumpToNextPage() {
    //跳转返回的是一个Future
    var result =
        Navigator.of(this.context).push(MaterialPageRoute(builder: (context) {
      return RoutePageA(title: "PageA");
    }));
    //使用Future取得回调值value
    result.then((value) {
      this.setState(() {
        nextActionName = value;
      });
      print(value);
    });
}

//pop时传回调参数
  leading: IconButton(
            icon: BackButtonIcon(),
            onPressed: () {
              Navigator.pop(context, "我是返回值");
            }),
      ),

命名路由

所谓“命名路由”(Named Route)即有名字的路由,我们可以先给路由起一个名字,然后就可以通过路由名字直接打开新的路由了,这为路由管理带来了一种直观、简单的方式。

路由表

要想使用命名路由,我们必须先提供并注册一个路由表(routing table),这样应用程序才知道哪个名字与哪个路由组件相对应。其实注册路由表就是给路由起名字,路由表的定义如下:

Map<String, WidgetBuilder> routes;

它是一个Map,key为路由的名字,是个字符串;value是个builder回调函数,用于生成相应的路由widget。我们在通过路由名字打开新路由时,应用会根据路由名字在路由表中查找到对应的WidgetBuilder回调函数,然后调用该回调函数生成路由widget并返回。

注册路由表

路由表的注册方式很简单,我们回到之前“计数器”的示例,然后在MyApp类的build方法中找到MaterialApp,添加routes属性,代码如下:

MaterialApp(
  title: 'Flutter Demo',
  initialRoute:"/", //名为"/"的路由作为应用的home(首页)
  theme: ThemeData(
    primarySwatch: Colors.blue,
  ),
  //注册路由表
  routes:{
   "new_page":(context) => NewRoute(),
   "/":(context) => MyHomePage(title: 'Flutter Demo Home Page'), //注册首页路由
  } 
);

通过路由名打开新路由页

要通过路由名称来打开新路由,可以使用Navigator 的pushNamed方法:

Future pushNamed(BuildContext context, String routeName,{Object arguments})
命名路由参数传递
Navigator.of(context).pushNamed("new_page", arguments: "hi");

在路由页通过RouteSetting对象获取路由参数:

class EchoRoute extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    //获取路由参数  
    var args=ModalRoute.of(context).settings.arguments;
    //...省略无关代码
  }
}

路由钩子(onGenerateRoute)

MaterialApp有一个onGenerateRoute属性,它在打开命名路由时可能会被调用,之所以说可能,是因为当调用Navigator.pushNamed(...)打开命名路由时,如果指定的路由名在路由表中已注册,则会调用路由表中的builder函数来生成路由组件;如果路由表中没有注册,才会调用onGenerateRoute来生成路由。

可以对某些指定的路由进行拦截,有时候不想改变页面结构,但是又想要求跳转到这个页面的时候可以用到

return MaterialApp(
  title: "Welcome to Flutter1",
  initialRoute: '/',
  // home: HomePage(),
  routes: RoutesMap.instance.initRoutes(context),
  onGenerateRoute: (RouteSettings settings) {
    String? routeName = settings.name;
    return MaterialPageRoute(
        fullscreenDialog: true,
        builder: (context) {
          return LoginPage();
        });
  },
);

 Navigator.pushNamed(context, "没注册的page");

会直接跳转到LoginPage

其他

initialRoute:是项目的根路由,初始化的时候最先展示的页面

onUnknownRoute(RouteFactory类型函数):在路由匹配不到的时候用到,一般都返回一个统一的错误页面或404页面

上一篇下一篇

猜你喜欢

热点阅读