Flutter實(shí)現(xiàn)Android跑馬燈及滾動廣告

簡介

本文介紹怎么在Flutter里使用ListView實(shí)現(xiàn)Android的跑馬燈芳誓,然后再擴(kuò)展一下睛约,實(shí)現(xiàn)上下滾動。

Github地址

該小控件已經(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í)行的滾動到頭部的話十厢,needsMoveToTopfalse等太,如果是已經(jīng)滾動到了尾部,needsMoveToToptrue蛮放,表示我們的下一次的行為是滾動到頭部缩抡,而不是開始滾動到整個(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)化伯襟。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末猿涨,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子姆怪,更是在濱河造成了極大的恐慌叛赚,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件稽揭,死亡現(xiàn)場離奇詭異俺附,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)溪掀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門事镣,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人揪胃,你說我怎么就攤上這事璃哟。” “怎么了喊递?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵随闪,是天一觀的道長。 經(jīng)常有香客問我册舞,道長蕴掏,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮盛杰,結(jié)果婚禮上挽荡,老公的妹妹穿的比我還像新娘。我一直安慰自己即供,他們只是感情好定拟,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著逗嫡,像睡著了一般青自。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上驱证,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天延窜,我揣著相機(jī)與錄音,去河邊找鬼抹锄。 笑死逆瑞,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的伙单。 我是一名探鬼主播获高,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼吻育!你這毒婦竟也來了念秧?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤布疼,失蹤者是張志新(化名)和其女友劉穎摊趾,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體缎除,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡严就,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了器罐。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片梢为。...
    茶點(diǎn)故事閱讀 37,997評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖轰坊,靈堂內(nèi)的尸體忽然破棺而出铸董,到底是詐尸還是另有隱情,我是刑警寧澤肴沫,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布粟害,位于F島的核電站,受9級特大地震影響颤芬,放射性物質(zhì)發(fā)生泄漏悲幅。R本人自食惡果不足惜套鹅,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望汰具。 院中可真熱鬧卓鹿,春花似錦、人聲如沸留荔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽聚蝶。三九已至杰妓,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間碘勉,已是汗流浹背巷挥。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留恰聘,地道東北人句各。 一個(gè)月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像晴叨,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子矾屯,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容