Flutter绘制轮播图控件KqSwiper

18 篇文章 0 订阅
  • 演示

  • 功能

1.可自定义设置宽高

2.可点击回调

3.可设置是否自动轮播

4.可设置指示器,抽象类IIndicator,IWidgetIndicator,IDrawIndicator

  • 代码
import 'dart:async';
import 'dart:ui' as ui;

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

class KqSwiper<T extends IShowData> extends StatefulWidget {
  /// 展示的数据
  final List<T> showData;

  /// 点击回调
  final Function(int index, T data)? onTap;

  /// 界面宽度
  final double width;

  /// 界面高度
  final double height;

  /// 是否开启自动滑动
  final bool isAutoSlide;

  ///指示器
  final IIndicator? indicator;

  const KqSwiper(
      {super.key,
      required this.showData,
      this.onTap,
      this.width = 360,
      this.height = 250,
      this.isAutoSlide = true,
      this.indicator});

  @override
  State<StatefulWidget> createState() => _KqSwiperState<T>();
}

class _KqSwiperState<T extends IShowData> extends State<KqSwiper<T>> {
  /// 执行自动滑动的时间间隔s
  static const int slideTime = 6;

  /// 图片数据
  final List<ui.Image> images = [];

  /// 当前正在显示的图片
  int index = 0;

  /// 前一张图片
  ui.Image? _preImage;

  /// 当前图片
  ui.Image? _curImage;

  /// 下一张图片
  ui.Image? _nextImage;

  /// 滑动偏移
  double _offset = 0;

  /// 执行滑动动画计时器
  Timer? timer;

  ///定时滑动计时器
  Timer? timer2;

  /// 指示器
  IIndicator? _indicator;

  /// 当Indicator的类型为widget时的widget列表值
  List<Widget>? _indicatorWidgetList;

  @override
  void initState() {
    _initImage();
    _indicator = widget.indicator ?? DrawIndicator();
    super.initState();
  }

  void _initImage() async {
    for (T data in widget.showData) {
      images.add(await getImage(data,
          width: widget.width.toInt(), height: widget.height.toInt()));
    }
    _showImage();
    _startSlide();
  }

  void _showImage() {
    if (index == 0) {
      _preImage = images[widget.showData.length - 1];
    } else {
      _preImage = images[index - 1];
    }
    _curImage = images[index];
    if (index == widget.showData.length - 1) {
      _nextImage = images[0];
    } else {
      _nextImage = images[index + 1];
    }
    _initWidgetIndicator();
    setState(() {});
  }

  Future<ui.Image> getImage(T data, {width, height}) async {
    ui.Image image;
    if (data.type == FileSourceType.network) {
      image = await getNetImage(data.path, width: width, height: height);
    } else if (data.type == FileSourceType.asset) {
      image = await getAssetImage(data.path, width: width, height: height);
    } else {
      image = await getNetImage(data.path, width: width, height: height);
    }
    return image;
  }

  void _animateTo(bool isSwiper, double hasSwiperWidth) {
    _initWidgetIndicator();
    double showCount = 0;
    if (isSwiper) {
      showCount = widget.width - hasSwiperWidth;
    } else {
      showCount = hasSwiperWidth;
    }
    //计时器,每1毫秒执行一次
    const period = Duration(milliseconds: 1);
    timer = Timer.periodic(period, (timer) {
      showCount--;
      if (_offset > 0) {
        if (isSwiper) {
          _offset++;
        } else {
          _offset--;
        }
      } else {
        if (isSwiper) {
          _offset--;
        } else {
          _offset++;
        }
      }
      //计时器结束条件
      if (showCount.toInt() == 0) {
        timer.cancel();
        _offset = 0;
        _showImage();
        _startSlide();
      }
      //只有当widget状态为mounted时才执行setState防止内存泄露
      if (mounted) {
        setState(() {});
      }
    });
  }

  void _initWidgetIndicator(){
    if (_indicator?.type == IndicatorType.widget) {
      _indicatorWidgetList =
          _indicator?.onIWidget(widget.showData.length, index);
    }
  }

  void _startSlide() {
    if (widget.isAutoSlide) {
      if (timer2 == null || !timer2!.isActive) {
        //计时器,每1毫秒执行一次
        const period = Duration(seconds: slideTime);
        timer2 = Timer.periodic(period, (timer) {
          if (index == widget.showData.length - 1) {
            index = 0;
          } else {
            index++;
          }
          _animateTo(true, 0);
        });
      }
    }
  }

  @override
  void dispose() {
    //退出时关闭计时器防止内存泄露
    timer?.cancel();
    timer2?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: widget.height,
      width: widget.width,
      child: RepaintBoundary(
          child: Stack(
              alignment: Alignment.center,
              fit: StackFit.expand,
              children: [
            GestureDetector(
              child: ClipRect(
                  child: CustomPaint(
                painter: _SwiperPinter(this, widget.showData.length, index,
                    _preImage, _curImage, _nextImage, _offset, _indicator!),
              )),
              onTap: () {
                widget.onTap?.call(index, widget.showData[index]);
              },
              onHorizontalDragDown: (DragDownDetails details) {
                timer2?.cancel();
              },
              onHorizontalDragUpdate: (DragUpdateDetails details) {
                _offset += details.delta.dx;
                setState(() {});
              },
              onHorizontalDragEnd: (DragEndDetails details) {
                bool isSwiper = false;
                if (_offset.abs() >= widget.width / 3) {
                  if (_offset < 0) {
                    if (index == widget.showData.length - 1) {
                      index = 0;
                    } else {
                      index++;
                    }
                  } else {
                    if (index == 0) {
                      index = widget.showData.length - 1;
                    } else {
                      index--;
                    }
                  }
                  isSwiper = true;
                }
                _animateTo(isSwiper, _offset.abs());
              },
            ),
            Positioned(
                bottom: 10,
                child: Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: _indicatorWidgetList ?? [])),
          ])),
    );
  }
}

class _SwiperPinter extends CustomPainter {
  final _KqSwiperState state;
  final int _length;
  final int _index;
  final ui.Image? _preImage;
  final ui.Image? _curImage;
  final ui.Image? _nextImage;
  final double _offset;
  final IIndicator _indicator;

  Paint selfPaint = Paint()
    ..color = Colors.blue
    ..style = PaintingStyle.fill
    ..isAntiAlias = true
    ..strokeCap = StrokeCap.butt
    ..strokeWidth = 30.0;

  _SwiperPinter(this.state, this._length, this._index, this._preImage,
      this._curImage, this._nextImage, this._offset, this._indicator);

  @override
  void paint(Canvas canvas, Size size) {
    if (_preImage == null || _curImage == null || _nextImage == null) {
      return;
    }
    canvas.drawImage(
        _preImage!, Offset(-state.widget.width + _offset, 0), selfPaint);
    canvas.drawImage(_curImage!, Offset(0 + _offset, 0), selfPaint);
    canvas.drawImage(
        _nextImage!, Offset(state.widget.width + _offset, 0), selfPaint);

    if (_indicator.type == IndicatorType.draw) {
      _indicator.onIDraw(
          canvas, _length, _index, state.widget.width, state.widget.height);
    }
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true;
  }
}

/// 文件源类型
enum FileSourceType {
  /// 网络文件
  network,

  /// 系统文件
  asset
}

/// 文件格式
abstract class IShowData {
  /// 展示的图片路径
  final String path;

  /// 图片路径位置
  final FileSourceType type;

  IShowData(this.path, this.type);
}

//获取网络图片 返回ui.Image
Future<ui.Image> getNetImage(String url, {width, height}) async {
  ByteData data = await NetworkAssetBundle(Uri.parse(url)).load(url);
  ui.Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List(),
      targetWidth: width, targetHeight: height);
  ui.FrameInfo fi = await codec.getNextFrame();
  return fi.image;
}

//获取本地图片,返回ui.Image
Future<ui.Image> getAssetImage(String asset, {width, height}) async {
  ByteData data = await rootBundle.load(asset);
  ui.Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List(),
      targetWidth: width, targetHeight: height);
  ui.FrameInfo fi = await codec.getNextFrame();
  return fi.image;
}

enum IndicatorType {
  /// widget类型
  widget,

  /// 绘制类型
  draw
}

/// 指示器,实现该类进行自定义绘制
abstract class IIndicator {
  final IndicatorType type;

  IIndicator(this.type);

  /// 自定义绘制
  /// length 指示器个数
  /// curIndex 指示器当前位置
  /// w 控件宽度
  /// h 控件高度
  onIDraw(Canvas canvas, int length, int curIndex, double w, double h);

  /// 自定义传widget
  /// length 指示器个数
  /// curIndex 指示器当前位置
  /// return List<Widget> 指示器列表
  List<Widget> onIWidget(int length, int curIndex);
}

/// 传widget的指示器抽象类,传widget的方式需要继承该类实现
abstract class IWidgetIndicator extends IIndicator {
  IWidgetIndicator() : super(IndicatorType.widget);

  @override
  onIDraw(Canvas canvas, int length, int curIndex, double w, double h) {
    ///啥也不干
  }

  @override
  List<Widget> onIWidget(int length, int curIndex) {
    return onWidget(length, curIndex);
  }

  /// 过渡方法
  List<Widget> onWidget(int length, int curIndex);
}

/// 自定义绘制的指示器抽象类,自定义绘制的方式需要继承该类实现
abstract class IDrawIndicator extends IIndicator {
  IDrawIndicator() : super(IndicatorType.draw);

  @override
  onIDraw(Canvas canvas, int length, int curIndex, double w, double h) {
    onDraw(canvas, length, curIndex, w, h);
  }

  @override
  List<Widget> onIWidget(int length, int curIndex) {
    ///啥也不干
    return [];
  }

  /// 过渡方法
  onDraw(Canvas canvas, int length, int curIndex, double w, double h);
}

/// 内置绘制指示器
class DrawIndicator extends IDrawIndicator {
  /// 指示器单个点x方向偏移
  final double _dotOffsetDx = 10;

  /// 指示器单个点y方向偏移
  final double _dotOffsetDy = 10;

  /// 指示器点的半径
  final double _dotRadius = 4;

  /// 指示器画笔
  final Paint _paint = Paint()
    ..style = PaintingStyle.fill
    ..isAntiAlias = true
    ..strokeCap = StrokeCap.butt
    ..strokeWidth = 30.0;

  @override
  onDraw(Canvas canvas, int length, int curIndex, double w, double h) {
    for (int i = 0; i < length; i++) {
      //绘制当前指示器
      if (i == curIndex) {
        _paint.color = Colors.blue;
        canvas.drawCircle(
            Offset(
                (w / 2 - (_dotRadius * 2 + (length - 1) * _dotOffsetDx) / 2) +
                    (_dotRadius * 2 + _dotOffsetDx) * i,
                h - _dotOffsetDy),
            _dotRadius,
            _paint);
        // 绘制非当前的其它指示器
      } else {
        _paint.color = Colors.grey;
        canvas.drawCircle(
            Offset(
                (w / 2 - (_dotRadius * 2 + (length - 1) * _dotOffsetDx) / 2) +
                    (_dotRadius * 2 + _dotOffsetDx) * i,
                h - _dotOffsetDy),
            _dotRadius,
            _paint);
      }
    }
  }
}

/// 内置widget指示器
class WidgetIndicator extends IWidgetIndicator {
  @override
  List<Widget> onWidget(int length, int curIndex) {
    List<Widget> widgets = [];
    for (int i = 0; i < length; i++) {
      if (curIndex == i) {
        widgets.add(Text(
          (i + 1).toString(),
          style:
              const TextStyle(color: Colors.red, backgroundColor: Colors.white),
        ));
      } else {
        widgets.add(Text(
          (i + 1).toString(),
          style: const TextStyle(
              color: Colors.black, backgroundColor: Colors.white),
        ));
      }
    }
    return widgets;
  }
}

代码可直接运行使用,代码量少,逻辑简单,二次开发方便。

  • 使用

构建数据:

class TestSwiperData extends IShowData {
  TestSwiperData(super.path, super.type);
}
List<TestSwiperData> imgList = [
    TestSwiperData(
        "https://alifei05.cfp.cn/creative/vcg/800/version23/VCG41175510742.jpg",
        FileSourceType.network),
    TestSwiperData(
        "https://tenfei04.cfp.cn/creative/vcg/veer/1600water/veer-147317368.jpg",
        FileSourceType.network),
    TestSwiperData(
        "assets/images/common/ic_xinzeng_16_blue.png", FileSourceType.asset),
  ];

使用:

KqSwiper<TestSwiperData>(
    width: 414.w,
    showData: imgList,
    indicator: WidgetIndicator(),
    onTap: (index,data){
        KqToast.showNormal("index=$index;image=${data.path}");
    },
)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

许天成

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值