此圖與正文無關,只是為了好看
寫在前面
上一篇文章寫了如何通過 CustomPaint
實現一個浮動導航欄,閱讀量不高,可能不是大家關心的東西。那麼這篇文章來寫一個常用功能————無限輪播圖。
此輪播圖的開發源於我的一個專案,文末可以看到,是因為 pub 上的外掛不滿足我的需求(或者說不適合我的需求),所以決定自己試著寫一個,先看一下最終效果。
圖片來源於網易雲音樂,聽歌時候順手扒的,侵權即刪
閱讀重點
實現起來其實很簡單,Flutter 提供了一個 PageView
元件,本身就可以做到這樣的滑動切換效果,只是在實現無限輪播的時候有個小問題,什麼問題呢?不著急,後面我會講。
首先從前端的角度思考一下(為什麼從前端的角度?因為我只是個前端)如何做無限輪播,通常我的做法(各位各顯神通)是在陣列圖片的頭部複製最後一張,在陣列圖片的尾部複製第一張,然後在輪播到最後一張後跳到第二張,輪播到第一張後跳到倒數第二張。所以,順著這個思路(慣性思維),我們先來實現這個無限輪播。
首先新建兩個檔案 carousel
和 CustomPageView
,CustomPageView
中就是複製的 PageView
的程式碼:
在 carousel
中新建一個 StatefulWidget
:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_samples/carousel/CustomPageView.dart';
class Carousel extends StatefulWidget {
@override
_State createState() => _State();
}
class _State extends State<Carousel> {
PageController _pageController = PageController(initialPage: 1);//索引從0開始,因為有增補,所以這裡設為1
int _currentIndex = 1;
List<String> _images = [
'images/1.png',
'images/2.png',
'images/3.png',
'images/4.png',
'images/5.png',
'images/6.png',
'images/7.png',
'images/8.png',
'images/9.png',
];
Timer _timer;//定時器
}
複製程式碼
第一個 import
是的 Timer
需要用的,其他的沒什麼好說的。
接著,設一個定時器,因為我們要做的是自動輪播:
//設定定時器
_setTimer() {
_timer = Timer.periodic(Duration(seconds: 4), (_) {
_pageController.animateToPage(_currentIndex + 1,
duration: Duration(milliseconds: 400), curve: Curves.easeOut);
});
}
複製程式碼
這裡通過 periodic
方法設定一個定時器,每隔 4 秒執行一次,執行的內容就是滑動到下一張。
接著,處理圖片陣列:
@override
Widget build(BuildContext context) {
List addedImages = [];
if (_images.length > 0) {
addedImages
..add(_images[_images.length - 1])
..addAll(_images)
..add(_images[0]);
}
return Scaffold(
appBar: AppBar(
elevation: 0.0,
title: Text('Carousel'),
centerTitle: true,
),
body: AspectRatio(
aspectRatio: 2.5,
child:
),
);
}
複製程式碼
這裡定義一個 addedImages
,表示是增補過後的圖片陣列(記得判斷一下 _images
是否為空,雖然我們這裡是寫死了的,但是思維要有)。
aspectRatio
表示的是寬高比,AspectRatio
會自動根據傳入的 aspectRatio
設定子元件的高度,而且高度會根據螢幕寬度的改變自動調整(後面給大家看效果),所以,要做適配的筒子們,記下筆記。
接著,編寫圖片部分的程式碼:
NotificationListener(
onNotification: (ScrollNotification notification) {
if (notification.depth == 0 &&
notification is ScrollStartNotification) {
if (notification.dragDetails != null) {
_timer.cancel();
}
} else if (notification is ScrollEndNotification) {
_timer.cancel();
_setTimer();
}
},
child: _images.length > 0
? CustomPageView(
physics: BouncingScrollPhysics(),
controller: _pageController,
onPageChanged: (page) {
int newIndex;
if (page == addedImages.length - 1) {
newIndex = 1;
_pageController.jumpToPage(newIndex);
} else if (page == 0) {
newIndex = addedImages.length - 2;
_pageController.jumpToPage(newIndex);
} else {
newIndex = page;
}
setState(() {
_currentIndex = newIndex;
});
},
children: addedImages
.map((item) => Container(
margin: EdgeInsets.all(10.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(5.0),
child: Image.asset(
item,
fit: BoxFit.cover,
),
),
))
.toList(),
)
: Container(),
),
複製程式碼
我們在 onNotification
中幹了兩件很重要的事,一個是在當使用者用手(也可以用腳)滑動輪播的時候取消定時器,然後在輪播滑動結束後重設定時器。
notification.depth
表示的是事件此時處於哪一級,什麼意思呢?在 Flutter 中,事件也是冒泡的,所以,源頭(也就是事件最初發出的那一級)是 0,如果不明白,可以一邊參考 web 的事件一邊看文件。
notification.dragDetails
可以拿到滑動的位移,我們這裡暫時不會用到,只是再確定一下使用者滑動了輪播。
輪播每切換一次,我們就在 CustomPageView
(也就是原有的 PageView
)的 onPageChanged
回撥中重新設定當期索引。
接下來是指示器部分:
Positioned(
bottom: 15.0,
left: 0,
right: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: _images
.asMap()
.map((i, v) => MapEntry(
i,
Container(
width: 6.0,
height: 6.0,
margin: EdgeInsets.only(left: 2.0, right: 2.0),
decoration: ShapeDecoration(
color: _currentIndex == i + 1
? Colors.red
: Colors.white,
shape: CircleBorder()),
)))
.values
.toList(),
),
)
複製程式碼
重點來了,在 dart
中對 List
遍歷的方法都沒有提供索引(好像是,記不清了),因此如何實現當前項高亮就是一個小問題了。有兩種方式,一是新建一個方法,在方法中通過 for
迴圈去處理(我不太喜歡);第二個就是文中的方式。
先將 List
通過 asMap
轉換成 Map
,此時 Map
中的 key
就是索引,value
就是值,接著通過 Map
的 map
方法就可以拿到索引了(不明白的筒子,記得看文件)。
接著在 initState
中呼叫定時器就可以了:
@override
void initState() {
print(_images.asMap());
if (_images.length > 0) {
_setTimer();
}
super.initState();
}
複製程式碼
看下效果:
眼尖的筒子可能已經發現問題了,那就是在滑動到第一張或者最後一張的時候會有閃爍,甚至如果是使用者去滑動的話,還會出現非理想切換:
這個就是我上面說過的用原有 PageView
做無限輪播會出現的小問題,在第一張和最後一張(實際上對所有圖片來說都是)滑動過半時,就會切換新頁。
實際上無限輪播的效果已經實現了,只是有這個小問題不和諧,因此只要解決了這個問題,無限輪播就完美了。
那麼如何解決這個問題呢?我們來看一下 PageView
的原始碼,其中有這樣一段程式碼:
onNotification: (ScrollNotification notification) {
if (notification.depth == 0 && widget.onPageChanged != null && notification is ScrollUpdateNotification) {
final PageMetrics metrics = notification.metrics;
final int currentPage = metrics.page.round();
if (currentPage != _lastReportedPage) {
_lastReportedPage = currentPage;
widget.onPageChanged(currentPage);
}
}
return false;
}
複製程式碼
小問題就出現在這一句:
notification is ScrollUpdateNotification
複製程式碼
這一句標識了 notification
的型別,讓其在滑動過程中不斷執行 if
內部的程式碼,一旦 metrics.page
的小數部分大於了 0.5,metrics.page.round()
就會得到新的 page
,就會進行切換。
所以我們將這裡的 ScrollUpdateNotification
改成 ScrollEndNotification
就可以了,就是在滑動結束後在執行內部判斷,就這麼簡單。
當然還可以給 PageController
的 viewportFraction
傳入一個值,比如 0.9,實現一個視差效果:
至此,我們的無限輪播就實現了,最後還有一個重要的東西,記得銷燬定時器:
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
複製程式碼
說好的自適應效果:
最後叨叨
文中所述的這種方式配上動畫足以實現大多數常規輪播效果,當然如果設計師能拿出更加犀利的效果圖,大家可能就要去研究一下 Scrollable
了,但這不是本文的重點,原始碼點這裡。
錄製了一套 Flutter 實戰教程,有興趣的可以看一下