06-📝Flutter核心知识|布局与滚动组件【单子布局、多子布局、JSON读取和解析、ListView、GridView、Slivers、监听滚动事件】

6-📝Flutter核心知识|布局与滚动组件【单子布局、多子布局、JSON读取和解析、ListView、GridView、Slivers、监听滚动事件】"

一、前言

本系列文章旨在快速复习并上手Flutter开发,并在适当分享在项目实战过程中遇到的一些比较有价值的知识内容:

本系列文章内容篇幅如下:

  • 一、了解Flutter开发
      1. Flutter的特性与应用场景
      1. Flutter绘制原理
      1. 与Flutter相关的技术原理
      1. 搭建Flutter开发环境
      1. 创建Flutter项目的几种方式
  • 二、快速入门Flutter开发知识大纲
      1. Dart语言快速入门
      1. Flutter的Widget
  • 三、常见应用功能模块与开源项目
      1. 常见应用功能模块
      1. 不错的开源项目

二、单子布局组件

单子布局组件的含义是其只有一个子组件,可以通过设置一些属性设置该子组件所在的位置信息等。

比较常用的单子布局组件有:Align、Center、Padding、Container。

1. Align组件

1.1. Align介绍

看到Align这个词,我们就知道它有我们的对齐方式有关。

  • 在其他端的开发中(iOS、Android、前端)Align通常只是一个属性而已
  • 但是Flutter中Align也是一个组件

我们可以通过源码来看一下Align有哪些属性:

const Align({
  Key key,
  this.alignment: Alignment.center, // 对齐方式,默认居中对齐
  this.widthFactor, // 宽度因子,不设置的情况,会尽可能大
  this.heightFactor, // 高度因子,不设置的情况,会尽可能大
  Widget child // 要布局的子Widget
})

这里我们特别解释一下widthFactorheightFactor作用:

  • 因为子组件在父组件中的对齐方式必须有一个前提,就是父组件得知道自己的范围(宽度和高度);
  • 如果widthFactorheightFactor不设置,那么默认Align会尽可能的大(尽可能占据自己所在的父组件);
  • 我们也可以对他们进行设置,比如widthFactor设置为3,那么相对于Align的宽度是子组件跨度的3倍;

1.2. Align演练

我们简单演练一下Align

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      showSemanticsDebugger: false,
      home: HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return  Scaffold(
      appBar: AppBar(
        title: Text("我的导航栏"),
        backgroundColor: Colors.green,
      ),
      body: HomePageBody(),
    );
  }
}

class HomePageBody extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return HomePageAlign();
  }
}

// 单子布局组件
class HomePageAlign extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Align(
      child: Icon(Icons.pets, size: 36, color: Colors.red),
      alignment: Alignment.bottomRight,
      widthFactor: 3,
      heightFactor: 3,
    );
  }
}

6-📝Flutter核心知识|布局与滚动组件【单子布局、多子布局、JSON读取和解析、ListView、GridView、Slivers、监听滚动事件】"

2. Center组件

2.1. Center介绍

Center组件我们在前面已经用过很多次了。

  • 事实上Center组件继承自Align,只是将alignment设置为Alignment.center

源码分析:

class Center extends Align {
  const Center({ 
    Key key, 
    double widthFactor, 
    double heightFactor, 
    Widget child 
  }) : super(key: key, widthFactor: widthFactor, heightFactor: heightFactor, child: child);
}

2.2. Center演练

我们将上面的代码Align换成Center

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      showSemanticsDebugger: false,
      home: HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return  Scaffold(
      appBar: AppBar(
        title: Text("我的导航栏"),
        backgroundColor: Colors.green,
      ),
      body: HomePageBody(),
    );
  }
}

class HomePageBody extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return HomePageCenter();
  }
}


// 表单TextField
class HomePageCenter extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Center(
      child: Icon(Icons.pets, size: 36, color: Colors.red),
      widthFactor: 20,
      heightFactor: 20,
    );
  }
}

6-📝Flutter核心知识|布局与滚动组件【单子布局、多子布局、JSON读取和解析、ListView、GridView、Slivers、监听滚动事件】"

3. Padding组件

3.1. Padding介绍

  • Padding组件在其他端也是一个属性而已,但是在Flutter中是一个Widget,
    • Flutter中没有Margin这样一个Widget,这是因为外边距也可以通过Padding来完成。
  • Padding通常用于设置子Widget到父Widget的边距(你可以称之为是父组件的内边距或子Widget的外边距)。

源码分析:

const Padding({
  Key key,
  @required this.padding, // EdgeInsetsGeometry类型(抽象类),使用EdgeInsets
  Widget child,
})

3.2. Padding演练

代码演练:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      showSemanticsDebugger: false,
      home: HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return  Scaffold(
      appBar: AppBar(
        title: Text("我的导航栏"),
        backgroundColor: Colors.green,
      ),
      body: HomePageBody(),
    );
  }
}

class HomePageBody extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return HomePagePadding();
  }
}


// 单子布局HomePagePadding
class HomePagePadding extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.all(20),
      child: Text(
        "莫听穿林打叶声,何妨吟啸且徐行。竹杖芒鞋轻胜马,谁怕?一蓑烟雨任平生。",
        style: TextStyle(
            color: Colors.redAccent,
            fontSize: 18
        ),
      ),
    );;
  }
}

6-📝Flutter核心知识|布局与滚动组件【单子布局、多子布局、JSON读取和解析、ListView、GridView、Slivers、监听滚动事件】"

4. Container组件

  • Container组件类似于其他Android中的View,iOS中的UIView
  • 如果你需要一个视图,有一个背景颜色图像有固定的尺寸需要一个边框圆角等效果,那么就可以使用Container组件

4.1. Container介绍

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,
})

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

  • 容器的大小可以通过widthheight属性来指定,也可以通过constraints来指定,如果同时存在时,widthheight优先。实际上Container内部会根据widthheight来生成一个constraints
  • colordecoration是互斥的,实际上,当指定color时,Container内会自动创建一个decoration;
  • decoration属性稍后我们详细学习;

4.2. Container演练

简单进行一个演示:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      showSemanticsDebugger: false,
      home: HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return  Scaffold(
      appBar: AppBar(
        title: Text("我的导航栏"),
        backgroundColor: Colors.green,
      ),
      body: HomePageBody(),
    );
  }
}

class HomePageBody extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return HomePageContainer();
  }
}


// 单子布局HomePagePadding
class HomePageContainer extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        color: Color.fromRGBO(3, 3, 255, .5),
        width: 100,
        height: 100,
        child: Icon(Icons.pets, size: 32, color: Colors.white),
      ),
    );
  }
}

6-📝Flutter核心知识|布局与滚动组件【单子布局、多子布局、JSON读取和解析、ListView、GridView、Slivers、监听滚动事件】"

4.3. BoxDecoration

Container有一个非常重要的属性 decoration

  • 他对应的类型是Decoration类型,但是它是一个抽象类。
  • 在开发中,我们经常使用它的实现类BoxDecoration来进行实例化。

BoxDecoration常见属性:

  const BoxDecoration({
    this.color, // 颜色,会和Container中的color属性冲突
    this.image, // 背景图片
    this.border, // 边框,对应类型是Border类型,里面每一个边框使用BorderSide
    this.borderRadius, // 圆角效果
    this.boxShadow, // 阴影效果
    this.gradient, // 渐变效果
    this.backgroundBlendMode, // 背景混合
    this.shape = BoxShape.rectangle, // 形变
  })

部分效果演示:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      showSemanticsDebugger: false,
      home: HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return  Scaffold(
      appBar: AppBar(
        title: Text("我的导航栏"),
        backgroundColor: Colors.green,
      ),
      body: HomePageBody(),
    );
  }
}

class HomePageBody extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return HomePageContainer();
  }
}


// 单子布局HomePagePadding
class HomePageContainer extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
//        color: Color.fromRGBO(3, 3, 255, .5),
        width: 150,
        height: 150,
        child: Icon(Icons.pets, size: 32, color: Colors.white),
        decoration: BoxDecoration(
            color: Colors.amber, // 背景颜色
            border: Border.all(
                color: Colors.redAccent,
                width: 3,
                style: BorderStyle.solid
            ), // 这里也可以使用Border.all统一设置
//            top: BorderSide(
//              color: Colors.redAccent,
//              width: 3,
//              style: BorderStyle.solid
//            ),
            borderRadius: BorderRadius.circular(20), // 这里也可以使用.only分别设置
            boxShadow: [
              BoxShadow(
                  offset: Offset(5, 5),
                  color: Colors.purple,
                  blurRadius: 5
              )
            ],
//          shape: BoxShape.circle, // 会和borderRadius冲突
            gradient: LinearGradient(
                colors: [
                  Colors.green,
                  Colors.red
                ]
            )
        ),
      ),
    );
  }
}

6-📝Flutter核心知识|布局与滚动组件【单子布局、多子布局、JSON读取和解析、ListView、GridView、Slivers、监听滚动事件】"

4.4. 实现圆角图像

上一个章节我们提到可以通过 Container+BoxDecoration来实现圆角图像。

实现代码如下:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      showSemanticsDebugger: false,
      home: HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return  Scaffold(
      appBar: AppBar(
        title: Text("我的导航栏"),
        backgroundColor: Colors.green,
      ),
      body: HomePageBody(),
    );
  }
}

class HomePageBody extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return HomePageContainer();
  }
}


// 单子布局HomePagePadding
class HomePageContainer extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        width: 200,
        height: 200,
        decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(20),
            image: DecorationImage(
              image: NetworkImage("https://tva1.sinaimg.cn/large/006y8mN6gy1g7aa03bmfpj3069069mx8.jpg"),
            )
        ),
      ),
    );
  }
}

6-📝Flutter核心知识|布局与滚动组件【单子布局、多子布局、JSON读取和解析、ListView、GridView、Slivers、监听滚动事件】"

三、多子布局组件

在开发中,我们经常需要将多个Widget放在一起进行布局,比如水平方向、垂直方向排列,甚至有时候需要他们进行层叠,比如图片上面放一段文字等;

这个时候我们需要使用多子布局组件(Multi-child layout widgets)。

比较常用的多子布局组件是Row、Column、Stack,我们来学习一下他们的使用。

1. Flex组件

事实上,我们即将学习的Row组件和Column组件都继承自Flex组件.

  • Flex组件和Row、Column属性主要的区别就是多一个direction。
  • 当direction的值为Axis.horizontal的时候,则是Row。
  • 当direction的值为Axis.vertical的时候,则是Column。

在学习Row和Column之前,我们先学习主轴交叉轴的概念。

因为Row是一行排布,Column是一列排布,那么它们都存在两个方向,并且两个Widget排列的方向应该是对立的。

它们之中都有主轴(MainAxis)和交叉轴(CrossAxis)的概念:

  • 对于Row来说,主轴(MainAxis)和交叉轴(CrossAxis)分别是下图
    6-📝Flutter核心知识|布局与滚动组件【单子布局、多子布局、JSON读取和解析、ListView、GridView、Slivers、监听滚动事件】"
  • 对于Column来说,主轴(MainAxis)和交叉轴(CrossAxis)分别是下图
    6-📝Flutter核心知识|布局与滚动组件【单子布局、多子布局、JSON读取和解析、ListView、GridView、Slivers、监听滚动事件】"

2. Row组件

2.1 Row介绍

Row组件用于将所有的子Widget排成一行,实际上这种布局应该是借鉴于Web的Flex布局。

如果熟悉Flex布局,会发现非常简单。

从源码中查看Row的属性:

Row({
  Key key,
  MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start, // 主轴对齐方式
  MainAxisSize mainAxisSize = MainAxisSize.max, // 水平方向尽可能大
  CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center, // 交叉处对齐方式
  TextDirection textDirection, // 水平方向子widget的布局顺序(默认为系统当前Locale环境的文本方向(如中文、英语都是从左往右,而阿拉伯语是从右往左))
  VerticalDirection verticalDirection = VerticalDirection.down, // 表示Row纵轴(垂直)的对齐方向
  TextBaseline textBaseline, // 如果上面是baseline对齐方式,那么选择什么模式(有两种可选)
  List<Widget> children = const <Widget>[],
}) 

部分属性详细解析:(不过文字是真的难描述,后续推出视频学习较差)

mainAxisSize

  • 表示Row在主轴(水平)方向占用的空间,默认是MainAxisSize.max,表示尽可能多的占用水平方向的空间,此时无论子widgets实际占用多少水平空间,Row的宽度始终等于水平方向的最大宽度
  • MainAxisSize.min表示尽可能少的占用水平空间,当子widgets没有占满水平剩余空间,则Row的实际宽度等于所有子widgets占用的的水平空间;

mainAxisAlignment:表示子Widgets在Row所占用的水平空间内对齐方式

  • 如果mainAxisSize值为MainAxisSize.min,则此属性无意义,因为子widgets的宽度等于Row的宽度
  • 只有当mainAxisSize的值为MainAxisSize.max时,此属性才有意义
  • MainAxisAlignment.start表示沿textDirection的初始方向对齐,
  • 如textDirection取值为TextDirection.ltr时,则MainAxisAlignment.start表示左对齐,textDirection取值为TextDirection.rtl时表示从右对齐。
  • MainAxisAlignment.endMainAxisAlignment.start正好相反;
  • MainAxisAlignment.center表示居中对齐。

crossAxisAlignment:表示子Widgets在纵轴方向的对齐方式

  • Row的高度等于子Widgets中最高的子元素高度
  • 它的取值和MainAxisAlignment一样(包含startendcenter三个值)
  • 不同的是crossAxisAlignment的参考系是verticalDirection,即verticalDirection值为VerticalDirection.downcrossAxisAlignment.start指顶部对齐,verticalDirection值为VerticalDirection.up时,crossAxisAlignment.start指底部对齐;而crossAxisAlignment.endcrossAxisAlignment.start正好相反;

2.2 Row演练

我们来对部分属性进行简单的代码演练,其他一些属性大家自己学习一下

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      showSemanticsDebugger: false,
      home: HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return  Scaffold(
      appBar: AppBar(
        title: Text("我的导航栏"),
        backgroundColor: Colors.green,
      ),
      body: HomePageBody(),
    );
  }
}

class HomePageBody extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return HomePageRow();
  }
}


// 单子布局HomePageRow
class HomePageRow extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child:Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,//子控件间的间隔均匀分布
        crossAxisAlignment: CrossAxisAlignment.end,// Row的 交叉轴(也就是竖轴) 的结束那条线(也就是底部)对齐
        mainAxisSize: MainAxisSize.max,//间隔尽可能大
        children: <Widget>[
          Container(color: Colors.red, width: 60, height: 60),
          Container(color: Colors.blue, width: 80, height: 80),
          Container(color: Colors.green, width: 70, height: 70),
          Container(color: Colors.orange, width: 100, height: 100),
        ],
      ),
    );
  }
}

6-📝Flutter核心知识|布局与滚动组件【单子布局、多子布局、JSON读取和解析、ListView、GridView、Slivers、监听滚动事件】"

2.3 mainAxisSize

默认情况下,Row会尽可能占据多的宽度,让子Widget在其中进行排布,这是因为mainAxisSize属性默认值是MainAxisSize.max

我们来看一下,如果这个值被修改为MainAxisSize.min会什么变化:

6-📝Flutter核心知识|布局与滚动组件【单子布局、多子布局、JSON读取和解析、ListView、GridView、Slivers、监听滚动事件】"

2.4 TextBaseline

关于TextBaseline的取值解析

6-📝Flutter核心知识|布局与滚动组件【单子布局、多子布局、JSON读取和解析、ListView、GridView、Slivers、监听滚动事件】"

2.5 Expanded

如果我们希望红色和黄色的Container Widget不要设置固定的宽度,而是占据剩余的部分,这个时候应该如何处理呢?

这个时候我们可以使用 Expanded 来包裹 Container Widget,并且将它的宽度不设置值;

  • flex属性,弹性系数,Row会根据两个Expanded的弹性系数来决定它们占据剩下空间的比例
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      showSemanticsDebugger: false,
      home: HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return  Scaffold(
      appBar: AppBar(
        title: Text("我的导航栏"),
        backgroundColor: Colors.green,
      ),
      body: HomePageBody(),
    );
  }
}

class HomePageBody extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return HomePageRow();
  }
}


// // 多子布局HomePageRow
// class HomePageRow extends StatelessWidget {
//   @override
//   Widget build(BuildContext context) {
//     return Center(
//       child:Row(
//         mainAxisAlignment: MainAxisAlignment.spaceEvenly,//子控件间的间隔均匀分布
//         crossAxisAlignment: CrossAxisAlignment.end,// Row的 交叉轴(也就是竖轴) 的结束那条线(也就是底部)对齐
//         mainAxisSize: MainAxisSize.max,//间隔尽可能大/小
//         children: <Widget>[
//           Container(color: Colors.red, width: 60, height: 60),
//           Container(color: Colors.blue, width: 80, height: 80),
//           Container(color: Colors.green, width: 70, height: 70),
//           Container(color: Colors.orange, width: 100, height: 100),
//         ],
//       ),
//     );
//   }
// }

// 多子布局HomePageRow+Expanded
class HomePageRow extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child:Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        crossAxisAlignment: CrossAxisAlignment.end,
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          Expanded(
            flex: 1,
            child: Container(color: Colors.red, height: 60),
          ),
          Container(color: Colors.blue, width: 80, height: 80),
          Container(color: Colors.green, width: 70, height: 70),
          Expanded(
            flex: 1,
            child: Container(color: Colors.orange, height: 100),
          )
        ],
      ),
    );
  }
}

6-📝Flutter核心知识|布局与滚动组件【单子布局、多子布局、JSON读取和解析、ListView、GridView、Slivers、监听滚动事件】"

3. Column组件

Column组件用于将所有的子Widget排成一列,学会了前面的Row后,Column只是和row的方向不同而已。

3.1 Column介绍

我们直接看它的源码:我们发现和Row属性是一致的,不再解释

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

3.2 Column演练

我们直接将Row的代码中Row改为Column,查看代码运行效果:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      showSemanticsDebugger: false,
      home: HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return  Scaffold(
      appBar: AppBar(
        title: Text("我的导航栏"),
        backgroundColor: Colors.green,
      ),
      body: HomePageBody(),
    );
  }
}

class HomePageBody extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return HomePageColumn();
  }
}

// 多子布局HomePageColumn+Expanded
class HomePageColumn extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child:Column(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        crossAxisAlignment: CrossAxisAlignment.end,
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          Expanded(
            flex: 1,
            child: Container(color: Colors.red, width: 60),
          ),
          Container(color: Colors.blue, width: 80, height: 80),
          Container(color: Colors.green, width: 70, height: 70),
          Expanded(
            flex: 1,
            child: Container(color: Colors.orange, width: 100),
          )
        ],
      ),
    );
  }
}


6-📝Flutter核心知识|布局与滚动组件【单子布局、多子布局、JSON读取和解析、ListView、GridView、Slivers、监听滚动事件】"

4. Stack组件

在开发中,我们多个组件很有可能需要重叠显示,比如在一张图片上显示文字或者一个按钮等。

  • 在Android中可以使用Frame来实现
  • 在Web端可以使用绝对定位
  • 在Flutter中我们需要使用层叠布局Stack

4.1. Stack介绍

我们还是通过源码来看一下Stack有哪些属性:

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

参数解析:

  • alignment
    • 此参数决定如何去对齐没有定位(没有使用Positioned)或部分定位的子widget。
    • 所谓部分定位,在这里 特指没有在某一个轴上定位:
      • left、right为横轴
      • top、bottom为纵轴
      • 只要包含某个轴上的一个定位属性就算在该轴上有定位
  • textDirection:
    • 和Row、Wrap的textDirection功能一样,都用于决定alignment对齐的参考系即:
    • textDirection的值为TextDirection.ltr,则alignment的start代表左,end代表右;
    • textDirection的值为TextDirection.rtl,则alignment的start代表右,end代表左。
  • fit
    • 此参数用于决定没有定位的子widget如何去适应Stack的大小。
    • StackFit.loose表示使用子widget的大小,
    • StackFit.expand表示扩伸到Stack的大小。
  • overflow:
    • 此属性决定如何显示超出Stack显示空间的子widget,
    • 值为Overflow.clip时,超出部分会被剪裁(隐藏)
    • 值为Overflow.visible 时则不会。

4.2. Stack演练

Stack会经常和Positioned一起来使用,Positioned可以决定组件在Stack中的位置,用于实现类似于Web中的绝对定位效果。

我们来看一个简单的演练:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      showSemanticsDebugger: false,
      home: HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return  Scaffold(
      appBar: AppBar(
        title: Text("我的导航栏"),
        backgroundColor: Colors.green,
      ),
      body: HomePageBody(),
    );
  }
}

class HomePageBody extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return HomePageStack();
  }
}

// 多子布局HomePageStack
class HomePageStack extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child:Stack(
        children: <Widget>[
          Container(
            color: Colors.deepOrangeAccent,
            width: 300,
            height: 300,
          ),
          Positioned(
              left: 20,
              top: 20,
              child: Icon(Icons.favorite, size: 50, color: Colors.white)
          ),
          Positioned(
            bottom: 20,
            right: 20,
            child: Text("你好啊,李银河", style: TextStyle(fontSize: 20, color: Colors.white)),
          )
        ],
      ),
    );
  }
}

6-📝Flutter核心知识|布局与滚动组件【单子布局、多子布局、JSON读取和解析、ListView、GridView、Slivers、监听滚动事件】"

注意: Positioned组件只能在Stack中使用。

四、JSON读取和解析

在开发中,我们经常会使用本地JSON或者从服务器请求数据后回去到JSON,拿到JSON后通常会将JSON转成Model对象来进行后续的操作,因为这样操作更加的方便,也更加的安全。

所以学习JSON的相关操作以及读取JSON后如何转成Model对象对于Flutter开发也非常重要。

1. JSON资源配置

JSON也属于一种资源,所以在使用之前需要先进行相关的配置

我们之前在学习使用Image组件时,用到了本地图片,本地图片必须在pubspec.yaml中进行配置:

6-📝Flutter核心知识|布局与滚动组件【单子布局、多子布局、JSON读取和解析、ListView、GridView、Slivers、监听滚动事件】"

2. JSON读取解析

JSON资源读取

如果我们希望读取JSON资源,可以使用package:flutter/services.dart包中的rootBundle

rootBundle中有一个loadString方法,可以去加载JSON资源

  • 但是注意,查看该方法的源码,你会发现这个操作是一个异步的。
  • 关于Future和async,这里就不再展开讲解,可以去查看之前的dart语法。
Future<String> loadString(String key, { bool cache = true }) async {
  ...省略具体代码,可以自行查看源码
}

代码如下:(不要试图拷贝这个代码去运行,是没办法运行的)

import 'package:flutter/services.dart' show rootBundle;

// 打印读取的结果是一个字符串
rootBundle.loadString("assets/yz.json").then((value) => print(value));

JSON字符串转化

拿到JSON字符串后,我们需要将其转成成我们熟悉的List和Map类型。

我们可以通过dart:convert包中的json.decode方法将其进行转化

代码如下:

// 1.读取json文件
String jsonString = await rootBundle.loadString("assets/yz.json");

// 2.转成List或Map类型
final jsonResult = json.decode(jsonString);

对象Model定义

将JSON转成了List和Map类型后,就可以将List中的一个个Map转成Model对象,所以我们需要定义自己的Model

class Anchor {
  String nickname;
  String roomName;
  String imageUrl;

  Anchor({
    this.nickname,
    this.roomName,
    this.imageUrl
  });

  Anchor.withMap(Map<String, dynamic> parsedMap) {
    this.nickname = parsedMap["nickname"];
    this.roomName = parsedMap["roomName"];
    this.imageUrl = parsedMap["roomSrc"];
  }
}

3. JSON解析代码

上面我们给出了解析的一个个步骤,下面我们给出完整的代码逻辑

这里我单独创建了一个anchor.dart的文件,在其中定义了所有的相关代码:

  • 准备一个Mock数据
    [
      {
        "nickname" : "昵称1",
        "roomName" : "名字1",
        "roomSrc" : "链接1"
      },
      {
        "nickname" : "昵称2",
        "roomName" : "名字2",
        "roomSrc" : "链接2"
      },
      {
        "nickname" : "昵称3",
        "roomName" : "名字3",
        "roomSrc" : "链接3"
      }
    ] 
    
  • 之后外界只需要调用我内部的getAnchors就可以获取到解析后的数据了
    import 'package:flutter/cupertino.dart';
    import 'package:flutter/material.dart';
    import 'package:flutter/services.dart' show rootBundle;
    import 'dart:convert';
    import 'dart:async';
    import 'dart:core';
    
    main(List<String> args) {
      runApp(
         MaterialApp(
           showSemanticsDebugger: false,
           home: Scaffold(
             appBar: AppBar(
               title: Text("我的导航栏"),
             ),
             body: Center(
               child: Column(
                 children: <Widget>[
                   Text("Json 转 模型"),
                   MaterialButton(
                       color: Colors.blue,
                       textColor: Colors.white,
                       child: Text("执行转换"),
                       onPressed: (){
                         loadJsonData().then((List<Anchor> list){
                             final anchor = list.first;
                             print(anchor.nickname);
                             print(anchor.roomName);
                             print(anchor.roomSrc);
                             print("==========我是一条分割线===========");
                         });
                      }
                  ),
                 ],
               ),
             ),
           ),
         )
      );
    }
    
    class Anchor {
      String nickname = "";
      String roomName = "";
      String roomSrc = "";
    
      Anchor({ required this.nickname, required this.roomName,  required this.roomSrc});
    
      factory Anchor.fromJson(Map<String, dynamic> json) {
        return Anchor(
            nickname: json["nickname"],
            roomName: json["roomName"],
            roomSrc: json["roomSrc"]
        );
      }
    }
    Future<List<Anchor>> loadJsonData() async {
      // 1.读取json文件
      String jsonString = await rootBundle.loadString("assets/json/mock.json");
      // 类型检查
      // final temp =  json.decode(jsonString);
      // print('Type of temp: ${temp.runtimeType}');
      // 2.转成List或Map类型
      List<dynamic> jsonMap = json.decode(jsonString);
      // 3.遍历List,并且转成Anchor对象放到另一个List中
      List<Anchor> anchors = [];
      for (Map<String, dynamic> map in jsonMap) {
          anchors.add(Anchor.fromJson(map));
      }
      return anchors;
    } 
    

6-📝Flutter核心知识|布局与滚动组件【单子布局、多子布局、JSON读取和解析、ListView、GridView、Slivers、监听滚动事件】"

五、ListView组件

移动端数据量比较大时,我们都是通过列表来进行展示的,比如商品数据、聊天列表、通信录、朋友圈等。

在Android中,我们可以使用ListView或RecyclerView来实现,在iOS中,我们可以通过UITableView来实现。

在Flutter中,我们也有对应的列表Widget,就是ListView。

1. ListView基础

1.1 ListView基本使用

ListView可以沿一个方向(垂直或水平方向,默认是垂直方向)来排列其所有子Widget。
一种最简单的使用方式是直接将所有需要排列的子Widget放在ListView的children属性中即可。
我们来看一下直接使用ListView的代码演练:

  • 为了让文字之间有一些间距,我使用了Padding Widget
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'dart:convert';
import 'dart:async';
import 'dart:core';

main(List<String> args) {
  runApp(
     MaterialApp(
       showSemanticsDebugger: false,
       home: HomePage(),
     )
  );
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("我的导航栏"),
      ),
      body: HomePageBody()
    );
  }
}

class HomePageBody extends StatelessWidget {
   @override
  Widget build(BuildContext context) {
     return Center(
       child: HomePageListView()
     );
  }
}

class HomePageListView extends StatelessWidget {
  final TextStyle textStyle = TextStyle(fontSize: 20, color: Colors.redAccent);
   @override
  Widget build(BuildContext context) {
     return ListView(
       children: <Widget>[
         Padding(
           padding: const EdgeInsets.all(8.0),
           child: Text("人的一切痛苦,本质上都是对自己无能的愤怒。", style: textStyle),
         ),
         Padding(
           padding: const EdgeInsets.all(8.0),
           child: Text("人活在世界上,不可以有偏差;而且多少要费点劲儿,才能把自己保持到理性的轨道上。", style: textStyle),
         ),
         Padding(
           padding: const EdgeInsets.all(8.0),
           child: Text("我活在世上,无非想要明白些道理,遇见些有趣的事。", style: textStyle),
         )
       ],
     );
  }
}

6-📝Flutter核心知识|布局与滚动组件【单子布局、多子布局、JSON读取和解析、ListView、GridView、Slivers、监听滚动事件】"

1.2 ListTile的使用

在开发中,我们经常见到一种列表,有一个图标或图片(Icon),有一个标题(Title),有一个子标题(Subtitle),还有尾部一个图标(Icon)。

这个时候,我们可以使用ListTile来实现:

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'dart:convert';
import 'dart:async';
import 'dart:core';

main(List<String> args) {
  runApp(
     MaterialApp(
       showSemanticsDebugger: false,
       home: HomePage(),
     )
  );
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("我的导航栏"),
        backgroundColor: Colors.green,
      ),
      body: HomePageBody()
    );
  }
}

class HomePageBody extends StatelessWidget {
   @override
  Widget build(BuildContext context) {
     return Center(
       child: HomePageListView()
     );
  }
}

class HomePageListView extends StatelessWidget {
  final TextStyle textStyle = TextStyle(fontSize: 20, color: Colors.redAccent);
   @override
  Widget build(BuildContext context) {
     return ListView(
       children: <Widget>[
         ListTile(
           leading: Icon(Icons.people, size: 36,),
           title: Text("联系人"),
           subtitle: Text("联系人信息"),
           trailing: Icon(Icons.arrow_forward_ios),
         ),
         ListTile(
           leading: Icon(Icons.email, size: 36,),
           title: Text("邮箱"),
           subtitle: Text("邮箱地址信息"),
           trailing: Icon(Icons.arrow_forward_ios),
         ),
         ListTile(
           leading: Icon(Icons.message, size: 36,),
           title: Text("消息"),
           subtitle: Text("消息详情信息"),
           trailing: Icon(Icons.arrow_forward_ios),
         ),
         ListTile(
           leading: Icon(Icons.map, size: 36,),
           title: Text("地址"),
           subtitle: Text("地址详情信息"),
           trailing: Icon(Icons.arrow_forward_ios),
         )
       ],
     );
  }
}

6-📝Flutter核心知识|布局与滚动组件【单子布局、多子布局、JSON读取和解析、ListView、GridView、Slivers、监听滚动事件】"

1.3 垂直方向滚动

我们可以通过设置 scrollDirection 参数来控制视图的滚动方向。

我们通过下面的代码实现一个水平滚动的内容:

  • 这里需要注意,我们需要给Container设置width,否则它是没有宽度的,就不能正常显示。
  • 或者我们也可以给ListView设置一个itemExtent,该属性会设置滚动方向上每个item所占据的宽度。
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'dart:convert';
import 'dart:async';
import 'dart:core';

main(List<String> args) {
  runApp(
      MaterialApp(
        showSemanticsDebugger: false,
        home: HomePage(),
      )
  );
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("我的导航栏"),
          backgroundColor: Colors.green,
        ),
        body: HomePageBody()
    );
  }
}

class HomePageBody extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
        child: HomePageListView()
    );
  }
}

class HomePageListView extends StatelessWidget {
  final TextStyle textStyle = TextStyle(fontSize: 20, color: Colors.redAccent);
  @override
  Widget build(BuildContext context) {
    return  ListView(
      scrollDirection: Axis.horizontal,//设置滚动方向
      itemExtent: 300,/*
        在Flutter中,itemExtent是在某些可滚动的小部件中使用的一个属性,
        比如ListView和GridView。它代表每个列表或网格中单个项的范围(高度或宽度,取决于滚动方向)。
        例如,在垂直方向的ListView中,itemExtent属性指定了每个项的高度,确保所有项都具有固定的高度。
        同样,在水平方向的ListView中,itemExtent将指定每个项的宽度。
        使用itemExtent的好处是,当列表或网格中的项具有固定的尺寸时,它可以通过避免动态测量每个项的大小来提高性能。这在处理包含许多项的大型列表或网格时特别有用。
       */
      children: <Widget>[
        Container(color: Colors.red, width: 200),
        Container(color: Colors.green, width: 200),
        Container(color: Colors.blue, width: 200),
        Container(color: Colors.purple, width: 200),
        Container(color: Colors.orange, width: 200),
      ],
    );
  }
}

6-📝Flutter核心知识|布局与滚动组件【单子布局、多子布局、JSON读取和解析、ListView、GridView、Slivers、监听滚动事件】"

2. ListView.build

通过构造函数中的children传入所有的子Widget有一个问题:默认会创建出所有的子Widget。

但是对于用户来说,一次性构建出所有的Widget并不会有什么差异,但是对于我们的程序来说会产生性能问题,而且会增加首屏的渲染时间。

我们可以ListView.build来构建子Widget,提供性能。

2.1. ListView.build基本使用

ListView.build适用于子Widget比较多的场景(类似于iOS中的UITableView)

  • 该构造函数将创建子Widget交给了一个抽象的方法,交给ListView进行管理
  • ListView会在真正需要的时候去创建子Widget,而不是一开始就全部初始化好。

该方法有两个重要参数:

  • itemBuilder:列表项创建的方法。当列表滚动到对应位置的时候,ListView会自动调用该方法来创建对应的子Widget。类型是IndexedWidgetBuilder,是一个函数类型。
  • itemCount:表示列表项的数量,如果为空,则表示ListView为无限列表。

我们还是通过一个简单的案例来认识它:

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'dart:convert';
import 'dart:async';
import 'dart:core';

main(List<String> args) {
  runApp(
     MaterialApp(
       showSemanticsDebugger: false,
       home: HomePage(),
     )
  );
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("我的导航栏"),
        backgroundColor: Colors.green,
      ),
      body: HomePageBody()
    );
  }
}

class HomePageBody extends StatelessWidget {
   @override
  Widget build(BuildContext context) {
     return Center(
       child: HomePageListView()
     );
  }
}

class HomePageListView extends StatelessWidget {
  final TextStyle textStyle = TextStyle(fontSize: 20, color: Colors.redAccent);
   @override
  Widget build(BuildContext context) {
     return ListView.builder(
         itemCount: 100,
         itemExtent: 80,
         itemBuilder: (BuildContext context, int index) {
           return ListTile(title: Text("标题$index"), subtitle: Text("详情内容$index"));
         }
     );
  }
}

6-📝Flutter核心知识|布局与滚动组件【单子布局、多子布局、JSON读取和解析、ListView、GridView、Slivers、监听滚动事件】"

2.2. ListView.build动态数据

在之前,我们搞了一个yz.json数据,我们现在动态的来通过JSON数据展示一个列表。

思考:这个时候是否依然可以使用StatelessWidget:

答案:不可以,因为当前我们的数据是异步加载的,刚开始界面并不会展示数据(没有数据),后面从JSON中加载出来数据(有数据)后,再次展示加载的数据。

  • 这里是有状态的变化的,从无数据,到有数据的变化。
  • 这个时候,我们需要使用StatefulWidget来管理组件。

展示代码如下:

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'dart:convert';
import 'dart:async';
import 'dart:core';

main(List<String> args) {
  runApp(
     MaterialApp(
       showSemanticsDebugger: false,
       home: HomePage(),
     )
  );
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("我的导航栏"),
        backgroundColor: Colors.green,
      ),
      body: HomePageBody()
    );
  }
}

class HomePageBody extends StatelessWidget {
   @override
  Widget build(BuildContext context) {
     return Center(
       child: HomePageListView()
     );
  }
}
class HomePageListView extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return  HomePageListViewState();
  }
}
class HomePageListViewState extends State<HomePageListView> {
  List<Anchor> anchors = [];
  // 在初始化状态的方法中加载数据
  @override
  void initState() {
    getAnchors().then((anchors) {
      setState(() {
        this.anchors = anchors;
      });
    });

    super.initState();
  }
   @override
  Widget build(BuildContext context) {
     return ListView.builder(
       itemBuilder: (BuildContext context, int index) {
         print("index:($index)");
         return Padding(
           padding: EdgeInsets.all(8),
           child: Column(
             crossAxisAlignment: CrossAxisAlignment.start,
             children: <Widget>[
               Image.network(
                 anchors[index%3].imageUrl,
                 fit: BoxFit.fitWidth,
                 width: MediaQuery.of(context).size.width,
               ),
               SizedBox(height: 8),
               Text(anchors[index%3].nickname, style: TextStyle(fontSize: 20),),
               SizedBox(height: 5),
               Text(anchors[index%3].roomName)
             ],
           ),
         );
       },
     );
  }
}



class Anchor {
  String nickname = "";
  String roomName = "";
  String imageUrl = "";

  Anchor({ required this.nickname, required this.roomName,  required this.imageUrl});

  factory Anchor.fromJson(Map<String, dynamic> json) {
    return Anchor(
        nickname: json["nickname"],
        roomName: json["roomName"],
        imageUrl: json["roomSrc"]
    );
  }
}
Future<List<Anchor>> getAnchors() async {
  // 1.读取json文件
  String jsonString = await rootBundle.loadString("assets/json/mock.json");
  // 类型检查
  // final temp =  json.decode(jsonString);
  // print('Type of temp: ${temp.runtimeType}');
  // 2.转成List或Map类型
  List<dynamic> jsonMap = json.decode(jsonString);
  // 3.遍历List,并且转成Anchor对象放到另一个List中
  List<Anchor> anchors = [];
  for (Map<String, dynamic> map in jsonMap) {
      anchors.add(Anchor.fromJson(map));
  }
  return anchors;
}

6-📝Flutter核心知识|布局与滚动组件【单子布局、多子布局、JSON读取和解析、ListView、GridView、Slivers、监听滚动事件】"

2.3. ListView.separated

ListView.separated可以生成列表项之间的分割器

  • 它比ListView.builder多了一个separatorBuilder参数
  • 该参数是一个分割器生成器。

下面我们看一个例子:奇数行添加一条蓝色下划线,偶数行添加一条红色下划线:

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'dart:core';

main(List<String> args) {
  runApp(
      MaterialApp(
        showSemanticsDebugger: false,
        home: HomePage(),
      )
  );
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("我的导航栏"),
          backgroundColor: Colors.green,
        ),
        body: HomePageBody()
    );
  }
}

class HomePageBody extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
        child: HomePageListView()
    );
  }
}

class HomePageListView extends StatelessWidget {

  Divider blueColor = Divider(color: Colors.blue);
  Divider redColor = Divider(color: Colors.red);

  @override
  Widget build(BuildContext context) {
    return ListView.separated(
        itemBuilder: (BuildContext context, int index) {
          return ListTile(
            leading: Icon(Icons.people),
            title: Text("联系人${index+1}"),
            subtitle: Text("联系人电话${index+1}"),
          );
        },
        separatorBuilder: (BuildContext context, int index) {
          return index % 2 == 0 ? redColor : blueColor;
        },
        itemCount: 100
    );
  }
}


6-📝Flutter核心知识|布局与滚动组件【单子布局、多子布局、JSON读取和解析、ListView、GridView、Slivers、监听滚动事件】"

六、GridView组件

GridView(类似于iOS的UICollectionView)用于展示多列的展示

  • 在开发中也非常常见,比如直播App中的主播列表、电商中的商品列表等等。
  • 在Flutter中我们可以使用GridView来实现,使用方式和ListView也比较相似。

1. GridView构造函数

我们先学习GridView构造函数的使用方法

一种使用GridView的方式就是使用构造函数来创建,和ListView对比有一个特殊的参数:gridDelegate

gridDelegate用于控制交叉轴的item数量或者宽度,需要传入的类型是SliverGridDelegate,但是它是一个抽象类,所以我们需要传入它的子类:

SliverGridDelegateWithFixedCrossAxisCount

SliverGridDelegateWithFixedCrossAxisCount({
  @required double crossAxisCount, // 交叉轴的item个数
  double mainAxisSpacing = 0.0, // 主轴的间距
  double crossAxisSpacing = 0.0, // 交叉轴的间距
  double childAspectRatio = 1.0, // 子Widget的宽高比
})

代码演练:

6-📝Flutter核心知识|布局与滚动组件【单子布局、多子布局、JSON读取和解析、ListView、GridView、Slivers、监听滚动事件】"

SliverGridDelegateWithMaxCrossAxisExtent

SliverGridDelegateWithMaxCrossAxisExtent({
  double maxCrossAxisExtent, // 交叉轴的item宽度
  double mainAxisSpacing = 0.0, // 主轴的间距
  double crossAxisSpacing = 0.0, // 交叉轴的间距
  double childAspectRatio = 1.0, // 子Widget的宽高比
})

代码演练:

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'dart:core';

main(List<String> args) {
  runApp(
      MaterialApp(
        showSemanticsDebugger: false,
        home: HomePage(),
      )
  );
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("我的导航栏"),
          backgroundColor: Colors.green,
        ),
        body: HomePageBody()
    );
  }
}

class HomePageBody extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
        child: HomePageGridView()
    );
  }
}

class HomePageGridView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GridView(
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 3,
          mainAxisSpacing: 10,
          crossAxisSpacing: 10,
          childAspectRatio: 1.0
      ),
      children: getGridWidgets(),
    );
  }

  List<Widget> getGridWidgets() {
    return List.generate(100, (index) {
      return Container(
        color: Colors.purple,
        alignment: Alignment(0, 0),
        child: Text("item$index", style: TextStyle(fontSize: 20, color: Colors.white)),
      );
    });
  }
} 

6-📝Flutter核心知识|布局与滚动组件【单子布局、多子布局、JSON读取和解析、ListView、GridView、Slivers、监听滚动事件】"
前面两种方式也可以不设置delegate

可以分别使用:GridView.count构造函数GridView.extent构造函数实现相同的效果,这里不再赘述。

2. GridView.build

和ListView一样,使用构造函数会一次性创建所有的子Widget,会带来性能问题,所以我们可以使用GridView.build来交给GridView自己管理需要创建的子Widget。

我们直接使用之前的数据来进行代码演练:

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'dart:convert';
import 'dart:async';
import 'dart:core';

main(List<String> args) {
  runApp(
     MaterialApp(
       showSemanticsDebugger: false,
       home: HomePage(),
     )
  );
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("我的导航栏"),
        backgroundColor: Colors.green,
      ),
      body: HomePageBody()
    );
  }
}

class HomePageBody extends StatelessWidget {
   @override
  Widget build(BuildContext context) {
     return HomePageGridView();
  }
}

class HomePageGridView extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return HomePageGridViewState();
  }
}


class HomePageGridViewState extends State<HomePageGridView> {
  List<Anchor> anchors = [];

  @override
  void initState() {
    getAnchors().then((anchors) {
      setState(() {
        this.anchors = anchors;
      });
    });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    double screenWidth = MediaQuery.of(context).size.width;
    double itemWidth = screenWidth/2.0-15;
    double itemHeight= itemWidth + 100;
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: GridView.builder(
          shrinkWrap: true,
          physics: ClampingScrollPhysics(),
          gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 2,
              mainAxisSpacing: 10,
              crossAxisSpacing: 10,
              childAspectRatio: 1.2
          ),
          itemCount: anchors.length,
          itemBuilder: (BuildContext context, int index) {
            return Container(
              // width: itemWidth,
              // height: 500,
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  ClipRect(
                    child:Container(
                      width: itemWidth,
                      height: 100,
                      child: Image(
                        image: NetworkImage(anchors[index].imageUrl),
                        fit: BoxFit.fitWidth,
                      ),
                    ),
                  ),
                  SizedBox(height: 5),
                  Text(anchors[index].nickname, style: TextStyle(fontSize: 16),),
                  Text(anchors[index].roomName, maxLines: 1, overflow: TextOverflow.ellipsis,)
                ],
              ),
            );
          }
      ),
    );
  }
}


class Anchor {
  String nickname = "";
  String roomName = "";
  String imageUrl = "";

  Anchor({ required this.nickname, required this.roomName,  required this.imageUrl});

  factory Anchor.fromJson(Map<String, dynamic> json) {
    return Anchor(
        nickname: json["nickname"],
        roomName: json["roomName"],
        imageUrl: json["roomSrc"]
    );
  }
}
Future<List<Anchor>> getAnchors() async {
  // 1.读取json文件
  String jsonString = await rootBundle.loadString("assets/json/mock.json");
  // 类型检查
  // final temp =  json.decode(jsonString);
  // print('Type of temp: ${temp.runtimeType}');
  // 2.转成List或Map类型
  List<dynamic> jsonMap = json.decode(jsonString);
  // 3.遍历List,并且转成Anchor对象放到另一个List中
  List<Anchor> anchors = [];
  for (Map<String, dynamic> map in jsonMap) {
    anchors.add(Anchor.fromJson(map));
  }
  return anchors;
}

6-📝Flutter核心知识|布局与滚动组件【单子布局、多子布局、JSON读取和解析、ListView、GridView、Slivers、监听滚动事件】"

七、Slivers

我们考虑一个这样的布局:

  • 一个滑动的视图中包括一个标题视图(HeaderView),
  • 一个列表视图(ListView)
    一个网格视图(GridView)。

我们怎么可以让它们做到统一的滑动效果呢?使用前面的滚动是很难做到的。

  • Flutter中有一个可以完成这样滚动效果的Widget:CustomScrollView,可以统一管理多个滚动视图。
  • CustomScrollView 中,每一个独立的,可滚动的Widget被称之为Sliver。
  • 补充:Sliver可以翻译成裂片、薄片,你可以将每一个独立的滚动视图当做一个小裂片。

1. Slivers的基本使用

因为我们需要把很多的Sliver放在一个CustomScrollView中,所以CustomScrollView有一个slivers属性,里面让我们放对应的一些Sliver:

  • SliverList:类似于我们之前使用过的ListView;
  • SliverFixedExtentList:类似于SliverList,只是可以设置滚动的高度;
  • SliverGrid:类似于我们之前使用过的GridView;
  • SliverPadding:设置Sliver的内边距,因为可能要单独给Sliver设置内边距;
  • SliverAppBar:添加一个AppBar,通常用来作为CustomScrollView的HeaderView;
  • SliverSafeArea:设置内容显示在安全区域(比如不让齐刘海挡住我们的内容)

我们简单演示一下:SliverGrid+SliverPadding+SliverSafeArea的组合

class HomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CustomScrollView(
      slivers: <Widget>[
        SliverSafeArea(
          sliver: SliverPadding(
            padding: EdgeInsets.all(8),
            sliver: SliverGrid(
              gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 2,
                crossAxisSpacing: 8,
                mainAxisSpacing: 8,
              ),
              delegate: SliverChildBuilderDelegate(
                (BuildContext context, int index) {
                  return Container(
                    alignment: Alignment(0, 0),
                    color: Colors.orange,
                    child: Text("item$index"),
                  );
                },
                childCount: 20
              ),
            ),
          ),
        )
      ],
    );
  }
}

6-📝Flutter核心知识|布局与滚动组件【单子布局、多子布局、JSON读取和解析、ListView、GridView、Slivers、监听滚动事件】"

2. Slivers的组合使用

这里我使用官方的示例程序,将SliverAppBar+SliverGrid+SliverFixedExtentList做出如下界面:

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'dart:convert';
import 'dart:async';
import 'dart:core';

main(List<String> args) {
  runApp(
     MaterialApp(
       showSemanticsDebugger: false,
       home: HomePage(),
     )
  );
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: HomePageBody()
    );
  }
}

class HomePageBody extends StatelessWidget {
   @override
  Widget build(BuildContext context) {
     return HomePageCustomScrollView();
  }
}

class HomePageCustomScrollView extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return showCustomScrollView();
  }
  Widget showCustomScrollView() {
    return  CustomScrollView(
      slivers: <Widget>[
        const SliverAppBar(
          expandedHeight: 250.0,
          flexibleSpace: FlexibleSpaceBar(
            title: Text('VanZhang Demo'),
            background: Image(
              image: NetworkImage(
                "https://tva1.sinaimg.cn/large/006y8mN6gy1g72j6nk1d4j30u00k0n0j.jpg",
              ),
              fit: BoxFit.cover,
            ),
          ),
        ),
         SliverGrid(
          gridDelegate:  SliverGridDelegateWithMaxCrossAxisExtent(
            maxCrossAxisExtent: 200.0,
            mainAxisSpacing: 10.0,
            crossAxisSpacing: 10.0,
            childAspectRatio: 4.0,
          ),
          delegate:  SliverChildBuilderDelegate(
                (BuildContext context, int index) {
              return  Container(
                alignment: Alignment.center,
                color: Colors.teal[100 * (index % 9)],
                child:  Text('grid item $index'),
              );
            },
            childCount: 10,
          ),
        ),
        SliverFixedExtentList(
          itemExtent: 50.0,
          delegate: SliverChildBuilderDelegate(
                  (BuildContext context, int index) {
                return  Container(
                  alignment: Alignment.center,
                  color: Colors.lightBlue[100 * (index % 9)],
                  child:  Text('list item $index'),
                );
              },
              childCount: 20
          ),
        ),
      ],
    );
  }
}

6-📝Flutter核心知识|布局与滚动组件【单子布局、多子布局、JSON读取和解析、ListView、GridView、Slivers、监听滚动事件】"

八、监听滚动事件

  • 对于滚动的视图,我们经常需要监听它的一些滚动事件,在监听到的时候去做对应的一些事情。
    • 比如视图滚动到底部时,我们可能希望做上拉加载更多;
    • 比如滚动到一定位置时显示一个回到顶部的按钮,点击回到顶部的按钮,回到顶部;
    • 比如监听滚动什么时候开始,什么时候结束;
  • 在Flutter中监听滚动相关的内容由两部分组成:ScrollControllerScrollNotification

1. ScrollController

在Flutter中,Widget并不是最终渲染到屏幕上的元素(真正渲染的是RenderObject),因此通常这种监听事件以及相关的信息并不能直接从Widget中获取,而是必须通过对应的Widget的Controller来实现。

ListView、GridView的组件控制器是ScrollController,我们可以通过它来获取视图的滚动信息,并且可以调用里面的方法来更新视图的滚动位置。

另外,通常情况下,我们会根据滚动的位置来改变一些Widget的状态信息,所以ScrollController通常会和StatefulWidget一起来使用,并且会在其中控制它的初始化、监听、销毁等事件。

我们来做一个案例,当滚动到1000位置的时候,显示一个回到顶部的按钮:

  • jumpTo(double offset)animateTo(double offset,...)
    • 这两个方法用于跳转到指定的位置,它们不同之处在于,后者在跳转时会执行一个动画,而前者不会。
  • ScrollController 间接继承自Listenable,我们可以根据ScrollController来监听滚动事件。
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'dart:core';

main(List<String> args) {
  runApp(
      MaterialApp(
        showSemanticsDebugger: false,
        home: HomePage(),
      )
  );
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: HomePageBody()
    );
  }
}

class HomePageBody extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return HomePageScrollController();
  }
}
class HomePageScrollController extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => HomePageScrollControllerState();
}

class HomePageScrollControllerState extends State<HomePageScrollController> {
  // 初始化ScrollController
  ScrollController _controller = ScrollController();
  bool _isShowTop = false;

  @override
  void initState() {
    // 监听滚动
    _controller.addListener(() {
      var tempSsShowTop = _controller.offset >= 1000;
      if (tempSsShowTop != _isShowTop) {
        setState(() {
          _isShowTop = tempSsShowTop;
        });
      }
    });

    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("ListView展示"),
      ),
      body: ListView.builder(
          itemCount: 100,
          itemExtent: 60,
          controller: _controller,
          itemBuilder: (BuildContext context, int index) {
            return ListTile(title: Text("item$index"));
          }
      ),
      floatingActionButton: !_isShowTop ? null : FloatingActionButton(
        child: Icon(Icons.arrow_upward),
        onPressed: () {
          _controller.animateTo(0, duration: Duration(milliseconds: 1000), curve: Curves.ease);
        },
      ),
    );
  }
}

6-📝Flutter核心知识|布局与滚动组件【单子布局、多子布局、JSON读取和解析、ListView、GridView、Slivers、监听滚动事件】"

2. NotificationListener

如果我们希望监听什么时候开始滚动,什么时候结束滚动,这个时候我们可以通过NotificationListener

  • NotificationListener是一个Widget,模板参数T是想监听的通知类型,如果省略,则所有类型通知都会被监听,如果指定特定类型,则只有该类型的通知会被监听。
  • NotificationListener需要一个onNotification回调函数,用于实现监听处理逻辑。
  • 该回调可以返回一个布尔值,代表是否阻止该事件继续向上冒泡,如果为true时,则冒泡终止,事件停止向上传播,如果不返回或者返回值为false 时,则冒泡继续。

案例: 列表滚动, 并且在中间显示滚动进度

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'dart:core';

main(List<String> args) {
  runApp(
     MaterialApp(
       showSemanticsDebugger: false,
       home: HomePage(),
     )
  );
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: HomePageBody()
    );
  }
}

class HomePageBody extends StatelessWidget {
   @override
  Widget build(BuildContext context) {
     return HomePageNotificationListener();
  }
}

class HomePageNotificationListener extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => HomePageNotificationListenerState();
}

class HomePageNotificationListenerState extends State<HomePageNotificationListener> {
  int _progress = 0;

  @override
  Widget build(BuildContext context) {
    return NotificationListener(
      onNotification: (ScrollNotification notification) {
        // 1.判断监听事件的类型
        if (notification is ScrollStartNotification) {
          print("开始滚动.....");
        } else if (notification is ScrollUpdateNotification) {
          // 当前滚动的位置和总长度
          final currentPixel = notification.metrics.pixels;
          final totalPixel = notification.metrics.maxScrollExtent;
          double progress = currentPixel / totalPixel;
          setState(() {
            _progress = (progress * 100).toInt();
          });
          print("正在滚动:${notification.metrics.pixels} - ${notification.metrics.maxScrollExtent}");
        } else if (notification is ScrollEndNotification) {
          print("结束滚动....");
        }
        return false;
      },
      child: Stack(
        alignment: Alignment(.9, .9),
        children: <Widget>[
          ListView.builder(
              itemCount: 100,
              itemExtent: 60,
              itemBuilder: (BuildContext context, int index) {
                return ListTile(title: Text("item$index"));
              }
          ),
          CircleAvatar(
            radius: 30,
            child: Text("$_progress%"),
            backgroundColor: Colors.black54,
          )
        ],
      ),
    );
  }
}

6-📝Flutter核心知识|布局与滚动组件【单子布局、多子布局、JSON读取和解析、ListView、GridView、Slivers、监听滚动事件】"

原文链接:https://juejin.cn/post/7260070602614505533 作者:没有故事的Zhang同学

(1)
上一篇 2023年7月27日 上午10:49
下一篇 2023年7月27日 上午11:04

相关推荐

发表回复

登录后才能评论