此圖與正文無關,只是爲了好看前端
上一篇文章寫了如何經過 CustomPaint
實現一個浮動導航欄,閱讀量不高,可能不是你們關心的東西。那麼這篇文章來寫一個經常使用功能————無限輪播圖。git
此輪播圖的開發源於個人一個項目,文末能夠看到,是由於 pub 上的插件不知足個人需求(或者說不適合個人需求),因此決定本身試着寫一個,先看一下最終效果。github
圖片來源於網易雲音樂,聽歌時候順手扒的,侵權即刪web
實現起來其實很簡單,Flutter 提供了一個 PageView
組件,自己就能夠作到這樣的滑動切換效果,只是在實現無限輪播的時候有個小問題,什麼問題呢?不着急,後面我會講。數組
首先從前端的角度思考一下(爲何從前端的角度?由於我只是個前端)如何作無限輪播,一般個人作法(各位各顯神通)是在數組圖片的頭部複製最後一張,在數組圖片的尾部複製第一張,而後在輪播到最後一張後跳到第二張,輪播到第一張後跳到倒數第二張。因此,順着這個思路(慣性思惟),咱們先來實現這個無限輪播。app
首先新建兩個文件 carousel
和 CustomPageView
,CustomPageView
中就是複製的 PageView
的代碼:async
在 carousel
中新建一個 StatefulWidget
:ide
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
須要用的,其餘的沒什麼好說的。post
接着,設一個定時器,由於咱們要作的是自動輪播:動畫
//設置定時器
_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 實戰教程,有興趣的能夠看一下