Flutter迴圈滑動的PageView

Go’ball 發表於 2020-12-02

序言

Android原生裡一般會使用ViewPager來實現Banner區域,當然Flutter中的PageView也可以實現類似的效果,今天就來擼一把迴圈滑動的PageView。

在Android中想要實現迴圈滑動的ViewPager,最常用的方法是,在原資料來源的基礎上,通過前後補位來操作:即準備新的資料集合list , 第一個位置插入原資料中的最後一個元素、最後一個位置插入原資料中的第一個元素,Flutter中PageView實現迴圈滑動的方法如出一轍,如下圖所示:

在這裡插入圖片描述
在使用者滑動過程中,當(2)被選中後,無動畫切換到2的位置;當(0)被選中後,此時無動畫切換到0的位置。即可實現迴圈滑動的PageView。

準備新的資料來源

這裡需要解釋下,如果只有一個資料的話,不考慮迴圈滑動

  ///初始化Page
  ///
  ///準備一個新的資料來源list
  ///在原資料data的基礎上,前後各新增一個view  data[data.length-1]、data[0]
  void _initWidget() {
    currentIndex = widget.controller.initialPage;
    if (widget.children == null || widget.children.isEmpty) return;
    if (widget.children.length == 1) {
      _children.addAll(widget.children);
    } else {
      _children.add(widget.children[widget.children.length - 1]);
      _children.addAll(widget.children);
      _children.add(widget.children[0]);
    }
  }

當使用者在滑動到新位置的Page後,會觸發PageView的回撥監聽onPageChanged(int index),引數即為新選中的Page索引,此時我們需要及時將頁面切換到正確的位置

///Page切換後的回撥,及時修復索引
  _onPageChanged(int index) async {
    if (index == 0) {
      //當前選中的是第一個位置,自動選中倒數第二個位置
      currentIndex = _children.length - 2;
      await Future.delayed(Duration(milliseconds: 400));
      widget.controller?.get()?.jumpToPage(currentIndex);
      realPosition = currentIndex - 1;
    } else if (index == _children.length - 1) {
      //當前選中的是倒數第一個位置,自動選中第二個索引
      currentIndex = 1;
      await Future.delayed(Duration(milliseconds: 400));
      widget.controller?.get()?.jumpToPage(currentIndex);
      realPosition = 0;
    } else {
      currentIndex = index;
      realPosition = index - 1;
      if (realPosition < 0) realPosition = 0;
    }
      setState(() {});
  }

你可能會發現在呼叫jumpToPage之前為什麼延遲了400毫秒,這裡做一個短暫的延遲是因為PageView在切換頁面後如果立即jumpToPage會出現卡頓的現象,做短暫延遲可以規避這個問題。

定時切換

目前已經實現了PageView的迴圈滑動,那麼現在我們加一個定時器,每隔2s自動切換下一個頁面。

///建立定時器
  void createTimer() {
    if (widget.isTimer) {
      cancelTimer();
      _timer = Timer.periodic(widget.delay, (timer) => _scrollPage());
    }
  }

  ///定時切換PageView的頁面
  void _scrollPage() {
    ++currentIndex;
    var next = currentIndex % _children?.length;
    widget.controller?.get()?.animateToPage(
          next,
          duration: widget.duration,
          curve: Curves.ease,
        );
  }

滑動衝突

到這裡就實現了可以定時自動迴圈滑動的PageView,但是看下實際效果你會發現,當使用者在滑動過程中,定時器還在進行,此時就需要取消定時器,當使用者手指離開後再開啟定時器自動輪播。

所以這裡你可以給PageView包裹一層NotificationListener來監聽使用者滑動

@override
  Widget build(BuildContext context) => NotificationListener(
        onNotification: (notification) => _onNotification(notification),
        child: PageView(
          scrollDirection: widget.scrollDirection,
          reverse: widget.reverse,
          controller: widget.controller?.get(),
          physics: widget.physics,
          pageSnapping: widget.pageSnapping,
          onPageChanged: (index) => _onPageChanged(index),
          children: _children,
          dragStartBehavior: widget.dragStartBehavior,
          allowImplicitScrolling: widget.allowImplicitScrolling,
          restorationId: widget.restorationId,
          clipBehavior: widget.clipBehavior,
        ),
      );
///Page滑動監聽
  _onNotification(notification) {
    if (notification is ScrollStartNotification) {
      isEnd = false;
    } else if (notification is UserScrollNotification) {
      //使用者滑動時回撥順序:start - user , end - user
      if (isEnd) {
        isUserGesture = false;
        _start();
        return;
      }
      isUserGesture = true;
      _stop();
    } else if (notification is ScrollEndNotification) {
      isEnd = true;
      if (isUserGesture) {
        _start();
      }
    }
  }

值得注意的是,自動滑動和使用者滑動都會觸發start、end事件,但是使用者滑動時會觸發user事件,滑動時回撥順序:start - user 、 end - user,所以只需要在user事件回撥中判斷是否手指離開了,即可區分使用者滑動和頁面滑動,實現使用者滑動狀態下暫停定時器,使用者手指離開後啟動定時器。

看下最終的實現效果,程式碼裡時加了頁面圓點指示器的,可以參考程式碼自定義配置。

視訊錄製效果