1. 程式人生 > >Flutter 擴充套件NestedScrollView (一)Pinned頭引起的bug解決

Flutter 擴充套件NestedScrollView (一)Pinned頭引起的bug解決

為什麼想要自己來定義NestedScrollView呢?

要從我提交的2個issue開始講:

1.當中的Pinned為true的Sliver元件對body裡面滾動元件的影響

2.當在裡面放上tabview,並且tab是快取狀態的時候,會出現滾動會互相影響的問題

沒有任何進展,用一個表情表達Flutter小組的意思

不過還好,有原始碼,還好我喜歡看原始碼。。 這一篇的篇幅估計很多,請先買好瓜子汽水前排坐好,開車了。。

NestedScrollView 是一個複雜的元件,它跟Sliver 系列是一夥的,最下層是個CustomScrollView.

Sliver系列的東東很多,我們下面來一一介紹一下。

CustomScrollView

是Sliver元件的老祖宗,全部的Sliver都放在這個裡面。

SliverList, which is a sliver that displays linear list of children.

SliverFixedExtentList, which is a more efficient sliver that displays linear list of children that have the same extent along the scroll axis. 比SliverList多一個就是相同的行高。這樣效能會更好

SliverPrototypeExtentList

SliverPrototypeExtentList arranges its children in a line along the main axis starting at offset zero and without gaps. Each child is constrained to the same extent as the prototypeItem along the main axis and the SliverConstraints.crossAxisExtent along the cross axis.

SliverGrid, which is a sliver that displays a 2D array of children. 可以設定每行的個數的Grid

SliverPadding, which is a sliver that adds blank space around another sliver.

SliverPersistentHeader A sliver whose size varies when the sliver is scrolled to the leading edge of the viewport. This is the layout primitive that SliverAppBar uses for its shrinking/growing effect.

非常好用的元件,SliverAppBar就是用這個實現的。這個元件的特點是可以創建出隨著滑動變化的可以Pinned的元素,大家經常用的什麼吸頂元件可以用這個很方便的構建,後面我會使用這個寫一個自定義效果的SliverAppbar。

SliverAppBar, which is a sliver that displays a header that can expand and float as the scroll view scrolls.

SliverToBoxAdapter 當你想把一個非Sliver的Widget放在CustomScrollview裡面的時候,你需要用這個包裹一下。

SliverSafeArea A sliver that insets another sliver by sufficient padding to avoid intrusions by the operating system. For example, this will indent the sliver by enough to avoid the status bar at the top of the screen.為了防止各種邊界的越界,比如說越過頂部的狀態列

SliverFillRemaining sizes its child to fill the viewport in the cross axis and to fill the remaining space in the viewport in the main axis. 使用這個它會填充完剩餘viewport裡面的全部空間

SliverOverlapAbsorber,SliverOverlapAbsorberHandle 這個上面2個是官方專門為了解決我們今天主角NestedScrollView中Pinned 元件對Body 裡面Scroll 狀態影響的,但官方做的不夠完美。

看原始碼是一件好玩的事情,大家跟我一起來吧。 flutter\packages\flutter\lib\src\widgets\nested_scroll_view.dart

首先我們看看第一個問題,從官方文件中的Sample可以看到NestedScrollView

DefaultTabController(
  length: _tabs.length, // This is the number of tabs.
  child: NestedScrollView(
    headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
      // These are the slivers that show up in the "outer" scroll view.
      return <Widget>[
        SliverOverlapAbsorber(
          // This widget takes the overlapping behavior of the SliverAppBar,
          // and redirects it to the SliverOverlapInjector below. If it is
          // missing, then it is possible for the nested "inner" scroll view
          // below to end up under the SliverAppBar even when the inner
          // scroll view thinks it has not been scrolled.
          // This is not necessary if the "headerSliverBuilder" only builds
          // widgets that do not overlap the next sliver.
          handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
          child: SliverAppBar(
            title: const Text('Books'), // This is the title in the app bar.
            pinned: true,
            expandedHeight: 150.0,
            // The "forceElevated" property causes the SliverAppBar to show
            // a shadow. The "innerBoxIsScrolled" parameter is true when the
            // inner scroll view is scrolled beyond its "zero" point, i.e.
            // when it appears to be scrolled below the SliverAppBar.
            // Without this, there are cases where the shadow would appear
            // or not appear inappropriately, because the SliverAppBar is
            // not actually aware of the precise position of the inner
            // scroll views.
            forceElevated: innerBoxIsScrolled,
            bottom: TabBar(
              // These are the widgets to put in each tab in the tab bar.
              tabs: _tabs.map((String name) => Tab(text: name)).toList(),
            ),
          ),
        ),
      ];
    },
    body: TabBarView(
      // These are the contents of the tab views, below the tabs.
      children: _tabs.map((String name) {
        return SafeArea(
          top: false,
          bottom: false,
          child: Builder(
            // This Builder is needed to provide a BuildContext that is "inside"
            // the NestedScrollView, so that sliverOverlapAbsorberHandleFor() can
            // find the NestedScrollView.
            builder: (BuildContext context) {
              return CustomScrollView(
                // The "controller" and "primary" members should be left
                // unset, so that the NestedScrollView can control this
                // inner scroll view.
                // If the "controller" property is set, then this scroll
                // view will not be associated with the NestedScrollView.
                // The PageStorageKey should be unique to this ScrollView;
                // it allows the list to remember its scroll position when
                // the tab view is not on the screen.
                key: PageStorageKey<String>(name),
                slivers: <Widget>[
                  SliverOverlapInjector(
                    // This is the flip side of the SliverOverlapAbsorber above.
                    handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
                  ),
                  SliverPadding(
                    padding: const EdgeInsets.all(8.0),
                    // In this example, the inner scroll view has
                    // fixed-height list items, hence the use of
                    // SliverFixedExtentList. However, one could use any
                    // sliver widget here, e.g. SliverList or SliverGrid.
                    sliver: SliverFixedExtentList(
                      // The items in this example are fixed to 48 pixels
                      // high. This matches the Material Design spec for
                      // ListTile widgets.
                      itemExtent: 48.0,
                      delegate: SliverChildBuilderDelegate(
                        (BuildContext context, int index) {
                          // This builder is called for each child.
                          // In this example, we just number each list item.
                          return ListTile(
                            title: Text('Item $index'),
                          );
                        },
                        // The childCount of the SliverChildBuilderDelegate
                        // specifies how many children this inner list
                        // has. In this example, each tab has a list of
                        // exactly 30 items, but this is arbitrary.
                        childCount: 30,
                      ),
                    ),
                  ),
                ],
              );
            },
          ),
        );
      }).toList(),
    ),
  ),
)
複製程式碼

可以看到官方用一個SliverOverlapAbsorber包裹了SliverAppbar,在下面body裡面,每一個list的上面都加了個SliverOverlapInjector。實際效果就是SliverOverlapInjector的高度就等於SliverAppbar的Pinned的高度。 如果不加入這些程式碼,當body裡面的list滾動到SliverAppbar下方的時候。。依然可以繼續向上滾動,也就是說body的滾動最上面點為0,而不是SliverAppbar的Pinned 高度。

為什麼會出現這種情況呢? 這要從Sliver的老祖宗CustomScrollView說起來。可能很多人發現,這些Sliver widgets(可以滾動的那種)沒有ScrollController這個東西(CustomScrollview和NestedScrollView除外)。其實當你把Sliver Widgets(可以滾動的那種)放到CustomScrollView裡面的時候將由CustomScrollView來統一處理各種Sliver Widgets(可以滾動的那種),每個Sliver Widgets(可以滾動的那種)都會attach 各自的ScrollPosition。比如說第一個列表滾動到頭了,第2個列表就會開始處理對應的ScrollPosition,將出現在viewport裡面的元素render出來。

在我們的主角NestedScrollView當中,有2個ScrollController.

class _NestedScrollController extends ScrollController {
  _NestedScrollController(
      this.coordinator, {
        double initialScrollOffset = 0.0,
        String debugLabel,
複製程式碼

一個是inner,一個outer。 outer是負責headerSliverBuilder裡面的滾動widgets inner是負責body裡面的滾動widgets 當outer滾動到底了之後,就會看看inner裡面是否有能滾動的東東,開始滾動。

為了解決1問題,我們這裡需要來處理outer這個ScrollController裡面控制的_NestedScrollPosition,問題1在於,當header裡面有多個pinned的widget的時候,我們outer能滾動的extent。應該要去減掉這個pinned的總的高度。這樣當滾動到pinned的元件下方的時候。我們就會開始滾動inner。

在_NestedScrollPosition 裡面

// The _NestedScrollPosition is used by both the inner and outer viewports of a
// NestedScrollView. It tracks the offset to use for those viewports, and knows
// about the _NestedScrollCoordinator, so that when activities are triggered on
// this class, they can defer, or be influenced by, the coordinator.
class _NestedScrollPosition extends ScrollPosition
    implements ScrollActivityDelegate {
  _NestedScrollPosition({
    @required ScrollPhysics physics,
    @required ScrollContext context,
    double initialPixels = 0.0,
    ScrollPosition oldPosition,
    String debugLabel,
    @required this.coordinator,
  }) : super(
複製程式碼

我override了applyContentDimensions方法

 @override
  bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
    if (debugLabel == 'outer' &&
        coordinator.pinnedHeaderSliverHeightBuilder != null) {
      maxScrollExtent =
          maxScrollExtent - coordinator.pinnedHeaderSliverHeightBuilder();
      maxScrollExtent = math.max(0.0, maxScrollExtent);
    }
    return super.applyContentDimensions(minScrollExtent, maxScrollExtent);
  }
複製程式碼

pinnedHeaderSliverHeightBuilder是我從最外層傳遞進來的用於獲取當時Pinned 為true的全部Sliver header的高度。。在這裡把outer最大的滾動extent減去了Pinned 的總的高度,這樣我們就完美解決了問題.1

Sample code

在我的demo裡面。pinned 的高度 由 status bar + appbar + 1個或者2個tabbar 組成。這裡為什麼要用個function而不是直接傳遞個算好的高度呢?因為在我的case裡面這個pinned的高度是會改變的。

 var tabBarHeight = primaryTabBar.preferredSize.height;
    var pinnedHeaderHeight =
        //statusBa height
        statusBarHeight +
            //pinned SliverAppBar height in header
            kToolbarHeight +
            //pinned tabbar height in header
            (primaryTC.index == 0 ? tabBarHeight * 2 : tabBarHeight);
    return NestedScrollViewRefreshIndicator(
      onRefresh: onRefresh,
      child: extended.NestedScrollView(
        headerSliverBuilder: (c, f) {
          return _buildSliverHeader(primaryTabBar);
        },
        //
        pinnedHeaderSliverHeightBuilder: () {
          return pinnedHeaderHeight;
        },
複製程式碼

最後放上 Github extended_nested_scroll_view,如果你有更好的方式解決這個問題或者有什麼不明白的地方,都請告訴我,由衷感謝。