06-📝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有哪些属性:

dart 复制代码
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

dart 复制代码
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,
    );
  }
}

2. Center组件

2.1. Center介绍

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

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

源码分析:

dart 复制代码
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

dart 复制代码
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,
    );
  }
}

3. Padding组件

3.1. Padding介绍

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

源码分析:

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

3.2. Padding演练

代码演练:

dart 复制代码
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
        ),
      ),
    );;
  }
}

4. Container组件

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

4.1. Container介绍

Container在开发中被使用的频率是非常高的,特别是我们经常会将其作为容器组件。

下面我们来看一下Container有哪些属性:

dart 复制代码
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演练

简单进行一个演示:

dart 复制代码
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),
      ),
    );
  }
}

4.3. BoxDecoration

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

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

BoxDecoration常见属性:

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

部分效果演示:

dart 复制代码
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
                ]
            )
        ),
      ),
    );
  }
}

4.4. 实现圆角图像

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

实现代码如下:

dart 复制代码
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"),
            )
        ),
      ),
    );
  }
}

三、多子布局组件

在开发中,我们经常需要将多个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)分别是下图
  • 对于Column来说,主轴(MainAxis)和交叉轴(CrossAxis)分别是下图

2. Row组件

2.1 Row介绍

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

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

从源码中查看Row的属性:

dart 复制代码
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演练

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

dart 复制代码
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),
        ],
      ),
    );
  }
}

2.3 mainAxisSize

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

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

2.4 TextBaseline

关于TextBaseline的取值解析

2.5 Expanded

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

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

  • flex属性,弹性系数,Row会根据两个Expanded的弹性系数来决定它们占据剩下空间的比例
dart 复制代码
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),
          )
        ],
      ),
    );
  }
}

3. Column组件

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

3.1 Column介绍

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

dart 复制代码
  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,查看代码运行效果:

dart 复制代码
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),
          )
        ],
      ),
    );
  }
}

4. Stack组件

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

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

4.1. Stack介绍

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

dart 复制代码
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中的绝对定位效果。

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

dart 复制代码
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)),
          )
        ],
      ),
    );
  }
}

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

四、JSON读取和解析

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

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

1. JSON资源配置

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

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

2. JSON读取解析

JSON资源读取

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

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

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

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

dart 复制代码
import 'package:flutter/services.dart' show rootBundle;

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

JSON字符串转化

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

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

代码如下:

dart 复制代码
// 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

dart 复制代码
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数据

    json 复制代码
    [
      {
        "nickname" : "昵称1",
        "roomName" : "名字1",
        "roomSrc" : "链接1"
      },
      {
        "nickname" : "昵称2",
        "roomName" : "名字2",
        "roomSrc" : "链接2"
      },
      {
        "nickname" : "昵称3",
        "roomName" : "名字3",
        "roomSrc" : "链接3"
      }
    ] 
  • 之后外界只需要调用我内部的getAnchors就可以获取到解析后的数据了

    dart 复制代码
    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;
    } 

五、ListView组件

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

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

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

1. ListView基础

1.1 ListView基本使用

ListView可以沿一个方向(垂直或水平方向,默认是垂直方向)来排列其所有子Widget。

一种最简单的使用方式是直接将所有需要排列的子Widget放在ListView的children属性中即可。

我们来看一下直接使用ListView的代码演练:

  • 为了让文字之间有一些间距,我使用了Padding Widget
dart 复制代码
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),
         )
       ],
     );
  }
}

1.2 ListTile的使用

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

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

dart 复制代码
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),
         )
       ],
     );
  }
}

1.3 垂直方向滚动

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

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

  • 这里需要注意,我们需要给Container设置width,否则它是没有宽度的,就不能正常显示。
  • 或者我们也可以给ListView设置一个itemExtent,该属性会设置滚动方向上每个item所占据的宽度。
dart 复制代码
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),
      ],
    );
  }
}

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为无限列表。

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

dart 复制代码
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"));
         }
     );
  }
}

2.2. ListView.build动态数据

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

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

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

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

展示代码如下:

dart 复制代码
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;
}

2.3. ListView.separated

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

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

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

dart 复制代码
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
    );
  }
}

六、GridView组件

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

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

1. GridView构造函数

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

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

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

SliverGridDelegateWithFixedCrossAxisCount

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

代码演练:

dart 复制代码

SliverGridDelegateWithMaxCrossAxisExtent

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

代码演练:

dart 复制代码
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)),
      );
    });
  }
} 

前面两种方式也可以不设置delegate

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

2. GridView.build

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

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

dart 复制代码
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;
}

七、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的组合

dart 复制代码
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
              ),
            ),
          ),
        )
      ],
    );
  }
}

2. Slivers的组合使用

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

dart 复制代码
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
          ),
        ),
      ],
    );
  }
}

八、监听滚动事件

  • 对于滚动的视图,我们经常需要监听它的一些滚动事件,在监听到的时候去做对应的一些事情。
    • 比如视图滚动到底部时,我们可能希望做上拉加载更多;
    • 比如滚动到一定位置时显示一个回到顶部的按钮,点击回到顶部的按钮,回到顶部;
    • 比如监听滚动什么时候开始,什么时候结束;
  • 在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来监听滚动事件。
dart 复制代码
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);
        },
      ),
    );
  }
}

2. NotificationListener

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

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

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

dart 复制代码
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,
          )
        ],
      ),
    );
  }
}
相关推荐
火柴就是我4 小时前
flutter 之真手势冲突处理
android·flutter
Speed1234 小时前
`mockito` 的核心“打桩”规则
flutter·dart
法的空间4 小时前
Flutter JsonToDart 支持 JsonSchema
android·flutter·ios
恋猫de小郭4 小时前
Android 将强制应用使用主题图标,你怎么看?
android·前端·flutter
玲珑Felone5 小时前
从flutter源码看其渲染机制
android·flutter
ALLIN1 天前
Flutter 三种方式实现页面切换后保持原页面状态
flutter
Dabei1 天前
Flutter 国际化
flutter
Dabei1 天前
Flutter MQTT 通信文档
flutter
Dabei1 天前
Flutter 中实现 TCP 通信
flutter
孤鸿玉1 天前
ios flutter_echarts 不在当前屏幕 白屏修复
flutter