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


時間:2019年06月04日17:09:41
做者:flutter教程網站長
未經容許禁止轉載

原文連接:www.flutterj.com/web


作企業項目遇到了個坑,
那這個坑是怎麼遇到的呢,剛開始是已經作好了商品詳情頁:


詳情頁面用的是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 widgets;
  final int type;

  BodyView(this.widgets, this.type);

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

class _BodyViewState extends State {
  @override
  Widget build(BuildContext context) {
    return new SingleChildScrollView(
      child: new Column(children: widget.widgets),
    );
  }
}複製代碼


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

class BodyView extends StatefulWidget {
  ...
}

class _BodyViewState extends State {
  Type typeOf() => T;
  ScrollController _innerC;

  @override
  void initState() {
    super.initState();
    PrimaryScrollController primaryScrollController =
        context.ancestorWidgetOfExactType(typeOf());
    _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 {
  ...
  
  _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 {
  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 widgets;
  final Widget headerView;
  final Widget background;
  final double height;

  @override
  State createState() => new SliverAppBarPageState();
}

class SliverAppBarPageState extends State 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 _sliverBuilder(BuildContext context, bool innerBoxIsScrolled) {
    return [
      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: [],
        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 widgets;
  final int type;

  BodyView(this.widgets, this.type);

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

class _BodyViewState extends State {
  Type typeOf() => 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());
    _innerC = primaryScrollController.controller;
  }

  @override
  Widget build(BuildContext context) {
    _actions(widget.type);
    return new SingleChildScrollView(
      child: new Column(children: widget.widgets),
    );
  }
}複製代碼
相關文章
相關標籤/搜索