Flutter中創(chuàng)建滾動(dòng)效果

<meta charset="utf-8">

image

1. 前言

Flutter作為時(shí)下最流行的技術(shù)之一风题,憑借其出色的性能以及抹平多端的差異優(yōu)勢像棘,早已引起大批技術(shù)愛好者的關(guān)注,甚至一些閑魚疙筹,美團(tuán)騰訊等大公司均已投入生產(chǎn)使用禁炒。雖然目前其生態(tài)還沒有完全成熟而咆,但身靠背后的Google加持,其發(fā)展速度已經(jīng)足夠驚人幕袱,可以預(yù)見將來對Flutter開發(fā)人員的需求也會隨之增長暴备。

無論是為了技術(shù)嘗鮮還是以后可能的工作機(jī)會,都9102年了们豌,作為一個(gè)前端開發(fā)者涯捻,似乎沒有理由不去嘗試它。正是帶著這樣的心理望迎,筆者也開始學(xué)習(xí)Flutter障癌,同時(shí)建了一個(gè)用于練習(xí)的倉庫,后續(xù)所有代碼都會托管在上面辩尊,歡迎star涛浙,一起學(xué)習(xí)。這是我寫的Flutter系列文章:

在之前的文章中摄欲,我們學(xué)習(xí)了如何使用ListViewGridView這兩個(gè)滾動(dòng)類型組件轿亮。今天,我們就來學(xué)習(xí)另一個(gè)滾動(dòng)組件CustomScrollView及其搭配使用的Sliver系列組件胸墙。掌握了它們我注,你就可以做一些有趣的滾動(dòng)效果啦~

2. 必備知識

在進(jìn)入今天的正題之前,我們先來簡單了解下今天的兩個(gè)主角CustomScrollViewSliverCustomScrollViewFlutter提供的可以用來自定義滾動(dòng)效果的組件迟隅,它可以像膠水一樣將多個(gè)Sliver粘合在一起但骨。

什么意思呢?舉個(gè)栗子(你也可以點(diǎn)擊這里youtube上的一個(gè)視頻):

假如頁面中同時(shí)存在一個(gè)List和一個(gè)Grid玻淑,雖然它們看起來是一個(gè)整體嗽冒,但是由于各自的滾動(dòng)效果是分離的,所以沒法保證一致的滾動(dòng)效果补履。

而使用CustomScrollView組件作為滾動(dòng)容器添坊,SliverListSliverGrid分別替代ListGrid作為CustomScrollView的子組件,滾動(dòng)效果再由CustomScrollView統(tǒng)一控制箫锤,這樣就可以了贬蛙。

其中SliverListSliverGrid就是我們前面提到的Sliver系列中的兩員雨女,除此之外,Sliver家族還有常用的幾個(gè):

  • SliverAppBar:Creates a material design app bar that can be placed in a CustomScrollView.
  • SliverPersistentHeader:Creates a sliver that varies its size when it is scrolled to the start of a viewport.
  • SliverFillRemaining:Creates a sliver that fills the remaining space in the viewport.
  • SliverToBoxAdapter:Creates a sliver that contains a single box widget.
  • SliverPadding:Creates a sliver that applies padding on each side of another sliver.

注意:由于CustomeScrollView的子組件只能是Sliver系列阳准,所以如果你想將一個(gè)普通組件塞進(jìn)CustomScrollView氛堕,那么務(wù)必將該組件用SliverToBoxAdapter包裹。

3. 熱身:SliverList / SliverGrid

前面講了那么多的概念似乎有些枯燥野蝇,接下來就讓我們從最簡單的一個(gè)例子入手來看看如何使用CustomScrollViewSliverList/SliverGrid讼稚。

其實(shí)CustomScrollView的用法很簡單,它有一個(gè)slivers屬性绕沈,是一個(gè)Widget數(shù)組锐想,將子組件都放在里面就可以了,其他的一些滾動(dòng)相關(guān)的屬性基本和我們之前學(xué)到的ListView差不多乍狐。

CustomScrollView(
  slivers: <Widget>[
    renderSliverA(),
    renderSliverB(),
    renderSliverC(),
  ],
)

再來看看SliverList赠摇,它只有一個(gè)delegate屬性,可以用SliverChildListDelegateSliverChildBuilderDelegate這兩個(gè)類實(shí)現(xiàn)浅蚪。前者將會一次性全部渲染子組件藕帜,后者將會根據(jù)視窗渲染當(dāng)前出現(xiàn)的元素,其效果可以和ListViewListView.build這兩個(gè)構(gòu)造函數(shù)類比惜傲。

SliverList(
  delegate: SliverChildListDelegate(
    <Widget>[
      renderA(),
      renderB(),
      renderC(),
    ]
  )
)

SliverList(
  delegate: SliverChildBuilderDelegate(
    (context, index) => renderItem(context, index),
    childCount: 10,
  )
)

通過上面的例子我們發(fā)現(xiàn)SliverList的使用方式和ListView大同小異洽故,而SliverGrid也是如此,這里就不再過多贅述盗誊,來看個(gè)兩列網(wǎng)格的例子:

SliverGrid.count(
  crossAxisCount: 2,
  children: <Widget>[
    renderA(),
    renderB(),
    renderC(),
    renderD()
  ]
)

接下來收津,就讓我們通過一個(gè)實(shí)際例子將上面的三點(diǎn)結(jié)合在一起。

代碼(完整版看這里

final List<Color> colorList = [
  Colors.red,
  Colors.orange,
  Colors.green,
  Colors.purple,
  Colors.blue,
  Colors.yellow,
  Colors.pink,
  Colors.teal,
  Colors.deepPurpleAccent
];

// Text組件需要用SliverToBoxAdapter包裹浊伙,才能作為CustomScrollView的子組件
Widget renderTitle(String title) {
  return SliverToBoxAdapter(
    child: Padding(
      padding: EdgeInsets.symmetric(vertical: 16),
      child: Text(
        title,
        textAlign: TextAlign.center,
        style: TextStyle(fontSize: 20),
      ),
    ),
  );
}

CustomScrollView(
  slivers: <Widget>[
    renderTitle('SliverGrid'),
    SliverGrid.count(
      crossAxisCount: 3,
      children: colorList.map((color) => Container(color: color)).toList(),
    ),
    renderTitle('SliverList'),
    SliverFixedExtentList(      // SliverList的語法糖撞秋,用于每個(gè)item固定高度的List
      delegate: SliverChildBuilderDelegate(
        (context, index) => Container(color: colorList[index]),
        childCount: colorList.length,
      ),
      itemExtent: 100,
    ),
  ],
)

效果圖

image

上面的例子中還有一點(diǎn)需要注意的是:我們將標(biāo)題組件放在了SliverToBoxAdapter內(nèi),因?yàn)?code>CustomScrollView只接受Sliver系列的組件嚣鄙。

4. 眼前一亮的SliverAppBar

AppBar是常用來構(gòu)建一個(gè)頁面頭部Bar的組件吻贿,在CustomScrollView中與其對應(yīng)的是SliverAppBar組件。它有什么神奇之處呢哑子?隨著頁面的滾動(dòng)舅列,頭部Bar將會有一個(gè)收起過渡的效果。我們先來看下效果:

float效果 snap效果 pinned效果
image

|

image

|

image

|

通過上面的預(yù)覽圖卧蜓,想必你肯定很好奇SliverAppBar中的過渡效果是如何實(shí)現(xiàn)的~先別急帐要,我們先來看下應(yīng)該如何使用它:

SliverAppBar(
  floating: true,
  snap: true,
  pinned: true,
  expandedHeight: 250,
  flexibleSpace: FlexibleSpaceBar(
    title: Text(this.title),
    background: Image.network(
      'http://img1.mukewang.com/5c18cf540001ac8206000338.jpg',
      fit: BoxFit.cover,
    ),
  ),
)

SliverAppBar最重要的幾個(gè)屬性在上面的例子中羅列出來。其中:

  • expandedHeight:展開狀態(tài)下appBar的高度弥奸,即圖中圖片所占空間榨惠;
  • flexibleSpace:空間大小可變的組件,Flutter給我們提供了一個(gè)現(xiàn)成的FlexibleSpaceBar組件,給我們處理好了title過渡的效果赠橙。

另外耽装,floating/snap/pinned這三個(gè)屬性可以指定SliverAppBar內(nèi)容滑出屏幕之后的表現(xiàn)形式。

  • float:向下滑動(dòng)時(shí)期揪,即使當(dāng)前CustomScrollView不在頂部掉奄,SliverAppBar也會跟著一起向下出現(xiàn);
  • snap:當(dāng)手指放開時(shí)凤薛,SliverAppBar會根據(jù)當(dāng)前的位置進(jìn)行調(diào)整姓建,始終保持展開收起的狀態(tài);
  • pinned:不同于float效果缤苫,當(dāng)SliverAppBar內(nèi)容滑出屏幕時(shí)引瀑,將始終渲染一個(gè)固定在頂部的收起狀態(tài)組件。

需要注意的是:snap效果一定要在floattrue時(shí)才會生效榨馁。另外,你也可以將這三者進(jìn)行組合使用帜矾。

5. 花樣多變的SliverPersistentHeader

在上一小節(jié)中我們見識到了SliverAppBar的神奇之處翼虫,其實(shí)它就是基于SliverPersistentHeader實(shí)現(xiàn)的。通過SliverPersistentHeader屡萤,我們還可以實(shí)現(xiàn)sticky吸頂?shù)男Ч?/p>

SliverPersistentHeader最重要的一個(gè)屬性是SliverPersistentHeaderDelegate珍剑,為此我們需要實(shí)現(xiàn)一個(gè)類繼承自SliverPersistentHeaderDelegate

class StickyTabBarDelegate extends SliverPersistentHeaderDelegate {

  @override
  double get minExtent => null;

  @override
  double get maxExtent => null;

  @override
  bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) => null;

  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) => null;
}

可以看到死陆,SliverPersistentHeaderDelegate的實(shí)現(xiàn)類必須實(shí)現(xiàn)其4個(gè)方法招拙。其中:

  • minExtent:收起狀態(tài)下組件的高度;
  • maxExtent:展開狀態(tài)下組件的高度措译;
  • shouldRebuild:類似于react中的shouldComponentUpdate别凤;
  • build:構(gòu)建渲染的內(nèi)容。

接下來领虹,我們就來實(shí)現(xiàn)一個(gè)TabBar吸頂?shù)男Ч?/p>

代碼(完整版看這里

CustomScrollView(
  slivers: <Widget>[
    SliverAppBar(
      // ...
    ),
    SliverPersistentHeader( // 可以吸頂?shù)腡abBar
      pinned: true,
      delegate: StickyTabBarDelegate(
        child: TabBar(
          labelColor: Colors.black,
          controller: this.tabController,
          tabs: <Widget>[
            Tab(text: 'Home'),
            Tab(text: 'Profile'),
          ],
        ),
      ),
    ),
    SliverFillRemaining(        // 剩余補(bǔ)充內(nèi)容TabBarView
      child: TabBarView(
        controller: this.tabController,
        children: <Widget>[
          Center(child: Text('Content of Home')),
          Center(child: Text('Content of Profile')),
        ],
      ),
    ),
  ],
)

class StickyTabBarDelegate extends SliverPersistentHeaderDelegate {
  final TabBar child;

  StickyTabBarDelegate({@required this.child});

  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    return this.child;
  }

  @override
  double get maxExtent => this.child.preferredSize.height;

  @override
  double get minExtent => this.child.preferredSize.height;

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

效果圖

image

根據(jù)上面的圖我們可以看到规哪,當(dāng)下方tab內(nèi)容滑出屏幕后,tabBar并沒有跟著一起滑走塌衰,而是粘在了頂部诉稍。可見SliverPersistentHeader的確可以滿足我們的sticky效果最疆。

不過SliverPersistentHeader的神奇可遠(yuǎn)不止如此哦~我們可以通過它自定義一些頭部的過渡效果杯巨,畢竟SliverAppBar也是通過它實(shí)現(xiàn)的。就比如下方這個(gè)電影詳情頁的頭部過渡效果努酸,這在一般的app種還是比較常見的服爷。

image

那么這種效果要如何實(shí)現(xiàn)呢?關(guān)鍵就在于build方法中的shrinkOffset屬性,它代表當(dāng)前頭部的滾動(dòng)偏移量层扶。我們可以根據(jù)它計(jì)算得到當(dāng)前收起頭部的背景顏色以及圖標(biāo)和文案的字體顏色箫章,這樣就能根據(jù)當(dāng)前位置得到過渡效果啦~

代碼(完整版看這里

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

  SliverCustomHeaderDelegate({
    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, isIcon) {
    if(shrinkOffset <= 50) {
      return isIcon ? Colors.white : Colors.transparent;
    } else {
      final int alpha = (shrinkOffset / (this.maxExtent - this.minExtent) * 255).clamp(0, 255).toInt();
      return Color.fromARGB(alpha, 0, 0, 0);
    }
  }

  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    return Container(
      height: this.maxExtent,
      width: MediaQuery.of(context).size.width,
      child: Stack(
        fit: StackFit.expand,
        children: <Widget>[
          // 背景圖
          Container(child: Image.network(this.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: Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: <Widget>[
                      IconButton(
                        icon: Icon(
                          Icons.arrow_back_ios,
                          color: this.makeStickyHeaderTextColor(shrinkOffset, true),    // 返回圖標(biāo)顏色
                        ),
                        onPressed: () => Navigator.pop(context),
                      ),
                      Text(
                        this.title,
                        style: TextStyle(
                          fontSize: 20,
                          fontWeight: FontWeight.w500,
                          color: this.makeStickyHeaderTextColor(shrinkOffset, false),   // 標(biāo)題顏色
                        ),
                      ),
                      IconButton(
                        icon: Icon(
                          Icons.share,
                          color: this.makeStickyHeaderTextColor(shrinkOffset, true),    // 分享圖標(biāo)顏色
                        ),
                        onPressed: () {},
                      ),
                    ],
                  ),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

上面的代碼雖然很長,但大部分是構(gòu)建widget的代碼镜会。所以檬寂,我們重點(diǎn)關(guān)注makeStickyHeaderTextColormakeStickyHeaderBgColor即可。這兩個(gè)方法都是根據(jù)當(dāng)前的shrinkOffset值計(jì)算過渡過程中的顏色值戳表。另外桶至,這里需要注意頭部在iPhoneX及以上的劉海頭涉及,可以用SafeArea組件解決問題匾旭。

6. 總結(jié)

本文首先介紹了CustomScrollViewSliver系列組件的概念及其關(guān)系镣屹,接著以SliverListSliverGrid結(jié)合的示例說明了其使用方法。然后价涝,又介紹了較常用的SliverAppBar組件女蜈,分別解釋了其float/snap/pinned各自的效果。最后色瘩,講解了SliverPersistentHeader組件的使用方法伪窖,并用實(shí)際例子加以說明其自定義過渡效果的用法。希望通過本文的介紹居兆,你可以用CustomScrollViewSliver系列組件創(chuàng)建出更有意思的滾動(dòng)效果~

轉(zhuǎn)自:http://www.reibang.com/p/5aeeb7ea776b

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末覆山,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子泥栖,更是在濱河造成了極大的恐慌簇宽,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件吧享,死亡現(xiàn)場離奇詭異魏割,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)钢颂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門见妒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人甸陌,你說我怎么就攤上這事须揣。” “怎么了钱豁?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵耻卡,是天一觀的道長。 經(jīng)常有香客問我牲尺,道長卵酪,這世上最難降的妖魔是什么幌蚊? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮溃卡,結(jié)果婚禮上溢豆,老公的妹妹穿的比我還像新娘。我一直安慰自己瘸羡,他們只是感情好漩仙,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著犹赖,像睡著了一般队他。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上峻村,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天麸折,我揣著相機(jī)與錄音,去河邊找鬼粘昨。 笑死垢啼,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的张肾。 我是一名探鬼主播芭析,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼捌浩!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起工秩,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤尸饺,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后助币,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體浪听,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年眉菱,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了迹栓。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,018評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡俭缓,死狀恐怖克伊,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情华坦,我是刑警寧澤愿吹,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站惜姐,受9級特大地震影響犁跪,放射性物質(zhì)發(fā)生泄漏椿息。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一坷衍、第九天 我趴在偏房一處隱蔽的房頂上張望寝优。 院中可真熱鬧,春花似錦枫耳、人聲如沸乏矾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽妻熊。三九已至,卻和暖如春仑最,著一層夾襖步出監(jiān)牢的瞬間扔役,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工警医, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留亿胸,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓预皇,卻偏偏與公主長得像侈玄,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子吟温,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評論 2 345