Flutter | 容器组件

2022-02-11 14:24:47 浏览数 (1)

Padding

Padding 可以给子节点添加填充(留白),和边距的效果类似,定义如下:

代码语言:javascript复制
Padding({
  ...
  EdgeInsetsGeometry padding,
  Widget child,
})
复制代码

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

EdgeInsets

  • fromLTRB(double left, double top, double right, double bottom) :分别指定四个方向的填充
  • all(double value):所有方向都使用相同的数值填充
  • only({left, top, right ,bottom }):可以设置具体某个方向的填充,可以同时指定多个方向
  • symmetric({ vertical, horizontal }):用于设置对称方向的填充
栗子
代码语言:javascript复制
class PaddingTest extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("padding"),
      ),
      body: Container(
        color: Colors.red,
        child: Padding(
          padding: EdgeInsets.fromLTRB(5, 10, 15, 20),
          child: Text("I am 345"),
        ),
      ),
    );
  }
}
复制代码

尺寸限制类容器

尺寸类限制容器用于限制容器的大小,Flutter 中提供了很多这样的属性,如 ConstrainedBoxSizedBoxUnconstrainedBoxAspectRatio 等。

ConstrainedBox

ConstrainedBox 用于对子组件添加额外的约束。例如设置最小高度等

栗子

代码语言:javascript复制
class BoxTest extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Box"),
      ),
      body: getConstrainedBox(),
    );
  }

  Widget getConstrainedBox() {
    return ConstrainedBox(
      constraints: BoxConstraints(minWidth: double.infinity, minHeight: 50),
      child: DecoratedBox(
  		decoration: BoxDecoration(color: Colors.red),
		),
    );
  }
}
复制代码

可以看到,虽然将 Container 的高度设置为了 10,但是最终显示的却是 50像素。这正是 ConstrainedBox 的最小高度生效了

BoxConstraints

代码语言:javascript复制
const BoxConstraints({
  this.minWidth = 0.0, //最小宽度
  this.maxWidth = double.infinity, //最大宽度
  this.minHeight = 0.0, //最小高度
  this.maxHeight = double.infinity //最大高度
})
复制代码

BoxConstraints 定义了一些便捷的构造函数,用于快速的生成 BoxConstraints

  • tigth(size) : 生成给定大小的限制
  • expand() : 可以生成一个尽可能大的 BoxConstraints
  • 还有一些其他的,如图所示:
SizedBox

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

代码语言:javascript复制
Widget getSizedBox() {
    return SizedBox(width: 50, height: 50, child: getRedBackground());
}
Widget getRedBackground() {
    return DecoratedBox(
      decoration: BoxDecoration(color: Colors.red),
    );
}
复制代码

实际上,SizedBox 只是 ConstrainedBox 的一个定制

代码语言:javascript复制
@override
RenderConstrainedBox createRenderObject(BuildContext context) {
  return RenderConstrainedBox(
    additionalConstraints: _additionalConstraints,
  );
}

BoxConstraints get _additionalConstraints {
  return BoxConstraints.tightFor(width: width, height: height);
}
复制代码

看上面源码可以知道, Sized 和 ConstrainedBox 都是通过 RenderConstrainedBox,他们的 createRenderObject 方法都是返回一个 RenderConstrainedBox

多重限制

如果一个组件有多个父级 ConstrainedBox ,那么最终是哪个生效,示例:

代码语言:javascript复制
Widget getConstrainedBoxS() {
  return ConstrainedBox(
    constraints: BoxConstraints(minWidth: 90, minHeight: 50),
    child: ConstrainedBox(
      constraints: BoxConstraints(minWidth: 50, minHeight: 90),
      child: getRedBackground(),
    ),
  );
}
复制代码

由上面知道的宽高可知,对于 minWidthminHeight 来说,是取父子中相应数值较大的。实际上,只有这样才能保证 父限制与子限制不冲突

UnconstrainedBox

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

代码语言:javascript复制
Widget getUnConstrainedBox() {
  return ConstrainedBox(
    constraints: BoxConstraints(minWidth: 90, minHeight: 50),
    //去除父级限制
    child: UnconstrainedBox(
      child: ConstrainedBox(
        constraints: BoxConstraints(minWidth: 50, minHeight: 90),
        child: getRedBackground(),
      ),
    ),
  );
}
复制代码

可以看到上面的父级的限制已经被取消了,最终显示的是 50x50。

但是,需要注意的是,这个限制并发真正的去除,看图可知左右还有留白,也就是说父限制是存在的,只不过它不影响子元素 getRedBackground() 的大小,但是仍然还占有相应的空间,这一点必须要注意。

那么有什么办法可以彻底去除限制吗,答案是否定的!所以在开发中如果要对子组件进行限制,那么就一点要注意,因为一旦限制指定条件,子组件如果要进行相关自定义大小时将可能非常困难!

在实际开发中,当我们发现已经使用了 SizedBox 或者 ConstrainedBox 给定子元素宽高,但是仍然没有效果时,几乎可以断定:已经有父元素设置了限制!

例如:Material 组件中的 AppBar 的右侧菜单中,我们使用 SizedBox 指定 loading 按钮的大小,代码如下:

代码语言:javascript复制
AppBar(
  title: Text("Box"),
  actions: [
    SizedBox(
      width: 20,
      height: 20,
      child: CircularProgressIndicator(
        strokeWidth: 3,
        valueColor: AlwaysStoppedAnimation(Colors.white70),
      ),
    )
  ],
),
复制代码

可以看到 loading 并没有因为设置的大小发生变化,这是应为 Appbar 中已经指定了 action 的限制条件,所以我们要按定义 laoding 的大小 就需要去除限制,如下:

代码语言:javascript复制
actions: [
  UnconstrainedBox(
    child: Padding(
      padding: EdgeInsets.all(10),
      child: SizedBox(
        width: 20,
        height: 20,
        child: CircularProgressIndicator(
          strokeWidth: 3,
          valueColor: AlwaysStoppedAnimation(Colors.white70),
        ),
      ),
    ),
  )
],
复制代码

上面使用了 Padding 走了一个内边距,目的是防止贴屏幕右侧的边

其他的容器限制类

除了上面介绍的容器外,还有一些其他的尺寸限制类容器,例如:

  • AspectRatio :可以知道子组件的长宽比
  • LimitedBox:用于指定最大宽高
  • FractionallySizedBox 可以根据父容器宽高比来设置子组件宽高等,

由于这些都使用比较简单,使用的时候可自行了解

装饰容器

DecoratedBox

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

代码语言:javascript复制
const DecoratedBox({
  Decoration decoration,
  DecorationPosition position = DecorationPosition.background,
  Widget child
})
复制代码
  • decoration:代表要绘制的装饰,他的类型为 Decoration 是一个抽象类,定义了一个接口 createBoxPainter() ,子类的主要职责是通过实现它来创建一个画笔,该笔用于绘制装饰。
  • position:此属性决定在哪里绘制 Decoration,它接受 DecorationPostition 的枚举类型,该枚举有两个类型:
    • background:在子组件之后绘制
    • foreground:在子组件之上绘制,即前景
BoxDecoration

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

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

下面实现一个带阴影的背景色渐变的按钮

代码语言:javascript复制
class DecoratedTest extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("DecoratedTest"),
      ),
      body: Container(
        child: DecoratedBox(
          //background:背景,foreground:前景
          position: DecorationPosition.background,
          decoration: BoxDecoration(
              //背景渐变
              gradient:
                  LinearGradient(colors: [Colors.red, Colors.orange[700]]),
              //圆角
              borderRadius: BorderRadius.circular(10),
              //阴影
              boxShadow: [
                BoxShadow(color: Colors.black26, offset: Offset(2, 2))
              ]),
          child: Padding(
            padding: EdgeInsets.symmetric(horizontal: 80, vertical: 20),
            child: Text(
              "Login",
              style: TextStyle(color: Colors.white),
            ),
          ),
        ),
      ),
    );
  }
}
复制代码

通过 DecoratedBox 可以对子元素进行装饰,上面这个例子中使用了渐变,圆角,阴影等进行装饰,效果如下:

其实装饰类 DecoratedBox 的功能类似于 android 中的 shap ,都是给控件添加各种样式。

变换 Transform

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

Matrix4 是一个 4D 矩阵,通过它我们可以实现各种矩形操作, 例子:

代码语言:javascript复制
class TransformTest extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Transform"),
      ),
      body: Container(
        color: Colors.black,
        child: getSkewTransform(),
      ),
    );
  }

  Widget getSkewTransform() {
    return Transform(
      alignment: Alignment(-1, -1), //
      transform: Matrix4.skewY(0.3), //沿 Y 轴倾斜0.3弧度
      child: Container(
        padding: const EdgeInsets.all(8),
        color: Colors.deepOrange,
        child: const Text('Apartment for rent!'),
      ),
    );
  }
}
复制代码
平移

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

代码语言:javascript复制
//平移
Widget getTranslate() {
  return DecoratedBox(
    decoration: BoxDecoration(color: Colors.red),
    child: Transform.translate(
      offset: Offset(10, 10),
      child: Text("hello world"),
    ),
  );
}
复制代码
旋转

Transform.rotate 可以对子组件进行旋转变化

代码语言:javascript复制
Widget getRotate() {
  return DecoratedBox(
    decoration: BoxDecoration(color: Colors.red),
    child: Transform.rotate(
      angle: math.pi / 2, //旋转90度
      child: Text("hello world"),
    ),
  );
}
复制代码

注意,math 需要导包

代码语言:javascript复制
import 'dart:math' as math;
复制代码
缩放

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

代码语言:javascript复制
Widget getScale() {
  return DecoratedBox(
    decoration: BoxDecoration(color: Colors.red),
    child: Transform.scale(
      scale: 1.5,
      child: Text("hello world"),
    ),
  );
}
复制代码
注意

Transform 的变化是在绘制阶段,而并不是在 layout 阶段,所以无论对 子组件做何种变化,其占用的空间的大小和在屏幕上的位置都是不变的,因为这些都是在布局阶段就确定的,例如:

代码语言:javascript复制
Widget getTest() {
  return Row(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
      getScale(),
      Text(
        "你好",
        style: TextStyle(color: Colors.green, fontSize: 18),
      )
    ],
  );
}
复制代码

由于 getScale 中的 Text 被放大后,占用的空间依然是红色的部分,所以第二个 Text 就会挨着红色的部分,最终就会出现重合

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

RotatedBox

RotatedBoxTransform.rotate 功能相似,但是有一点不同:RotatedBox 的变化是在 layout 阶段,会影响在子组件的位置和大小

将上面的栗子修改一下:

代码语言:javascript复制
Widget getTest() {
  return Row(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
      getRotate(),
      Text(
        "你好",
        style: TextStyle(color: Colors.green, fontSize: 18),
      )
    ],
  );
}
Widget getRotate() {
  return DecoratedBox(
    decoration: BoxDecoration(color: Colors.red),
    child: RotatedBox(
      quarterTurns: 1, //旋转90度(1/4)
      child: Text("hello world"),
    ),
  );
}
复制代码

由于 RotatedBox 是作用于 layout 阶段,所以子组件会旋转 90 度(而不是绘制内容),decoration 会作用到子组件所占的实际空间上,所以最终的效果如上图

Container

在前面已经使用过很多次 Container 组件,Container 是一个组合类容器,它本身不对应具体的RenderObject,它是DecoratedBoxConstrainedBox、TransformPaddingAlign等组件组合的一个多功能容器

所以我们只需要通过一个 Container 组件可以实现同时装饰,变化,限制的场景。

代码语言:javascript复制
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,
})
复制代码

Container 的大多数属性都已经介绍过了,但是有两点需要说一下:

  • 容器的大小可以通过 widthheight 属性来指定,也可以通过 constraints 来限定;如果同时存在,则 widthheight 优先。实际上 Container 内部会根据 width 和 height 来生成一个 constraints
  • color 和 decoration 是互斥的,如果同时指定就会报错! 实际上,当指定 color 时,Container 内会自动创建一个 decoration
栗子
代码语言:javascript复制
class ContainerTest extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Container"),
      ),
      body: Container(
        margin: EdgeInsets.only(top: 50, left: 120),
        constraints: BoxConstraints.tightFor(width: 200, height: 150),
        decoration: BoxDecoration(
         borderRadius: BorderRadius.circular(12),
            //背景径向渐变
            gradient: RadialGradient(
                colors: [Colors.red, Colors.orange],
                center: Alignment.topLeft,
                radius: 58),
            //卡片阴影
            boxShadow: [
              BoxShadow(
                  color: Colors.black54, offset: Offset(2, 2), blurRadius: 12),
            ]),
        child: Text("521", style: TextStyle(color: Colors.white, fontSize: 40)),
        transform: Matrix4.rotationZ(.2),
        alignment: Alignment(0,0),
      ),
    );
  }
}
复制代码

可以看到 ,Contrainer 具备多种组件的功能,通过源码可以看到,它正是前面我们介绍过得多种组件组合而成,在 Flutter 中,Container 组件也是组合优先于继承的实例

Padding 和 Margin
代码语言:javascript复制
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!"),
),
复制代码

效果和 Android 中 padding/margin 中的差不多,padding 是内边距,margin 是外边距

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

代码语言:javascript复制
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!"),
  ),
),
复制代码

实际上就是给最外层套了一个Padding

Scaffold,TabBar,底部导航

一个完整的路由页面可能会包含导航栏,抽屉菜单(Drawer) 以及底部 Tab 导航栏菜单等,如果每个路由页面都要开发者手动去完成,这会是一个无聊且麻烦的事情。

幸运的是 Flutter Material 组件库中提供了一些现成的组件来减少我们的开发任务

Scaffold

Scaffold 是一个路由页的骨架,使用它可以很容易的拼装出一个完整的页面

我们实现一个页面,他包含

1,导航栏,导航栏的按钮

2,抽屉菜单

3,底部导航

4,右下角悬浮按钮

实现代码如下:

代码语言:javascript复制
class ScaffoldRoute extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _ScaffoldRouteState();
  }
}

class _ScaffoldRouteState extends State<ScaffoldRoute> {
  int _selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("App"),
        actions: [
          IconButton(
            icon: Icon(Icons.share),
            onPressed: () => {},
          )
        ],
      ),
      drawer: Drawer(
        child: Text("抽屉",style: TextStyle(fontSize: 40,color:Colors.blue),),
      ),
      bottomNavigationBar: BottomNavigationBar(
        items: [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: "首页"),
          BottomNavigationBarItem(icon: Icon(Icons.list_alt), label: "列表"),
          BottomNavigationBarItem(icon: Icon(Icons.person), label: "用户")
        ],
        currentIndex: _selectedIndex,
        fixedColor: Colors.blue,
        onTap: _onItemTapped,
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: _add,
      ),
    );
  }

  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }
  void _add() {}
}
复制代码

上面代码中我们用到的组件有:

  • AppBar:一个导航栏骨架
  • MyDrawer:抽屉菜单
  • BottomNavigationBar:底部导航栏
  • FloatingActionButton:漂浮按钮
AppBar

Appbar 是一个 Material 风格的导航栏,通过他可以设置标题,导航栏菜单,导航底部tab等

代码语言:javascript复制
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,
  ...   //其它属性见源码注释
})
复制代码

如果给 Scaffold 添加了抽屉菜单,默认情况下, Scaffold 会自动将 AppBar 的 leading 设置为菜单按钮(如上面截图所示),点击它可以打开抽屉菜单。

如果想要自定义菜单图标,可以手动设置 leading。如:

代码语言:javascript复制
AppBar(
  title: Text("App"),
  leading: Builder(builder: (context) {
    return IconButton(
      icon: Icon(
        Icons.dashboard,
        color: Colors.white,
      ),
      onPressed: () => {Scaffold.of(context).openDrawer()},
    );
  }),
  ...........  
 )   
复制代码

可以看到左侧的菜单已经替换成功

打开抽屉的方法在 ScaffoldState 中,通过 Scaffold.of() 可以获取腹肌最近的 Scaffold 组件的 State 对象

ToolBar

下面,在 AppBar 中通过 Bottom 属性创建一个 TabBar 组件,他可以快速的生成 Tab 菜单,

代码语言:javascript复制
class _ScaffoldRouteState extends State<ScaffoldRoute> with SingleTickerProviderStateMixin{
  int _selectedIndex = 0;

  TabController _tabController;
  List tabs = ["新闻", "历史", "图片"];

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: tabs.length, vsync: this);
  }
  
   AppBar(
        title: Text("App"),
        bottom: TabBar(
          controller: _tabController,
          tabs: tabs.map((e) => Tab(text: e)).toList(),
        ),
        ........
      )
  
  }
复制代码

上面代码创建了一个 TabController,它是用于 监听 Tab 菜单切换的,然后通过 tabBar 生成了一个菜单栏。

TabBar 的 tabs 属性接受一个 Widget 数组,表示每一个 Tab 子菜单,我们可以自定义组件样式,也可以像例子中一样直接使用 Tab 组件

Tab 组件有三个可选参数,除了可以知道文字外,还可以指定 Tab 菜单图标,或者自定义 组件样式,定义如下:

代码语言:javascript复制
Tab({
  Key key,
  this.text, // 菜单文本
  this.icon, // 菜单图标
  this.child, // 自定义组件样式
})
复制代码

开发者可根据实际的需求定制

TabBarView

通过 TabBar 我们只能生成一个静态菜单,真正的Tab页面还没有实现。由于 Tab 菜单和 Tab 页面的切换需要通过,我们需要通过 TabController 去监听 Tab菜单的切换,然后在去切换 Tab 页面, 代码如:

代码语言:javascript复制
_tabController.addListener((){  
  switch(_tabController.index){
    case 1: ...;
    case 2: ... ;   
  }
});
复制代码

如果 Tab 页面可以滑动切换的话,还需要在滑动过程中更新 TabBar 指示器的偏移,显然,这样是非常麻烦的!

为此,Material 库提供了一个 TabBarView 组件,通过它不仅可以轻松实现 Tab 页,而且可以非常容易配合 TabBar 来实现通过切换和滑动状态的同步,如下:

代码语言:javascript复制
body: TabBarView(
  controller: _tabController,
  children: tabs.map((e) {
    return Container(
      alignment: Alignment.center,
      child: Text(e),
    );
  }).toList(),
),
复制代码

现在,无论是点击 Tab 还是 滑动页面, Tab 呀页面都会切换,

在上面的例子中,TabBar 和 TabBarView 的 controller 都是同一个,正是如此, TabBar 和 TabBarView 正是通过一个 controller 来实现菜单切换和滑动状态同步的,效果如下:

另外,Material 组件库也提供了一个 PageView 组件,它和 TabBarView 功能类似,下面将上面的例子重新整理一下,使用 pageView ,让 下面的 导航栏也动起来

代码语言:javascript复制
class ScaffoldRoute extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _ScaffoldRouteState();
  }
}

class _ScaffoldRouteState extends State<ScaffoldRoute>
    with SingleTickerProviderStateMixin {
  int _selectedIndex = 0;

  PageController _pageController;
  TabController _tabController;
  List tabs = ["新闻", "历史", "图片"];

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: tabs.length, vsync: this);
    _pageController = PageController();
    _tabController.addListener(() {
      print(_tabController.index);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: PageView(
        controller: _pageController,
        children: [
          home(),
          Container(
            alignment: Alignment.center,
            child: Text("列表"),
          ),
          Container(
            alignment: Alignment.center,
            child: Text("用户"),
          )
        ],
      ),
      bottomNavigationBar: BottomNavigationBar(
        items: [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: "首页"),
          BottomNavigationBarItem(icon: Icon(Icons.list_alt), label: "列表"),
          BottomNavigationBarItem(icon: Icon(Icons.person), label: "用户")
        ],
        currentIndex: _selectedIndex,
        fixedColor: Colors.blue,
        onTap: _onItemTapped,
      ),
    );
  }

  Widget home() {
    return Scaffold(
      drawer: Drawer(
        child: Text(
          "抽屉",
          style: TextStyle(fontSize: 40, color: Colors.blue),
        ),
      ),
      appBar: AppBar(
        title: Text("App"),
        leading: Builder(builder: (context) {
          return IconButton(
            icon: Icon(
              Icons.dashboard,
              color: Colors.white,
            ),
            onPressed: () => {Scaffold.of(context).openDrawer()},
          );
        }),
        actions: [
          IconButton(
            icon: Icon(Icons.share),
            onPressed: () => {},
          )
        ],
        bottom: TabBar(
          controller: _tabController,
          tabs: tabs.map((e) => Text(e)).toList(),
        ),
      ),
      body: TabBarView(
        controller: _tabController,
        children: tabs.map((e) {
          return Container(
            alignment: Alignment.center,
            child: Text(e),
          );
        }).toList(),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: _add,
      ),
    );
  }

  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
    _pageController.jumpToPage(index);
  }

  void _add() {}
}
复制代码

效果如下:

抽屉菜单 Drawer

Scaffold 的 drawer 和 endDrawer 属性分别可以接受一个 Widget 来作为 左,右抽屉菜单。上面的实例中也使用了左抽屉菜单,下面修改一下:

代码语言:javascript复制
class DrawerTest extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return  Drawer(
      child: MediaQuery.removePadding(
        context: context,
        //移除抽屉菜单顶部,默认留白
        removeTop: true,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Padding(
              padding: const EdgeInsets.only(top: 38),
              child: Row(
                children: [
                  Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 16),
                    child: ClipOval(
                      child: Image.asset(
                        "images/icon.png",
                        width: 80,
                      ),
                    ),
                  ),
                  Text(
                    "文档",
                    style: TextStyle(fontWeight: FontWeight.bold),
                  )
                ],
              ),
            ),
            Expanded(
              child: ListView(
                children: [
                  ListTile(
                    leading: const Icon(Icons.add),
                    title: const Text('Add account'),
                  ),
                  ListTile(
                    leading: const Icon(Icons.settings),
                    title: const Text('Manage accounts'),
                  ),
                ],
              ),
            )
          ],
        ),
      ),
    );
  }
}
复制代码
代码语言:javascript复制
drawer: Drawer(
  child: DrawerTest(),
),
复制代码

最终效果如下:

抽屉菜单通常将 Drawer 组件作为根节点,他是限额了 Materal 风格的菜单面板,MediaQuery.removePadding 可以移除 Drawer 默认的一些留白

底部 Tab 导航栏

我们可以通过 Scaffold 的 BottomNavigationBar 属性来设置底部导航,如上面的示例,我们通过 Material 组件提供的 BottomNavigationBarBottomNavigationBarItem 来实现底部导航栏,代码也非常简单

但是如果要实现一些特殊的效果要怎么做呢,示例:

代码语言:javascript复制
bottomNavigationBar: BottomAppBar(
        color: Colors.white,
        shape: CircularNotchedRectangle(),
        child: Row(
          children: [
            IconButton(
              icon: Icon(Icons.home),
              onPressed: () => _pageController.jumpToPage(0),
            ),
            SizedBox(), //中间位置空出
            IconButton(
              icon: Icon(Icons.business),
              onPressed: () => _pageController.jumpToPage(2),
            )
          ],
          //均分底部导航栏的空间
          mainAxisAlignment: MainAxisAlignment.spaceAround,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: () => _pageController.jumpToPage(1),
      ),
      floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
复制代码

效果如下:

可以看到,上面的代码中没有打孔位置的属性,实际上,打孔位置取决于 FloatingActionButton 的位置,上面的位置为 FloatingActionButtonLocation.centerDocked ,所以打孔的位置在底部导航栏的正中间

BottomAppBar 的 shape 属性决定洞的外形,CircularNotchedRectangle 实现了一个圆形的外形,我们也可以进行自定义;

剪裁

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

裁剪 Widget

作用

ClipOval

子组件为正方形时剪裁为内贴圆形,为矩形时,裁切Wie内贴椭圆

ClipRRect

将子组件剪裁为圆角矩形

ClipRect

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

栗子

代码语言:javascript复制
Widget avatar = Image.asset("images/avatar.jpg", width: 80,);
复制代码
代码语言:javascript复制
Scaffold(
  appBar: AppBar(
    title: Text("剪裁"),
  ),
  body: Center(
    child: Column(
      children: [
        //原图
        avatar,
        //剪裁为圆形
        ClipOval(
          child: avatar,
        ),
        //剪裁为圆角矩形
        ClipRRect(
          borderRadius: BorderRadius.circular(5),
          child: avatar,
        ),
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            //Align:调整子组件的位置,
            Align(
              alignment: Alignment.topLeft,
              widthFactor: .5,//自身的 = 子组件 x widthFactor
              child: avatar,
            ),
            Text(
              "Hello World",
              style: TextStyle(color: Colors.green),
            )
          ],
        ),
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // 将溢出的部分剪裁
            ClipRect(
              child: Align(
                alignment: Alignment.topLeft,
                widthFactor: .5,
                child: avatar,
              ),
            ),
            Text(
              "Hello World",
              style: TextStyle(color: Colors.green),
            )
          ],
        )
      ],
    ),
  ),
);
复制代码

需要注意的是 Align 的 widthFactor 为 0.5 之后,图片的实际宽度等于 0.5 *80 ,即宽度的一半

CustomClippear

如果我们想要剪裁子组件的特定区域,比如,在上面示例的图片中,如果只想截取图片中 40 * 30 向上范围应该怎么做?

这个时候可以使用 CustomClipper 来自定义剪裁的区域,如下

1,自定义 CustomClipper:

代码语言:javascript复制
class MyClipper extends CustomClipper<Rect> {
  @override
  Rect getClip(Size size) {
    return Rect.fromLTWH(10, 15, 40, 30);
  }

  @override
  bool shouldReclip(covariant CustomClipper<dynamic> oldClipper) => false;

}
复制代码
  • getClip 获取剪裁区域的方法, 图片大小为 80*80,我们返回的区域为 Rect.fromLTWH(10, 15, 40, 30) , 即图片中 40 * 30 像素的范围
  • shouldReclip 是否重新剪裁,如果在应用中,剪裁区域始终不会发生变化时应该返回 false,这样就不会触发重新裁切,避免不必要的开销。如果会发生变化(比如在剪裁区域执行一个动画),那么就变化后应该返回 true 来重新执行裁切

2,通过 ClipRect 来执行剪裁

代码语言:javascript复制
DecoratedBox(
  decoration: BoxDecoration(color: Colors.red),
  child: ClipRect(
    child: avatar,
    clipper: MyClipper(),
  ),
)
复制代码

效果如上所示,可以看到是剪裁成功了,但是图片所占用的大小任然是 80 * 80 的,这是因为剪裁是在 layout 完成后的绘制阶段进行的,所以不会影响 组件的大小,这个 Transform 原理是相似的。

参考自 Flutter 实战

0 人点赞