跨平台

Flutter容器类组件

2020-10-19  本文已影响0人  平安喜乐698
目录

  1. Container容器
  2. 装饰容器DecoratedBox
  3. 填充(Padding)
  4. 尺寸限制类容器
  5. 剪裁

Flutter官方并没有对Widget进行官方分类,对其分类主要是为了对Widget进行功能区分。

容器类Widget和布局类Widget都作用于其子Widget,不同点:
    1. 布局类Widget一般都需要接收一个widget数组(children),他们直接或间接继承自(或包含)MultiChildRenderObjectWidget ;而容器类Widget一般只需要接收一个子Widget(child),他们直接或间接继承自(或包含)SingleChildRenderObjectWidget。
    2. 布局类Widget是按照一定的排列方式来对其子Widget进行排列;而容器类Widget一般只是包装其子Widget,对其添加一些修饰(补白或背景色等)、变换(旋转或剪裁等)、或限制(大小等)。

1. Container容器(属于基础组件库)

类似于html的div。

本身不对应具体的RenderObject。它是DecoratedBox、ConstrainedBox、Transform、Padding、Align等组件组合的一个多功能容器,所以只需通过一个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,
})

说明:
    1. 容器的大小可以通过width、height属性来指定,也可以通过constraints来指定;如果它们同时存在时,width、height优先。本质上Container内部会根据width、height来生成一个constraints。
    2. color和decoration是互斥的,如果同时设置它们则会报错!本质上,当指定color时,Container内会自动创建一个decoration。
    3. alignment:Alignment.left、...

例1

Container(
  margin: EdgeInsets.only(top: 50.0, left: 120.0), // 外边距
  constraints: BoxConstraints.tightFor(width: 200.0, height: 150.0), //  宽高
  decoration: BoxDecoration(// 背景装饰
      gradient: RadialGradient( // 渐变
          colors: [Colors.red, Colors.orange],
          center: Alignment.topLeft,
          radius: .98
      ),
      boxShadow: [ // 阴影
        BoxShadow(
            color: Colors.black54,
            offset: Offset(2.0, 2.0),
            blurRadius: 4.0
        )
      ]
  ),
  transform: Matrix4.rotationZ(.2), // 形变(旋转)
  alignment: Alignment.center, // 居中
  child: Text( // 文本
    "5.20", style: TextStyle(color: Colors.white, fontSize: 40.0),
  ),
);
运行结果

例2(Padding和Margin)

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!"),
),
...

====================
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!"),
  ),
),
...

运行结果

变换(Transform)

Matrix4是一个4D矩阵,通过它可以实现各种矩阵操作.
Transform的变换是应用在绘制阶段,而并不是应用在布局(layout)阶段,所以无论对子组件应用何种变化,其占用空间的大小和在屏幕上的位置都是固定不变的,因为这些是在布局阶段就确定的。由于矩阵变化只会作用在绘制阶段,所以在某些场景下,在UI需要变化时,可以直接通过矩阵变化来达到视觉上的UI改变,而不需要去重新触发build流程,这样会节省layout的开销,所以性能会比较好。如Flow组件,它内部就是用矩阵变换来更新UI,除此之外,Flutter的动画组件中也大量使用了Transform以提高性能。

Container(
  color: Colors.black,
  child: new Transform(
    alignment: Alignment.topRight, //相对于坐标系原点的对齐方式
    transform: new Matrix4.skewY(0.3), //沿Y轴倾斜0.3弧度
    child: new Container(
      padding: const EdgeInsets.all(8.0),
      color: Colors.deepOrange,
      child: const Text('Apartment for rent!'),
    ),
  ),
);
运行结果

 Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: <Widget>[
    DecoratedBox(
      decoration:BoxDecoration(color: Colors.red),
      child: Transform.scale(scale: 1.5,
          child: Text("Hello world")
      )
    ),
    Text("你好", style: TextStyle(color: Colors.green, fontSize: 18.0),)
  ],
)
由于第一个Text应用变换(放大)后,其在绘制时会放大,但其占用的空间依然为红色部分,所以第二个Text会紧挨着红色部分,最终就会出现文字重合
运行结果
  1. 平移
Transform.translate接收一个offset参数,可以在绘制时沿x、y轴对子组件平移指定的距离。

例
DecoratedBox(
  decoration:BoxDecoration(color: Colors.red),
  //默认原点为左上角,左移20像素,向上平移5像素  
  child: Transform.translate(
    offset: Offset(-20.0, -5.0),
    child: Text("Hello world"),
  ),
)
运行结果
  1. 旋转
Transform.rotate可以对子组件进行旋转变换

例
DecoratedBox(
  decoration:BoxDecoration(color: Colors.red),
  child: Transform.rotate(
    //旋转90度
    angle:math.pi/2 ,
    child: Text("Hello world"),
  ),
);
要使用math.pi需先进行导包:import 'dart:math' as math;
运行结果
  1. 缩放
Transform.scale可以对子组件进行缩小或放大

例
DecoratedBox(
  decoration:BoxDecoration(color: Colors.red),
  child: Transform.scale(
      scale: 1.5, //放大到1.5倍
      child: Text("Hello world")
  )
);
运行结果

RotatedBox

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

例
Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: <Widget>[
    DecoratedBox(
      decoration: BoxDecoration(color: Colors.red),
      //将Transform.rotate换成RotatedBox  
      child: RotatedBox(
        quarterTurns: 1, //旋转90度(1/4圈)
        child: Text("Hello world"),
      ),
    ),
    Text("你好", style: TextStyle(color: Colors.green, fontSize: 18.0),)
  ],
),
由于RotatedBox是作用于layout阶段,所以子组件会旋转90度(而不只是绘制的内容),decoration会作用到子组件所占用的实际空间上。
运行结果

2. 装饰容器DecoratedBox

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

定义如下:
const DecoratedBox({
  Decoration decoration,
  DecorationPosition position = DecorationPosition.background,
  Widget child
})

说明:

1. decoration
将要绘制的装饰,它的类型为Decoration。Decoration是一个抽象类,它定义了一个接口 createBoxPainter(), 子类的主要职责是需要通过实现它来创建一个画笔,该画笔用于绘制装饰。通常会直接使用BoxDecoration类(Decoration的子类)。
  BoxDecoration({
    Color color,   // 颜色
    DecorationImage image,  // 图片,DecorationImage(image: fit:)
    BoxBorder border, // 边框
    BorderRadiusGeometry borderRadius, // 圆角,BorderRadius.circular(宽/2)切圆,BorderRadius.all(Radius.circular(宽/2))切圆
    List<BoxShadow> boxShadow, // 阴影,可以指定多个
    Gradient gradient, // 渐变,LinearGradient类用于定义线性渐变的类,其它渐变配置类:RadialGradient、SweepGradient
    BlendMode backgroundBlendMode, // 背景混合模式
    BoxShape shape = BoxShape.rectangle, // 形状
  })

2. position
决定在哪里绘制Decoration,它接收DecorationPosition的枚举类型,该枚举类有两个值:
  1. background:在子组件之后绘制,即背景装饰。
  2. foreground:在子组件之上绘制,即前景装饰。

 DecoratedBox(
    decoration: BoxDecoration(
      gradient: LinearGradient(colors:[Colors.red,Colors.orange[700]]), //背景渐变
      borderRadius: BorderRadius.circular(3.0), //3像素圆角
      boxShadow: [ // 阴影
        BoxShadow(
            color:Colors.black54,
            offset: Offset(2.0,2.0),
            blurRadius: 4.0
        )
      ]
    ),
  child: Padding(padding: EdgeInsets.symmetric(horizontal: 80.0, vertical: 18.0),
    child: Text("Login", style: TextStyle(color: Colors.white),),
  )
)
运行结果

3. 填充(Padding)

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

定义:
Padding({
  EdgeInsetsGeometry padding,
  Widget child,
})

说明:
1. EdgeInsetsGeometry是一个抽象类,开发中,一般都使用EdgeInsets类(EdgeInsetsGeometry的子类)提供的便捷方法:
    1. fromLTRB(double left, double top, double right, double bottom):分别指定四个方向的填充。
    2. all(double value) : 所有方向均使用相同数值的填充。
    3. only({left, top, right ,bottom }):可以设置具体某个方向的填充(可以同时指定多个方向)。
    4. symmetric({ vertical, horizontal }):用于设置对称方向的填充,vertical指top和bottom,horizontal指left和right。

class PaddingTestRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Padding(
      //上下左右各添加16像素补白
      padding: EdgeInsets.all(16.0),
      child: Column(
        //显式指定对齐方式为左对齐,排除对齐干扰
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Padding(
            //左边添加8像素补白
            padding: const EdgeInsets.only(left: 8.0),
            child: Text("Hello world"),
          ),
          Padding(
            //上下各添加8像素补白
            padding: const EdgeInsets.symmetric(vertical: 8.0),
            child: Text("I am Jack"),
          ),
          Padding(
            // 分别指定四个方向的补白
            padding: const EdgeInsets.fromLTRB(20.0,.0,20.0,20.0),
            child: Text("Your friend"),
          )
        ],
      ),
    );
  }
}
运行结果

4. 尺寸限制类容器

限制容器大小,Flutter中提供了多种这样的容器

  ConstrainedBox
  SizedBox
  UnconstrainedBox
  AspectRatio  指定子组件的长宽比
  LimitedBox 指定最大宽高
  FractionallySizedBox 根据父容器宽高的百分比来设置子组件宽高等
  1. ConstrainedBox

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

定义一个redBox,它是一个背景颜色为红色的盒子,不指定它的宽度和高度
Widget redBox=DecoratedBox(
  decoration: BoxDecoration(color: Colors.red),
);
实现一个最小高度为50,宽度尽可能大的红色容器
ConstrainedBox(
  constraints: BoxConstraints(
    minWidth: double.infinity, //宽度尽可能大
    minHeight: 50.0 //最小高度为50像素
  ),
  child: Container(
      height: 5.0, 
      child: redBox 
  ),
)
虽然将Container的高度设置为5像素,但是最终却是50像素,这正是ConstrainedBox的最小高度限制生效了。
运行结果

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。
  1. SizedBox

给子元素指定固定的宽高

实际上ConstrainedBox和SizedBox都是通过RenderConstrainedBox来渲染的,ConstrainedBox和SizedBox的createRenderObject()方法都返回的是一个RenderConstrainedBox对象:
@override
RenderConstrainedBox createRenderObject(BuildContext context) {
  return new RenderConstrainedBox(
    additionalConstraints: ...,
  );
}

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)

多重限制

有多重限制时,对于minWidth和minHeight来说,是取父子中相应数值较大的。实际上,只有这样才能保证父限制与子限制不冲突。

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生效的是父ConstrainedBox,而minHeight是子ConstrainedBox生效。
  1. UnconstrainedBox

UnconstrainedBox不会对子组件产生任何限制,它允许其子组件按照其本身大小绘制。

一般情况下,很少直接使用此组件,经常用于"去除"多重限制。
在实际开发中,当使用SizedBox或ConstrainedBox给子元素指定了宽高,但是仍然没有效果时,几乎可以断定:已经有父元素已经设置了限制。

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。

UnconstrainedBox对父组件限制的“去除”并非是真正的去除:上面例子中虽然红色区域大小是90×20,但上方仍然有80的空白空间。也就是说父限制的minHeight(100.0)仍然是生效的,只不过它不影响最终子元素redBox的大小,但仍然还是占有相应的空间,可以认为此时的父ConstrainedBox是作用于子UnconstrainedBox上,而redBox只受子ConstrainedBox限制。

没有办法可以彻底去除父ConstrainedBox的限制。在定义一个通用的组件时,如果要对子组件指定限制,那么一定要注意,因为一旦指定限制条件,子组件如果要进行相关自定义大小时将可能非常困难,因为子组件在不更改父组件的代码的情况下无法彻底去除其限制条件。
运行结果

Material组件库中的AppBar(导航栏)的右侧菜单中,我们使用SizedBox指定了loading按钮的大小,代码如下:

 AppBar(
   title: Text(title),
   actions: <Widget>[
         SizedBox(
             width: 20, 
             height: 20,
             child: CircularProgressIndicator(
                 strokeWidth: 3,
                 valueColor: AlwaysStoppedAnimation(Colors.white70),
             ),
         )
   ],
)

右侧loading按钮大小并没有发生变化!这正是因为AppBar中已经指定了actions按钮的限制条件,所以要自定义loading按钮大小,就必须通过UnconstrainedBox来“去除”父元素的限制,代码如下:
AppBar(
  title: Text(title),
  actions: <Widget>[
      UnconstrainedBox(
            child: SizedBox(
              width: 20,
              height: 20,
              child: CircularProgressIndicator(
                strokeWidth: 3,
                valueColor: AlwaysStoppedAnimation(Colors.white70),
              ),
          ),
      )
  ],
)
未使用UnconstrainedBox 使用UnconstrainedBox

5. 剪裁

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

1. ClipOval
子组件为正方形时剪裁为内贴圆形,为矩形时,剪裁为内贴椭圆

2. ClipRRect    
将子组件剪裁为圆角矩形

3. ClipRect 
剪裁子组件到实际占用的矩形大小(溢出部分剪裁)

import 'package:flutter/material.dart';

class ClipTestRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 头像  
    Widget avatar = Image.asset("imgs/avatar.png", width: 60.0);
    return Center(
      child: Column(
        children: <Widget>[
          avatar, // 不剪裁
          ClipOval(child: avatar), // 剪裁为圆形
          ClipRRect( // 剪裁为圆角矩形
            borderRadius: BorderRadius.circular(5.0),
            child: avatar,
          ), 
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Align(
                alignment: Alignment.topLeft,
                widthFactor: .5,// 宽度设为原来宽度一半,另一半会溢出
                child: avatar,
              ),
              Text("你好世界", style: TextStyle(color: Colors.green),)
            ],
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              ClipRect(  // 将溢出部分剪裁
                child: Align(
                  alignment: Alignment.topLeft,
                  widthFactor: .5,  // 宽度设为原来宽度一半
                  child: avatar,
                ),
              ),
              Text("你好世界",style: TextStyle(color: Colors.green))
            ],
          ),
        ],
      ),
    );
  }
}
运行结果

例(自定义剪裁区域)

截取图片中部40×30像素的范围


1. 首先,自定义一个CustomClipper:
class MyClipper extends CustomClipper<Rect> {

  @override
  //  getClip()是用于获取剪裁区域的接口,由于图片大小是60×60,计算之后即图片中部40×30像素的范围。
  Rect getClip(Size size) => Rect.fromLTWH(10.0, 15.0, 40.0, 30.0);

  @override
  // shouldReclip() 接口决定是否重新剪裁。如果在应用中,剪裁区域始终不会发生变化时应该返回false,这样就不会触发重新剪裁,避免不必要的性能开销。如果剪裁区域会发生变化(比如在对剪裁区域执行一个动画),那么变化后应该返回true来重新执行剪裁。
  bool shouldReclip(CustomClipper<Rect> oldClipper) => false;
}

2. 然后,通过ClipRect来执行剪裁,为了看清图片实际所占用的位置,设置一个红色背景:
DecoratedBox(
  decoration: BoxDecoration(
    color: Colors.red
  ),
  child: ClipRect(
      clipper: MyClipper(), // 使用自定义的clipper
      child: avatar
  ),
)

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

猜你喜欢

热点阅读