學習利用 PageView 搭配 PageController 做出滑動式前後 Page 淡入淡出的效果。
有些 App 在第一次啓動時顯示一系列的畫面,提供教學或説明該 App 有什麽功能,例如: 剛好最近也有做到類似的需求,因此,我藉由找到幾篇教學來説明如何做到。 要做到多個 Page 之間活動切換,可以使用 PageView 來做到。 説明幾個重要的地方:
- PageView
提供整頁畫面可滑動切換的基本元件,每一頁都是相同大小。 適合一些結構比較簡單的情況使用,例如:多張照片滑動瀏覽,或是廣告版位的自動滑動等。 可搭配 PageController 控制在滑動時的移動效果(offset)或是調整顯示的大小或坐標等。 例如下面簡單的例子:
PageView( children:
[ Container( color: Colors.yellow, ), Container( color: Colors.blue, ), Container( color: Colors.deepPurple, ), ], ) - reverse:設定是否要 Scroll 的方向是否反過來;
- scrollDirection:設定 Scroll 是要左右滑動或是上下滑動;
- pageSnapping:設定滑動時是否自動下一頁,如果設定 false,在滑動時會卡住前後一頁;
例如:假設 PageView 加入了 A,B,C,D 四個 Page,首先會顯示 A,如果往左邊滑動時,PageView 會生成:A -> half of B & half of A -> B; 假設在 B 往右邊滑動時,PageView 會生成: B-> half of A & half of B -> A;
而這樣的邏輯剛好可以搭配 PageController 來處理,做到我們要的效果。 - PageController
讓我們可以操作 PageView 中每一頁的顯示,例如:讓我們在根據畫面(viewport size 的增量)控制 offset。 幾個重點屬性與方法:
- initialPage:設定第一個要顯示的 Page;
- keepPage:當 scrollable 被重建時回復到現在的 Page;
- page:取得現在顯示的 Page;
- animateToPage:設定動畫讓 PagView 從現在的 Page 移動到特定的 Page;
- jumpToPage:設定移動到特定的 Page;
- offset:代表 scrollable widget 目前 scroll 的 offset;其值為 double get offset => position.pixels; 這個值搭配 PageView 設定 scrollDirection 的方向代表不同的對象; offset 值會從 0 ~ Width*(N-1),例如:PageView 設定 width 是 200,有 3 個 Page,那就是 0 ~ 200*(3-1),每翻過一個新的 Page,offset 就加上 width 直到最後。
接著參考 Flutter之使用PageView实现图片预览视差效果 做出如上圖在切換 Page 時做出 Page 在相同位置做淡入淡出效果。 裏面有提到幾個重點:
- 需要 PageController 的 offset 來計算每個 Page 在移動距離佔整個畫面的比例:var pageOffset = controller.offset / width;(如果您的 Widget 是有固定的寬度,那就是 Page 移動距離佔指定寬度的比例)。 同上面所說 offset 代表是目前 PageController 移動的位置,需要去除以寬度才會知道當前的 Page 移動的比例;
- 計算出目前移動時左邊的畫面(var currentLeftPageIndex = pageOffset.floor();)是誰,相反地如果您的滑動是垂直的話,那就是計算您上面的畫面是誰; 根據 Flutter之使用PageView实现图片预览视差效果 的介紹,翻頁時最多只能看到兩頁,例如:第 0 頁翻到第 1 頁時,只會看到第 0 跟第 1 頁各一半,再翻過去後只剩下第 1 頁。所以計算 currentLeftPageIndex 再翻頁時就是第 0 頁以此類推。
- var currentPageOffsetPercent = pageOffset - currentLeftPageIndex; 利用當前頁的 offset 減去左邊頁來得到當前左頁的偏移值,值在 0 ~ 1 之間。代表從第 0 頁翻到第 1 頁中間變量的過程,這個值也是用來做淡入淡出效果的依據。
- 因爲要固定 Page 的位置做淡入淡出效果,所以要設定被翻出的 Page 為負數的偏移值,如:(pageOffset - index) * width。
利用完整的程式碼來說明:
PageController _controller;
var pageOffset = 0.0;
var screenWidth = 0.0;
var images = [
'http://0rz.tw/y2yyL',
'http://0rz.tw/RTyOI',
'http://0rz.tw/GZfs8',
'http://0rz.tw/myTYd',
'http://0rz.tw/HuKP2'
];
@override
void initState() {
super.initState();
_controller = PageController(initialPage: 0);
// 監聽 PageController 的 Scroller 變化
_controller.addListener(_offsetChanged);
}
void _offsetChanged() {
// 每次的移動都重新計算對應的偏移值與特效
setState(() {
pageOffset = _controller.offset / screenWidth;
});
}
@override
Widget build(BuildContext context) {
// 預設使用螢幕的寬度
screenWidth = MediaQuery.of(context).size.width;
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: PageView.builder(
controller: _controller,
itemCount: images.length,
itemBuilder: (context, index) {
// 計算每次異動時左邊的 Page 是哪個 index
var currentLeftPageIndex = pageOffset.floor();
// 計算現在畫面 Offset 佔的比例
var currentPageOffsetPercent = pageOffset - currentLeftPageIndex;
// 加入移動的特效
return Transform.translate(
// 因爲是水平滑動,所以設定 offset 的 X 值,因爲 Page 固定不動
// 所以要先用 pageOffset 減去 index 得到 負數
// 如果是垂直滑動,請設定 offset 的 Y 值
offset: Offset((pageOffset - index) * screenWidth, 0),
// 加入調整透明度效果
child: Opacity(
// 如果現在左邊的 index 等於正要建立的 index,則讓它透明度變淡,因爲它要退出畫面了
// 相反地是要顯示,則使用原本的 currentPageOffsetPercent
opacity: currentLeftPageIndex == index
? 1 - currentPageOffsetPercent
: currentPageOffsetPercent,
child: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: NetworkImage(images[index]),
fit: BoxFit.cover))),
),
);
},
));
}
效果如下圖: 想要加入 indicator 顯示 PageView 到哪一個 Page 呢? 可以利用 PageView example with dots indicator 的範例,搭配上方建立的 PageController 來配合,所以調整程式碼把 PageView example with dots indicator 建立的 indicator widget 加入到上方範例:
Stack(
alignment: Alignment.topCenter,
children: [
PageView.builder(
controller: _controller,
itemCount: images.length,
itemBuilder: (context, index) {
...
},),
// 加入 indicator, 並使用相同的 PageController
DotsIndicator(
color: Colors.white,
itemCount: images.length,
controller: _controller,
)
],));
上方程式利用 Stack 堆疊兩個 widget 做出效果,並在 DotIndicator 使用 PageController 顯示的是那一個 index:
double selectedness = Curves.easeOut.transform(
max(
0.0,
1.0 - ((controller.page ?? controller.initialPage) - index).abs(),
),);
另外,如果要做到每個 Page 翻頁時,聯動另一個 ListView 做切換,要怎麼做呢? 需要搭配其他 Widgets:
- IgnorePointer
- ScrollController
負責保存 scroll 狀態,讓具有 scroll 功能的 widget 可以移動或紀錄目前的位置。 例如:利用 IgnorePointer 取消了 ListView 的互動,因此需要加入它來移動到指定的項目;
利用程式碼來說明:
final ScrollController _scrollController = ScrollController();
var texts = ['GOW 1', 'GOW 2', 'GOW 3', 'GEARS OF WAR JUDGMENT', 'GEARS OF WAR:ULTIMATE EDITION'];
void _offsetChanged() {
setState(() {
// 利用 PageController.offset 來移動
_scrollController.jumpTo(_controller.offset);
});
}
Scaffold(
appBar: AppBar(
title: Text(widget.title)),
body: Stack(
alignment: AlignmentDirectional.bottomStart,
children: [
PageView.builder(
.....
),
// Indicator
Padding(
padding: const EdgeInsets.only(bottom: 30),
child: DotsIndicator(
color: Colors.white,
itemCount: images.length,
controller: _controller,
)),
// 利用 IgnorePointer 忽略 ListView 的滑動
IgnorePointer(
child: ListView.builder(
// 改利用 ScrollController 來操作 ListView
controller: _scrollController,
scrollDirection: Axis.horizontal,
itemCount: texts.length,
itemBuilder: (context, index) {
return Container(
alignment: Alignment.bottomLeft,
// 設定 width 與 Page 一致
width: MediaQuery.of(context).size.width,
padding: const EdgeInsets.only(left: 10, bottom: 50),
child: Text(
texts[index],
style: TextStyle(fontSize: 20, color: Colors.white),
));
},
))
],
));
_scrollController.jumpTo(_controller.offset); 代表 ListView 原理跟 PageView 一樣,offset 代表目前 scroll 移動的距離,所以需要把每一個 Item 的寬度設定跟 Page 一樣(螢慕寬度)。 效果如下: 範例程式,可以到 pageviwer_sample 下載,如果有問題歡迎直接開 issues 給我。
======
開始學習 Flutter 順手記錄開發時遇到的問題與找到的解決方法,盡可能用自己的話説明。希望對大家有所幫助,謝謝。
References
- Flutter之使用PageView实现图片预览视差效果
- Synchronising widget animations with the scroll of a PageView in Flutter
- PageView example with dots indicator
- 用 Flutter 实现 PageView 指示器
- Build page indicators for the PageView
- Flutter : let’s know the ScrollController and ScrollNotification
- Programmatically scrolling to the end of a ListView
- Flutter — BoxDecoration Cheat Sheet
- Flutter-你还在滥用StatefulWidget吗
- Flutter 的那一兩件事 — Layout
- How to change status bar color in Flutter?
- Drawing Custom Shapes in Flutter using CustomPainter
- Flutter - Drawing a rectangle in bottom
- Flutter 簡易動畫
- 初見 Widget 概念
- Fade a widget in and out
- Flutter Animated Series : Animated Opacity