Flutter - 按部就班 Sliver

列表組件在移動端上尤其重要,Sliver 做爲 Flutter 列表組件中重要的一部分,開發者很是有必要了解 Sliver 的原理和用法。android

兩種類型的佈局

Flutter 的佈局能夠分爲兩種:數組

  • Box ( RenderBox ): 2D 繪製佈局
  • Sliver ( RenderSliver ):滾動佈局

重要的概念

Sliver

Sliver 是 Flutter 中的一個概念,表示可滾動佈局中的一部分,它的 child 能夠是普通的 Box Widget。緩存

ViewPort

  • ViewPort 是一個顯示窗口,它內部可包含多個 Sliver;
  • ViewPort 的寬高是肯定的,它內部 Slivers 的寬高之和是能夠大於自身的寬高的;
  • ViewPort 爲了提升性能採用懶加載機制,它只會繪製可視區域內容 Widget。

ViewPort 有一些重要屬性:bash

class Viewport extends MultiChildRenderObjectWidget {
  /// 主軸方向
  final AxisDirection axisDirection;
  /// 縱軸方向
  final AxisDirection crossAxisDirection;
  /// center 決定 viewport 的 zero 基準線,也就是 viewport 從哪一個地方開始繪製,默認是第一個 sliver
  /// center 必須是 viewport slivers 中的一員的 key
  final Key center;
  
/// 錨點,取值[0,1],和 zero 的相對位置,好比 0.5 表明 zero 被放到了 Viewport.height / 2 處
  final double anchor;
  /// 滾動的累計值,確切的說是 viewport 從什麼地方開始顯示
  final ViewportOffset offset;
  /// 緩存區域,也就是相對有頭尾須要預加載的高度
  final double cacheExtent;
  /// children widget
  List<Widget> slivers;
  }
複製代碼

一圖勝千言:markdown

上圖中假設每一個 sliver 的 height 都相等且等於屏幕高度的 ⅕,這樣設置 center = sliver1,屏幕的第一個顯示的應該是 sliver 1,可是由於 anchor = 0.2,0.2 * viewport.height 正好等於 sliver1 的高度,因此屏幕上顯示的第一個是 sliver 2。app

ScrollPostion

ScrollPosition 決定了 Viewport 哪些區域是可見的,它包含了Viewport 的滾動信息,它的主要成員變量以下:dom

abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
  // 滾動偏移量
  double _pixels;
  // 設置滾動響應效果,好比滑動中止後的動畫
  final ScrollPhysics physics;
  // 保存當前的滾動偏移量到 PageStore 中,當 Scrollable 重建後能夠恢復到當前偏移量
  final bool keepScrollOffset;
  // 最小滾動值
  double _minScrollExtent;
  // 最大滾動值
  double _maxScrollExtent;
  ...
}
複製代碼

ScrollPosition 的類繼承關係以下:ide

|-- Listenable
|---- ChangeNotifier
|------ ScrollPosition
|-------- ScrollPositionWithSingleContext
複製代碼

因此 ScrollPosition 能夠做爲被觀察者,當數據改變的時候能夠通知觀察者。佈局

Scrollable

Scrollable 是一個可滾動的 Widget,它主要負責:性能

  • 監聽用戶的手勢,計算滾動狀態發出 Notification
  • 計算 offset 通知 listeners

Scrollable 自己不具備繪製內容的能力,它經過構造注入的 viewportBuilder 來建立一個 Viewport 來顯示內容,當滾動狀態變化的時候,Scrollable 就會不斷的更新 Viewport 的 offset ,Viewport 就會不斷的更新顯示內容。

Scrollable 主要結構以下:

Widget result = _ScrollableScope(
      scrollable: this,
      position: position,
      child: Listener(
        onPointerSignal: _receivedPointerSignal,
        child: RawGestureDetector(
          gestures: _gestureRecognizers,
          ...,
          child: Semantics(
            ...
            child: IgnorePointer(
			...
              child: widget.viewportBuilder(context, position),
            ),
          ),
        ),
      ),
    );
複製代碼
  • _ScrollableScope 繼承自 InheritedWidget,這樣它的 children 能夠方便的獲取 scrollable 和 position;
  • RawGestureDetector 負責手勢監聽,手勢變化時會回調 _gestureRecognizers;
  • viewportBuilder 會生成 viewport;

SliverConstraints

和 Box 佈局使用 BoxConstraints 做爲約束相似,Sliver 佈局採用 SliverConstraints 做爲約束,但相對於 Box 要複雜的多,能夠理解爲 SliverConstraints 描述了 Viewport 和它內部的 Slivers 之間的佈局信息:

class SliverConstraints extends Constraints {
  // 主軸方向
  final AxisDirection axisDirection;
  // 窗口增加方向
  final GrowthDirection growthDirection;
  // 若是 Direction 是 AxisDirection.down,scrollOffset 表明 sliver 的 top 滑過 viewport 的 top 的值,沒滑過 viewport 的 top 時 scrollOffset 爲 0。
  final double scrollOffset;
  // 上一個 sliver 覆蓋下一個 sliver 的大小(只有上一個 sliver 是 pinned/floating 纔有效)
  final double overlap;
  // 輪到當前 sliver 開始繪製了,須要 viewport 告訴 sliver 當前還剩下多少區域能夠繪製,受 viewport 的 size 影響
  final double remainingPaintExtent;
  // viewport 主軸上的大小
  final double viewportMainAxisExtent;
  // 緩存區起點(相對於 scrolloffset),若是 cacheExtent 設置爲 0,那麼 cacheOrigin 一直爲 0
  final double cacheOrigin;
  // 剩餘的緩存區大小
  final double remainingCacheExtent;

  ...
}
複製代碼

上圖中的 sliver1 會被 SliverAppBar(pinned = true)遮住,遮住的大小就是 overlap,此時 overlap 會一直大於 0,若是設置像 iOS bouncing 那樣的滑動效果,那麼當 list 滾動到頂部繼續滑動的時候 overlap 會小於 0(此刻並無東西遮蓋 sliver1,而是 sliver1 的 top 和 viewport 的 top 有間距)。

SliverGeometry

Viewport 經過 SliverConstraints 告知它內部的 sliver 本身的約束信息,好比還有多少空間可用、offset 等,那麼Sliver 則經過 SliverGeometry 反饋給 Viewport 須要佔用多少空間量。

class SliverGeometry extends Diagnosticable {
  // sliver 能夠滾動的範圍,能夠認爲是 sliver 的高度(若是是 AxisDierction.Down) 
  final double scrollExtent;
  // 繪製起點(默認是 0.0),是相對於 sliver 開始 layout 的起點而言的,不會影響下一個 sliver 的 layoutExtent,會影響下一個 sliver 的paintExtent
  final double paintOrigin;
  // 繪製範圍
  final double paintExtent;
  // 佈局範圍,當前 sliver 的 top 到下一個 sliver 的 top 的距離,範圍是[0,paintExtent],默認是 paintExtent,會影響下一個 sliver 的 layout 位置
  final double layoutExtent;
  // 最大繪製大小,必須 >= paintExtent
  final double maxPaintExtent;
  // 若是 sliver 被 pinned 在邊界的時候,這個大小爲 Sliver 的自身的高度,其餘狀況爲0,好比 pinned app bar
  final double maxScrollObstructionExtent;
  // 點擊有效區域的大小,默認爲paintExtent
  final double hitTestExtent;
  // 是否可見,visible = (paintExtent > 0)
  final bool visible;
  // 是否須要作clip,省得chidren溢出
  final bool hasVisualOverflow;
  // 當前 sliver 佔用了 SliverConstraints.remainingCacheExtent 多少像素值
  final double cacheExtent;
  ...
}
複製代碼

Sliver 佈局過程

RenderViewport 在 layout 它內部的 slivers 的過程以下:

這個 layout 過程是一個自上而下的線性過程:

  • 給 sliver1 輸入 SliverConstrains1 而且獲得輸出結果(SliverGeometry1) ,
  • 根據 SliverGeometry1 從新生成一個新的 SliverConstrains2 輸入給 sliver2 獲得 SliverGeometry2
  • 直至最後一個 sliver 具體的過程能夠查看 RenderViewport 的 layoutChildSequence 方法。

ScrollView

以 ScrollView 爲例,咱們串聯上面介紹的幾個 Widget 之間的關係。 先來看 ScrollView 的 build 方法:

@override
  Widget build(BuildContext context) {
    final List<Widget> slivers = buildSlivers(context);
    final AxisDirection axisDirection = getDirection(context);

    final ScrollController scrollController = primary
      ? PrimaryScrollController.of(context)
      : controller;
    final Scrollable scrollable = Scrollable(
      ...
      controller: scrollController,
      viewportBuilder: (BuildContext context, ViewportOffset offset) {
        return buildViewport(context, offset, axisDirection, slivers);
      },
    );
    return primary && scrollController != null
      ? PrimaryScrollController.none(child: scrollable)
      : scrollable;
  }
複製代碼

能夠看到 ScrollView 建立了一個 Scrollable,並傳入了構造 ViewPort 的 buildViewPort 方法。 上面講過 Scrollable 負責手勢監聽,經過 buildViewPort 建立視圖,在手勢變化的時候不停的更新 ViewPort,大概流程以下:

自定義 Sliver

CustomPinnedHeader 光看一些概念會難以理解,最好的方式是 debug 一下,咱們能夠 copy 一下 SliverToBoxAdapter 的代碼自定義一個 Sliver 調試一下各個參數加深理解。

class CustomSliverWidget extends SingleChildRenderObjectWidget {
  const CustomSliverWidget({Key key, Widget child})
      : super(key: key, child: child);

  @override
  RenderObject createRenderObject(BuildContext context) {
    return CustomSliver();
  }
}
/// 一個 StickPinWidget
/// 主要講述 Sliveronstraints 和 SliverGeometry 參數的做用
class CustomSliver extends RenderSliverSingleBoxAdapter {
  @override
  void performLayout() {
    ...
    // 將 SliverConstraints 轉化爲 BoxConstraints 對 child 進行 layout
    child.layout(constraints.asBoxConstraints(), parentUsesSize: true);
    ...
    // 計算繪製大小
    final double paintedChildSize =
        calculatePaintOffset(constraints, from: 0.0, to: childExtent);
    // 計算緩存大小
    final double cacheExtent =
        calculateCacheOffset(constraints, from: 0.0, to: childExtent);
    ...
    // 輸出 SliverGeometry 
    geometry = SliverGeometry(
      scrollExtent: childExtent,
      paintExtent: paintedChildSize,
      cacheExtent: cacheExtent,
      maxPaintExtent: childExtent,
      paintOrigin: 0,
      hitTestExtent: paintedChildSize,
      hasVisualOverflow: childExtent > constraints.remainingPaintExtent || constraints.scrollOffset > 0.0,
    );
    setChildParentData(child, constraints, geometry);
  }
}

複製代碼

咱們把它放到 CustomScrollView 中:

eturn Scaffold(
        body: CustomScrollView(
          slivers: <Widget>[
        CustomSliverWidget(
            child: Container(
            color: Colors.red,
            height: 100,
            child: Center(
              child: Text("CustomSliver"),
            ),
        )),
        _buildListView(),
      ],
    ));

複製代碼

效果以下:

咱們修改 paintOrigin 爲 10 的話,發現 CustomSliverWidget 的 layout 位置沒有變,但繪製的起始點下移了 10 px,而且它下一個的 Sliver - item0 的 layout 沒有被影響,可是 paint 時被遮住了一部分:

再作一個簡單的修改,將 sliver 的繪製起始位置改成滑動的偏移量:

geometry = SliverGeometry(
      ...
      paintOrigin: constraints.scrollOffset,
      visible: true,
     );
複製代碼

此時你會發現 CustomSliver 能夠固定在頭部:

咱們嘗試修改 paintExtrent 以下:

geometry = SliverGeometry(
      //將繪製範圍改成 sliver 的高度
      paintExtent: childExtent,
      ...
    );
複製代碼

在滑動的過程,CustomSliver 只是繪製變了,layout 沒有變,致使下面 item0 沒有被滑動,這是由於 layoutExtent 默認等於 paintExtent,咱們將 paintExtent 賦值了常量,滑動過程當中只有 paintOrigin 在改變,layout 的初始位置和高度並無改變,它會一直佔據着位置。

CustomRefreshWidget

接下來咱們再作一個簡單的下拉刷新 Widget,效果很簡單,下拉的時候顯示,釋放的時候縮回:

class CustomRefreshWidget extends SingleChildRenderObjectWidget {
  const CustomRefreshWidget({Key key, Widget child})
      : super(key: key, child: child);

  @override
  RenderObject createRenderObject(BuildContext context) {
    return SimpleRefreshSliver();
  }
}

/// 一個簡單的下拉刷新 Widget
class SimpleRefreshSliver extends RenderSliverSingleBoxAdapter {
  @override
  void performLayout() {
    ...
    final bool active = constraints.overlap < 0.0;
    /// 頭部滑動的距離
    final double overscrolledExtent =
        constraints.overlap < 0.0 ? constraints.overlap.abs() : 0.0;
    double layoutExtent = child.size.height;
    print("overscrolledExtent:${overscrolledExtent - layoutExtent}");
    child.layout(
      constraints.asBoxConstraints(
        maxExtent: layoutExtent + overscrolledExtent,
      ),
      parentUsesSize: true,
    );
    if (active) {
      geometry = SliverGeometry(
        scrollExtent: layoutExtent,
        /// 繪製起始位置
        paintOrigin: min(overscrolledExtent - layoutExtent, 0),
        paintExtent: max(max(child.size.height, layoutExtent) ,0.0,),
        maxPaintExtent: max(max(child.size.height, layoutExtent) ,0.0,),
        /// 佈局佔位
        layoutExtent: min(overscrolledExtent, layoutExtent),
      );
    } else {
      /// 若是不想顯示能夠直接設置爲 zero
      geometry = SliverGeometry.zero;
    }
    setChildParentData(child, constraints, geometry);
  }
}
複製代碼

能夠看到有3個關鍵的參數

  • constraints.overlap:List 第一個 Sliver 的 top 距離屏幕 top 的距離
  • paintOrigin:RefreshWidget 的繪製起始位置
  • layoutExtent:RefreshWidget 的高度

items 的 top 與屏幕頂部的距離就是 constraints.overlap,它是一個小於等於 0 的值。

  • 未操做時,overlap == 0,直接返回一個空 Widget(SliverGeometry.zero)
  • 下拉時,overlap < 0, 這時候將 paintOrigin = min(overscrolledExtent - RefreshWidget.height, 0) 就可讓 RefreshWidget 慢慢的拉下來。
  • 處理完 Paint 後,不要忘記處理 layout,前面說過,SliverGeometry 的 layoutExtent 會影響下一個 Sliver 的佈局位置,因此 layoutExtent 也須要隨着滑動而逐漸變大 layoutExtent = min(-overlap, RefreshWidget.height)

Scrolling Widget

經常使用的 List 以下,咱們按照它包裹的內容分紅了 3 類:

ListView

ListView.builder(
        itemCount: 50,
        itemBuilder: (context,index) {
          return Container(
            color: ColorUtils.randomColor(),
            height: 50,
          );
        }
複製代碼

CustomScrollView

CustomScrollView(
      slivers: <Widget>[
        SliverAppBar(...),
        SliverToBoxAdapter(
          child:ListView(...),
        ),
        SliverList(...),
        SliverGrid(...),
      ],
    )
複製代碼

NestedScrollView

NestedScrollView 其實裏面是一個CustomScrollView,它的 headers 是 Sliver 的數組,body是被包裹在 SliverFillRemaining 中的,body 能夠接受 Box。

NestedScrollView(
      headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
        return <Widget>[
          SliverAppBar(
            expandedHeight: 100,
            pinned: true,
            title: Text("Nest"),
          ),
          SliverToBoxAdapter(
            child: Text("second bar"),
          )
        ];
      },
      body: ListView.builder(
          itemCount: 20,
          itemBuilder: (BuildContext context, int index) {
            return Text("item: $index");
          }),
    );

複製代碼

設計 CustomScrollView 的緣由

複雜列表嵌套

若是直接使用 ListView 嵌套 ListView 會報錯:

Vertical viewport was given unbounded height.

大體意思是在 layout 階段父 Listview 不能判斷子 Listview 的高度,這個錯誤能夠經過設置內部的 Listview 的 shrinkWrap = true 來修正(shrinkWrap = true 表明 ListView 的高度等於它 content 的高度)。

ListView.builder(
        itemCount: 20,
        itemBuilder: (BuildContext context, int index) {
          return ListView.builder(
              shrinkWrap: true,
              itemCount: 5,
              itemBuilder: (BuildContext context, int index) {
                return Text("item: $index");
              });
        });
複製代碼

可是這樣作的話性能會比較差,由於內部的列表每次都要計算出全部 content 的高度,這個時候使用 CustomScrollView 更爲合適:

CustomScrollView(
      slivers: <Widget>[
        SliverList(
            delegate: SliverChildBuilderDelegate(
                (context, index) => Container(...),
                childCount: 50)
                ),
        SliverList(
            delegate: SliverChildBuilderDelegate(
                (context, index) => Container(...),
                childCount: 50)
                )
      ],
    );
複製代碼
滑動特效

CustomScrollView 可讓它內部的 Slivers 進行聯動,好比作一個可伸縮的 TitleBar 、中間區域能夠固定的 header、下拉刷新組件等等。

Slivers

Flutter 提供了不少的 Sliver 組件,下面咱們主要說一下它們的做用是什麼:

SliverAppBar

相似於 android 中 CollapsingToolbarLayout,能夠根據滑動作伸縮佈局,並提供了 actions,bottom 等提升效率的屬性。

SliverList / SliverGrid

用法和 ListView / GridView 基本一致。 此外,ListView = SliverList + Scrollable,也就是說 SliverList 不具有處理滑動事件的能力,因此它必須配合 CustomScrollView 來使用。

SliverFixedExtentList

它比 SliverList 多了修飾詞 FixedExtent,意思是它的 item 在主軸方向上具備固定的高度/寬度。

設計它的緣由是在 item 高度/寬度全都同樣的場景下使用,它的效率比 SliverList 高,由於它不用經過 item 的 layout 過程就能夠知道每一個 item 的範圍。

在使用的時候必須傳入 itemExtent:

SliverFixedExtentList(
  itemExtent: 50.0,
  delegate: SliverChildBuilderDelegate(
	...
        );
    },
  ),
)
複製代碼

SliverPersistentHeader

SliverPersistentHeader 是一個能夠固定/懸浮的 header,它能夠設置在列表的任意位置,顯示的內容須要設置 SliverPersistentHeaderDelegate。

SliverPersistentHeader(
          pinned: true,
          delegate: ...,
)
複製代碼

SliverPersistentHeaderDelegate 是一個抽象類,咱們須要本身實現它,它的實現很簡單,只有四個必需要實現的成員:

class CustomDelegate extends SliverPersistentHeaderDelegate {
  /// 最大高度
  @override
  double get maxExtent => 100;
  /// 最小高度
  @override
  double get minExtent => 50;

  /// shrinkOffset: 當前 sliver 頂部越過屏幕頂部的距離
  /// overlapsContent: 下方是否還有 content 顯示
  @override
  Widget build(
      BuildContext context, double shrinkOffset, bool overlapsContent) {
        return your widget
    );
  }
  /// 是否須要刷新
  @override
  bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
    return maxExtent != oldDelegate.maxExtent ||
        minExtent != oldDelegate.minExtent;
  }
}
複製代碼

在實際運用中沉浸式的設計是很常見的,使用 SliverPersistentHeaderDelegate 能夠輕鬆的實現沉浸式的效果:

它的實現原理就是根據 shrinkOffset 動態調整狀態欄的樣式和標題欄的顏色,實現代碼見下面的 沉浸式 Header。

SliverToBoxAdapter

將 BoxWidget 轉變爲 Sliver:因爲 CustomScrollView 只能接受 Sliver 類型的 child,因此不少經常使用的 Widget 沒法直接添加到 CustomScrollView 中,此時只須要將 Widget 用 SliverToBoxAdapter 包裹一下就能夠了。 最多見的使用就是 SliverList 不支持橫向模式,可是又沒法直接將 ListView 直接添加到 CustomScrollView 中,此時用 SliverToBoxAdapter 包裹一下:

CustomScrollView(
      slivers: <Widget>[
        SliverToBoxAdapter(
          child: _buildHorizonScrollView(),
        ),
      ],
    ));

 Widget _buildHorizonScrollView() {
    return Container(
      height: 50,
      child: ListView.builder(
          scrollDirection: Axis.horizontal,
          primary: false,
          shrinkWrap: true,
          itemCount: 15,
          itemBuilder: (context, index) {
            return Container(
              color: ColorUtils.randomColor(),
              width: 50,
              height: 50,
            );
          }),
    );
  } 
複製代碼

SliverPadding

能夠用在 CustomScrollView 中的 Padding。 須要注意的是不要用它來包裹 SliverPersistentHeader ,由於它會使 SliverPersistentHeader 的 pinned 失效,若是 SliverPersistentHeader 非要使用 Padding 效果,能夠在 delegate 內部使用 Padding。

  • wrong code:
SliverPadding(
            padding: EdgeInsets.symmetric(horizontal: 16),
            sliver: SliverPersistentHeader(
              pinned: true,
              floating: false,
              delegate: Delegate(),
            ),
          )
複製代碼
  • correct code:
class Delegate extends SliverPersistentHeaderDelegate {
  @override
  Widget build(
          BuildContext context, double shrinkOffset, bool overlapsContent) =>
      Padding(
        padding: EdgeInsets.symmetric(horizontal: 16),
        child: Container(
          color: Colors.yellow,
        ),
      );
  ...
}
複製代碼

SliverSafeArea

用法和 SafeArea 一致。

SliverFillRemaining

能夠填充屏幕剩餘控件的 Sliver。

部分實例代碼:

沉浸式 Header

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

class GradientSliverHeaderDelegate extends SliverPersistentHeaderDelegate {
  final double collapsedHeight;
  final double expandedHeight;
  final double paddingTop;
  final String coverImgUrl;
  final String title;

  GradientSliverHeaderDelegate({
    this.collapsedHeight,
    this.expandedHeight,
    this.paddingTop,
    this.coverImgUrl,
    this.title,
  });

  @override
  double get minExtent => this.collapsedHeight + this.paddingTop;

  @override
  double get maxExtent => this.expandedHeight;

  @override
  bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
    return true;
  }

  Color makeStickyHeaderBgColor(shrinkOffset) {
    final int alpha = (shrinkOffset / (this.maxExtent - this.minExtent) * 255)
        .clamp(0, 255)
        .toInt();
    return Color.fromARGB(alpha, 255, 255, 255);
  }

  Color makeStickyHeaderTextColor(shrinkOffset) {
    if (shrinkOffset <= 50) {
      return Colors.white;
    } else {
      final int alpha = (shrinkOffset / (this.maxExtent - this.minExtent) * 255)
          .clamp(0, 255)
          .toInt();
      return Color.fromARGB(alpha, 0, 0, 0);
    }
  }

  Brightness getStatusBarTheme(shrinkOffset) {
    return shrinkOffset <= 50 ? Brightness.light : Brightness.dark;
  }

  @override
  Widget build(
      BuildContext context, double shrinkOffset, bool overlapsContent) {
    SystemUiOverlayStyle systemUiOverlayStyle = SystemUiOverlayStyle(
        statusBarColor: Colors.transparent,
        statusBarIconBrightness: getStatusBarTheme(shrinkOffset));
    SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle);

    return Container(
      height: this.maxExtent,
      width: MediaQuery.of(context).size.width,
      child: Stack(
        fit: StackFit.expand,
        children: <Widget>[
          // 背景圖
          Container(
              child: Image.asset(
            coverImgUrl,
            fit: BoxFit.cover,
          )),
          // 收起頭部
          Positioned(
            left: 0,
            right: 0,
            top: 0,
            child: Container(
              color: this.makeStickyHeaderBgColor(shrinkOffset), // 背景顏色
              child: SafeArea(
                bottom: false,
                child: Container(
                    height: this.collapsedHeight,
                    child: Center(
                      child: Text(
                        this.title,
                        style: TextStyle(
                          fontSize: 20,
                          fontWeight: FontWeight.w500,
                          color: this
                              .makeStickyHeaderTextColor(shrinkOffset), // 標題顏色
                        ),
                      ),
                    )),
              ),
            ),
          ),
        ],
      ),
    );
  }
}
複製代碼
class CustomRefreshWidget extends SingleChildRenderObjectWidget {
  const CustomRefreshWidget({Key key, Widget child})
      : super(key: key, child: child);

  @override
  RenderObject createRenderObject(BuildContext context) {
    return SimpleRefreshSliver();
  }
}

/// 一個簡單的下拉刷新 Widget
class SimpleRefreshSliver extends RenderSliverSingleBoxAdapter {
  @override
  void performLayout() {
    if (child == null) {
      geometry = SliverGeometry.zero;
      return;
    }
    child.layout(constraints.asBoxConstraints(), parentUsesSize: true);
    double childExtent;
    switch (constraints.axis) {
      case Axis.horizontal:
        childExtent = child.size.width;
        break;
      case Axis.vertical:
        childExtent = child.size.height;
        break;
    }
    assert(childExtent != null);
    final double paintedChildSize =
        calculatePaintOffset(constraints, from: 0.0, to: childExtent);

    assert(paintedChildSize.isFinite);
    assert(paintedChildSize >= 0.0);
    final bool active = constraints.overlap < 0.0;
    final double overscrolledExtent =
        constraints.overlap < 0.0 ? constraints.overlap.abs() : 0.0;
    double layoutExtent = child.size.height;
    print("overscrolledExtent:${overscrolledExtent - layoutExtent}");
    child.layout(
      constraints.asBoxConstraints(
        maxExtent: layoutExtent
            // Plus only the overscrolled portion immediately preceding this
            // sliver.
            +
            overscrolledExtent,
      ),
      parentUsesSize: true,
    );
    if (active) {
      geometry = SliverGeometry(
        scrollExtent: layoutExtent,
        /// 繪製起始位置
        paintOrigin: min(overscrolledExtent - layoutExtent, 0),
        paintExtent: max(max(child.size.height, layoutExtent) ,0.0,),
        maxPaintExtent: max(max(child.size.height, layoutExtent) ,0.0,),
        /// 佈局佔位
        layoutExtent: min(overscrolledExtent, layoutExtent),
      );
    } else {
      /// 若是不想顯示能夠直接設置爲 zero
      geometry = SliverGeometry.zero;
    }
    setChildParentData(child, constraints, geometry);
  }
}
複製代碼

使用:

@override
  Widget build(BuildContext context) {
    return Scaffold(
        body: CustomScrollView(
        /// android 須要設置彈簧效果 overlap 纔會起做用
      physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()),
        CustomRefreshWidget(
          child: Container(
            height: 100,
            color: Colors.purple,
            child: Row(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.center,

              children: <Widget>[
                Text(
                  "RefreshWidget",
                  style: TextStyle(color: Colors.white),
                ),
                Padding(
                  padding: EdgeInsets.only(left: 10.0),
                  child: CupertinoActivityIndicator(),
                )
              ],
            ),
          ),
        ),
        ...
        _buildListView(),
      ],
    ));
  }

複製代碼
相關文章
相關標籤/搜索