flutter-banner

augfun發表於2020-11-28

效果如下

 

main.dart:

import 'package:flutter/material.dart';

import 'banner_view.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      //關閉右上角的debug符號
      debugShowCheckedModeBanner: false,
      title: 'OpenSource China',
      home: Container(
        alignment: Alignment.center,
        height: 200.0,
        child: new BannerView([
          Image.asset('assets/pic1.png'),
          Image.asset('assets/pic2.png'),
          Image.asset('assets/pic3.png'),
          Image.asset('assets/pic4.png')
        ]),
      ),
    );
  }
}

banner_view.dart

library banner_view;
import 'dart:async';

import 'package:flutter/material.dart';

import 'indicator/IndicatorWidget.dart';
//indicator container builder
///[indicatorWidget] indicator widget, position the indicator widget into container
typedef Widget IndicatorContainerBuilder(BuildContext context, Widget indicatorWidget);

const String TAG = 'BannerView';
/// Created by yangxiaowei
/// BannerView
class BannerView extends StatefulWidget{
    
    final List<Widget> banners;
    //init index
    final int initIndex;
    //switch interval
    final Duration intervalDuration;
    //animation duration
    final Duration animationDuration;
    final IndicatorContainerBuilder indicatorBuilder;
    final Widget indicatorNormal;
    final Widget indicatorSelected;
    //the margin of between indicator items
    final double indicatorMargin;
    final PageController controller;
    //whether cycle rolling
    final bool cycleRolling;
    //whether auto rolling
    final bool autoRolling;
    final Curve curve;
    final ValueChanged onPageChanged;
    final bool log;

    BannerView(this.banners, {
        Key key,
        this.initIndex = 0, 
        this.intervalDuration = const Duration(seconds: 1),
        this.animationDuration = const Duration(milliseconds: 500),
        this.indicatorBuilder,
        this.indicatorNormal,
        this.indicatorSelected,
        this.indicatorMargin = 5.0,
        this.controller,
        this.cycleRolling = true,
        this.autoRolling = true,
        this.curve = Curves.easeInOut,
        this.onPageChanged,
        this.log = true,
    }): 
        assert(banners?.isNotEmpty ?? true), 
        assert(null != indicatorMargin),
        assert(null != intervalDuration),
        assert(null != animationDuration),
        assert(null != cycleRolling),
        super(key: key);

    @override
    _BannerViewState createState() => new _BannerViewState();
}

/// Created by yangxiaowei
class _BannerViewState extends State<BannerView> {

    List<Widget> _originBanners = [];
    List<Widget> _banners = [];
    Duration _duration;
    PageController _pageController;
    int _currentIndex = 0;

    @override
    void initState() {
        super.initState();
        _Logger.debug = widget.log ?? true;

        this._isActive = true;

        this._originBanners = widget.banners;
        this._banners = this._banners..addAll(this._originBanners);
        
        if(widget.cycleRolling) {
            Widget first = this._originBanners[0];
            Widget last = this._originBanners[this._originBanners.length - 1];
            
            this._banners.insert(0, last);
            this._banners.add(first);
            this._currentIndex = widget.initIndex + 1;
        }else {
            this._currentIndex = widget.initIndex;
        }

        this._duration = widget.intervalDuration;
        this._pageController = widget.controller ?? PageController(initialPage: this._currentIndex);
        
        this._nextBannerTask();
    }

    Timer _timer;
    void _nextBannerTaskBy({int milliseconds = 0}) {
        if(!mounted) {
            return;
        }

        if(!widget.autoRolling) {
            return;
        }

        this._cancel();

        _timer = new Timer(new Duration(milliseconds: _duration.inMilliseconds + milliseconds), () {
            this._doChangeIndex();
        });
    }

    void _nextBannerTask() {
        this._nextBannerTaskBy(milliseconds: 0);
    }

    void _cancel() {
        _timer?.cancel();
    }

    void _doChangeIndex({bool increment = true}) {
        if(!mounted) {
            return;
        }
        if(increment) {
            this._currentIndex++;
        }else{
            this._currentIndex--;
        }
        this._currentIndex = this._currentIndex % this._banners.length;
        _Logger.d(TAG, "_doChangeIndex  $_currentIndex .");
        if(0 == this._currentIndex) {
            this._pageController.jumpToPage(this._currentIndex + 1);
            this._nextBannerTaskBy(milliseconds: -_duration.inMilliseconds);
            setState(() {});
        }else{
            this._pageController.animateToPage(
                this._currentIndex, 
                duration: widget.animationDuration,
                curve: widget.curve,
            );
        }
    }

    @override
    Widget build(BuildContext context) {
        
        return this._generateBody();
    }

    /// compose the body, banner view and indicator view
    Widget _generateBody() {
        return new Stack(
            children: <Widget>[
                this._renderBannerBody(),
                this._renderIndicator(),
            ],
        );
    }

    /// Banner container
    Widget _renderBannerBody() {

        Widget pageView = new PageView.builder(
            itemBuilder: (context, index) {

                Widget widget = this._banners[index];
                return new GestureDetector(
                    child: widget,
                );
            },  
            controller: this._pageController,
            itemCount: this._banners.length,  
            onPageChanged: (index) {
                _Logger.d(TAG, '**********   changed  index: $index  cu: $_currentIndex');
                this._currentIndex = index;
                this._nextBannerTask();
                setState(() {});
                if(null != widget.onPageChanged) {
                    widget.onPageChanged(index);
                }
            },
            physics: new ClampingScrollPhysics(),
        );

        // return pageView;
        return new NotificationListener(
            child: pageView,
            onNotification: (notification) {
                this._handleScrollNotification(notification);
                return true;
            },
        );
    }

    void _handleScrollNotification(Notification notification) {
        void _resetWhenAtEdge(PageMetrics pm) {
            if(null == pm || !pm.atEdge) {
                return;
            }
            if(!widget.cycleRolling) {
                return;
            }
            try{
                if(this._currentIndex == 0) {
                    this._pageController.jumpToPage(this._banners.length - 2);
                }else if(this._currentIndex == this._banners.length - 1) {
                    this._pageController.jumpToPage(1);
                }
                setState(() {});
            }catch (e){
                _Logger.d(TAG, 'Exception: ${e?.toString()}');
            }
        }

        void _handleUserScroll(UserScrollNotification notification) {
            UserScrollNotification sn = notification;
                    
            PageMetrics pm = sn.metrics;
            var page = pm.page;
            var depth = sn.depth;
            
            var left = page == .0 ? .0 : page % (page.round());
            
            if(depth == 0) {
                _Logger.d(TAG, '**  page: $page  , left: $left ,  atEdge: ${pm.atEdge} ,  index: $_currentIndex');

                if(left == 0) {
                    setState(() {
                        _resetWhenAtEdge(pm);
                    });
                }
            }
        }

        if(notification is UserScrollNotification) {
            if(_isStartByUser) {
                return;
            }
            if(_isEndByUser) {
                _isEndByUser = false;
                
            }else {
                _Logger.d(TAG, '#########   手動開始');
                _isStartByUser = true;
                this._cancel();
            }

            _handleUserScroll(notification);
        }else if(notification is ScrollEndNotification) {
            _Logger.d(TAG, '#########   ${notification.runtimeType}    $_isStartByUser');

            if(_isEndByUser) {
                return;
            }
            if(_isStartByUser) {
                _Logger.d(TAG, '#########   手動結束');
                _isEndByUser = true;
                _isStartByUser = false;
            } else {
                _isEndByUser = false;
            }

            this._nextBannerTask();
        }
    }

    bool _isEndByUser = false;
    bool _isStartByUser = false;

    /// indicator widget
    Widget _renderIndicator() {
        
        int index = widget.cycleRolling ? this._currentIndex - 1 : this._currentIndex;
        index = index <= 0 ? 0 : index;
        index = index % _originBanners.length;
        return new IndicatorWidget(
            size: this._originBanners.length,
            currentIndex: index,
            indicatorBuilder: this.widget.indicatorBuilder,
            indicatorNormal: this.widget.indicatorNormal,
            indicatorSelected: this.widget.indicatorSelected,
            indicatorMargin: this.widget.indicatorMargin,
        );
    }

    bool _isActive = true;
    @override
    void deactivate() {
        super.deactivate();
        _isActive = !_isActive;
        if(_isActive) {
            _nextBannerTask();
        } else {
            _cancel();
        }
    }

    @override
    void dispose() {
        _isActive = false;
        _pageController?.dispose();
        _cancel();
        super.dispose();
    }
}

class _Logger {
    static bool debug = true;
    static void d(String tag, String msg) {
        if(debug) {
            print('$tag - $msg');
        }
    }
}

IndicatorWidget.dart

import 'package:flutter/material.dart';

import '../banner_view.dart';
import 'IndicatorUtil.dart';
//Created by yangxiaowei at 2018/06/06
//indicator view of banner
class IndicatorWidget extends StatelessWidget {
    final IndicatorContainerBuilder indicatorBuilder;
    final Widget indicatorNormal;
    final Widget indicatorSelected;
    final double indicatorMargin;
    final int size;
    final int currentIndex;
    
    IndicatorWidget({
        Key key,
        this.size,
        this.currentIndex,
        this.indicatorBuilder,
        this.indicatorNormal,
        this.indicatorSelected,
        this.indicatorMargin = 5.0,
    }): 
        assert(indicatorMargin != null),
        assert(size != null && size > 0),
        assert(currentIndex != null && currentIndex >= 0),
        super(key: key);
    
    @override
    Widget build(BuildContext context) {

        return this._renderIndicator(context);
    }

    //indicator container
    Widget _renderIndicator(BuildContext context) {
        
        Widget smallContainer = new Container(
            // color: Colors.purple[100],
            child: new Row(
                mainAxisSize: MainAxisSize.min,
                children: _renderIndicatorTag(),
            ),
        );

        if(null != this.indicatorBuilder) {
            return this.indicatorBuilder(context, smallContainer);
        }

        //default implement
        return new Align(
            alignment: Alignment.bottomCenter,
            child: new Opacity(
                opacity: 0.5,
                child: new Container(
                    height: 40.0,
                    padding: new EdgeInsets.symmetric(horizontal: 16.0),
                    color: Colors.black45,
                    alignment: Alignment.centerRight,
                    child: smallContainer,
                ),
            ),
        );
    }

    //generate every indicator item
    List<Widget> _renderIndicatorTag() {
        List<Widget> indicators = [];
        final int len = this.size;
        Widget selected = this.indicatorSelected ?? IndicatorUtil.generateIndicatorItem(normal: false);
        Widget normal = this.indicatorNormal ?? IndicatorUtil.generateIndicatorItem(normal: true);

        for(var index = 0; index < len; index++) {
            indicators.add(index == this.currentIndex ? selected : normal);
            if(index != len - 1) {
                indicators.add(new SizedBox(width: this.indicatorMargin,));
            }
        }

        return indicators;
    }
}

IndicatorUtil.dart

import 'package:flutter/material.dart';
//Created by yangxiaowei at 2018/05/16
//generate indicator item widget
class IndicatorUtil {
    static Widget generateIndicatorItem({bool normal = true, double indicatorSize = 8.0}) {

        return new Container(
            width: indicatorSize,
            height: indicatorSize,
            decoration: new BoxDecoration(
                shape: BoxShape.circle,
                color: normal ? Colors.white : Colors.red,
            ),
        );
    }
}

參考使用:

add in pubspec.yaml

banner_view: "^1.2.0"
    or
    banner_view: 
        git: https://github.com/yangxiaoweihn/BannerView.git

使用方法:

import 'package:banner_view/banner_view.dart';
    new Container(
        alignment: Alignment.center,
        height: 200.0,
        child: new BannerView(
            [...]
        ),
    );

support properties:

final List<Widget> banners;
    //init index
    final int initIndex;
    //switch interval
    final Duration intervalDuration;
    //animation duration
    final Duration animationDuration;
    final IndicatorContainerBuilder indicatorBuilder;
    final Widget indicatorNormal;
    final Widget indicatorSelected;
    //the margin of between indicator items
    final double indicatorMargin;
    final PageController controller;
    //whether cycle rolling
    final bool cycleRolling;
    //whether auto rolling
    final bool autoRolling;
    final Curve curve;
    final ValueChanged onPageChanged;
    final bool log;

參考路徑:https://github.com/yangxiaoweihn/BannerView