flutter 自定义组件-抽奖大转盘

import 'dart:async';
import 'dart:math';
import 'dart:ui' as ui;
import 'dart:ui';

import 'package:demo/widget/luck/luck_entity.dart';
import 'package:demo/widget/luck/luck_util.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

class LuckDrawWidget extends StatefulWidget {
  ///抽奖相关数据
  @required
  LuckEntity chartEntity;

  double startTurns = .0;
  double radius = 130;

  LuckDrawWidget(this.chartEntity, {this.radius, this.startTurns});

  @override
  _LuckDrawWidgetState createState() => _LuckDrawWidgetState();
}

class _LuckDrawWidgetState extends State<LuckDrawWidget>
    with TickerProviderStateMixin {
  ///这个是 自动
  AnimationController autoAnimationController;

  Animation<double> tween;

  double turns = .0;

  GlobalKey _key = GlobalKey();

  ///角加速度,类似摩擦力 的作用 ,让惯性滚动 减慢,这个意思是每一秒 ,角速度 减慢vA个pi。
  double vA = 40.0;

  Offset offset;

  double pBy;

  double pBx;

  double pAx;

  double pAy;

  double mCenterX;
  double mCenterY;

  Animation<double> _valueTween;

  double animalValue;

  @override
  void initState() {
    super.initState();
    //获取中心图片资源
    getPoint();
    //获取每条数据item
    getResours();
  }

  getPoint() => getAssetImage(
        widget?.chartEntity?.luckPic,
        width: widget?.chartEntity?.centerWidth,
        height: widget?.chartEntity?.centerHeight,
      )
          .then((value) => widget?.chartEntity?.image = value)
          .whenComplete(() => setState(() {}));

  getResours() => widget?.chartEntity?.entitys?.forEach((e) async => ((e.pic
              .contains("http") ||
          e.pic.contains("https"))
      ? await getNetImage(e?.pic?.trim(), width: e?.width, height: e?.height)
          .then((value) => e.image = value)
          .whenComplete(() => setState(() {}))
      : await getAssetImage(e?.pic?.trim(), width: e?.width, height: e?.height)
          .then((value) => e.image = value)
          .whenComplete(() => setState(() {}))));

  //获取网络图片 返回ui.Image
  Future<ui.Image> getNetImage(String url, {width, height}) async {
    try {
      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;
    } catch (e) {
      return null;
    }
  }

  //获取本地图片 返回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;
  }

  // 获取图片 本地为false 网络为true
  Future<ui.Image> loadImage(var path, bool isUrl) async {
    ImageStream stream;
    if (isUrl) {
      stream = NetworkImage(path).resolve(ImageConfiguration.empty);
    } else {
      stream = AssetImage(path, bundle: rootBundle)
          .resolve(ImageConfiguration.empty);
    }
    Completer<ui.Image> completer = Completer<ui.Image>();
    listener(ImageInfo frame, bool synchronousCall) {
      final ui.Image image = frame.image;
      completer.complete(image);
      stream.removeListener(ImageStreamListener(listener));
    }

    stream.addListener(ImageStreamListener(listener));
    return completer.future;
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 2 * widget.radius,
      height: 2 * widget.radius,
      child: GestureDetector(
        child: CustomPaint(
          painter: PieChartPainter(
            _key,
            turns,
            widget.startTurns,
            widget.chartEntity.entitys,
            widget?.chartEntity?.image,
            widget.chartEntity.centerHeight,
            widget.chartEntity.centerWidth,
          ),
          key: _key,
        ),
        onPanEnd: _onPanEnd,
        onPanDown: _onPanDown,
        onPanUpdate: _onPanUpdate,
      ),
    );
  }

  void _onPanUpdate(DragUpdateDetails details) {
    pBx = details.globalPosition.dx;
    //后面的 点的 x坐标
    pBy = details.globalPosition.dy;
    //后面的点的 y坐标
    double dTurns = getTurns();
    setState(() {
      turns += dTurns;
    });
    pAx = pBx;
    pAy = pBy;
  }

  void _onPanDown(DragDownDetails details) {
    if (offset == null) {
      //获取position
      RenderBox box = _key.currentContext.findRenderObject();
      offset = box.localToGlobal(Offset.zero);
      mCenterX = offset.dx + 130;
      mCenterY = offset.dy + 130;
    }

    pAx = details.globalPosition.dx; //初始的点的 x坐标
    pAy = details.globalPosition.dy; //初始的点的 y坐标
  }

  double getTurns() {
    ///计算 之前的点相对于水平的 角度
    ///

    ///
    /// o点(offset.dx+130,offset.dy+130).
    /// C点 (offset.dx+260,offset.dy+130).
    /// oc距离  130
    ///
    /// A点 (pAx,pAy),
    /// B点  (pBx,pBy).

    /// AC距离
    double acDistance = LuckUtil.distanceForTwoPoint(
        offset.dx + 2 * widget.radius, offset.dy + widget.radius, pAx, pAy);

    /// AO距离

    double aoDistance = LuckUtil.distanceForTwoPoint(
        offset.dx + widget.radius, offset.dy + widget.radius, pAx, pAy);

    ///计算 cos aoc 的值 ,然后拿到 角 aoc
    ///
    double ocdistance = widget.radius;

    int c = 1;

    if (pAy < (offset.dy + widget.radius)) {
      c = -1;
    }

    double cosAOC = (aoDistance * aoDistance +
            ocdistance * ocdistance -
            acDistance * acDistance) /
        (2 * aoDistance * ocdistance);
    double AOC = c * acos(cosAOC);

    /// BC距离
    double bcDistance = LuckUtil.distanceForTwoPoint(
        offset.dx + 2 * widget.radius, offset.dy + widget.radius, pBx, pBy);

    /// BO距离
    double boDistance = LuckUtil.distanceForTwoPoint(
        offset.dx + widget.radius, offset.dy + widget.radius, pBx, pBy);

    c = 1;
    if (pBy < (offset.dy + widget.radius)) {
      c = -1;
    }

    ///计算 cos boc 的值,然后拿到角 boc;
    double cosBOC = (boDistance * boDistance +
            ocdistance * ocdistance -
            bcDistance * bcDistance) /
        (2 * boDistance * ocdistance);
    double BOC = c * acos(cosBOC);

    return BOC - AOC;
  }

  ///抬手的时候 , 惯性滑动
  void _onPanEnd(DragEndDetails details) {
    double vx = details.velocity.pixelsPerSecond.dx;
    double vy = details.velocity.pixelsPerSecond.dy;
    if (vx != 0 || vy != 0) {
      onFling(vx, vy);
    }
  }

  void onFling(double velocityX, double velocityY) {
    //获取触点到中心点的线与水平线正方向的夹角
    double levelAngle = LuckUtil.getPointAngle(mCenterX, mCenterY, pBx, pBy);
    //获取象限
    int quadrant = LuckUtil.getQuadrant(pBx - mCenterX, pBy - mCenterY);
    //到中心点距离
    double distance =
        LuckUtil.distanceForTwoPoint(mCenterX, mCenterY, pBx, pBy);
    //获取惯性绘制的初始角度
    double inertiaInitAngle = LuckUtil.calculateAngleFromVelocity(
        velocityX, velocityY, levelAngle, quadrant, distance);

    if (inertiaInitAngle != null && inertiaInitAngle != 0) {
      //如果角速度不为0; 则进行滚动

      /// 按照 va的加速度 拿到 滚动的时间 。 也就是 结束后 惯性动画的 执行 时间, 高中物理
      double t = LuckUtil.abs(inertiaInitAngle) / vA;
      double s = t * inertiaInitAngle / 2;

      animalValue = turns;
      var time = new DateTime.now();
      int direction = 1;

      ///方向控制参数
      if (inertiaInitAngle < 0) {
        direction = -1;
      }
      autoAnimationController = AnimationController(
          duration: Duration(milliseconds: (t * 1000).toInt()), vsync: this)
        ..addListener(() {
          var animalTime = new DateTime.now();
          int t1 =
              animalTime.millisecondsSinceEpoch - time.millisecondsSinceEpoch;
          setState(() {
            double s1 = (2 * inertiaInitAngle - direction * vA * (t1 / 1000)) *
                t1 /
                (2 * 1000);
            turns = animalValue + s1;
          });
        });

      autoAnimationController.forward();
    }
  }

  @override
  void dispose() {
    super.dispose();
    if (autoAnimationController != null) {
      autoAnimationController.dispose();
    }
  }
}

class PieChartPainter extends CustomPainter {
  GlobalKey _key = GlobalKey();

  double turns = .0;
  double startTurns = .0;

  @required
  int centerHeight;
  @required
  int centerWidth;
  @required
  List<LuckItem> entitys;
  @required
  ui.Image _image;

  PieChartPainter(
    this._key,
    this.turns,
    this.startTurns,
    this.entitys,
    this._image,
    this.centerHeight,
    this.centerWidth,
  );

  double startAngles = 0;

  @override
  void paint(Canvas canvas, Size size) {
    drawAcr(canvas, size);
  }

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

  void drawAcr(Canvas canvas, Size size) {
    startAngles = 0;
    Rect rect = Rect.fromCircle(
        center: Offset(size.width / 2, size.height / 2),
        radius: size.width / 2);
    Paint paint = Paint()
      ..color = Colors.red
      ..strokeWidth = 1.0
      ..isAntiAlias = true
      ..style = PaintingStyle.fill;
    //画扇形
    drawCircule(canvas, rect, size, paint);
    //画中心图片
    drawCenterPic(canvas, size, paint);
    //绘制内容
    drawContent(canvas, size, paint);
  }

  drawCircule(Canvas canvas, Rect rect, Size size, Paint paint) {
    for (int i = 0; i < entitys.length; i++) {
      paint..color = entitys[i].bgColor ?? Colors.green;
      canvas.drawArc(rect, 2 * pi * startAngles + turns + startTurns,
          2 * pi * entitys[i].percent, true, paint);
      startAngles += entitys[i].percent;
    }
    startAngles = 0;
  }

  drawCenterPic(Canvas canvas, Size size, Paint paint) {
    if (_image != null) {
      canvas.save();
      //画中心按钮图片
      canvas.drawImage(
        _image,
        Offset(
            (size.width - centerWidth) / 2, (size.height - centerHeight) / 2),
        paint,
      );
      canvas.restore();
    }
  }

  drawContent(Canvas canvas, Size size, Paint paint) {
    for (int i = 0; i < entitys.length; i++) {
      canvas.save();
      // 新建一个段落建造器,然后将文字基本信息填入;
      ParagraphBuilder pb = ParagraphBuilder(ParagraphStyle(
        textDirection: TextDirection.ltr,
        // 字体对齐方式
        textAlign: TextAlign.right,
        fontWeight: FontWeight.w500,
        fontStyle: FontStyle.normal,
        fontSize: 12.0,
        maxLines: 5,
        ellipsis: "...",
      ));
      pb.pushStyle(ui.TextStyle(
        color: entitys[i].nameColor ?? Colors.white,
        background: paint..color = Colors.white,
        height: 1,
      ));
      double roaAngle =
          2 * pi * (startAngles + entitys[i].percent / 2) + turns + startTurns;
      pb.addText(entitys[i].name);
      //计算扇形文字宽度
      // 设置文本的宽度约束
      ParagraphConstraints pc = ParagraphConstraints(width: 20);
      // 这里需要先layout,将宽度约束填入,否则无法绘制
      Paragraph paragraph = pb.build()..layout(pc);
      // 文字左上角起始点
      var startX = (centerHeight / 2 + entitys[i].height);
      var offsetAngles =
          (sin((startAngles + (entitys[i].percent / 2)) * (pi / 180)));
      Offset offset =
          Offset(startX, startX * offsetAngles - entitys[i].height / 2);
      canvas.translate(size.width / 2, size.height / 2);
      canvas.rotate((1) * roaAngle);
      if (entitys[i].image != null) {
        Offset offsetPic = Offset(centerHeight / 2 + 3.0,
            (centerHeight / 2) * offsetAngles - entitys[i].height / 2);
        canvas.drawImageRect(
            entitys[i].image,
            Offset(0.0, 0.0) &
                Size(entitys[i].width.toDouble(), entitys[i].height.toDouble()),
            offsetPic &
                Size(entitys[i].width.toDouble(), entitys[i].height.toDouble()),
            paint);
      }
      canvas.drawParagraph(paragraph, offset);
      canvas.restore();
      startAngles += entitys[i].percent;
    }
  }
}

class LuckEntity {
  final String luckPic; //抽奖按钮  现在考虑是否支持网图动态配置
  final int centerHeight;
  final int centerWidth;

  ui.Image image;
  final List<LuckItem> entitys;

  LuckEntity({
    this.luckPic,
    this.centerHeight = 90,
    this.centerWidth = 90,
    this.image,
    this.entitys,
  });

  @override
  String toString() {
    return 'ChartEntity{luckPic: $luckPic, entitys: $entitys}';
  }
}

class LuckItem {
  final String pic; //扇形图片链接
  @required
  ui.Image image;
  int height;
  int width;
  @required
  final String name; //扇形名字
  final Color nameColor; //扇形名字颜色
  @required
  final Color bgColor; //扇形背景颜色
  @required
  final double percent; //百分比

  LuckItem({
    this.pic,
    this.image,
    this.height = 30,
    this.width = 30,
    this.name,
    this.nameColor,
    this.bgColor,
    this.percent,
  });

  @override
  String toString() {
    return 'ChartEntity{ pic: $pic, name: $name, nameColor: $nameColor, bgColor: $bgColor, percent: $percent}';
  }
}
上一篇:WebRTC源码级深度解析


下一篇:canvas刮刮卡