滑动布局-ScrollView、SingleChildScrollView 、ListView 、GridView

本文介绍了Flutter中的滑动布局组件,包括ScrollView的属性如scrollDirection和reverse,SingleChildScrollView的使用,ListView的itemExtent和构建方式,以及GridView的gridDelegate配置,如SliverGridDelegateWithFixedCrossAxisCount和SliverGridDelegateWithMaxCrossAxisExtent。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

滑动布局-ScrollView、SingleChildScrollView 、ListView 、GridView

可滑动的Widget-ScrollView

我在用Column进行布局的时候,由于子节点过多,导致出现了OverFlow的提示,这个就是告诉我们内容超出范围了,这个在Android里来做就不会给你提示了,他会直接截取掉,不显示,为了解决这个错误我第一时间想到的就是像Android一样给他套一个ScrollView,Flutter的ScrollView不是一个可以直接使用的滑动widget,他是一个抽象类,这里记录一下ScrollView的一些属性,也是相当于记录了ScrollView子类的一些共性参数

ScrollView({
  Key key,
  this.scrollDirection = Axis.vertical,
  this.reverse = false,
  this.controller,
  bool primary,
  ScrollPhysics physics,
  this.shrinkWrap = false,
  this.center,
  this.anchor = 0.0,
  this.cacheExtent,
  this.semanticChildCount,
  this.dragStartBehavior = DragStartBehavior.start,
})

scrollDirection: 设置滑动的方向,水平方向或者竖直方向
reverse:是否安装阅读方向相反的方向滑动,默认为false,这个参数就是说我的初始位置是在哪,这个会根据语言环境来改变的,比说国语的阅读方向是从左到右,那么他的起始点就在左边,如果将参数设置为true,表示按阅读方向相反的方向滑动,那么他的起始点就在右边,直接看图可能会好一些

 SingleChildScrollView(
        reverse: true,
        scrollDirection: Axis.horizontal,
        child: Row(
        mainAxisAlignment: MainAxisAlignment.end,
        children: <Widget>[
          Text(
            "编号:",
            style: TextStyle(color: Colors.red),
          ),
          Text("头0000000000001 "),

          Text("0000000000001 00000000000010000000000001000000000000100000000000010000000000001000000000000100000000000010000000000001000000000000100000000000010000000000001000000000000尾")
            ,

        ],
      ),
)

  • 默认的情况,不设置或设置为false的情况下

image-20190627153520267.png

  • 设置为true的情况下,我一进来看到的就是尾部,就是Row中的最后端

image-20190627153357835.png

controller:接收一个ScrollController(默认是用PrimaryScrollController)这个参数是用于控制滚动位置和监听滚动事件,具体用法看后续
primary :是否使用默认的PrimaryScrollController
physics :决定滑动widget如何响应用户的操作,由于滑动列表滑动到边界的时候会出现一些效果,Android与iOS是不一样的,所以我们可以通过这个参数来设置统一效果ClampingScrollPhysics:Android下微光效果。BouncingScrollPhysics:iOS下弹性效果
shrinkWrap:该属性决定是否根据子widget的长度来决定滚动范围,默认情况下为false,默认情况下,滚动范围会尽可能的占据位置,但是如果滚动视图处在于一个无边界的widget中,该参数就必须设置为true
cacheExtent:设置缓存数量,在可见视图的之前和之后

可滑动的widget-SingleChildScrollView

singleChildScrollView他接收一个子类,我们可以把他想成Android中的ScrollViwe,他的参数与ScrollView的一些参数一样,其实ScrollView中的参数我们可以把他当成是滑动widget的一些基本参数

SingleChildScrollView({
  Key key,
  this.scrollDirection = Axis.vertical,
  this.reverse = false,
  this.padding,
  bool primary,
  this.physics,
  this.controller,
  this.child,
  this.dragStartBehavior = DragStartBehavior.start,
})
1234567891011

我们可以看到他的参数基本上在Scrollview中都有,只是多了一个child而已

ListView

ListView是我们用的比较多的,也是比较常见的一个列表控件,他是继承自ScrollView,参数基本一样

ListView({
  Key key,
  Axis scrollDirection = Axis.vertical,
  bool reverse = false,
  ScrollController controller,
  bool primary,
  ScrollPhysics physics,
  bool shrinkWrap = false,
  EdgeInsetsGeometry padding,
  this.itemExtent,
  bool addAutomaticKeepAlives = true,
  bool addRepaintBoundaries = true,
  bool addSemanticIndexes = true,
  double cacheExtent,
  List<Widget> children = const <Widget>[],
  int semanticChildCount,
  DragStartBehavior dragStartBehavior = DragStartBehavior.start,
})
123456789101112131415161718

itemExtent:子widget的高度或者宽度,如果滑动方向是水平的,那么就是子widget的宽度,如果方向是垂直的,那么就是子widget的高度;如果对子widget有特殊的宽高设置要求,我们最好采用itemExtent来设置子widget的宽或者高,因为提前设置好宽高比动态去获取宽高会更高效
addAutomaticKeepAlives:该字段表示是否将列表项包裹在AutomaticKeepAlive Widget中,默认为true,效果就是,列表子widget滑出可见视图时,其状态还会被保持,数据还会保持,并且列表项不会被GC掉
addRepaintBoundaries:表示是否将列表项包裹在RepaintBoundary中,默认为true,如果包裹则列表项不会重绘

  • ListView的用法
  1. 通过构造来创建,通过构造来创建的方式只能用于数据较少的时候,他的children接收一个widget的list,这种方式和singleChildScrollView+线性布局的方式没什么区别,都是先把widget创建好,然后再表现出来,没有达到需要的时候再创建,所以只适合数据量较少的时候
ListView(
	  children: data == null ? _loging() : _item(),
	)
  
  List<Widget> _item() {
    return data.map((item) {
      return Card(
        child: Padding(
          padding: const EdgeInsets.all(10.0),
          child: _getRow(item),
        ),
        elevation: 3.0,
        margin: const EdgeInsets.all(10.0),
      );
    }).toList();
  }
12345678910111213141516
  1. 通过ListView.build来创建,这种方式适合数据项比较多的或者是无限数据(无限数据就是我们请求后端不停的拉数据,这时候的数据是无法确定的)的时候,他是在列表项真正要显示的时候才会被创建
ListView.builder(
    itemCount: 20,
    itemBuilder: (BuildContext context,int index){
  return ListTile(title:Text("ssss"+"$index"));
})
12345

itemCount:条目的数量,如果是无限数据的时候就不要去设置了
itemBuilder:接收一个IndexedWidgetBuilder,返回一个widget,当滑动到对应的index时,返回对应的widget

  1. 通过ListView.separated来创建,这个和ListView.build一样,只是多了分割线的设置separatorBuilder
ListView.separated(
    itemCount: 20,
    separatorBuilder: (BuildContext context,int index){
      return Divider(color: Colors.red,);
    },
    itemBuilder: (BuildContext context,int index){
  return ListTile(title:Text("ssss"+"$index"));
})
12345678

separatorBuilder:分割线Builder,返回一个分割线

  • ListVIew上拉加载于下拉刷新
class home extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    // TODO: implement createState
    return HomeState();
  }
}

class myState extends State {
  List data;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    _getItemData();
  }

  void _getItemData() async {
    await http
        .get("http://www.wanandroid.com/project/list/1/json?cid=1")
        .then((http.Response response) {
      var convJson = json.decode(response.body);
      convJson = convJson["data"]["datas"];
      print(convJson);

      setState(() {
        data = convJson;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
      appBar: AppBar(
        title: Text("网络数据列表"),
      ),
      body: ListView(
        children: data == null ? _loging() : _item(),
      ),
    );
  }

  List<Widget> _item() {
    return data.map((item) {
      return Card(
        child: Padding(
          padding: const EdgeInsets.all(10.0),
          child: _getRow(item),
        ),
        elevation: 3.0,
        margin: const EdgeInsets.all(10.0),
      );
    }).toList();
  }

  Widget _getRow(item) {
    return Row(
      children: <Widget>[
        Column(
          children: <Widget>[
            Text(
              "${item["title"]}",
              style: TextStyle(color: Colors.blue, fontSize: 20.0),
            ),
            Text(
              "${item["desc"]}",
              maxLines: 3,
            ),
          ],
        ),
        ClipRect(
          child: FadeInImage.assetNetwork(
            placeholder: "images/lake.jpg",
            image: "${item["envelopePic"]}",
            width: 50.0,
            height: 50.0,
            fit: BoxFit.fitWidth,
          ),
        )
      ],
    );
  }

  List<Widget> _loging() {
    return <Widget>[
      Container(
          height: 300.0,
          child: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                CircularProgressIndicator(
                  strokeWidth: 1.0,
                ),
                Text("记载中。。。")
              ],
            ),
          ))
    ];
  }
}

class HomeState extends State {
  List data;

  ScrollController _scrollController = ScrollController();

  int page = 1;

  bool isLoad = false;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();

    _pullNet(); //默认第一次进入的时候请求数据

    _scrollController.addListener(() {
      //滑动监听
      if (_scrollController.position.pixels ==
          _scrollController.position.maxScrollExtent) {
        print("滑动到底部");
        page++;
        _pullNet(); //调用加载更多方法,这里我们可以传入page,此处随意写个方法代替下
      }
    });
  }

  void _pullNet() async {
    if (!isLoad) {
      isLoad = true;
      await http
          .get(
              "http://www.wanandroid.com/project/list/1/json?cid=1")
          .then((http.Response response) {
        var converDatajson = json.decode(response.body);
        converDatajson = converDatajson["data"]["datas"];
        print(converDatajson);

        setState(() {
          List listData = converDatajson; //每次请求到的数据
//        data.insertAll(data.length - 1, listData);
          if(page==1){
            data=listData;
          }else {
            data.addAll(listData);
          }
          isLoad = false; //将正在加载中的标示改回来
        });
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
        appBar: AppBar(
          title: Text("listview"),
        ),
        body: data == null
            ? _loading()
            : RefreshIndicator(
                child: ListView.separated(
                  itemCount: data.length + 1, //加1的目的就是为了能够在底部给他加上一个加载中的widget
                  separatorBuilder: (BuildContext context, int index) {
                    //设置一个灰色的分割线
                    return Divider(
                      color: Colors.grey,
                    );
                  },
                  itemBuilder: (BuildContext context, int index) {
                    if (index > data.length-1) {
                      //当index大于数据长度的时候我们返回一个转圈圈的widget
                      //当前是最后一个widget
                      return Container(
                          //给条目返回一个转圈圈的加载widget
                          alignment: Alignment.center,
                          padding: const EdgeInsets.all(6.0),
                          child: SizedBox(
                            width: 30.0,
                            height: 30.0,
                            child: CircularProgressIndicator(
                              strokeWidth: 2.0,
                            ),
                          ));
                    }else {
                      return _item(index); //如果数据不大于,那么直接返回列表项
                    }
                  },
                  controller: _scrollController, //添加一个滑动控制器
                ),
                onRefresh: _RefreshPullNet, //下拉请求数据
              ));
  }

  Future<Null> _RefreshPullNet() async{
    //下拉刷新的时候将page改为1
    page = 1;
    _pullNet();
  }

  Widget _item(index) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(10.0),
        child: _getRowWidget(data[index]),
      ),
      elevation: 3.0,
      margin: const EdgeInsets.all(10.0),
    );
  }

  Widget _getRowWidget(item) {
    return Row(
      children: <Widget>[
        Flexible(
          flex: 1,
          fit: FlexFit.tight, //这里就是相当于matchParent
          child: Stack(
            children: <Widget>[
              Column(
                children: <Widget>[
                  Text(
                    "${item["title"]}".trim(),
                    style: TextStyle(color: Colors.blue, fontSize: 20.0),
                    textAlign: TextAlign.left,
                  ),
                  Text(
                    "${item["desc"]}".trim(),
                    maxLines: 3,
                  )
                ],
              )
            ],
          ),
        ),
        ClipRect(
          child: FadeInImage.assetNetwork(
            placeholder: "images/lake.jpg",
            image: "${item["envelopePic"]}",
            width: 50.0,
            height: 50.0,
            fit: BoxFit.fitWidth,
          ),
        ),
      ],
    );
  }

  Widget _loading() {
    return
      Container(
        height: 300.0,
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              CircularProgressIndicator(
                strokeWidth: 1.0,
              ),
              Text("加载中..."),
            ],
          ),
        ),
      );
  }

  @override
  void dispose() {
    // TODO: implement dispose
    super.dispose();
    _scrollController.dispose(); //记得将scrollController dispose掉
  }
}

GridView

GridView与ListView一样都是我们用的比较多的滑动列表,他们都是继承自ScrollView,他的效果和Android的GridView是一样的,我们创建一个三列的GridView,子节点有listdata.length个(listdata为请求到的数据),使用如下

第一种:

GridView.count(
  crossAxisCount: 3,
  children: listData.map((item) {
    return Text("${item["SS"]}");
  }).toList(),
)
123456

第二种:

GridView.count(
    crossAxisCount: 3,
    children: List.generate(
      listData.length,
      (index) {
        return Text("${listData[index]}");
      },
    ))
12345678

这两种方法其实都是返回一个widget集合,效果是一样的,GridView是继承自ScrollView的,所以参数基本上我们可以
GridView总共提供了四种调用方式给我们,我们可以根据自己的需要去选择调用

GridView.builder  //数据量较多的时候采用此种方式
GridView.custom
GridView.count    //内部就是SliverGridDelegateWithFixedCrossAxisCount算法
GridView.extent    //内部就是SliverGridDelegateWithMaxCrossAxisExtent算法
1234

GridView与ListView的差距在于设置网格,我们关注一下gridDelegate这个参数,他是用于控制gridView的子节点的展示,flutter给我们提供了两个可选项,SliverGridDelegateWithFixedCrossAxisCount和SliverGridDelegateWithMaxCrossAxisExtent
SliverGridDelegateWithFixedCrossAxisCount:实现了一个横轴子节点为固定数量的算法

SliverGridDelegateWithFixedCrossAxisCount({
  @required double crossAxisCount, 
  double mainAxisSpacing = 0.0,
  double crossAxisSpacing = 0.0,
  double childAspectRatio = 1.0,
})
123456

crossAxisCount:横轴元素的数量
mainAxisSpacing :主轴方向的间距
crossAxisSpacing:横轴方向子元素的间距
childAspectRatio:子元素在横轴长度和主轴长度的比例
SliverGridDelegateWithMaxCrossAxisExtent:实现了一个横轴子节点为固定最大长度的算法

SliverGridDelegateWithMaxCrossAxisExtent({
  double maxCrossAxisExtent,
  double mainAxisSpacing = 0.0,
  double crossAxisSpacing = 0.0,
  double childAspectRatio = 1.0,
})
123456

maxCrossAxisExtent:设置子节点的最大长度,就是通过限制子节点的最大长度来达到每一行能展示的条目

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值