Flutter之使用PageView實現圖片預覽視差效果
繼續Flutter系列部落格的更新,google在昨天12月14日釋出了Flutter的1.0(Stable)版本,不同於google挖的其他坑,Flutter自發布release版本號到第一個stable版本之間也才過了十個月,可見google對Flutter的定位絕不是一個實驗性質的框架。
雖然有人說放版本號不要錢,但這最起碼也說明了官方的態度,像Facebook的ReactNative,已經陸陸續續更新了三年,目前為止的最新版是0.57,仍舊沒有釋出1.0,加上其他因素已經有很多開發者放棄了使用ReactNative和相關開源庫的維護。
Google在昨天放出的公告上還有更勁爆的訊息,Flutter支援桌面應用開發和Web應用開發的框架將會陸續放出來,屆時Flutter將真正成為跨APP/桌面/Web的跨平臺UI框架,非常值得期待, ofollow,noindex">相關連結放上(梯子自備)。
碎碎念有點多了,本篇記錄如何使用PageView元件實現圖片預覽的視差效果,效果圖如下:

建立工程
開啟AndroidStudio,建立新的Flutter工程,調整main.dar檔案,清理無用程式碼便於demo演示,清理後得到如下程式碼:
void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: MyHomePage(title: 'Flutter Demo'), ); } } class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( ), ); } } 複製程式碼
上面程式碼沒什麼可說的,繼續。
使用PageView
在'_MyHomePageState'中加入一個 PageView
元件,並顯示幾張圖片,通過修改build方法:
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Container( child: PageView( children: <Widget>[ Image.network('https://ws1.sinaimg.cn/large/0065oQSqgy1fwgzx8n1syj30sg15h7ew.jpg'), Image.network('https://ws1.sinaimg.cn/large/0065oQSqly1fw8wzdua6rj30sg0yc7gp.jpg'), Image.network('https://ws1.sinaimg.cn/large/0065oQSqly1fw0vdlg6xcj30j60mzdk7.jpg'), Image.network('https://ws1.sinaimg.cn/large/0065oQSqly1fuo54a6p0uj30sg0zdqnf.jpg'), ], ), width: 200, height: 200, ), ), ); } 複製程式碼
上面的PageView的基礎用法,通過上面的程式碼,我們能得到一個可以左右翻頁的圖片預覽元件。
接下來建立一個 PageController
來監聽PageView的翻頁回撥,在 _MyHomePageState
類中建立PageController物件,在initState方法中新增監聽,並在build方法中將建立的PageController物件指定給PageView元件。程式碼如下
class _MyHomePageState extends State<MyHomePage> { var imgUrlList = [ 'https://ws1.sinaimg.cn/large/0065oQSqgy1fwgzx8n1syj30sg15h7ew.jpg', 'https://ws1.sinaimg.cn/large/0065oQSqly1fw8wzdua6rj30sg0yc7gp.jpg', 'https://ws1.sinaimg.cn/large/0065oQSqly1fw0vdlg6xcj30j60mzdk7.jpg', 'https://ws1.sinaimg.cn/large/0065oQSqly1fuo54a6p0uj30sg0zdqnf.jpg' ]; PageController controller = new PageController(); @override void initState() { super.initState(); controller.addListener(() { }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Container( child: PageView( controller: controller, children: imgUrlList .map((item) => buildPageItem(imgUrlList.indexOf(item), item)) .toList(), ), width: 200, height: 200, ), ), ); } Widget buildPageItem(int index, String imgUrl) { return Image.network(imgUrl); } 複製程式碼
在上面的程式碼中,我額外的將PageView的Item抽到了一個單獨的build方法中,一方面精簡了程式碼,還可以根據情況靈活接入不同的資料,統一控制樣式,這在實際開發中也是一個比較實用的技巧。
給PageView加上特效
以上都是PageView基礎的使用,熟悉的可以直接從這裡開始看,下面開始給PageView加特效。
這個特效本質上是給PageView的Item加的,對於PageView這個容易我們並沒有任何額外處理。
基本的思路是:
1.監聽PageView的滾動回撥
2.計算PageView每幀滾動相對於PageView寬度的百分比
3.通知Item根據計算出的滾動百分比進行相應的偏移和縮放
通過上面程式碼能夠看到我們把元件的寬高設為了200,這裡只用到了寬度200,當然你也可以設定其他寬度。
首先完成1步和2步的思路,在controller的回撥方法中獲取PageView的offset,並計算百分比:
var pageOffset = 0.0; @override void initState() { super.initState(); controller.addListener(() { setState(() { pageOffset = controller.offset / 200; }); }); } 複製程式碼
controller.offset獲取到的是邏輯畫素值,也就是0到200*(N-1)的數值,每翻一頁,都會有從0到到200的增量,翻到最後一頁就是600。把這個值除以寬度200,得到一個以每頁寬度為單位的偏移百分比數值,並儲存到_MyHomePageState的成員變數pageOffset中,以便渲染Item使用。
接下來進行Item的渲染,這也是實現特效最核心的過程。
要用的是Transform的 translate
和 scale
方法,也就是偏移和縮放效果。
將這兩個效果巢狀到item的最外層渲染:
Widget buildPageItem(int index, String imgUrl) { var currentLeftPageIndex = pageOffset.floor(); var currentPageOffsetPercent = pageOffset - currentLeftPageIndex; return Transform.translate( offset: Offset((pageOffset - index) * 200, 0), child: Transform.scale( scale: currentLeftPageIndex == index ? 1 - currentPageOffsetPercent : currentPageOffsetPercent, child: Image.network(imgUrl), ), ); } 複製程式碼
這樣就實現了?
是的,核心程式碼就是這幾行,乍一看可能有些懵逼,下面我來逐行解釋一下。
pageOffset.floor()
得到的 currentLeftPageIndex
是指當前翻頁進度應該位於頁面左側的item的index,你可能會問頁面左側是什麼意思,以普通的PageView為例,一次翻頁過程,最多隻能看到兩頁,假如預設是第0頁,你在翻向第1頁的過程中,第0頁露了一半,第1頁也露了一半,只有完全翻到第1也的時候第0也才會完全看不到。所以這裡的 currentLeftPageIndex
就是指第0頁到第1頁時的第0頁,第1頁到第2頁時的第1頁,第2頁到第3頁時的第2頁,以此類推。
pageOffset - currentLeftPageIndex
得到了變數 currentPageOffsetPercent
,將PageView的全域性偏移值減去當前左側頁面的頁碼,得到的是當前左側頁面的偏移值,這個值的範圍是0到1,也就是從第0頁到第1頁的翻動過程,這個變數會從0變到1,所有頁面在翻動的過程這個值都是0到1。
解釋了上面兩個變數之後,下面的就容易理解了。
PageView翻頁過程是Item是移動的,而要實現Item不移動,則需要給Item等值的負方向偏移,也就是使用Transform.translate巢狀並賦上橫向偏移值(pageOffset - index) * 200。 縮放的話需要考慮兩個情況,上面講到翻頁過程只會看兩個Item,所以scale的值也需要根據 currentLeftPageIndex == index
來判斷圖片是如何縮放的,若 currentLeftPageIndex == index
則是在渲染左側的Item,反之則是在渲染右側的Item,然後用 Transform.scale
巢狀元件即可。
看到這裡,你可能會有些疑問,buildPageItem不是渲染所以的Item嗎?為什麼你的邏輯上只考慮了左右兩個Item,其他Item不會影響到佈局嗎? 答案在這裡:在Flutter的PageView元件中,偏移量外的Item是不進行渲染繪製的,也就是說在正常的PageView中,從第0頁翻到第1頁過程中,第2頁和第3頁並不會被繪製,這不是懶載入,因為你在第2頁翻向第3頁的過程中,第0頁和第1頁也不會被繪製,這和PageView的Item偏移後是否應該出現在頁面上無關。所以上面的邏輯就解釋得通了,我們在處理左右兩個Item時根本就不用考慮其餘的Item,因為PageView已經控制了他們不會出現在頁面上。
關於偏移元件也可以使用 FractionalTranslation
來實現,它接收的引數是基於自身百分比的,效果一樣:
FractionalTranslation( translation: Offset(pageOffset - index, 0), child: Transform.scale( scale: currentLeftPageIndex == index ? 1 - currentPageOffsetPercent : currentPageOffsetPercent, child: Image.network(imgUrl), ), ); 複製程式碼
本篇完
更多幹貨移步我的個人部落格www.nightfarmer.top/