Flutter用NestedScrollView的項目必須知道的坑

時間:2019年06月04日17:09:41

作者:flutter教程網站長

未經允許禁止轉載

 

原文鏈接: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';

import 'package:bh_duomaike_app/util/tools.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),
    );
  }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章