Day08 - Flutter -滚动Widget

简介: Day08 - Flutter -滚动Widget

概述


  • ListView
  • GridView
  • sliver
  • 滚动的监听


一、ListView



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

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

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


  • 1.1、ListView 基本创建ListView可以沿一个方向(垂直或水平方向,默认是垂直方向)来排列其所有子Widget。一种最简单的使用方式是直接将所有需要排列的子Widget放在ListView的children属性中即可。我们来看一下直接使用ListView的代码演练:
  • 1>、为了让文字之间有一些间距,我使用了Padding Widget


image.png

image.png

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

提示:我们可以通过 List.generate创建子 Widget

  • List.generate(100, (index):第一个参数是加载多少个Widget, 第二个是第几个


class MyHomeBody extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
     return ListView(
        children: List.generate(100, (index) {
           return Text("Hello World $index");
        })
     );
  }
}


  • 2>、ListTile的使用
    在开发中,我们经常见到一种列表,有一个图标或图片(Icon),有一个标题(Title),有一个子标题(Subtitle),还有尾部一个图标(Icon)。
    这个时候,我们可以使用ListTile来实现:


image.png

class MyHomeBody extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
      return ListView(
          children: <Widget>[
               ListTile(
                  leading: Icon(Icons.people, size: 20,),
                  title: Text("联系人"),
                  subtitle: Text("联系人信息"),
                  trailing: Icon(Icons.arrow_right),
               ),
               ListTile(
                  leading: Icon(Icons.people, size: 20,),
                  title: Text("邮箱"),
                  subtitle: Text("邮箱地址信息"),
                  trailing: Icon(Icons.arrow_right),
               ),
          ],
      );
   }
}


  • 3>、垂直方向滚动,默认是垂直方向
    我们可以通过设置 scrollDirection 参数来控制视图的滚动方向
    我们通过下面的代码实现一个水平滚动的内容:
    这里需要注意,我们需要给Container设置width,否则它是没有宽度的,就不能正常显示。或者我们也可以给ListView设置一个 itemExtent该属性会设置滚动方向上每个item所占据的宽度


image.png


image.png

class MyHomeBody extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
      return ListView(
          scrollDirection: Axis.horizontal,
          itemExtent: 200,
          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),
          ],
      );
  }
}


  • 1.2、ListView.build 创建
    通过构造函数中的children传入所有的子Widget有一个问题:默认会创建出所有的子Widget。
    但是对于用户来说,一次性构建出所有的Widget并不会有什么差异,但是对于我们的程序来说会产生性能问题,而且会增加首屏的渲染时间。
    我们可以ListView.build来构建子Widget,提供性能。


class MyHomeBody extends StatelessWidget {
     @override
     Widget build(BuildContext context) {
         return ListView.builder(
            // 创建多少个 row
            itemCount: 50,
            // 滚动方向的 row 宽度
            itemExtent: 100,
            // 生成 Widget
            itemBuilder: (BuildContext ctx, int index) {
               return ListTile(title: Text("标题$index"), subtitle: Text("详情内容$index"));
            }
         );
     }
}


  • 1.3、ListView.separated 创建(带分割线)
    ListView.separated 可以生成列表项之间的分割器,它除了比ListView.builder多了一个separatorBuilder参数,该参数是一个分割器生成器。
    下面我们看一个例子:奇数行添加一条蓝色下划线,偶数行添加一条红色下划线:


image.png

image.png


class MyHomeBody extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
        return ListView.separated(
            itemBuilder: (BuildContext context, int index) {
               return ListTile(
                  leading: Icon(Icons.people),
                  trailing: Icon(Icons.arrow_right),
                  title: Text("联系人${index+1}"),
                  subtitle: Text("联系人电话${index+1}"),
               );
            },
            itemCount: 10,
            separatorBuilder: (BuildContext context, int index) {
               return Divider(
                 // 每个Widget 之间的距离
                 height: 30,
                 // 距离左边的距离
                 indent: 16,
                 // 距离右边的距离
                 endIndent: 16,
                 // 每条分割线的高度
                 thickness: 10,
                 color: index % 2 == 0 ? Colors.red : Colors.green,
               );
             },
         );
    }
}


二、GridView 组件


GridView用于展示多列的展示,在开发中也非常常见,比如直播App中的主播列表、电商中的商品列表等等。

在Flutter中我们可以使用GridView来实现,使用方式和ListView也比较相似。

  • 2.1、GridView构造函数使用GridView的方式就是使用构造函数来创建,和ListView对比有一个特殊的参数:gridDelegategridDelegate用于控制交叉轴的item数量或者宽度,需要传入的类型是SliverGridDelegate,但是它是一个抽象类,所以我们需要传入它的子类:
  • SliverGridDelegateWithFixedCrossAxisCount


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


class MyHomeBody extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
       return GridView(
           gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 3,
              crossAxisSpacing: 20,
              mainAxisSpacing: 20,
              // 宽 / 高
              childAspectRatio: 2
           ),
           children: List.generate(100, (index) {
                return Container(
                    color: Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256)),
                );
           }
       ),
    );
  }
}
  • SliverGridDelegateWithMaxCrossAxisExtent


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


class MyHomeBody extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
        return GridView(
            gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent (
                maxCrossAxisExtent: 100,
                mainAxisSpacing: 20,
                crossAxisSpacing: 20,
                childAspectRatio: 2
            ),
            children: List.generate(100, (index) {
                return Container(
                   color: Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256)),
                );
            }
        ),
     );
   }
}

提示:前面两种方式也可以不设置delegate,可以分别使用:GridView.count构造函数和GridView.extent构造函数实现相同的效果


  • 2.2. GridView.build
    和ListView一样,使用构造函数会一次性创建所有的子Widget,会带来性能问题,所以我们可以使用GridView.build来交给GridView自己管理需要创建的子Widget。
    我们直接使用之前的数据来进行代码演练:


image.png


class MyHomeBody extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
      return Padding(
         padding: const EdgeInsets.all(5.0),
         child: GridView.builder(
              shrinkWrap: true,
              physics: ClampingScrollPhysics(),
              gridDelegate: SliverGridDelegateWithFixedCrossAxisCount (
                  crossAxisCount: 2,
                  mainAxisSpacing: 10,
                  crossAxisSpacing: 10,
                  childAspectRatio: 1.2
              ),
              itemCount: 10,
              itemBuilder: (BuildContext context, int index) {
                  return Container(
                       child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: <Widget>[
                              Image.network('http://image.xcar.com.cn/attachments/a/day_200323/2020032314_59939b0716c40f9be872JrmcP75B4KfO.jpg-app'),
                              SizedBox(height: 5),
                              Text('王三', style: TextStyle(fontSize: 6),), 
                          ],
                       ),
                  );
              }
          ),
     );
  }
}


三、Sliver



  • 3.1、Sliver 的简单介绍
    我们考虑一个这样的布局:一个滑动的视图中包括一个标题视图(HeaderView),一个列表视图(ListView),一个网格视图(GridView)。
    我们怎么可以让它们做到统一的滑动效果呢?使用前面的滚动是很难做到的。
    Flutter中有一个可以完成这样滚动效果的Widget:CustomScrollView,可以统一管理多个滚动视图。
    在CustomScrollView中,每一个独立的,可滚动的Widget被称之为Sliver。
    补充:Sliver可以翻译成裂片、薄片,你可以将每一个独立的滚动视图当做一个小裂片。
  • 3.2、Slivers 的基本使用因为我们需要把很多的Sliver放在一个CustomScrollView中,所以CustomScrollView有一个slivers属性,里面让我们放对应的一些Sliver:不可以放弃他的
  • SliverList:类似于我们之前使用过的ListView;
  • SliverFixedExtentList:类似于SliverList只是可以设置滚动的高度;
  • SliverGrid:类似于我们之前使用过的GridView;
  • SliverPadding:设置Sliver的内边距,因为可能要单独给Sliver设置内边距;
  • SliverAppBar:添加一个AppBar,通常用来作为CustomScrollView的HeaderView;
  • SliverSafeArea:设置内容显示在安全区域(比如不让齐刘海挡住我们的内容),也就是可以滚动过安全区域



image.png


class MyHomeBody1 extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
       return CustomScrollView(
            slivers: <Widget>[
                SliverGrid(
                     gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                         crossAxisCount: 2,
                         crossAxisSpacing: 16,
                         childAspectRatio: 2,
                         mainAxisSpacing: 16
                     ),
                     delegate: SliverChildBuilderDelegate(
                         (BuildContext context, int index) {
                             return Container(
                                color: Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256)),
                             );
                         },
                         childCount: 10
                     )
                ),
            ],
      );
   }
}


  • 3.3、Slivers的组合使用:SliverAppBar、SliverGrid、SliverList 的设置



image.png

image.png



class MyHomeBody extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
        return CustomScrollView(
            slivers: <Widget>[
                SliverAppBar(
                   // true: bar不动
                   // false: bar动
                   pinned: true,
                   // bar 的高度
                  expandedHeight: 200,
                  flexibleSpace: FlexibleSpaceBar(
                      title: Text("Hello World!"),
                      background: Image.asset("assets/images/iron.png", fit: BoxFit.cover,),
                  ),
               ),
               SliverSafeArea(
                  sliver: SliverPadding(
                     padding: EdgeInsets.all(16),
                     sliver: SliverGrid(
                          gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                          crossAxisCount: 2,
                          crossAxisSpacing: 16,
                          childAspectRatio: 2,
                          mainAxisSpacing: 16
                     ),
                     delegate: SliverChildBuilderDelegate(
                         (BuildContext context, int index) {
                           return Container(
                              color: Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256)),
                           );
                        },
                        childCount: 6
                     )
                  ),
                ),
              ),
              SliverList(
                 delegate: SliverChildBuilderDelegate(
                     (BuildContext context, int index) {
                        return ListTile(
                           leading: Icon(Icons.people),
                           title: Text("联系人"),
                        );
                     },
                     childCount: 20
                 ),
              )
         ],
     );
  }
}


四、滚动的监听



对于滚动的视图,我们经常需要监听它的一些滚动事件,在监听到的时候去做对应的一些事情。

比如视图滚动到底部时,我们可能希望做上拉加载更多;

比如滚动到一定位置时显示一个回到顶部的按钮,点击回到顶部的按钮,回到顶部;

比如监听滚动什么时候开始,什么时候结束;

Flutter 中监听滚动相关的内容由两部分组成ScrollControllerScrollNotification


  • 4.1、ScrollController 监听,可以预先设置offset,也可以监听滚动的位置,缺点是:无法检测股东开始和结束在Flutter中,Widget并不是最终渲染到屏幕上的元素(真正渲染的是RenderObject),因此通常这种监听事件以及相关的信息并不能直接从Widget中获取,而是必须通过对应的Widget的Controller来实现。ListView、GridView的组件控制器是ScrollController,我们可以通过它来获取视图的滚动信息,并且可以调用里面的方法来更新视图的滚动位置。另外,通常情况下,我们会根据滚动的位置来改变一些Widget的状态信息,所以ScrollController通常会和StatefulWidget一起来使用,并且会在其中控制它的初始化、监听、销毁等事件。我们来做一个案例,当滚动到500位置的时候,显示一个回到顶部的按钮:
  • jumpTo(double offset)、animateTo(double offset,...):这两个方法用于跳转到指定的位置,它们不同之处在于,后者在跳转时会执行一个动画,而前者不会。
  • ScrollController间接继承自Listenable,我们可以根据ScrollController来监听滚动事件。


image.png

image.png



  • 代码如下


class HomePage extends StatefulWidget {
   @override
   _HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
   // 设置变量 _controller 并设置偏移量
   ScrollController _controller = ScrollController(initialScrollOffset: 200);
   /* 默认设置为 false */
   bool _isFloatingActionButton = false;
   @override
   void initState() {
       // TODO: implement initState
       super.initState();
       _controller.addListener(() {
          print("监听到滚动");
          setState(() {
              _isFloatingActionButton = _controller.offset > 500 ? true : false;
          });
      });
   }
   @override
   Widget build(BuildContext context) {
      return Scaffold(
         appBar: AppBar(
             title: Text("列表滚动测试"),
         ),
         body: ListView.builder(
             controller: _controller,
             itemCount: 20,
             itemBuilder: (BuildContext context, int index) {
               return ListTile(
                  leading: Icon(Icons.people),
                  title: Text("测试 $index"),
               );
             }
         ),
         floatingActionButton: _isFloatingActionButton ? FloatingActionButton(
            child: Icon(Icons.arrow_upward),
            onPressed: () {
              // 返回到顶部
              _controller.animateTo(0, duration: Duration(milliseconds: 200), curve: Curves.easeIn);
            },
         ) : null,
     );
   }
}


  • 4.2、ScrollNotification如果我们希望监听什么时候开始滚动,什么时候结束滚动,这个时候我们可以通过NotificationListener。
  • NotificationListener是一个Widget,模板参数T是想监听的通知类型,如果省略,则所有类型通知都会被监听,如果指定特定类型,则只有该类型的通知会被监听。
  • NotificationListener需要一个onNotification回调函数,用于实现监听处理逻辑。
  • 该回调可以返回一个布尔值,代表是 false 阻止该事件继续向上冒泡,如果为true时,则冒泡终止,事件停止向上传播,如果不返回或者返回值为false 时,则冒泡继续。
    案例: 列表滚动, 并且在中间显示滚动进度


class HomePage extends StatefulWidget {
   @override
    _HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
    int _progress = 0;
    @override
    Widget build(BuildContext context) {
         return Scaffold(
              appBar: AppBar(
                 title: Text("列表滚动测试"),
              ),
              body: NotificationListener(
                 onNotification: (ScrollNotification notification ) {
                      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 true;
                 },
                 child: Stack(
                      alignment: Alignment(0.9, 0.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,
                          )
                      ],
                 ),
           ),
       );
    }
 }


目录
相关文章
|
27天前
深入理解Flutter鸿蒙next版本 中的Widget继承:使用extends获取数据与父类约束
本文详细介绍了Flutter中如何通过继承其他Widget来创建自定义组件。首先解释了Widget继承的基本概念,包括StatelessWidget和StatefulWidget的区别。接着通过具体示例展示了如何继承StatelessWidget和StatefulWidget,并在子类中访问父类的build方法和状态。最后,结合多个自定义Widget展示了如何在实际应用中灵活使用继承和组合来构建复杂的UI。
73 8
|
25天前
|
容器
flutter&鸿蒙next 使用 InheritedWidget 实现跨 Widget 传递状态
在 Flutter 中,状态管理至关重要。本文详细介绍了如何使用 InheritedWidget 实现跨 Widget 的状态传递。InheritedWidget 允许数据在 Widget 树中向下传递,适用于多层嵌套的场景。通过一个简单的计数器示例,展示了如何创建和使用 InheritedWidget,包括其基础概念、工作原理及代码实现。虽然 InheritedWidget 较底层,但它是许多高级状态管理解决方案的基础。
95 2
|
25天前
|
UED 开发者 容器
Flutter&鸿蒙next 的 Sliver 实现自定义滚动效果
Flutter 提供了强大的滚动组件,如 ListView 和 GridView,但当需要更复杂的滚动效果时,Sliver 组件是一个强大的工具。本文介绍了如何使用 Sliver 实现自定义滚动效果,包括 SliverAppBar、SliverList 等常用组件的使用方法,以及通过 CustomScrollView 组合多个 Sliver 组件实现复杂布局的示例。通过具体代码示例,展示了如何实现带有可伸缩 AppBar 和可滚动列表的页面。
89 1
|
2月前
|
容器
flutter:第一个flutter&Widget的使用 (二)
本文介绍了Flutter框架下的基本组件及其用法,包括简单的 Stateless Widget 如文本和按钮,以及更复杂的 StatefulWidget 示例。详细解释了如何使用 `context` 获取祖先小部件的信息,并展示了 `MaterialApp` 的属性及用途。此外,还探讨了 `StatefulWidget` 与 `StatelessWidget` 的区别,以及 `AppBar` 的常见属性配置方法。适合Flutter初学者参考学习。
|
25天前
|
Dart JavaScript 前端开发
Flutter 的 Widget 概述与常用 Widgets 与鸿蒙 Next 的对比
Flutter 是 Google 开发的开源 UI 框架,用于快速构建高性能的移动、Web 和桌面应用。Flutter 通过 Widget 构建 UI,每个 UI 元素都是 Widget,包括文本、按钮、图片等。Widget 不仅描述外观,还描述行为,是不可变的。常见的 Widget 包括结构型(Container、Column、Row)、呈现型(Text、Image)、交互型(ElevatedButton)和状态管理型(StatefulWidget)。Flutter 与鸿蒙 Next 在组件化架构、开发语言、布局系统、性能和跨平台支持方面各有优势
69 0
|
5月前
|
UED
Flutter-无限循环滚动标签
Flutter-无限循环滚动标签
86 0
|
5月前
Flutter-底部弹出框(Widget层级)
文章描述了如何在Flutter中使用DraggableScrollableSheet创建一个底部弹出框,同时保持其可手势滑动关闭。作者遇到问题并提出对原控件进行扩展,以支持头部和列表布局的滑动关闭功能。
189 0
|
5月前
Flutter 滚动距离来设置TabBar的位置,点击TabBar滚动的到指定的位置
Flutter 滚动距离来设置TabBar的位置,点击TabBar滚动的到指定的位置
|
7月前
|
前端开发 UED 开发者
【Flutter前端技术开发专栏】Flutter中的列表与滚动视图优化
【4月更文挑战第30天】Flutter开发中,优化列表和滚动视图至关重要。本文介绍了几种优化方法:1) 使用`ListView.builder`和`GridView.builder`实现懒加载;2) 复用子组件以减少实例创建;3) 利用`CustomScrollView`和`Slivers`提升滚动性能;4) 通过`NotificationListener`监听滚动事件;5) 使用`KeepAlive`保持列表项状态。掌握这些技巧能提升应用性能和用户体验。
104 1
【Flutter前端技术开发专栏】Flutter中的列表与滚动视图优化
|
6月前
Flutter StreamBuilder 实现局部刷新 Widget
Flutter StreamBuilder 实现局部刷新 Widget
47 0
下一篇
无影云桌面