Flutter 拆輪子之flutter_swiper自動無線輪播卡片

藍色微笑ing發表於2020-01-02

Flutter 拆輪子之flutter_swiper自動無線輪播卡片

前言

flutter_swiper支援卡片輪播無限迴圈,適用於於Android端、iOS端。

本文介紹外掛的具體使用方法及深入分析它的程式碼是如何運作的。領會一下外掛開發者的思想,並從中改進自身程式碼的不足。好吧,其實在說我自己啦,程式碼毛病多,思路亂,維護成本高,所以想通過閱讀大神的程式碼提升一下自身水平。我嘗試了自己再寫一遍外掛的程式碼,感覺確實有所提升。而且,細節確實非常多。

flutter_swiper當前版本:flutter_swiper 1.1.6

pub.dev地址:pub.dev/packages/fl…

github地址:github.com/best-flutte…

使用方法簡介

Swiper的屬性還是比較多的,在這就不列舉了,pub.dev和github上都有展示。

import 'package:flutter_swiper/flutter_swiper.dart';

Swiper(
  itemBuilder: (BuildContext context, int index) {
    return Image.asset(
      images[index],
      fit: BoxFit.fill,
    );
  },
  indicatorLayout: PageIndicatorLayout.COLOR,
  autoplay: true,
  itemCount: images.length,
  pagination: SwiperPagination(),
  control: SwiperControl(),
);
複製程式碼

原始碼目錄結構

先簡要分析一下每個檔案的主要功能,在腦海中能有一個大致的輪廓。

flutter_swiper

  • lib/
    • src/
      • custom_layout.dart
      • swiper.dart
      • swiper_control.dart
      • swiper_controller.dart
      • swiper_pagination.dart
      • swiper_plugin.dart
    • flutter_swiper.dart
  • pubspec.yaml

Flutter 拆輪子之flutter_swiper自動無線輪播卡片

pubspec.yaml

有兩個依賴項,這兩個外掛和flutter_swiper是同一個開發者。開發者github地址:github.com/best-flutte….

transformer_page_view: ^0.1.6
flutter_page_indicator: ^0.0.3
複製程式碼

transformer_page_view:頁面切換及切換動畫。pub.dev/packages/tr…

flutter_page_indicator:頁面指示器,支援NONE、SLIDE、WARM、COLOR、SCALE、DROP. pub.dev/packages/fl…

swiper.dart

外掛最主要的類,有三個構造方法,原始構造方法Swiper(), Swiper.children()方法, Swiper.list()方法, 後兩個方法通過接收引數轉換,呼叫了Swiper().

支援四種Layout佈局,DEFAULT, STACK, TINDER, CUSTOM.

custom_layout.dart

當Layout為CUSTOM時,定義了CustomLayoutOption及多種TransformBuilder, 當頁面轉換時,增加了addOpacity, addTranslate, addScale, addRotate多種動畫效果。

swiper_controller.dart

SwiperController間接方式繼承了ChangeNotifier,記錄使用者執行的動作event,並通知監聽者。主要功能是,控制頁面的切換,主要方法有:

  • startAutoplay():自動播放
  • stopAutoplay():停止播放
  • move(int index, {bool animation: true}):移動到某頁
  • next({bool animation: true}):下一頁
  • previous({bool animation: true}):上一頁

繼承關係:

SwiperController extends IndexController...
IndexController extends ChangeNotifier...
複製程式碼

SwiperController方法示例:

static const int START_AUTO_PLAY = 2;
static const int STOP_AUTO_PLAY = 3;

int index;
bool autoplay;

void startAutoplay() {
  event = SwiperController.START_AUTO_PLAY;
  this.autoplay = true;
  notifyListeners();
}
複製程式碼

呼叫者示例:

controller.addListener(_onController);

void _onController() {
  switch (_controller.event) {
    case SwiperController.START_AUTO_PLAY:
      if (_timer == null) _startAutoplay();
      break;
  }
}
複製程式碼

swiper_control.dart

SwiperControl主要是構建向左向右的按鈕,點選按鈕的時候,通過SwiperController的previous和next方法,通知監聽者。

Flutter 拆輪子之flutter_swiper自動無線輪播卡片

swiper_pagination.dart

SwiperPagination,自定義頁碼指示器。

持有SwiperPlugin物件,並通過SwiperPlugin的build方法渲染頁面指示器。

Widget build(BuildContext context, SwiperPluginConfig config) {
  ...
}
複製程式碼

SwiperPluginConfig物件擁有Swiper的很多屬性,像itemCount, loop, PageController, SwiperController等。

外掛的頁碼指示器並未實現頁碼的點選功能,如果讀著想實現Dots, Slide等的點選後跳轉,可以在Build方法中增加GestureDetector,通過SwiperController物件實現跳轉。

swiper_plugin.dart

SwiperPlugin是一個抽象類,主要服務於SwiperPagination,包含一個build方法,注意build方法持有SwiperPluginConfig物件。

它的實現類有SwiperControl, FractionPaginationBuilder, RectSwiperPaginationBuilder, DotSwiperPaginationBuilder, SwiperCustomPagination, SwiperPagination.

abstract class SwiperPlugin {
  const SwiperPlugin();

  Widget build(BuildContext context, SwiperPluginConfig config);
}
複製程式碼

flutter_swiper.dart

這個dart檔案主要是匯入以上各個檔案,供呼叫者外掛者使用。

library flutter_swiper;

export 'src/swiper.dart';
export 'src/swiper_pagination.dart';
export 'src/swiper_control.dart';
export 'src/swiper_controller.dart';
export 'src/swiper_plugin.dart';
複製程式碼

實現思路

  1. 最基礎還是PageView佈局。
  2. NotificationListener包裹PageView實現滾動監聽。
  3. SwiperController實現控制PageView的操作,上一頁、下一頁、自動滾動、停止滾動等操作。
  4. SwiperControl實現前一個、後一個頁面的切換,不同於SwiperController的地方是,SwiperControl通過build實現Button的渲染。
  5. SwiperPagination渲染頁碼指示器。
  6. GestureDetector包裹PageView的Item Widget,實現條目點選監聽。
  7. ItemCount是實際Item的個數,PageView的itemCount傳遞的是實際的ItemCount加上一個中間值1000000000,實現無限迴圈。
  8. 監聽PageView的onPageChanged實現頁面切換監聽。
  9. 定義Timer,實現每autoplayDelay下呼叫SwiperController的next方法切換下一頁。
  10. 通過AnimatedBuilder, Opacity, Transform.rotate, Transform.translate, Transform.scale等實現Page切換的動畫。

功能實現

當你閱讀上面的介紹之後,有沒有想試一下自己實現一下。接下來我們一起從簡單到複雜,一步步實現它的功能(時間有限,並未實現所有功能)。如果你有什麼問題可以一起討論。

實現步驟

  1. 第一步:初步封裝PageView
    • 實現條目點選
    • 實現切換監聽
  2. 第二步:自定義SwiperController
    • 下一頁
    • 上一頁
  3. 第三步:實現無限迴圈
  4. 第四步:實現自動輪播及轉換動畫
  5. 第五步:實現頁碼展示

第一步:初步封裝PageView

建立程式名為flutter_swiper,並新增assets三張輪播圖片,執行Package get。

Flutter 拆輪子之flutter_swiper自動無線輪播卡片

Widget _buildSwiper() {
  IndexedWidgetBuilder itemBuilder;
  if (widget.onTap != null) {
    itemBuilder = _wrapTap;
  } else {
    itemBuilder = widget.itemBuilder;
  }
  return PageView.builder(
    controller: _pageController,
    itemCount: widget.itemCount,
    itemBuilder: itemBuilder,
    onPageChanged: widget.onIndexChanged,
  );
}


複製程式碼

第二步:自定義SwiperController

自定義SwiperController,實現Next,Previous.

Flutter 拆輪子之flutter_swiper自動無線輪播卡片

import 'dart:async';
import 'package:flutter/foundation.dart';

class SwiperController extends ChangeNotifier {
  /// Next page
  static const int NEXT = 1;

  /// Previous page
  static const int PREVIOUS = -1;

  Completer _completer;

  /// Current index
  int index;

  /// Current event
  int event;

  SwiperController();

  Future next() {
    this.event = NEXT;
    _completer = Completer();
    notifyListeners();
    return _completer.future;
  }

  Future previous() {
    this.event = PREVIOUS;
    _completer = Completer();
    notifyListeners();
    return _completer.future;
  }

  void complete() {
    if (!_completer.isCompleted) {
      _completer.complete();
    }
  }
}

class _SwiperState extends State<Swiper> {
  ...
  
  @override
  void initState() {
    super.initState();
    ...
    // SwiperController
    _swiperController = widget.swiperController;
    _swiperController.addListener(_onController);
  }
  
  void _onController() {
    int event = widget.swiperController.event;
    int index = _pageController.page.floor();
    switch (event) {
      case SwiperController.PREVIOUS:
        index--;
        break;
      case SwiperController.NEXT:
        index++;
        break;
    }
    if (index < 0 || index >= widget.itemCount) return;
    _pageController.jumpToPage(index);
    widget.swiperController.complete();
    _activeIndex = index;
  }
}
複製程式碼

第三步:實現無限迴圈

迴圈引數定義:

  • bool loop:標記無限迴圈模式。

如果是無限迴圈模式,把Swiper.itemCount加上一個常量值(2000000000)傳遞給PageView.itemCount,Swiper.index加上常量值(1000000000)傳遞給PageView.PageController.initialPage.

注意點:

  1. PageView的itemBuilder中,index是Swiper的index,即沒有加上常量值的。
  2. PageView的onPageChanged中,index是Swiper的index,即沒有加上常量值的。
  3. SwiperController的Previous和Next,是加上常量值之後的。

總之,需要注意PageView的實際RealIndex,和Swiper的RenderIndex.

Flutter 拆輪子之flutter_swiper自動無線輪播卡片

const int kMaxValue = 2000000000;
const int kMiddleValue = 1000000000;

int getRealItemCount() {
  if (widget.itemCount == 0) return 0;
  return widget.loop ? widget.itemCount + kMaxValue : widget.itemCount;
}

int getRealIndexFromRenderIndex({int index, bool loop}) {
  int initPage = index;
  if (loop) {
    initPage += kMiddleValue;
  }
  return initPage;
}

int getRenderIndexFromRealIndex({int index, bool loop, int itemCount}) {
  if (itemCount == 0) return 0;
  int renderIndex;
  if (loop) {
    renderIndex = index - kMiddleValue;
    renderIndex = renderIndex % itemCount;
    if (renderIndex < 0) {
      renderIndex += itemCount;
    }
  } else {
    renderIndex = index;
  }
  return renderIndex;
}
複製程式碼

第四步:實現自動輪播及轉換動畫

輪播引數定義:

  • bool autoPlay:標記自動輪播模式。
  • int autoPlayDelay:輪播模式下,當前頁面停留時間。
  • bool autoPlayDisableOnInteraction:輪播模式下,使用者主動滑動是否停止輪播。

轉換動畫引數定義:

  • int duration:轉換持續時間, 即PageController.animationTo的duration.
  • Curve curve:轉換曲線

輪播實現思路:

  1. 監聽SwiperController的 START_AUTO_PLAY 和 STOP_AUTO_PLAY.
  2. 建立定時器 Timer 週期執行 pageController.animateToPage(duration, curve) 方法。

注意點:

  1. 使用者主動滾動PageView的時候,需要停止動畫,即通過NotificationListener包裹PageView監聽ScrollNotification.
  2. 停止輪播時,Timer需要及時銷燬。
  3. 兩個Duration時間的區別,轉換動畫引數duration是轉換過程的快慢,輪播引數autoPlayDelay是動畫結束後,當前頁面的停留時間。

Flutter 拆輪子之flutter_swiper自動無線輪播卡片

/// Controller auto play and stop
abstract class _SwiperTimerMixin extends State<Swiper> {
  Timer _timer;

  SwiperController _controller;

  @override
  void initState() {
    _controller = widget.controller;
    _controller ??= SwiperController();
    _controller.addListener(_onController);
    _handleAutoPlay();
    super.initState();
  }

  void _onController() {
    switch (_controller.event) {
      case SwiperController.START_AUTO_PLAY:
        if (_timer == null) _startAutoPlay();
        break;
      case SwiperController.STOP_AUTO_PLAY:
        if (_timer != null) _stopAutoPlay();
        break;
    }
  }

  @override
  void didUpdateWidget(Swiper oldWidget) {
    if (_controller != oldWidget.controller) {
      if (oldWidget.controller != null) {
        oldWidget.controller.removeListener(_onController);
        _controller = oldWidget.controller;
        _controller.addListener(_onController);
      }
    }
    _handleAutoPlay();
    super.didUpdateWidget(oldWidget);
  }

  @override
  void dispose() {
    _controller?.removeListener(_onController);
    _stopAutoPlay();
    super.dispose();
  }

  bool get autoPlayEnabled => _controller.autoPlay ?? widget.autoPlay;

  void _handleAutoPlay() {
    if (autoPlayEnabled && _timer != null) return;
    _stopAutoPlay();
    if (autoPlayEnabled) _startAutoPlay();
  }

  void _startAutoPlay() {
    assert(_timer == null, "Timer must be stopped before start!");
    _timer = Timer.periodic(
      Duration(milliseconds: widget.autoPlayDelay),
      _onTimer,
    );
  }

  void _onTimer(Timer timer) => _controller.next(animation: true);

  void _stopAutoPlay() {
    if (_timer != null) {
      _timer.cancel();
      _timer = null;
    }
  }
}
複製程式碼

Flutter 拆輪子之flutter_swiper自動無線輪播卡片

  • transformer_page_view

    對頁面跳轉動畫及迴圈的封裝

  • transformer_page_controller

    對無限迴圈的封裝,主要是RealIndex和RenderIndex的互轉

這裡面需要注意的是didUpdateWidget(oldWidget)方法,如果在State類,例如_SwiperState,_TransformerPageViewState等,這些類中維護了一些變數,就要注意這些變數在傳入新值時候的更新處理。

示例:

class _TransformerPageViewState extends State<TransformerPageView> {
  ...
  @override
  void didUpdateWidget(TransformerPageView oldWidget) {
    int index = widget.index ?? 0;
    bool created = false;
    // PageController changed
    // Here '_pageController' is oldWidget.pageController
    if (_pageController != widget.pageController) {
      if (widget.pageController != null) {
        _pageController = widget.pageController;
      } else {
        created = true;
        _pageController = TransformerPageController(
          initialPage: widget.index,
          itemCount: widget.itemCount,
          loop: widget.loop,
        );
      }
    }
    // Index changed
    if (_pageController.getRenderIndexFromRealIndex(_activeIndex) != index) {
      _activeIndex = _pageController.initialPage;
      if (!created) {
        int initPage = _pageController.getRealIndexFromRenderIndex(index);
        _pageController.animateToPage(
          initPage,
          duration: widget.duration,
          curve: widget.curve,
        );
      }
    }
    // SwiperController changed
    if (_swiperController != widget.controller) {
      if (_swiperController != null) {
        _swiperController.removeListener(onChangeNotifier);
      }
      _swiperController = widget.controller;
      if (_swiperController != null) {
        _swiperController.addListener(onChangeNotifier);
      }
    }
    super.didUpdateWidget(oldWidget);
  }
  ...
}
複製程式碼

第五步:實現頁碼展示

主要是把swiper的widget和pagination的widget用Stack包裹起來。

SwiperPagination需要持有Swiper的引數,如itemCount, loop, swiperController, pageController等引數。

class SwiperPaginationConfig {
  final int activeIndex;
  final int itemCount;
  final bool loop;
  final PageController pageController;
  final SwiperController swiperController;

  const SwiperPaginationConfig({
    this.activeIndex,
    this.itemCount,
    this.swiperController,
    this.pageController,
    this.loop,
  }) : assert(swiperController != null);
}

/// Here only Dots Pagination
class SwiperPagination {
  /// color when current index,if set null, will be Theme.of(context).primaryColor
  final Color activeColor;

  /// if set null, will be Theme.of(context).scaffoldBackgroundColor
  final Color color;

  ///Size of the dot when activate
  final double activeSize;

  ///Size of the dot
  final double size;

  /// Space between dots
  final double space;

  /// Distance between pagination and the container
  final EdgeInsetsGeometry margin;

  final Key key;

  const SwiperPagination({
    this.key,
    this.activeColor,
    this.color,
    this.size: 10.0,
    this.activeSize: 10.0,
    this.space: 3.0,
    this.margin: const EdgeInsets.all(10.0),
  });

  Widget build(BuildContext context, SwiperPaginationConfig config) {
    Color activeColor = this.activeColor;
    Color color = this.color;
    // Default
    activeColor ??= Theme.of(context).primaryColor;
    color ??= Theme.of(context).scaffoldBackgroundColor;

    List<Widget> children = [];
    int itemCount = config.itemCount;
    int activeIndex = config.activeIndex;

    for (int i = 0; i < itemCount; ++i) {
      bool active = i == activeIndex;
      children.add(Container(
        key: Key("Pagination_$i"),
        margin: EdgeInsets.all(space),
        child: ClipOval(
          child: Container(
            color: active ? activeColor : color,
            width: active ? activeSize : size,
            height: active ? activeSize : size,
          ),
        ),
      ));
    }
    return Row(
      key: key,
      mainAxisSize: MainAxisSize.min,
      children: children,
    );
  }
}
複製程式碼

目錄結構圖

Flutter 拆輪子之flutter_swiper自動無線輪播卡片

程式碼地址

github.com/smiling1990…

相關文章