Flutter循環(huán)滑動(dòng)的PageView

序言

Android原生里一般會(huì)使用ViewPager來(lái)實(shí)現(xiàn)Banner區(qū)域,當(dāng)然Flutter中的PageView也可以實(shí)現(xiàn)類似的效果,今天就來(lái)擼一把循環(huán)滑動(dòng)的PageView。

在Android中想要實(shí)現(xiàn)循環(huán)滑動(dòng)的ViewPager,最常用的方法是,在原數(shù)據(jù)源的基礎(chǔ)上,通過(guò)前后補(bǔ)位來(lái)操作:即準(zhǔn)備新的數(shù)據(jù)集合list , 第一個(gè)位置插入原數(shù)據(jù)中的最后一個(gè)元素、最后一個(gè)位置插入原數(shù)據(jù)中的第一個(gè)元素,F(xiàn)lutter中PageView實(shí)現(xiàn)循環(huán)滑動(dòng)的方法如出一轍,如下圖所示:

在這里插入圖片描述

在用戶滑動(dòng)過(guò)程中,當(dāng)(2)被選中后,無(wú)動(dòng)畫切換到2的位置;當(dāng)(0)被選中后,此時(shí)無(wú)動(dòng)畫切換到0的位置。即可實(shí)現(xiàn)循環(huán)滑動(dòng)的PageView。

準(zhǔn)備新的數(shù)據(jù)源

這里需要解釋下,如果只有一個(gè)數(shù)據(jù)的話,不考慮循環(huán)滑動(dòng)

  /// 初始化Page
  /// 準(zhǔn)備一個(gè)新的數(shù)據(jù)源list
  /// 在原數(shù)據(jù)data的基礎(chǔ)上,前后各添加一個(gè)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]);
    }
  }

當(dāng)用戶在滑動(dòng)到新位置的Page后,會(huì)觸發(fā)PageView的回調(diào)監(jiān)聽onPageChanged(int index),參數(shù)即為新選中的Page索引,此時(shí)我們需要及時(shí)將頁(yè)面切換到正確的位置

/// Page切換后的回調(diào),及時(shí)修復(fù)索引
  _onPageChanged(int index) async {
    if (index == 0) {
      // 當(dāng)前選中的是第一個(gè)位置,自動(dòng)選中倒數(shù)第二個(gè)位置
      currentIndex = _children.length - 2;
      await Future.delayed(Duration(milliseconds: 400));
      widget.controller?.get()?.jumpToPage(currentIndex);
      realPosition = currentIndex - 1;
    } else if (index == _children.length - 1) {
      // 當(dāng)前選中的是倒數(shù)第一個(gè)位置,自動(dòng)選中第二個(gè)索引
      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(() {});
  }

你可能會(huì)發(fā)現(xiàn)在調(diào)用jumpToPage之前為什么延遲了400毫秒,這里做一個(gè)短暫的延遲是因?yàn)镻ageView在切換頁(yè)面后如果立即jumpToPage會(huì)出現(xiàn)卡頓的現(xiàn)象,做短暫延遲可以規(guī)避這個(gè)問(wèn)題。

定時(shí)切換

目前已經(jīng)實(shí)現(xiàn)了PageView的循環(huán)滑動(dòng),那么現(xiàn)在我們加一個(gè)定時(shí)器,每隔2s自動(dòng)切換下一個(gè)頁(yè)面。

/// 創(chuàng)建定時(shí)器
  void createTimer() {
    if (widget.isTimer) {
      cancelTimer();
      _timer = Timer.periodic(widget.delay, (timer) => _scrollPage());
    }
  }

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

/// 開始定時(shí)滑動(dòng)
  void _start() {
    if (!widget.isTimer) return;
    if (!isActive) return;
    if (_children.length <= 1) return;
    createTimer();
  }

/// 停止定時(shí)滑動(dòng)
  void _stop() {
    if (!widget.isTimer) return;
    cancelTimer();
  }

/// 取消定時(shí)器
  void cancelTimer() {
    _timer?.cancel();
  }

滑動(dòng)沖突

到這里就實(shí)現(xiàn)了可以定時(shí)自動(dòng)循環(huán)滑動(dòng)的PageView,但是看下實(shí)際效果你會(huì)發(fā)現(xiàn),當(dāng)用戶在滑動(dòng)過(guò)程中,定時(shí)器還在進(jìn)行,此時(shí)就需要取消定時(shí)器,當(dāng)用戶手指離開后再開啟定時(shí)器自動(dòng)輪播。

所以這里你可以給PageView包裹一層NotificationListener來(lái)監(jiān)聽用戶滑動(dòng)

@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滑動(dòng)監(jiān)聽
  _onNotification(notification) {
    if (notification is ScrollStartNotification) {
      isEnd = false;
    } else if (notification is UserScrollNotification) {
      // 用戶滑動(dòng)時(shí)回調(diào)順序:start - user , end - user
      if (isEnd) {
        isUserGesture = false;
        _start();
        return;
      }
      isUserGesture = true;
      _stop();
    } else if (notification is ScrollEndNotification) {
      isEnd = true;
      if (isUserGesture) {
        _start();
      }
    }
  }

值得注意的是,自動(dòng)滑動(dòng)和用戶滑動(dòng)都會(huì)觸發(fā)start、end事件,但是用戶滑動(dòng)時(shí)會(huì)觸發(fā)user事件,滑動(dòng)時(shí)回調(diào)順序:start - user 、 end - user,所以只需要在user事件回調(diào)中判斷是否手指離開了,即可區(qū)分用戶滑動(dòng)和頁(yè)面滑動(dòng),實(shí)現(xiàn)用戶滑動(dòng)狀態(tài)下暫停定時(shí)器,用戶手指離開后啟動(dòng)定時(shí)器。

看下最終的實(shí)現(xiàn)效果,代碼里時(shí)加了頁(yè)面圓點(diǎn)指示器的,可以參考代碼自定義配置。

插件地址:

Github
pub.dev

UI
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容