Flutter容器类组件

2022-03-24 17:09:55 浏览数 (1)

Flutter容器类组件

容器类Widget与布局类Widget都用作用户界面设计,两者的不同在于:

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

⚠️注意, Flutter官方并没有对Widget进行如此划分。中文版《Flutter实战》对其分类主要是方便讨论和对Widget功能区分记忆。

1.填充(Paddinig)
1.1 Padding介绍

Padding组件在Android、IOS端只是一个属性,但在Flutter中Padding是一个独立的Widget。

Padding通常用于设置子Widget到父Widget的边距,这部分边距可以称为父组件的内边距,或者子Widget的外边距。

⚠️注意:在Flutter中不存在名为Margin的Widget,因为内外边距也可以通过Padding来完成。

源码分析:

代码语言:txt复制
const Padding({
  Key key,
  @required this.padding, // EdgeInsetsGeometry类型(抽象类),使用EdgeInsets
  Widget child,
})

其中的属性this.padding要求传入抽象类EdgeInsetsGeometry,常用其子类EdgeInsets。

1.2 EdgeInsets类

EdgeInsets提供了以下便携方法:

  • fromLTRB(double left, double top, double right, double bottom):分别指定四个方向的填充。
  • all(double value): 所有方向均使用相同数值的填充。
  • only({left, top, right ,bottom }):可以设置具体某个方向的填充(可以同时指定多个方向)。
  • symmetric({ vertical, horizontal }):用于设置对称方向的填充,vertical指top和bottom,horizontal指left和right。
1.3 Padding演练
代码语言:java复制
class PaddingDemo extends StatelessWidget {
  const PaddingDemo({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
      	 // Padding Widget
        Padding(
	      padding: EdgeInsets.all(8),
	      child: Text("莫听穿林打叶声,何妨吟啸且徐行,竹杖芒鞋轻胜马,谁怕?一蓑烟雨任平生!"),
	     ),
	     
	     // 普通Widget
        Text("莫听穿林打叶声,何妨吟啸且徐行,竹杖芒鞋轻胜马,谁怕?一蓑烟雨任平生!"),
      ],
    );
  }
}

效果图如下:

<center>

undefined

</center>

2.装饰容器(DecoratedBox)
2.1 DecoratedBox介绍

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

DecoratedBox的定义如下:

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

其中的属性this.decoration要求传入抽象类Decoration,常用其子类BoxDecoration。

2.2 BoxDecoration类介绍

BoxDecoration的定义如下:

代码语言:txt复制
const BoxDecoration({
  Color color, //颜色
  DecorationImage image,//图片
  BoxBorder border, //边框
  BorderRadiusGeometry borderRadius, //圆角
  List<BoxShadow> boxShadow, //阴影,可以指定多个
  Gradient gradient, //渐变
  BlendMode backgroundBlendMode, //背景混合模式
  BoxShape shape = BoxShape.rectangle, //形状
})
2.3 DecoratedBox演练
代码语言:txt复制
class DecoratedBoxDemo extends StatelessWidget {
  const DecoratedBoxDemo({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: DecoratedBox(
        decoration: BoxDecoration(
          // 线性颜色渐变
          gradient: LinearGradient(
            colors: [Colors.blue, Colors.lightBlueAccent],
          ),
          
          // 圆角
          borderRadius: BorderRadius.circular(3.0),
          
          // 阴影
          boxShadow: [
            BoxShadow(
              color: Colors.black54,
              offset: Offset(2.0, 2.0),
              blurRadius: 4.0
            )
          ]
        ),
        
        // DecoratedBox的子Widget
        child: Padding(
          padding: EdgeInsets.symmetric(horizontal: 80, vertical: 18),
          child: Text(
            "装饰测试",
            style: TextStyle(color: Colors.white),
          ),
        ),
      ),
    );
  }
}

效果图如下:

<center>

undefined

</center>

3.变换(Transform)

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

3.1 Transform - 倾斜
代码语言:txt复制
class HomeBody extends StatelessWidget {
  const HomeBody({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: DecoratedBox(
        decoration: BoxDecoration(color: Colors.red),
        child: Transform(
          alignment: Alignment.topRight,
          transform: Matrix4.skewY(0.3),
          child: Text("Transform倾斜示例"),
        ),
      ),
    );
  }
}

效果图如下:

<center>

undefined

</center>

3.2 Transform - 平移

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

代码语言:txt复制
class HomeBody extends StatelessWidget {
  const HomeBody({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: DecoratedBox(
        decoration:BoxDecoration(color: Colors.red),

        //默认原点为左上角,左移20像素,向上平移5像素
        child: Transform.translate(
          offset: Offset(-20.0, -5.0),
          child: Text("Transform平移"),
        ),
      ),
    );
  }
}

效果图如下:

<center>

undefined

</center>

3.3 Transform - 旋转

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

代码语言:txt复制
class HomeBody extends StatelessWidget {
  const HomeBody({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: DecoratedBox(
        decoration: BoxDecoration(color: Colors.red),
        child: Transform.rotate(
          angle: math.pi, // 旋转180度
          child: Text("Transform旋转"),
        )
      ),
    );
  }
}

效果图如下:

<center>

undefined

</center>

3.4 Transform - 缩放

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

代码语言:txt复制
class HomeBody extends StatelessWidget {
  const HomeBody({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: DecoratedBox(
          decoration: BoxDecoration(color: Colors.red),
          child: Transform.scale(
            scale: 1.5,
            child: Text("Transform缩放"),
          )
      ),
    );;
  }
}

效果图如下:

<center>

undefined

</center>

3.5 Transform注意事项
  • Transform的变换是应用在绘制阶段,而并不是应用在布局(layout)阶段,所以无论对子组件应用何种变化,其占用空间的大小和在屏幕上的位置都是固定不变的,因为这些是在布局阶段就确定的。
代码语言:txt复制
class HomeBody extends StatelessWidget {
  const HomeBody({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.center,
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          DecoratedBox(
            decoration: BoxDecoration(color: Colors.red),
            child: Transform.scale(scale: 1.5, child: Text("绘制阶段")),
          ),
          Text("紧靠重叠", style: TextStyle(color: Colors.green))
        ],
      ),
    );
  }
}

效果图如下:

<center>

undefined

</center>

由于第一个Text应用变换(放大)后,其在绘制时会放大,但其占用的空间依然为红色部分,所以第二个Text会紧挨着红色部分,最终就会出现文字重合。

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

RotatedBoxTransform.rotate功能相似,它们都可以对子组件进行旋转变换,但是有一点不同:RotatedBox的变换是在layout阶段,会影响在子组件的位置和大小。我们将上面介绍Transform.rotate时的示例改一下:

代码语言:txt复制
class HomeBody extends StatelessWidget {
  const HomeBody({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Center(
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // 第一个组件使用 RotatedBox进行旋转
            DecoratedBox(
              decoration: BoxDecoration(color: Colors.red),
              child: RotatedBox(
                quarterTurns: 2,
                child: Text("RotatedBox示例"),
              ),
            ),
            
            // 第二个组件紧邻左组件
            Text("紧靠", style: TextStyle(color: Colors.green))
          ]
      )
    );
  }
}

效果图如下:

<center>

undefined

</center>

由于RotatedBox是作用于layout阶段,所以子组件会旋转180度(而不只是绘制的内容),decoration会作用到子组件所占用的实际空间上,所以最终就是上图的效果。该示例可以和前面Transform.rotate示例对比理解。

4.Container(组合类容器)
4.1 Container介绍

Container是一个组合类容器,它是DecoratedBoxConstrainedBoxTransformPaddingAlign等组件组合的一个多功能容器,所以我们只需通过一个Container组件可以实现同时需要装饰、变换、限制的场景。下面是Container的定义:

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

大多数属性在介绍其它容器时都已经介绍过了,不再赘述,但有两点需要说明:

  • 容器的大小可以通过width、height属性来指定,也可以通过constraints来指定,如果同时存在时,width、height优先。实际上Container内部会根据width、height来生成一个constraints。
  • color和decoration是互斥的,实际上,当指定color时,Container内会自动创建一个decoration。
4.2 Container演练
代码语言:txt复制
class HomeBody extends StatelessWidget {
  const HomeBody({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        decoration: BoxDecoration(
          gradient: LinearGradient(
            colors: [Colors.blueAccent, Colors.lightBlueAccent],
          ),
          boxShadow: [BoxShadow(
            color: Colors.black54,
            offset: Offset(2.0, 2.0),
            blurRadius: 4.0
          )]
        ),
        transform: Matrix4.rotationZ(0.4),
        width: 100,
        height: 100,
        child: Icon(Icons.pets, size: 32, color: Colors.white,),
      ),
    );
  }
}

效果图如下:

<center>

undefined

</center>

4.3 Padding和Margin介绍

padding与margin都是常用的设置空间的属性,下面用代码演示区分二者的区别。

代码语言:txt复制
class HomeBody extends StatelessWidget {
  const HomeBody({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Container(
            margin: EdgeInsets.all(20),
            color: Colors.blue,
            child: Text("Margin and Padding"),
          ),
          Container(
            padding: EdgeInsets.all(20),
            color: Colors.blue,
            child: Text("Margin and Padding"),
          )
        ],
      ),
    );
  }
}

效果图如下:

<center>

undefined

</center>

直观的感觉就是margin的留白是在容器外部,而padding的留白是在容器内部,读者需要记住这个差异。事实上,Container内margin和padding都是通过Padding 组件来实现的,上面的示例代码实际上等价于:

代码语言:txt复制
class HomeBody extends StatelessWidget {
  const HomeBody({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Padding(padding: EdgeInsets.all(20),
            child: DecoratedBox(
              decoration: BoxDecoration(color: Colors.blue),
              child: Text("Margin and Padding"),
            ),
          ),
          DecoratedBox(
            decoration: BoxDecoration(color: Colors.blue),
            child: Padding(
              padding: EdgeInsets.all(20),
              child: Text("Margin and Padding"),
            ),
          )
        ],
      ),
    );
  }
}
5. Scaffold(脚手架)
5.1 Scaffold介绍

一个完整的路由页可能会包含导航栏、抽屉菜单(Drawer)以及底部 Tab 导航菜单等。Flutter Material 组件库提供了一些现成的组件来减少我们的开发任务。Scaffold,中文称之为脚手架,为开发者提供了路由页面的整体架构,开发者可以借助它快速便携地实现一个完整的页面。

下面构造一个完整的路由页面对其进行讲解:

  1. 导航栏
  2. 导航栏右侧分享按钮
  3. 抽屉菜单
  4. 底部导航栏
  5. 悬浮按钮
代码语言:txt复制
class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

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

class _HomePageState extends State<HomePage> {
  int _selectedIndex = 1;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Scaffold示例'),
        actions: [
          IconButton(
            onPressed: () {
              print("分享按钮被点击");
            },
            icon: Icon(Icons.share)
          )
        ],
      ),

      drawer: null, // 后面会对drawer进行讲解

      bottomNavigationBar: BottomNavigationBar(
        backgroundColor: Colors.white,
        unselectedItemColor: Colors.grey,
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: "首页", activeIcon: Icon(Icons.home, color: Colors.deepOrange)),//, activeIcon: Icon(Icons.home, color: Colors.deepOrange,)),
          BottomNavigationBarItem(icon: Icon(Icons.search), label: "分类", activeIcon: Icon(Icons.search, color: Colors.deepOrange)),
          BottomNavigationBarItem(icon: Icon(Icons.shopping_cart), label: "购物车", activeIcon: Icon(Icons.shopping_cart, color: Colors.deepOrange)),
          BottomNavigationBarItem(icon: Icon(Icons.perm_identity), label: "我", activeIcon: Icon(Icons.perm_identity, color: Colors.deepOrange)),
        ],
        currentIndex: _selectedIndex,
        fixedColor: Colors.deepOrange,
        onTap: (index) {
          setState(() {
            _selectedIndex = index;
          });
        },
      ),
      body: Text("Scaffold示例"),

      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: () {
          print("浮窗按钮被点击");
        },
      ),
    );
  }
}

效果图如下:

<center>

undefined

</center>

在上面这段代码中,用到了以下组件:

<center>

组件名称

解释

appBar

导航栏

drawer

抽屉菜单

bottomNavigationBar

底部导航栏

floatingActionButton

浮动按钮

</center>

5.2 AppBar

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

代码语言:txt复制
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属性。如:

代码语言:txt复制
appBar: AppBar(
        title: Text('Scaffold示例'),
        leading: Builder(builder: (context) {
          return IconButton(
            onPressed: (){
              // 调用父类打开抽屉
              Scaffold.of(context).openDrawer();
            },
            icon: Icon(Icons.dashboard, color:Colors.orange));
        }),
        actions: [
          IconButton(
            onPressed: () {
              print("分享按钮被点击");
            },
            icon: Icon(Icons.share)
          )
        ],
      ),

效果图如下:

<center>

undefined

</center>

代码中打开抽屉菜单的方法在ScaffoldState中,通过Scaffold.of(context)可以获取父级最近的Scaffold 组件的State对象。

5.3 Drawer介绍

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

代码语言:txt复制
drawer: Drawer(
 child: Column(
   children: [
     Padding(padding: EdgeInsets.only(left: 20, top: 100),
       child: Row(
         children: [
           Container(
             height:100,
             width: 100,
             child: Image.asset("assets/images/room.jpg"),
           ),
           SizedBox(width: 10),
           Text("leiyangyuan", style: TextStyle(fontWeight: FontWeight.bold))
         ],
       ),
     ),
   ],
 ),
),

效果图如下:

<center>

undefined

</center>

5.4 FloatingActionButton介绍

FloatingActionButton是Material设计规范中的一种特殊Button,通常悬浮在页面的某一个位置作为某种常用动作的快捷入口,如首页示例中页面右下角的"➕"号按钮。我们可以通过Scaffold的floatingActionButton属性来设置一个FloatingActionButton,同时通过floatingActionButtonLocation属性来指定其在页面中悬浮的位置。

5.5 BottomNavigationBar介绍

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

下面这部分代码可以绘出一个美观的底部导航栏:

代码语言:txt复制
  bottomNavigationBar: BottomAppBar(
    color: Colors.white,
    shape: CircularNotchedRectangle(), // 导航栏底部打圆形孔
    child: Row(
      children: [
        IconButton(onPressed: (){

        },icon: Icon(Icons.home)),
        SizedBox(),
        IconButton(onPressed: () {

        }, icon: Icon(Icons.business))
      ],
      mainAxisAlignment: MainAxisAlignment.spaceAround,
    ),
  ),

  floatingActionButton: FloatingActionButton(
    child: Icon(Icons.add),
    onPressed: () {
      print("浮窗按钮被点击");
    },
  ),
  floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,

上面的代码通过将底部导航栏挖出一个圆形孔,然后将浮动按钮至于底部导航栏中间,以达到下面的效果:

<center>

undefined

</center>

5.6 页面body

最后就是页面的 Body 部分了,Scaffold 有一个 body 属性,接收一个 Widget,我们可以传任意的 Widget ,在后面介绍滑动组件时,会涉及到 TabBarView,它是一个可以进行页面切换的组件,在多 Tab 的 App 中,一般都会将 TabBarView 作为 Scaffold 的 Body。

undefined

undefined

undefined

undefined

undefined

undefined

undefined

undefined

undefined

undefined

undefined

undefined

undefined

undefined

0 人点赞