flutter用NestedScrollView的项目必须知道的坑

时间:2019年06月04日17:09:41

作者:CrazyQ1

转载请注名

本人来自:http://www.flutterj.com/

做企业项目遇到了个坑,

那这个坑是怎么遇到的呢,刚开始是已经做好了商品详情页:

详情页面用的是NestedScrollView组件,轮播图那一块用的是SliverAppBar,

也就是写在NestedScrollView的头部,然后下面的都是在身体部分了,

身体部分是可以滑动的,刚开始是没任何问题,正常滑动运行,

但是来了这个需求

是在商品详情加个tabbar,然后我就加在SliverAppBar里面的bottom内个,

加上去显示也是没什么问题,但是锚点这个需求实现的时候就来了问题了。

大家都知道,想要锚点(jumpTo到指定位置),嘚让他的body也加个控制器啊,

然后我就把之前给的滚动组件

new SingleChildScrollView(

  child: new Column(children: widget.widgets),

);

改成了

new ListView(children: widget.widgets);

虽然SingleChildScrollView也是可以加控制器并且jumpTo的,

但是我感觉用ListView比较舒服,代码也比较简洁,所以就用这个,

但是用哪个实现的效果都是差不多的。

然鹅

惊人的一幕就出现了。

NestedScrollView的头部内容完全固定,滑动body部分是不能控制到头部的,

但是滑动头部就是可以控制头部,

也就是头部和身体部分 分开了。

这是为什么呢?

因为NestedScrollView是有内外两个控制器的:

out控制header,inner控制body。只有当out不能滚动了才会滚动inner

body不写控制器就没事,写了就出现这种情况,

而且我去测试了下打印控制器最大滚动位置发现只有300左右,

也就是只能打印出头部的,

print(_C.position.maxScrollExtent);

那我要怎么去实现这个功能啊,只能在轮播图内跳来跳去,

难道是贫穷限制了我的想象吗?

头部固定解决方案:(不是唯一的)

既然都说了是有内外两个控制器那我们一定有办法来获取并使用他的内部控制器,

第一步:(尝试封装body为有状态类来从context中取到内控制器)

@override

  Widget build(BuildContext context) {

    return new Scaffold(

      body: new NestedScrollView(

          controller: _ctl,

          headerSliverBuilder: _sliverBuilder,

          body: new BodyView(widget.widgets, type)),

    );

  }

BodyView就是我们封装的,

class BodyView extends StatefulWidget {

  final List<Widget> widgets;

  final int type;

  BodyView(this.widgets, this.type);

  @override

  _BodyViewState createState() => _BodyViewState();

}

class _BodyViewState extends State<BodyView> {

  @override

  Widget build(BuildContext context) {

    return new SingleChildScrollView(

      child: new Column(children: widget.widgets),

    );

  }

}

第二步:(type是干啥的先不用管)

class BodyView extends StatefulWidget {

  ...

}

class _BodyViewState extends State<BodyView> {

  Type typeOf<T>() => T;

  ScrollController _innerC;

  @override

  void initState() {

    super.initState();

    PrimaryScrollController primaryScrollController =

        context.ancestorWidgetOfExactType(typeOf<PrimaryScrollController>());

    _innerC = primaryScrollController.controller;

  }

  @override

  Widget build(BuildContext context) {

    ...

  }

}

我们定义了一个类型和控制器,然后再初始化的时候写了一个主控制器,

主控制器的值是从上下文的父类取的类型,然后typeOf的泛型就是我们写的

主控制器,那么内控制器就是等于我们取到的这个控制器,

头部固定问题就完美解决了

只要能取到,就算不用也是可以的

当然也可以直接使用:

@override

Widget build(BuildContext context) {

  _actions(widget.type);

  return PrimaryScrollController(controller: _innerC, child: new SingleChildScrollView(

    child: new Column(children: widget.widgets),

  ));

}

这个都无所谓的。

但是我们发现两个控制器开始分开的,打印外控制器最大滚动还是300左右,

但是打印内控制器最大滚动位置是body的全部,2k左右,

那么我这个需求还有没有解决方案了?

当然是有的:

点击锚点跳转解决方案

第一步(直接使用外部控制器jumpTo

@override

void initState() {

  super.initState();

  tabs = ['商品', '评价', '详情'];

  _tabC = new TabController(length: tabs.length, vsync: this);

  _tabC.addListener(() => _onTabChanged());

}

_onTabChanged() {

  setState(() {

    switch (_tabC.index) {

      case 0:

        _ctl.jumpTo(0.1);

        type = 0;

        break;

      case 1:

        type = 1;

        break;

      case 2:

        type = 2;

        break;

    }

  });

}

_tabC就是外部控制器,在初始化的时候监听tabbar是否被点击,

如果被点击的话直接写个tab改变的方法,tabbar的三个Bar分别是0,1,2,

所以我们也接收一个0,1,2,来处理,

然后直接给它jumpTo跳转,然后那个type就是我们的BodyView接收的

具体有什么用呢?

class BodyView extends StatefulWidget {

...

}

class _BodyViewState extends State<BodyView> {

  ...

  _actions(int type) {

    setState(() {

      _binding.addPostFrameCallback((callback) {

        switch (type) {

          case 1:

            _innerC.jumpTo(1000);

            print(_innerC.position.maxScrollExtent);

            break;

          case 2:

            _innerC.jumpTo(2000);

            break;

        }

      });

    });

  }

  @override

  void initState() {

    ...

  }

  @override

  Widget build(BuildContext context) {

    _actions(widget.type);

    ...

  }

}

我们可以看到,这边也是监听接收的int类型,

如果监听到传过来的是0的话就调到我们的顶部,(heard控制器控制)

如果监听到传过来的是1的话就调到我们想要到的评论的位置。

如果监听到传过来的是2的话就跳到我们想要的商品详情的位置。

Position为null的解决方案

当我以为这样就没问题的时候发现又出现了一个错误,

真的是坑一个接着一个啊,

解决方案为:

调用第一帧绘制完毕之后再执行jumpTo

具体:

class BodyView extends StatefulWidget {

    ...

}

class _BodyViewState extends State<BodyView> {

  WidgetsBinding _binding = WidgetsBinding.instance;

  _actions(int type) {

    setState(() {

      _binding.addPostFrameCallback((callback) {

        ...

    });

  }

}

我们写了一个小部件绑定的东西,让他能监听第一帧是否绘制完毕,

绘制完毕之后再执行jumpTo

这样就只差获取评论和商品详情的组件位置然后传入具体的Offset就完美执行了,

因为时间关系就到这了,任何问题可以加我微信:zonggeyl_com来问我。

接下来我把我这个文件的整体代码发出来,能看的懂的可以看一下,

直接运行肯定是不能运行的,因为里面调用的资源文件和封装你们都没有,

要懂查看和使用,

import 'package:flutter/material.dart';

class SliverAppBarPage extends StatefulWidget {

  SliverAppBarPage({

    this.widgets,

    this.headerView,

    this.height = 200,

    this.background,

  });

  final List<Widget> widgets;

  final Widget headerView;

  final Widget background;

  final double height;

  @override

  State<StatefulWidget> createState() => new SliverAppBarPageState();

}

class SliverAppBarPageState extends State<SliverAppBarPage>

    with TickerProviderStateMixin {

  TabController _tabC;

  ScrollController _ctl = new ScrollController();

  int type;

  List tabs;

  WidgetsBinding _binding = WidgetsBinding.instance;

  @override

  void initState() {

    super.initState();

    tabs = ['商品', '评价', '详情'];

    _tabC = new TabController(length: tabs.length, vsync: this);

    _tabC.addListener(() => _onTabChanged());

  }

  _onTabChanged() {

    setState(() {

      switch (_tabC.index) {

        case 0:

          _binding.addPostFrameCallback((callback) => _ctl.jumpTo(0.1));

          type = 0;

          break;

        case 1:

          type = 1;

          break;

        case 2:

          type = 2;

          break;

      }

    });

  }

  List<Widget> _sliverBuilder(BuildContext context, bool innerBoxIsScrolled) {

    return <Widget>[

      new SliverAppBar(

        centerTitle: true,

        expandedHeight: widget.height,

        floating: false,

        pinned: true,

        backgroundColor: Colors.white,

        elevation: 0,

        brightness: Brightness.light,

        leading: new InkWell(

          child: innerBoxIsScrolled

              ? new Container(

                  width: 15,

                  height: 20.0,

                  child: new Image.asset('assets/images/nav_ic_back.webp',

                      color: innerBoxIsScrolled ? mainFontColor : Colors.white),

                )

              : new Container(

                  padding: EdgeInsets.only(left: 10.0),

                  alignment: Alignment.center,

                  child: new Container(

                    height: 35,

                    width: 35,

                    decoration: BoxDecoration(

                        color: Color.fromRGBO(0, 0, 0, 0.2),

                        borderRadius: BorderRadius.circular(17.5)),

                    child: new Image.asset('assets/images/nav_ic_back.webp',

                        color:

                            innerBoxIsScrolled ? mainFontColor : Colors.white),

                  ),

                ),

          onTap: () => Navigator.pop(context),

        ),

        title: new Text(

          innerBoxIsScrolled ? '商品详情' : '',

          style: TextStyle(color: Color(0xff000000), fontSize: 19.0),

        ),

        bottom: innerBoxIsScrolled

            ? new PreferredSize(

                child: new Container(

                  padding: EdgeInsets.symmetric(horizontal: 80.0),

                  child: new TabBar(

                      controller: _tabC,

                      indicatorSize: TabBarIndicatorSize.label,

                      labelColor: Color(0xffFF4F73),

                      indicatorColor: Color(0xffFF4F73),

                      unselectedLabelColor: Color(0xff000000),

                      labelStyle: new TextStyle(fontSize: 14.0),

                      labelPadding: EdgeInsets.only(bottom: 20),

                      indicatorPadding: EdgeInsets.only(

                          bottom: 15, top: 10, left: 5, right: 5.0),

                      tabs: tabs.map((item) => new Text('$item')).toList()),

                ),

                preferredSize: Size(30, 50))

            : null,

        actions: <Widget>[],

        flexibleSpace: new FlexibleSpaceBar(

            centerTitle: true,

            title: widget.headerView,

            background: widget.background),

      ),

    ];

  }

  @override

  Widget build(BuildContext context) {

    return new Scaffold(

      body: new NestedScrollView(

          controller: _ctl,

          headerSliverBuilder: _sliverBuilder,

//        body: new SingleChildScrollView(

//          controller: _ctl,

//            child: new Column(children: widget.widgets)),

//      ),

          body: new BodyView(widget.widgets, type)),

    );

  }

}

class BodyView extends StatefulWidget {

  final List<Widget> widgets;

  final int type;

  BodyView(this.widgets, this.type);

  @override

  _BodyViewState createState() => _BodyViewState();

}

class _BodyViewState extends State<BodyView> {

  Type typeOf<T>() => T;

  ScrollController _innerC;

  WidgetsBinding _binding = WidgetsBinding.instance;

  _actions(int type) {

    setState(() {

      _binding.addPostFrameCallback((callback) {

        switch (type) {

          case 1:

            _innerC.jumpTo(1000);

            print(_innerC.position.maxScrollExtent);

            break;

          case 2:

            _innerC.jumpTo(2000);

            break;

        }

      });

    });

  }

  @override

  void initState() {

    super.initState();

    PrimaryScrollController primaryScrollController =

        context.ancestorWidgetOfExactType(typeOf<PrimaryScrollController>());

    _innerC = primaryScrollController.controller;

  }

  @override

  Widget build(BuildContext context) {

    _actions(widget.type);

    return new SingleChildScrollView(

      child: new Column(children: widget.widgets),

    );

  }

}


关注公众号“Flutter前线”,各种Flutter项目实战经验技巧,干活知识,Flutter面试题答案,等你来领取。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343