在 Flutter 中實現一個無限輪播

此圖與正文無關,只是爲了好看前端

寫在前面

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

此輪播圖的開發源於個人一個項目,文末能夠看到,是由於 pub 上的插件不知足個人需求(或者說不適合個人需求),因此決定本身試着寫一個,先看一下最終效果。github

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

閱讀重點

實現起來其實很簡單,Flutter 提供了一個 PageView 組件,自己就能夠作到這樣的滑動切換效果,只是在實現無限輪播的時候有個小問題,什麼問題呢?不着急,後面我會講。數組

首先從前端的角度思考一下(爲何從前端的角度?由於我只是個前端)如何作無限輪播,一般個人作法(各位各顯神通)是在數組圖片的頭部複製最後一張,在數組圖片的尾部複製第一張,而後在輪播到最後一張後到第二張,輪播到第一張後到倒數第二張。因此,順着這個思路(慣性思惟),咱們先來實現這個無限輪播。app

首先新建兩個文件 carouselCustomPageViewCustomPageView 中就是複製的 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 就是值,接着經過 Mapmap 方法就能夠拿到索引了(不明白的筒子,記得看文檔)。

接着在 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 就能夠了,就是在滑動結束後在執行內部判斷,就這麼簡單。

固然還能夠給 PageControllerviewportFraction 傳入一個值,好比 0.9,實現一個視差效果:

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

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

說好的自適應效果:

最後叨叨

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

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

相關文章
相關標籤/搜索