簡介
本文介紹怎么在Flutter里使用ListView實(shí)現(xiàn)Android的跑馬燈芳誓,然后再擴(kuò)展一下睛约,實(shí)現(xiàn)上下滾動。
該小控件已經(jīng)成功上傳到pub.dev,安裝方式:
dependencies:
flutterswitcher: ^0.0.1
效果圖
先上效果圖:
垂直模式
水平模式
上代碼
主要有兩種滾動模式探孝,垂直模式和水平模式,所以我們定義兩個(gè)構(gòu)造方法誉裆。
參數(shù)分別有滾動速度(單位是pixels/second
)顿颅、每次滾動的延遲、滾動的曲線變化和children
為空的時(shí)候的占位控件足丢。
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);
}
實(shí)現(xiàn)思路
實(shí)現(xiàn)思路有兩種:
第一種是用
ListView
粱腻;第二種是用
CustomPaint
自己畫;
這里我們選擇用ListView
方式實(shí)現(xiàn)斩跌,方便后期擴(kuò)展可手動滾動绍些,如果用CustomPaint
,實(shí)現(xiàn)起來就比較麻煩耀鸦。
接下來我們分析一下究竟該怎么實(shí)現(xiàn):
垂直模式
首先分析一下垂直模式柬批,如果想實(shí)現(xiàn)循環(huán)滾動,那么children
的數(shù)量就應(yīng)該比原來的多一個(gè)袖订,當(dāng)滾動到最后一個(gè)的時(shí)候氮帐,立馬跳到第一個(gè),這里的最后一個(gè)實(shí)際就是原來的第一個(gè)洛姑,所以用戶不會有任何察覺上沐,這種實(shí)現(xiàn)方式在前端開發(fā)中應(yīng)用很多,比如實(shí)現(xiàn)PageView
的循環(huán)滑動吏口,所以這里我們先定義childCount
:
_initalizationElements() {
_childCount = 0;
if (widget.children != null) {
_childCount = widget.children.length;
}
if (_childCount > 0 && widget._scrollDirection == Axis.vertical) {
_childCount++;
}
}
當(dāng)children
改變的時(shí)候奄容,我們重新計(jì)算一次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++
昂勒,接下來,實(shí)現(xiàn)一下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,
);
},
),
);
},
);
}
接下來實(shí)現(xiàn)垂直滾動的主要邏輯:
_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
有沒有加載完成戈盈,然后當(dāng)前的滾動方向是不是垂直的,不是就直接返回,然后當(dāng)前的index
是最后一個(gè)的時(shí)候塘娶,立馬跳到第一個(gè)归斤,index
初始化為0,接下來刁岸,取消前一個(gè)定時(shí)器脏里,開一個(gè)新的定時(shí)器,定時(shí)器的時(shí)間為我們傳進(jìn)來的間隔時(shí)間虹曙,然后每間隔widget.delayedDuration
的時(shí)間滾動一次迫横,這里調(diào)用ScrollController.animateTo
,滾動距離為每個(gè)item
的高度乘以當(dāng)前的索引酝碳,滾動時(shí)間根據(jù)滾動速度算出來:
Duration _computeScrollDuration(double extent) {
return Duration(milliseconds: (extent * Duration.millisecondsPerSecond / widget.scrollDelta).floor());
}
這里是我們小學(xué)就學(xué)過的矾踱,距離 = 速度 x 時(shí)間
,所以根據(jù)距離和速度我們就可以得出需要的時(shí)間疏哗,這里乘以Duration.millisecondsPerSecond
的原因是轉(zhuǎn)換成毫秒呛讲,因?yàn)槲覀兊乃俣仁?code>pixels/second。
當(dāng)完成當(dāng)前滾動的時(shí)候返奉,進(jìn)行下一次贝搁,這里遞歸調(diào)用_animateVertical
,這樣我們就實(shí)現(xiàn)了垂直的循環(huán)滾動衡瓶。
水平模式
接下去實(shí)現(xiàn)水平模式徘公,和垂直模式類似:
_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
,因?yàn)樗侥J较孪耄孜捕家nD关面,所以我們加個(gè)參數(shù)判斷下,如果是當(dāng)前執(zhí)行的滾動到頭部的話十厢,needsMoveToTop
傳false
等太,如果是已經(jīng)滾動到了尾部,needsMoveToTop
傳true
蛮放,表示我們的下一次的行為是滾動到頭部缩抡,而不是開始滾動到整個(gè)列表。
接下來我們看看在哪里開始滾動包颁。
首先在頁面加載的時(shí)候我們開始滾動瞻想,然后還有當(dāng)方向和childCount
改變的時(shí)候,重新開始滾動娩嚼,所以:
@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);
}
});
}
這里在頁面繪制完成的時(shí)候蘑险,我們判斷,如果ScrollController
沒有加載岳悟,childCount == 0
或者大小沒有計(jì)算完成的時(shí)候直接返回佃迄,然后獲取position
泼差,取消上一個(gè)計(jì)時(shí)器,然后把列表滾到頭部呵俏,index
初始化為0堆缘,判斷是垂直模式,開始垂直滾動普碎,如果是水平模式開始水平滾動吼肥。
這里注意,垂直滾動的時(shí)候随常,每次的滾動距離是每個(gè)item的高度潜沦,而水平滾動的時(shí)候,滾動距離是列表可滾動的最大長度绪氛。
到這里我們已經(jīng)實(shí)現(xiàn)了Android的跑馬燈,而且還增加了垂直滾動涝影,是不是很簡單呢枣察。
如有問題、意見和建議燃逻,都可以在評論區(qū)里告訴我序目,我將及時(shí)修改和參考你的意見和建議,對代碼做出優(yōu)化伯襟。