在 Flutter 中實現一個無限輪播

MeFelixWang發表於2019-06-13

在 Flutter 中實現一個無限輪播

此圖與正文無關,只是為了好看

寫在前面

上一篇文章寫了如何通過 CustomPaint 實現一個浮動導航欄,閱讀量不高,可能不是大家關心的東西。那麼這篇文章來寫一個常用功能————無限輪播圖。

此輪播圖的開發源於我的一個專案,文末可以看到,是因為 pub 上的外掛不滿足我的需求(或者說不適合我的需求),所以決定自己試著寫一個,先看一下最終效果。

在 Flutter 中實現一個無限輪播

圖片來源於網易雲音樂,聽歌時候順手扒的,侵權即刪

閱讀重點

實現起來其實很簡單,Flutter 提供了一個 PageView 元件,本身就可以做到這樣的滑動切換效果,只是在實現無限輪播的時候有個小問題,什麼問題呢?不著急,後面我會講。

首先從前端的角度思考一下(為什麼從前端的角度?因為我只是個前端)如何做無限輪播,通常我的做法(各位各顯神通)是在陣列圖片的頭部複製最後一張,在陣列圖片的尾部複製第一張,然後在輪播到最後一張後到第二張,輪播到第一張後到倒數第二張。所以,順著這個思路(慣性思維),我們先來實現這個無限輪播。

首先新建兩個檔案 carouselCustomPageViewCustomPageView 中就是複製的 PageView 的程式碼:

在 Flutter 中實現一個無限輪播

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 就是值,接著通過 Mapmap 方法就可以拿到索引了(不明白的筒子,記得看文件)。

接著在 initState 中呼叫定時器就可以了:

 @override
  void initState() {
    print(_images.asMap());
    if (_images.length > 0) {
      _setTimer();
    }
    super.initState();
  }
複製程式碼

看下效果:

在 Flutter 中實現一個無限輪播

眼尖的筒子可能已經發現問題了,那就是在滑動到第一張或者最後一張的時候會有閃爍,甚至如果是使用者去滑動的話,還會出現非理想切換:

在 Flutter 中實現一個無限輪播

這個就是我上面說過的用原有 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 就可以了,就是在滑動結束後在執行內部判斷,就這麼簡單。

在 Flutter 中實現一個無限輪播

當然還可以給 PageControllerviewportFraction 傳入一個值,比如 0.9,實現一個視差效果:

在 Flutter 中實現一個無限輪播

至此,我們的無限輪播就實現了,最後還有一個重要的東西,記得銷燬定時器:

@override
void dispose() {
    _timer?.cancel();
    super.dispose();
}
複製程式碼

說好的自適應效果:

在 Flutter 中實現一個無限輪播

最後叨叨

文中所述的這種方式配上動畫足以實現大多數常規輪播效果,當然如果設計師能拿出更加犀利的效果圖,大家可能就要去研究一下 Scrollable 了,但這不是本文的重點,原始碼點這裡

錄製了一套 Flutter 實戰教程,有興趣的可以看一下

相關文章