美文网首页
flutter-banner轮播

flutter-banner轮播

作者: cg1991 | 来源:发表于2019-10-25 20:25 被阅读0次

    问题背景

    因为最近做商城App,需要用到轮播,发现flutter的控件库里面没有这个控件(当然了,可能是我自己没有找到),于是就决定自己动手做一个banner轮播图片了。

    框架

    • 整体框架就是一个PageView,Indicator指示器,一个定时器。

    • PageView用来展示需要播放的Widget,此处不一定必须限定死要展示Image.

    • Indicator作为当前图片的指示器,需要给出当前获取焦点Page的指示。而且Indicator要可以设置半径,选中颜色,未选中的颜色。最后还要可以设置指示器的位置,左上右下。

    • 定时器,当设置需要定时播放的时候,就按照设置的间隔时间播放对应的Widget。这里需要考虑的就是当手动点击的时候,需要取消定时循环,点击放开的时候,重新定时。当界面销毁的时候,销毁定时器。

    • Page点击和被展示回调。只需要在Page被点击和被播放展示的时候回调到对应的方法就行了。

    实现

    1、实现PageView

    上代码:

    _controller = PageController(
      initialPage: widget.initPage,
    );
    Widget pageView = GestureDetector(
      onHorizontalDragDown: _onTaped,
      onTap: _onPageClicked,
      child: PageView(
        children: widget.childWidget,
        scrollDirection: widget.scrollDirection,
        onPageChanged: onPageChanged,
        controller: _controller,
      ),
    );
    

    PageController是Page控制器,可以设置初始显示的Page,也可以在多个图片轮播的时候,是否缓存页面,也可以设置在滚动方向上,在视图显示区域的比例。initialPage可以给外部调用,默认设置为0。

    2、实现Indicator

    上代码:

    Widget indicatorWidget = Row(
      mainAxisAlignment: MainAxisAlignment.center,
      mainAxisSize: MainAxisSize.max,
      crossAxisAlignment: CrossAxisAlignment.center,
      children: widget.childWidget.map(
        (f) {
          int index = widget.childWidget.indexOf(f);
          Widget indicatorWidget = Container(
            width: Size.fromRadius(widget.indicatorRadius).width,
            height: Size.fromRadius(widget.indicatorRadius).height,
            margin: EdgeInsets.only(right: widget.indicatorSpaceBetween),
            decoration: new BoxDecoration(
              shape: BoxShape.circle,
              color: index == selectedPage
                  ? widget.indicatorSelectedColor
                  : widget.indicatorColor,
            ),
          );
          return indicatorWidget;
        },
      ).toList(),
    );
    

    当需要轮播多个Widget的时候,默认指示器按行排列,一字排开居中显示。单个子Indicator是个圆圈,颜色,半径可以外部设置。并且选中的Page的指示器要是选中的颜色,其他的为未选中颜色。

    3、合并Indicator和PageView

    将这两个Widget合并起来,一般来说他们是上下级关系,Indicator在PageView之上:

    Widget stackWidget = widget.enableIndicator
        ? Stack(
            alignment: AlignmentDirectional.bottomCenter,
            children: <Widget>[
              pageView,
              Positioned(
                bottom: 6,
                child: Align(
                  alignment: widget.indicatorAlign,
                  child: indicatorWidget,
                ),
              ),
            ],
          )
        : pageView;
    

    当外部设置展示Indicator的时候,直接就展示PageView,否则的话,需要用Stack将这两个Widget堆积起来。PageView在下,Indicator在上,此时它在什么位置,也是可以外部设置,通过Align可以指定指示器的位置,一般是在底部偏上一点,所以这里将Positioned的bottom参数设置了一个值。当然了,我们也可以将这个位置和位置的偏移距离对外提供设置的接口。

    这里做最后一个工作,给这个stackWidget设置一个文字方向,因为有些国家的文字阅读方向是从右往左,此时如果不设置的话,会报错,我这里写死了,从左往右:

    Widget parent = Directionality(
        textDirection: TextDirection.ltr,
        child: Container(
            width: widget.width, height: widget.height, child: stackWidget));
    

    当然Android是提供了判断方法的:

    Configuration config = getResources().getConfiguration();
    if(config.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
        //in Right To Left layout
    }
    

    至此,Widget层面算是写完了。

    4、定时器

    定时触发,这在flutter里面是提供了方法的:

    void _startTimer() {
        if (widget.autoDisplayInterval <= 0) {
          return;
        }
        var oneSec = Duration(seconds: widget.autoDisplayInterval);
        _timer = new Timer.periodic(oneSec, (Timer timer) {
          ++selectedPage;
          selectedPage = selectedPage % widget.childWidget.length;
          _controller?.jumpToPage(selectedPage);
          onPageChanged(selectedPage);
        });
    }
    

    autoDisplayInterval时间间隔由外部设置,不设置的话,默认不自动播放,如果设置大于0的数字就是自动播放了。时间间隔是秒。另外每次触发的时候,将选中播放的页面自加1,然后跟总的页面数取余,就是当前应该播放的页面,同时需要手动设置跳转到指定的页面,同时将播放页的序数回调给调用者。

    在这里,当我们滑动页面的时候,如果不处理,就会出现滑动混乱的情况。这里我们需要做出处理。记得在PageView定义的时候,最外层包裹了一个GestureDetector,我们需要处理Page被点击和被拖住水平滑动的场景,于是就有了 处理onHorizontalDragDown和onTap这两个回调函数的场景了。

    onTap用于处理点击,直接携带序号参数回调给调用者。

    onHorizontalDragDown用于处理水平滑动按下,具体的处理是:

    void _onHorizontalDragDown(DragDownDetails details) {
        if (_timer == null) {
          return;
        }
        _releaseTimer();
        Future.delayed(Duration(seconds: 2));
        _startTimer();
    }
    

    每次按下点击的时候,停止计时,将计时器释放掉,同时延时2秒,如果在这期间有新的拖动事件进来,再次取消。

    如果延时2秒,没有其他处理,继续定时播放处理。
    看看定时销毁的方法:

    void _releaseTimer() {
        if (_timer != null && !_timer.isActive) {
          return;
        }
        _timer?.cancel();
        _timer = null;
    }
    

    对外提供

    下一步就是将定义的过程中需要调用者传递的参数对外暴露,通过构造的时候传递进来:

    BannerWidget(
      {Key key,
      @required this.childWidget,//子视图列表
      this.enableIndicator = true,//是否允许显示指示器
      this.initPage = 0,//初始页面序号
      this.height = 36,//高度
      this.width = 340,//宽度
      this.indicatorRadius = 4,//指示器半径
      this.indicatorColor = Colors.white,//指示器默认颜色
      this.indicatorSelectedColor = Colors.red,//页面被选中指示器颜色
      this.scrollDirection = Axis.horizontal,//滚动方向(默认水平)
      this.indicatorSpaceBetween = 6,//指示器之间的间距
      this.autoDisplayInterval = 0,//自动播放时间间隔(不设置或设置为0,则不自动播放)
      this.onPageSelected,//页面被展示回调
      this.indicatorAlign = Alignment.bottomCenter,//指示器位置
      this.onPageClicked})//页面被点击回调
      : super(key: key);
    

    被播放的子视图列表必须提供。从上到下依次是:是否允许显示指示器,初始页面序号,高度,宽度,指示器半径,指示器默认颜色,页面被选中指示器颜色,滚动方向(默认水平),指示器之间的间距,自动播放时间间隔(不设置或设置为0,则不自动播放),页面被展示回调,指示器位置,页面被点击回调。

    如何使用

    var images = {
      "https://ws1.sinaimg.cn/large/610dc034ly1fitcjyruajj20u011h412.jpg",
      "https://ws1.sinaimg.cn/large/610dc034ly1fjfae1hjslj20u00tyq4x.jpg",
      "http://ww1.sinaimg.cn/large/610dc034ly1fjaxhky81vj20u00u0ta1.jpg",
      "https://ws1.sinaimg.cn/large/610dc034ly1fivohbbwlqj20u011idmx.jpg",
      "https://ws1.sinaimg.cn/large/610dc034ly1fj78mpyvubj20u011idjg.jpg",
      "https://ws1.sinaimg.cn/large/610dc034ly1fj3w0emfcbj20u011iabm.jpg",
      "https://ws1.sinaimg.cn/large/610dc034ly1fiz4ar9pq8j20u010xtbk.jpg",
      "https://ws1.sinaimg.cn/large/610dc034ly1fis7dvesn6j20u00u0jt4.jpg",
      "https://ws1.sinaimg.cn/large/610dc034ly1fir1jbpod5j20ip0newh3.jpg",
      "https://ws1.sinaimg.cn/large/610dc034ly1fik2q1k3noj20u00u07wh.jpg",
      "https://ws1.sinaimg.cn/large/610dc034ly1fiednrydq8j20u011itfz.jpg",
      "http://ww1.sinaimg.cn/large/610dc034ly1ffmwnrkv1hj20ku0q1wfu.jpg",
    };
    
    void main() => runApp(BannerWidget(
          width: 340,
          height: 56,
          autoDisplayInterval: 2,
          childWidget: images.map((f) {
            return Image.network(
              f,
              width: 340,
              height: 48,
            );
          }).toList(),
        ));
    

    最后一公里

    import 'dart:async';
    
    import 'package:flutter/material.dart';
    
    class BannerWidget extends StatefulWidget {
      BannerWidget(
          {Key key,
          @required this.childWidget,
          this.enableIndicator = true,
          this.initPage = 0,
          this.height = 36,
          this.width = 340,
          this.indicatorRadius = 4,
          this.indicatorColor = Colors.white,
          this.indicatorSelectedColor = Colors.red,
          this.scrollDirection = Axis.horizontal,
          this.indicatorSpaceBetween = 6,
          this.autoDisplayInterval = 0,
          this.onPageSelected,
          this.indicatorAlign = Alignment.bottomCenter,
          this.onPageClicked})
          : super(key: key);
    
      final double width;
      final double height;
    
      final List<Widget> childWidget;
      final Axis scrollDirection;
    
      final ValueChanged<int> onPageSelected;
      final ValueChanged<int> onPageClicked;
    
      final int initPage;
    
      final bool enableIndicator;
    
      final Color indicatorSelectedColor;
      final Color indicatorColor;
      final double indicatorRadius;
    
      final double indicatorSpaceBetween;
    
      final int autoDisplayInterval;
    
      final Alignment indicatorAlign;
    
      @override
      __BannerWidgetState createState() => __BannerWidgetState();
    }
    
    class __BannerWidgetState extends State<BannerWidget> {
      int selectedPage = 0;
      PageController _controller;
      Timer _timer;
      int lastTapDownTime = 0;
    
      void onPageChanged(int index) {
        setState(() {
          selectedPage = index;
        });
        widget?.onPageSelected(index);
      }
    
      void _startTimer() {
        if (widget.autoDisplayInterval <= 0) {
          return;
        }
        var oneSec = Duration(seconds: widget.autoDisplayInterval);
        _timer = new Timer.periodic(oneSec, (Timer timer) {
          ++selectedPage;
          selectedPage = selectedPage % widget.childWidget.length;
          _controller?.jumpToPage(selectedPage);
          onPageChanged(selectedPage);
        });
      }
    
      void _releaseTimer() {
        if (_timer != null && !_timer.isActive) {
          return;
        }
        _timer?.cancel();
        _timer = null;
      }
    
      void _onHorizontalDragDown(DragDownDetails details) {
        if (_timer == null) {
          return;
        }
        _releaseTimer();
        Future.delayed(Duration(seconds: 2));
        _startTimer();
      }
    
      void _onPageClicked() {
        widget?.onPageClicked(selectedPage);
      }
    
      @override
      void initState() {
        super.initState();
        _startTimer();
      }
    
      @override
      Widget build(BuildContext context) {
        _controller = PageController(
          initialPage: widget.initPage,
        );
        Widget pageView = GestureDetector(
          onHorizontalDragDown: _onHorizontalDragDown,
          onTap: _onPageClicked,
          child: PageView(
            children: widget.childWidget,
            scrollDirection: widget.scrollDirection,
            onPageChanged: onPageChanged,
            controller: _controller,
          ),
        );
    
        Widget indicatorWidget = Row(
          mainAxisAlignment: MainAxisAlignment.center,
          mainAxisSize: MainAxisSize.max,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: widget.childWidget.map(
            (f) {
              int index = widget.childWidget.indexOf(f);
              Widget indicatorWidget = Container(
                width: Size.fromRadius(widget.indicatorRadius).width,
                height: Size.fromRadius(widget.indicatorRadius).height,
                margin: EdgeInsets.only(right: widget.indicatorSpaceBetween),
                decoration: new BoxDecoration(
                  shape: BoxShape.circle,
                  color: index == selectedPage
                      ? widget.indicatorSelectedColor
                      : widget.indicatorColor,
                ),
              );
              return indicatorWidget;
            },
          ).toList(),
        );
    
        Widget stackWidget = widget.enableIndicator
            ? Stack(
                alignment: AlignmentDirectional.bottomCenter,
                children: <Widget>[
                  pageView,
                  Positioned(
                    bottom: 6,
                    child: Align(
                      alignment: widget.indicatorAlign,
                      child: indicatorWidget,
                    ),
                  ),
                ],
              )
            : pageView;
    
        Widget parent = Directionality(
            textDirection: TextDirection.ltr,
            child: Container(
                width: widget.width, height: widget.height, child: stackWidget));
        return parent;
      }
    
      @override
      void dispose() {
        super.dispose();
        _releaseTimer();
      }
    }
    
    var images = {
      "https://ws1.sinaimg.cn/large/610dc034ly1fitcjyruajj20u011h412.jpg",
      "https://ws1.sinaimg.cn/large/610dc034ly1fjfae1hjslj20u00tyq4x.jpg",
      "http://ww1.sinaimg.cn/large/610dc034ly1fjaxhky81vj20u00u0ta1.jpg",
      "https://ws1.sinaimg.cn/large/610dc034ly1fivohbbwlqj20u011idmx.jpg",
      "https://ws1.sinaimg.cn/large/610dc034ly1fj78mpyvubj20u011idjg.jpg",
      "https://ws1.sinaimg.cn/large/610dc034ly1fj3w0emfcbj20u011iabm.jpg",
      "https://ws1.sinaimg.cn/large/610dc034ly1fiz4ar9pq8j20u010xtbk.jpg",
      "https://ws1.sinaimg.cn/large/610dc034ly1fis7dvesn6j20u00u0jt4.jpg",
      "https://ws1.sinaimg.cn/large/610dc034ly1fir1jbpod5j20ip0newh3.jpg",
      "https://ws1.sinaimg.cn/large/610dc034ly1fik2q1k3noj20u00u07wh.jpg",
      "https://ws1.sinaimg.cn/large/610dc034ly1fiednrydq8j20u011itfz.jpg",
      "http://ww1.sinaimg.cn/large/610dc034ly1ffmwnrkv1hj20ku0q1wfu.jpg",
    };
    
    void main() => runApp(BannerWidget(
          width: 340,
          height: 56,
          autoDisplayInterval: 2,
          childWidget: images.map((f) {
            return Image.network(
              f,
              width: 340,
              height: 48,
            );
          }).toList(),
        ));
    
    

    相关文章

      网友评论

          本文标题:flutter-banner轮播

          本文链接:https://www.haomeiwen.com/subject/xnyevctx.html