簡介
本文介紹怎麼在Flutter裡使用ListView實現Android的跑馬燈,然後再擴充套件一下,實現上下滾動。
該小控制元件已經成功上傳到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
,因為水平模式下,首尾都要停頓,所以我們加個引數判斷下,如果是當前執行的滾動到頭部的話,needsMoveToTop
傳false
,如果是已經滾動到了尾部,needsMoveToTop
傳true
,表示我們的下一次的行為是滾動到頭部,而不是開始滾動到整個列表。
接下來我們看看在哪裡開始滾動。
首先在頁面載入的時候我們開始滾動,然後還有當方向和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的跑馬燈,而且還增加了垂直滾動,是不是很簡單呢。
如有問題、意見和建議,都可以在評論區裡告訴我,我將及時修改和參考你的意見和建議,對程式碼做出優化。