Banners are an essential component in Android or iOS development. This component is also required in FLUTTER. For example, implementing the following banner,

It generally includes the following parts.

  1. Selection of data sources and multicast controls for presentation.
  2. An indicator that identifies the current location (the default indicator).
  3. Whether automatic rotation and rotation time.
  4. Width and height of the banner.
  5. Whether the banner has rounded corners.
  6. How the banner image is displayed.
  7. A callback to the current selected location (if you don’t use the default indicator, you need to know the current selected location if you need to implement the indicator yourself).

Now that we’ve identified the elements we need, let’s look at the implementation of each part.

  1. The data source of the banner is usually a String array, but to achieve infinite sliding, we use the way of bit complement (that is, add the last data of the source data at the first of the source data, then add the first data of the source data at the end of the source data). In other words, if the source data is 012, if you swipe right at the position of 0, 2 will appear, and if you swipe left at the position of 2, 0 will appear, so the completed data should be 20120. The pagination control uses PageView. When initializing, we select position 1 (which is the 0th position of the source data). With sliding, we switch PageView to position 1 (the real first data) if we select the penultimate data (the first source data to be filled, such as the last 0 of 20120). If the first position is selected (the last position of the source data to be filled, such as the first 2 of 20120), we let PageView select the last position to the last (the real last data). This ensures that whenever you swipe left or right on that piece of data, it has corresponding data left or right. So the source data is defined as final List

    dataList. In the initState method of the statefullWidget we construct the target data we need:

    if (widget.dataList ! = null && widget.dataList.length > 0) { addedImgs .. add(widget.dataList.last) .. addAll(widget.dataList) .. add(widget.dataList.first); }

    For the first position and the last position should automatically switch to the corresponding original position, we are in PageView’s onPageChange method to operate on it, Of course for the currently selected location callback (we use the custom typedef OnBannerPageChanged = void Function(int index);) , we should also call it here. If you are careful, you can see that setState has been marked out. The reason we don’t use setState is because we only want to update the currentIndex of the Indicator. If we do this, the Widget will have to be rebuilt. ValueNotifier

    realPos = new ValueNotifier(0) This will automatically trigger the redraw Indicator when the realPos value changes.

If (page == addeDimgs.length - 1) {// if (page == addeDimgs.length - 1) {// if (page == addeDimgs.length - 1) { Select second index _currentIndex = 1; await Future.delayed(Duration(milliseconds: 50)); _pageController.jumpToPage(_currentIndex); realPos.value = 0; } else if (page == 0) {currentIndex = addeDimgs.length - 2; await Future.delayed(Duration(milliseconds: 50)); _pageController.jumpToPage(_currentIndex); realPos.value = _currentIndex - 1; } else { _currentIndex = page; realPos.value = _currentIndex - 1; if (realPos.value < 0) realPos.value = 0; } if (widget.onBannerPageChanged ! = null) { widget.onBannerPageChanged(realPos.value); } //setState(() {}); }Copy the code
  1. For example, the default indicator shown in the figure is yellow RRect selected and gray when not selected. (If open source or for others to use, the position of the indicator, selected and unselected color should be extracted corresponding variables, here I will write down the lazy.) Implementation scheme is similar to the Android custom View, but the use of CustomPainter. It’s very simple to use paint to draw round and rounded rectangles on canvas
Class BannerSliderIndicator extends CustomPainter {// int count; ValueNotifier<int> currentIndex; /// unselected Color normalColor; /// selectColor selectColor; /// Paint mPaint; // unselected radius double normalCircleRadius; // double space; // Double rectangleWidth; // Double rectangleHeight; // rectangleCorner is rectangleCorner; double preDelta; RRect rect; BannerSliderIndicator({this. count,this.currentIndex}):super(repaint: currentIndex) { this.count = count; mPaint = Paint(); mPaint .. isAntiAlias = true .. style = PaintingStyle.fill; normalColor = Color(0xffdcdcdc); selectColor = Color(0xfffdc133); normalCircleRadius = 3.w; space = 9.w; rectangleWidth = 16.w; rectangleHeight = 6.w; rectangleCorner = Radius.circular(3.w); preDelta = 5.w; } @override void paint(Canvas canvas, Size size) { if (count < 1) return; double indicatorWidth = normalCircleRadius * 2 * count + space * (count - 1) + preDelta * 2; Double left = (sie.width - indicatorWidth) / 2.0; for (int i = 0; i < count; i++) { mPaint.. color = i == currentIndex.value ? selectColor : normalColor; if (i == currentIndex.value) { rect = RRect.fromLTRBAndCorners( left, size.height / 2 - normalCircleRadius, left + rectangleWidth, size.height / 2 - normalCircleRadius + rectangleHeight, topLeft: rectangleCorner, topRight: rectangleCorner, bottomLeft: rectangleCorner, bottomRight: rectangleCorner); left += rectangleWidth + space; canvas.drawRRect(rect, mPaint); } else { canvas.drawCircle(Offset(left + normalCircleRadius, size.height / 2), normalCircleRadius, mPaint); left += 2 * normalCircleRadius + space; } } } @override bool shouldRepaint(BannerSliderIndicator oldDelegate) { return oldDelegate.currentIndex ! = currentIndex; }}Copy the code

In the indicator above, the currentIndex parameter that determines the current selected position is ValueNotifier, so that when this value changes, shouldRepaint should return true, which will be explained in the next article. Here we write the indicator directly under the banner. We wrap the banner and indicator directly with the column (of course, if you provide the configuration of the indicator position, you will need to choose a different container depending on the configuration).

child: ! widget.showIndicator ? _buildPager() : Column( children: [ _buildPager(), Container( height: 30.h, child: Center( child: CustomPaint( size: Size(ScreenUtil().screenWidth, 30.h), painter: BannerSliderIndicator( count: widget.dataList == null ? 0 : widget.dataList.length, currentIndex: realPos), ), ), ), ], )Copy the code
  1. If auto rotation is enabled, it is used in conjunction with Timer. If auto rotation is enabled, the Timer needs to be enabled after build. In the StatefullWidget we can add FrameCallback to listen for build completion. This callback is triggered each time a build completes. The callback is used as follows
@override void initState() { WidgetsBinding.instance.addPostFrameCallback((timeStamp) { _startTimer(); }); super.initState(); } void _startTimer() { if (widget.dataList ! = null && widget.dataList.length > 1 && widget.isAuto) _timer = Timer.periodic( Duration(seconds: widget.intervalTime), (timer) => _scrollToPage()); }Copy the code

In startTimer, we judge that if there is no graph, a graph or no rotation, we will not start the rotation. If the rotation is enabled, we will set its rotation interval intervalTime. If not, our default value is 2 seconds.

  1. The width and height of the banner is used to define the size of the container to display the banner, as you will see later in the full version of the code.
  2. Need elements can be seen from above, we have sent the content display element 6, to achieve the caller to themselves and there is need to this parameter, set up a rounded corners, because if the content of PageView each page is set up, in the process of sliding, border by hand slide can see two pages is not rounded corners. So we need to build PageView’s Child as follows
ClipRRect(
        borderRadius: BorderRadius.circular(widget.bannerRadius),
        child: widget.itemBuilder(context, url, realPos),
      )
Copy the code
  1. Typedef ItemBuilder = Widget Function(BuildContext Context, String URL, int realPos); To build an image display widget. The reason for this approach is: first, the way to display images may be image. asset or Image.network, or it may be a three-party library such as ExtendedImage.network. Second, you can perform custom processing such as click, double click, and long press events on the Widget that displays the Image (just click on the GestureDetector layer on the Image coat).

  2. A callback to the currently selected location. The current selected location is distributed to the caller (the caller needs to know the current location if custom indicator is required), such as the following

This effect requires us to customize a Widget to display the current position and total amount, and update the current indicator during banner switching. Of course, in order not to rebuild the entire page, Final ValueNotifier

new_counter = ValueNotifier(1) Instead of currentIndex. Update this variable in the onPageChanged method

onBannerPageChanged: (index) { print("currentIndex=$index") new_counter.value = index + 1; },Copy the code

Build this custom Indicator through ValueListenableBuilder while building so that you don’t have to call setState every time in onBannerPageChanged to build the entire page

ValueListenableBuilder(
         valueListenable: new_counter,
         builder: _builderWithValue)
         
Widget _builderWithValue(BuildContext context, int value, Widget child) {
    return Container(
      constraints: BoxConstraints(minHeight: 26.w, minWidth: 75.w),
      decoration: CommonWidgets.bdRadius13LeftC000000T50(),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Image.asset(
            "assets/images/xx_sjxq_tp.png",
            width: 23.w,
            height: 23.w,
            fit: BoxFit.fill,
          ),
          CommonWidgets.text(
              "$value/${resDto?.data?.merchantDto?.imgList?.length ?? 0}",
              size: 15.sp,
              color: MyConstant.instance.colorBase.colorFDC133)
        ],
      ),
    );
  }
Copy the code

So we’ve done all of those things. So the full banner is as follows

Class CustomBannerWidget extends StatefulWidget {/// Final List<String> dataList; final OnBannerPageChanged onBannerPageChanged; final bool showIndicator; final ItemBuilder itemBuilder; final double bannerWidth; final double bannerHeight; final double bannerRadius; final int intervalTime; final bool isAuto; CustomBannerWidget(this.itemBuilder, {this.onBannerPageChanged, this.dataList, this.showIndicator = true, This.bannerwidth, this.bannerheight, this.bannerradius = 0.0, this.intervalTime = 2, this.isauto = true}); @override State<StatefulWidget> createState() { return _CustomBannerWidgetState(); } } class _CustomBannerWidgetState extends State<CustomBannerWidget> { PageController _pageController = PageController(initialPage: 1); int _currentIndex = 1; List<String> addedImgs = []; bool isEnd = false; bool isUserGesture = false; ValueNotifier<int> realPos = new ValueNotifier(0); Timer _timer; @override void initState() { WidgetsBinding.instance.addPostFrameCallback((timeStamp) { _startTimer(); }); super.initState(); addedImgs.clear(); if (widget.dataList ! = null && widget.dataList.length > 0) { addedImgs .. add(widget.dataList.last) .. addAll(widget.dataList) .. add(widget.dataList.first); } } @override void dispose() { _stopTimer(); super.dispose(); } @override Widget build(BuildContext context) { print( "bannerWidth = ${widget.bannerWidth} bannerHeight = ${widget.bannerHeight}"); return widget.dataList == null || widget.dataList.length <= 0 ? Container( width: widget.bannerWidth, height: widget.bannerHeight, ) : NotificationListener( onNotification: (ScrollNotification notification) => _onNotification(notification), child: ! widget.showIndicator ? _buildPager() : Column( children: [ _buildPager(), Container( height: 30.h, child: Center( child: CustomPaint( size: Size(ScreenUtil().screenWidth, 30.h), painter: BannerSliderIndicator( count: widget.dataList == null ? 0 : widget.dataList.length, currentIndex: realPos), ), ), ), ], )); } _onPageChanged(int page) async {// if (page == addeDimgs.length -1) {// if (page == addeDimgs.length -1) {// if (page == addeDimgs.length -1) { Select second index _currentIndex = 1; await Future.delayed(Duration(milliseconds: 50)); _pageController.jumpToPage(_currentIndex); realPos.value = 0; } else if (page == 0) {currentIndex = addeDimgs.length - 2; await Future.delayed(Duration(milliseconds: 50)); _pageController.jumpToPage(_currentIndex); realPos.value = _currentIndex - 1; } else { _currentIndex = page; realPos.value = _currentIndex - 1; if (realPos.value < 0) realPos.value = 0; } if (widget.onBannerPageChanged ! = null) { widget.onBannerPageChanged(realPos.value); } } _onNotification(ScrollNotification notification) { if (notification.depth == 0 && notification is ScrollStartNotification) { if (notification.dragDetails ! = null) { _stopTimer(); } } else if (notification is ScrollEndNotification) { _stopTimer(); _startTimer(); } } void _startTimer() { if (widget.dataList ! = null && widget.dataList.length > 1 && widget.isAuto) _timer = Timer.periodic( Duration(seconds: widget.intervalTime), (timer) => _scrollToPage()); } void _scrollToPage() { ++_currentIndex; var next = _currentIndex % addedImgs.length; _pageController.animateToPage(next, duration: Duration(milliseconds: 50), curve: Curves.ease); } void _stopTimer() { if (_timer ! = null) { _timer.cancel(); } } List<Widget> _buildChildren(BuildContext context) { List<Widget> childWidgets = []; for (var url in addedImgs) { childWidgets.add(ClipRRect( borderRadius: BorderRadius.circular(widget.bannerRadius), child: widget.itemBuilder(context, url, realPos.value), )); } return childWidgets; } Widget _buildPager() { return Container( width: widget.bannerWidth, height: widget.bannerHeight, child: PageView( onPageChanged: (index) => _onPageChanged(index), controller: _pageController, children: _buildChildren(context), )); }}Copy the code

To use, simply build the Widget where it is needed, such as

CustomBannerWidget(
              (context, url, int index) {
                print("bannerIndex = $index");

                return GestureDetector(
                  onTap: (){
                    print("current url is $url");

                  },
                  child: ExtendedImage.network(
                  url,
                  fit: BoxFit.cover,
                  width: 367.w,
                  height: 143.h,
                ),);
              },
              dataList: newList,
              bannerHeight: 143.h,
              bannerWidth: 367.w,
              bannerRadius: 8.w,
            )
Copy the code

We have done our job and welcome your criticism and correction.