Flutter - 布局組件-5-可滾動(dòng)Widget

1.ListView

  • 類似于iOS的UITableview;
屬性(ListView) 類型 可選? 作用
children List<Widget> 命名可選 列表內(nèi)顯示的子組件
scrollDirection Axis(enum) 命名可選(默認(rèn):Axis.vertical) 滾動(dòng)方向
itemExtent double 命名可選 行高(如果設(shè)置橫向滾動(dòng),則表示列寬)
屬性(ListTile),類似于tableviewcell 類型 可選? 作用
leading Widget 命名可選 左側(cè)圖標(biāo)(不一定是圖標(biāo),只要是Widget)
title Widget 命名可選 主標(biāo)題
subtitle Widget 命名可選 副標(biāo)題
trailing Widget 命名可選 右側(cè)圖標(biāo)(不一定是圖標(biāo),只要是Widget)
onTap 閉包 命名可選 點(diǎn)擊回調(diào)
1.1 ListView基本使用

ListView可以沿一個(gè)方向(垂直或水平方向,默認(rèn)是垂直方向)來排列其所有子Widget。
一種最簡單的使用方式是直接將所有需要排列的子Widget放在ListView的children屬性中即可。

我們來看一下直接使用ListView的代碼演練:

class MyHomeBody extends StatelessWidget {
  const MyHomeBody({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: List.generate(39, (index) {

        //ListTile類似于tableviewcell
        return ListTile(
          
          leading: Icon(Icons.pets),
          
          trailing: IconButton(icon: Icon(Icons.favorite,color: Colors.red,),),
          
          title: Text('第 ${index+1} 行'),
          
          subtitle: Text('副標(biāo)題'),
          
          onTap: () {
            print('點(diǎn)擊了tableview ${index+1}');
          },
          
        );
      }),
    );
  }
}
1.2 ListView的滾動(dòng)方向--scrollDirection
class MyHomeBody2 extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return ListView(
      scrollDirection: Axis.horizontal,

      //children 在滾動(dòng)方向的高度(垂直滾動(dòng))或者寬度(橫向滾動(dòng))
      itemExtent: 100,

      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.3 ListView的構(gòu)造方法
構(gòu)造方法 使用場(chǎng)景
ListView();默認(rèn)的構(gòu)造方法 當(dāng)已經(jīng)有固定個(gè)數(shù)的chidren,且chidren的個(gè)數(shù)不是十分龐大的時(shí)候可以使用這個(gè)方法,因?yàn)檫@個(gè)方法默認(rèn)會(huì)盡可能多地創(chuàng)建出子組件,以方便ListView進(jìn)行展示
ListView.builder(); 當(dāng)有不確定數(shù)量,或者大量的cell顯示的時(shí)候,可以使用這個(gè)方式,系統(tǒng)會(huì)自動(dòng)在ListView將要顯示某個(gè)cell的時(shí)候,才把它build出來;
ListView.separated(); 有3個(gè)@required參數(shù): 前面兩個(gè)和buider方法一樣,第三個(gè)參數(shù)是separatorBuilder,用來添加一個(gè)cell與cell之間的分割線的,這個(gè)方法創(chuàng)建ListView不可以給cell設(shè)置高度
//使用builder構(gòu)造器創(chuàng)建ListView
class MyHomeBody2 extends StatelessWidget {

  @override
  Widget build(BuildContext context) {

    final double screenWidth = MediaQuery.of(context).size.width;

    return ListView.builder(
      itemBuilder: (BuildContext context, int index) {
        return ListTile(

            //左側(cè)圖標(biāo)
            leading: Icon(Icons.pets),
            //右側(cè)圖標(biāo)
            trailing: IconButton(
              icon: Icon(
                Icons.favorite,
                color: Colors.red,
              ),
            ),
            //主標(biāo)題
            title: Text('第 ${index + 1} 行'),
            //副標(biāo)題
            subtitle: Text('副標(biāo)題'),
            onTap: () {
              print('點(diǎn)擊了tableview ${index + 1}');
            });
      },
      itemCount: 10,
      scrollDirection: Axis.vertical,
    );
  }
}



//separated
class MyHomeBody3 extends StatelessWidget {

  @override
  Widget build(BuildContext context) {

    final double screenWidth = MediaQuery.of(context).size.width;

    return ListView.separated(
        itemBuilder: (BuildContext context, int index) {
          return ListTile(

            //左側(cè)圖標(biāo)
              leading: Icon(Icons.pets),
              //右側(cè)圖標(biāo)
              trailing: IconButton(
                icon: Icon(
                  Icons.favorite,
                  color: Colors.red,
                ),
              ),
              //主標(biāo)題
              title: Text('第 ${index + 1} 行'),
              //副標(biāo)題
              subtitle: Text('副標(biāo)題'),
              onTap: () {
                print('點(diǎn)擊了tableview ${index + 1}');
              });
        },
        separatorBuilder: (BuildContext context, int index){
          return Divider(
            color: Colors.red,
            //Divider的高度
            height: 1,
            //左側(cè)邊距
            indent: 10,
            //右側(cè)邊距
            endIndent: 30,
            //分割線的高度
            thickness: 1,
          );
        },
        itemCount: 100);
  }
}

image.png

2. GridView

構(gòu)造方法 意義
GridView() 默認(rèn)構(gòu)造方法中有個(gè)感覺參數(shù):gridDelegate,用來指定構(gòu)造的方式的
GridView.builder() 作用類似于ListView的builder();方法
GridView.count() 在默認(rèn)構(gòu)造方法的基礎(chǔ)上選擇了SliverGridDelegateWithFixedCrossAxisCount作為delegate
GridView.extent() 這個(gè)構(gòu)造方法是在默認(rèn)構(gòu)造器方法的基礎(chǔ)上,指定了代理模式為: SliverGridDelegateWithMaxCrossAxisExtent
2.1 Demo
//GridView_builder_Demo
class GridView_builder_Demo extends StatelessWidget {
  const GridView_builder_Demo({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {

    final double width = MediaQuery.of(context).size.width;
    return GridView.builder(
      gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
        maxCrossAxisExtent: width/3,
        crossAxisSpacing: 8,
        mainAxisSpacing: 8,
        childAspectRatio: 0.8,
      ),
      itemBuilder: (BuildContext context, int index) {
        return Container(
          color:Color.fromARGB(100, Random().nextInt(256),
              Random().nextInt(256), Random().nextInt(256)),
        );
      },
      itemCount: 10,
    );
  }
}


//GridView 的默認(rèn)構(gòu)造器案例 代理2
class GridViewDemo_Constructor2 extends StatelessWidget {
  const GridViewDemo_Constructor2({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final width = (MediaQuery.of(context).size.width)/2;
    return Padding(
      padding: EdgeInsets.only(left: 8, right: 8, top: 8, bottom: 8),
      child: GridView(
        gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
          maxCrossAxisExtent: width,
          crossAxisSpacing: 8,
          mainAxisSpacing: 8,
          childAspectRatio: 0.8,
        ),
        children: List.generate(30, (index) {
          return Container(
            color: Color.fromARGB(100, Random().nextInt(256),
                Random().nextInt(256), Random().nextInt(256)),
            width: 20,
            height: 20,
          );
        }),
      ),
    );
  }
}

//GridView 的默認(rèn)構(gòu)造器案例 代理1
class GridViewDemo_Constructor1 extends StatelessWidget {
  const GridViewDemo_Constructor1({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.only(left: 8, right: 8, top: 8, bottom: 8),
      child: GridView(
        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
          //指定交叉軸上,可以擺放多少個(gè)item
          crossAxisCount: 3,
          //指定item的 寬/高 比例
          childAspectRatio: 1,
          //設(shè)定交叉軸上 item 之間的間隔
          crossAxisSpacing: 8,
          mainAxisSpacing: 8,
        ),
        children: List.generate(30, (index) {
          return Container(
            color: Color.fromARGB(100, Random().nextInt(256),
                Random().nextInt(256), Random().nextInt(256)),
            width: 20,
            height: 20,
          );
        }),
      ),
    );
  }
}
image.png

3.Sliver

  • ListView 繼承于 BoxScrollView ,
  • BoxScrollView 繼承于 ScrollView ,且BoxScrollView是abstract(抽象類)
  • ScrollView 繼承于 StatelessWidget , ScrollView 是 abstract(抽象類)

直接繼承于Widget的類才會(huì)@override(重寫)Widget build(BuildContext context);方法

看一下ScrollView源碼中重寫的build方法

@override
  Widget build(BuildContext context) {


     
    final List<Widget> slivers = buildSlivers(context);
    //這個(gè)才是滾動(dòng)視圖內(nèi),真正可以滾動(dòng)的內(nèi)容
    // 繼續(xù)查源碼,可以發(fā)現(xiàn)buildSlivers();方法在ScrollView內(nèi)沒有實(shí)現(xiàn),需要它的子類去實(shí)現(xiàn)
    // Build the list of widgets to place inside the viewport.Subclasses should override this method to build the slivers for the inside of the viewport.
    // 翻譯: 構(gòu)建要放置在視窗內(nèi)的小部件列表。子類應(yīng)該重寫這個(gè)方法來為視窗內(nèi)部構(gòu)建切片。
   // 那么ScrollView 的子類有哪些呢?
   //1.CustomScrollView 2.BoxScrollView(abstract) 
   // BoxScrollVeiw的子類 : 1. ListView  2.GridView
  

    //后續(xù)實(shí)現(xiàn)部分省略,可自行查看Flutter源碼
  }

BoxScrollVeiw中重寫的 buildSlivers();

@override
  List<Widget> buildSlivers(BuildContext context) {
    Widget sliver = buildChildLayout(context);
    EdgeInsetsGeometry effectivePadding = padding;
    if (padding == null) {
      final MediaQueryData mediaQuery = MediaQuery.of(context, nullOk: true);
      if (mediaQuery != null) {
        // Automatically pad sliver with padding from MediaQuery.
        final EdgeInsets mediaQueryHorizontalPadding =
            mediaQuery.padding.copyWith(top: 0.0, bottom: 0.0);
        final EdgeInsets mediaQueryVerticalPadding =
            mediaQuery.padding.copyWith(left: 0.0, right: 0.0);
        // Consume the main axis padding with SliverPadding.
        effectivePadding = scrollDirection == Axis.vertical
            ? mediaQueryVerticalPadding
            : mediaQueryHorizontalPadding;
        // Leave behind the cross axis padding.
        sliver = MediaQuery(
          data: mediaQuery.copyWith(
            padding: scrollDirection == Axis.vertical
                ? mediaQueryHorizontalPadding
                : mediaQueryVerticalPadding,
          ),
          child: sliver,
        );
      }
    }

    if (effectivePadding != null)
      sliver = SliverPadding(padding: effectivePadding, sliver: sliver);

   // 最后返回的是一個(gè)Widget數(shù)組,數(shù)組元素是sliver,
   // sliver的初始化在上面:
    /// Subclasses should override this method to build the layout model.
    // 子類需要重寫此方法
   // Widget sliver = buildChildLayout(context);
   // 所以BoxScrollVeiw的子類: 1. ListView  2.GridView 重寫這個(gè)方法來實(shí)現(xiàn)滾動(dòng)視圖

    return <Widget>[ sliver ];
  
  }
3.1. Slivers的基本使用

因?yàn)槲覀冃枰押芏嗟腟liver放在一個(gè)CustomScrollView中,所以CustomScrollView有一個(gè)slivers屬性,里面讓我們放對(duì)應(yīng)的一些Sliver:

  • SliverList:類似于我們之前使用過的ListView;
  • SliverFixedExtentList:類似于SliverList只是可以設(shè)置滾動(dòng)的高度;
  • SliverGrid:類似于我們之前使用過的GridView;
  • SliverPadding:設(shè)置Sliver的內(nèi)邊距,因?yàn)榭赡芤獑为?dú)給Sliver設(shè)置內(nèi)邊距;
  • SliverAppBar:添加一個(gè)AppBar,通常用來作為CustomScrollView的HeaderView;
  • SliverSafeArea:設(shè)置內(nèi)容顯示在安全區(qū)域(比如不讓齊劉海擋住我們的內(nèi)容)

使用CustomScrollView演示一下:SliverGrid+SliverPadding+SliverSafeArea的組合

CustomScrollView的參數(shù)/屬性 類型 可選? 作用
scrollDirection Axis (enum) 命名可選 滑動(dòng)方向
controller ScrollController 命名可選 類似于管理器
slivers <Widget>[] 命名可選 被滾動(dòng)視圖顯示的item內(nèi)容
SliverGrid
delegate SliverChildDelegate @required 用來構(gòu)建單元格內(nèi)容的
gridDelegate SliverGridDelegate @required 用來構(gòu)造滾動(dòng)視圖樣式的
class HomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CustomScrollView(
      //滾動(dòng)方向
      scrollDirection: Axis.vertical,
      //item
      slivers: [
        //SliverSafeArea 用來設(shè)置 滾動(dòng)視圖在安全區(qū)域內(nèi)展示,
        SliverSafeArea(
          //SliverPadding ,用來設(shè)置滾動(dòng)視圖中,單元格在橫向和縱向的 paddding
          sliver: SliverPadding(
            padding: EdgeInsets.symmetric(horizontal: 8,vertical: 8),
            sliver: SliverGrid(
              gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                //每一行顯示多少個(gè)
                crossAxisCount: 3,
                //item的 寬/高 比
                childAspectRatio: 1,
                //主軸上,單元格之間的講個(gè)
                mainAxisSpacing: 8,
                //交叉軸上單元格的間隔
                crossAxisSpacing: 8,
              ),
              delegate: SliverChildBuilderDelegate(
                (BuildContext context, int index) {
                  return Container(
                    color: Color.fromARGB(
                        Random().nextInt(256),
                        Random().nextInt(256),
                        Random().nextInt(256),
                        Random().nextInt(256)),
                    child: Text('item${index}',style: TextStyle(color: Colors.red,fontSize: 30),),
                    alignment: Alignment.center,
                  );
                },
                childCount: 30,
              ),
            ),
          ),
        ),
      ],
    );
  }
}
image.png
3.2. Slivers的組合使用

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

class HomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return showCustomScrollView();
  }

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

4.滾動(dòng)的監(jiān)聽

在滾動(dòng)視圖中實(shí)現(xiàn)滾動(dòng)監(jiān)聽,有兩種方案

  • 方案1: 使用controller進(jìn)行監(jiān)聽
  • 方案2: 使用NotificationListener進(jìn)行監(jiān)聽

方案1:demo

class RF_ContentPage extends StatefulWidget {
  const RF_ContentPage({
    Key key,
  }) : super(key: key);

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

class _RF_ContentPageState extends State<RF_ContentPage> {
  //定義一個(gè)ScrollController 對(duì)象,對(duì)滾動(dòng)視圖進(jìn)行管理的
  final ScrollController rfc = ScrollController(initialScrollOffset: 100);
  // bool值,用于判斷是否需要顯示floatbutton
  bool isShowFb = false;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('監(jiān)聽滑動(dòng)'),
      ),
      body: ListView.builder(
        itemBuilder: (BuildContext context, int index) {
          return ListTile(
            leading: Icon(Icons.person),
            title: Text('第 ${index + 1}行'),
          );
        },
        controller: rfc,
      ),
      //floatbutton,根據(jù)isShowFB這個(gè)bool值進(jìn)行判斷顯示
      floatingActionButton: isShowFb ? FloatingActionButton(onPressed: (){
        rfc.animateTo(0, duration: Duration(seconds: 1), curve: Curves.easeInQuad);
      },
        child: Icon(Icons.arrow_upward),
      ) : null,
    );
  }


// 重寫initState方法,對(duì)滾動(dòng)進(jìn)行監(jiān)聽;
  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    //ListView的controller通過添加listener添加滾動(dòng)監(jiān)聽
    rfc.addListener(() {
      print('當(dāng)前偏移量:${rfc.offset}');
      setState(() {
        isShowFb = rfc.offset >= 100;
      });
    });
  }

 @override
  void dispose() {
    // TODO: implement dispose
    super.dispose();
    rfc.dispose();
  }
  
}

方案2demo

class RF_ContentPage extends StatefulWidget {
  const RF_ContentPage({
    Key key,
  }) : super(key: key);

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

class _RF_ContentPageState extends State<RF_ContentPage> {
  final ScrollController rfc = ScrollController(initialScrollOffset: 100);
  bool isShowFb = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('監(jiān)聽滑動(dòng)'),
      ),
      body: NotificationListener(
        onNotification: (ScrollNotification notified) {
          // if(notified is ScrollStartNotification ){
          //   print('開始滾動(dòng)');
          // }else if(notified is ScrollEndNotification){
          //   print('結(jié)束滾動(dòng)');
          // }else if( notified is ScrollUpdateNotification){
          //   print('正在滾動(dòng)');
          // }

          //滾動(dòng)偏移量
          print('滾動(dòng)偏移量:${notified.metrics.pixels}');

          return true;
        },
        child: ListView.builder(
          itemBuilder: (BuildContext context, int index) {
            return ListTile(
              leading: Icon(Icons.person),
              title: Text('第 ${index + 1}行'),
            );
          },
          controller: rfc,
        ),
      ),
      floatingActionButton: isShowFb
          ? FloatingActionButton(
              onPressed: () {
                rfc.animateTo(0,
                    duration: Duration(seconds: 1), curve: Curves.easeInQuad);
              },
              child: Icon(Icons.arrow_upward),
            )
          : null,
    );
  }


 @override
  void dispose() {

    rfc.dispose();
    // TODO: implement dispose
    super.dispose();
    
  }
  
}

總結(jié):
controller:可以直接去的滾動(dòng)的控制對(duì)象,對(duì)齊進(jìn)行控制,但是不能監(jiān)聽滾動(dòng)的開始,結(jié)束狀態(tài)
NotificationListener: 可以獲取各種滾動(dòng)狀態(tài),但是不持有滾動(dòng)的管理對(duì)象

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容