Flutter實現Android跑馬燈及滾動廣告

Changlei發表於2020-04-17

簡介

本文介紹怎麼在Flutter裡使用ListView實現Android的跑馬燈,然後再擴充套件一下,實現上下滾動。

Github地址

該小控制元件已經成功上傳到pub.dev,安裝方式:

dependencies:
   flutterswitcher: ^0.0.1
複製程式碼

效果圖

先上效果圖:

垂直模式

垂直滾動

水平模式

水平滾動

上程式碼

主要有兩種滾動模式,垂直模式和水平模式,所以我們定義兩個構造方法。 引數分別有滾動速度(單位是pixels/second)、每次滾動的延遲、滾動的曲線變化和children為空的時候的佔位控制元件。

class Switcher {
  const Switcher.vertical({
    Key key,
    @required this.children,
    this.scrollDelta = _kScrollDelta,
    this.delayedDuration = _kDelayedDuration,
    this.curve = Curves.linearToEaseOut,
    this.placeholder,
  })  : assert(scrollDelta != null && scrollDelta > 0 && scrollDelta <= _kMaxScrollDelta),
        assert(delayDuration != null),
        assert(curve != null),
        spacing = 0,
        _scrollDirection = Axis.vertical,
        super(key: key);
  
  const Switcher.horizontal({
    Key key,
    @required this.children,
    this.scrollDelta = _kScrollDelta,
    this.delayedDuration = _kDelayedDuration,
    this.curve = Curves.linear,
    this.placeholder,
    this.spacing = 10,
  })  : assert(scrollDelta != null && scrollDelta > 0 && scrollDelta <= _kMaxScrollDelta),
        assert(delayDuration != null),
        assert(curve != null),
        assert(spacing != null && spacing >= 0 && spacing < double.infinity),
        _scrollDirection = Axis.horizontal,
        super(key: key);
}
複製程式碼

實現思路

實現思路有兩種:

  • 第一種是用ListView

  • 第二種是用CustomPaint自己畫;

這裡我們選擇用ListView方式實現,方便後期擴充套件可手動滾動,如果用CustomPaint,實現起來就比較麻煩。

接下來我們分析一下究竟該怎麼實現:

垂直模式

首先分析一下垂直模式,如果想實現迴圈滾動,那麼children的數量就應該比原來的多一個,當滾動到最後一個的時候,立馬跳到第一個,這裡的最後一個實際就是原來的第一個,所以使用者不會有任何察覺,這種實現方式在前端開發中應用很多,比如實現PageView的迴圈滑動,所以這裡我們先定義childCount

_initalizationElements() {
  _childCount = 0;
  if (widget.children != null) {
    _childCount = widget.children.length;
  }
  if (_childCount > 0 && widget._scrollDirection == Axis.vertical) {
    _childCount++;
  }
}
複製程式碼

children改變的時候,我們重新計算一次childCount

@override
void didUpdateWidget(Switcher oldWidget) {
  var childrenChanged = (widget.children?.length ?? 0) != (oldWidget.children?.length ?? 0);
  if (widget._scrollDirection != oldWidget._scrollDirection || childrenChanged) {
    _initalizationElements();
    _initializationScroll();
  }
  super.didUpdateWidget(oldWidget);
}
複製程式碼

這裡判斷如果是垂直模式,我們就childCount++,接下來,實現一下build方法:

@override
Widget build(BuildContext context) {
  if (_childCount == 0) {
    return widget.placeholder ?? SizedBox.shrink();
  }
  return LayoutBuilder(
    builder: (context, constraints) {
      return ConstrainedBox(
        constraints: constraints,
        child: ListView.separated(
          itemCount: _childCount,
          physics: NeverScrollableScrollPhysics(),
          controller: _controller,
          scrollDirection: widget._scrollDirection,
          padding: EdgeInsets.zero,
          itemBuilder: (context, index) {
            final child = widget.children[index % widget.children.length];
            return Container(
              alignment: Alignment.centerLeft,
              height: constraints.constrainHeight(),
              child: child,
            );
          },
          separatorBuilder: (context, index) {
            return SizedBox(
              width: widget.spacing,
            );
          },
        ),
      );
    },
  );
}
複製程式碼

接下來實現垂直滾動的主要邏輯:

_animateVertical(double extent) {
  if (!_controller.hasClients || widget._scrollDirection != Axis.vertical) {
    return;
  }
  if (_selectedIndex == _childCount - 1) {
    _selectedIndex = 0;
    _controller.jumpTo(0);
  }
  _timer?.cancel();
  _timer = Timer(widget.delayedDuration, () {
    _selectedIndex++;
    var duration = _computeScrollDuration(extent);
    _controller.animateTo(extent * _selectedIndex, duration: duration, curve: widget.curve).whenComplete(() {
      _animateVertical(extent);
    });
  });
}
複製程式碼

解釋一下這段邏輯,先判斷ScrollController有沒有載入完成,然後當前的滾動方向是不是垂直的,不是就直接返回,然後當前的index是最後一個的時候,立馬跳到第一個,index初始化為0,接下來,取消前一個定時器,開一個新的定時器,定時器的時間為我們傳進來的間隔時間,然後每間隔widget.delayedDuration的時間滾動一次,這裡呼叫ScrollController.animateTo,滾動距離為每個item的高度乘以當前的索引,滾動時間根據滾動速度算出來:

Duration _computeScrollDuration(double extent) {
  return Duration(milliseconds: (extent * Duration.millisecondsPerSecond / widget.scrollDelta).floor());
}
複製程式碼

這裡是我們小學就學過的,距離 = 速度 x 時間,所以根據距離和速度我們就可以得出需要的時間,這裡乘以Duration.millisecondsPerSecond的原因是轉換成毫秒,因為我們的速度是pixels/second

當完成當前滾動的時候,進行下一次,這裡遞迴呼叫_animateVertical,這樣我們就實現了垂直的迴圈滾動。

水平模式

接下去實現水平模式,和垂直模式類似:

_animateHorizonal(double extent, bool needsMoveToTop) {
  if (!_controller.hasClients || widget._scrollDirection != Axis.horizontal) {
    return;
  }
  _timer?.cancel();
  _timer = Timer(widget.delayedDuration, () {
    if (needsMoveToTop) {
      _controller.jumpTo(0);
      _animateHorizonal(extent, false);
    } else {
      var duration = _computeScrollDuration(extent);
      _controller.animateTo(extent, duration: duration, curve: widget.curve).whenComplete(() {
        _animateHorizonal(extent, true);
      });
    }
  });
}
複製程式碼

這裡解釋一下needsMoveToTop,因為水平模式下,首尾都要停頓,所以我們加個引數判斷下,如果是當前執行的滾動到頭部的話,needsMoveToTopfalse,如果是已經滾動到了尾部,needsMoveToToptrue,表示我們的下一次的行為是滾動到頭部,而不是開始滾動到整個列表。

接下來我們看看在哪裡開始滾動。

首先在頁面載入的時候我們開始滾動,然後還有當方向和childCount改變的時候,重新開始滾動,所以:

@override
void initState() {
  super.initState();
  _initalizationElements();
  _initializationScroll();
}

@override
void didUpdateWidget(Switcher oldWidget) {
  var childrenChanged = (widget.children?.length ?? 0) != (oldWidget.children?.length ?? 0);
  if (widget._scrollDirection != oldWidget._scrollDirection || childrenChanged) {
    _initalizationElements();
    _initializationScroll();
  }
  super.didUpdateWidget(oldWidget);
}
複製程式碼

然後是_initializationScroll方法:

_initializationScroll() {
  SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
    if (!mounted) {
      return;
    }
    var renderBox = context?.findRenderObject() as RenderBox;
    if (!_controller.hasClients || _childCount == 0 || renderBox == null || !renderBox.hasSize) {
      return;
    }
    var position = _controller.position;
    _timer?.cancel();
    _timer = null;
    position.moveTo(0);
    _selectedIndex = 0;
    if (widget._scrollDirection == Axis.vertical) {
      _animateVertical(renderBox.size.height);
    } else {
      var maxScrollExtent = position.maxScrollExtent;
      _animateHorizonal(maxScrollExtent, false);
    }
  });
}
複製程式碼

這裡在頁面繪製完成的時候,我們判斷,如果ScrollController沒有載入,childCount == 0或者大小沒有計算完成的時候直接返回,然後獲取position,取消上一個計時器,然後把列表滾到頭部,index初始化為0,判斷是垂直模式,開始垂直滾動,如果是水平模式開始水平滾動。

這裡注意,垂直滾動的時候,每次的滾動距離是每個item的高度,而水平滾動的時候,滾動距離是列表可滾動的最大長度

到這裡我們已經實現了Android的跑馬燈,而且還增加了垂直滾動,是不是很簡單呢。

如有問題、意見和建議,都可以在評論區裡告訴我,我將及時修改和參考你的意見和建議,對程式碼做出優化。

相關文章