跨平台Flutter

Flutter了解之自定义组件

2020-11-12  本文已影响0人  平安喜乐698
目录
  1. 组合其它组件(最简单,优先考虑)
  2. 自绘组件 (CustomPaint与Canvas)

当Flutter提供的现有组件无法满足需求,或者为了共享代码需要封装一些通用组件,这时就需要自定义组件。

3种方式

1. 组合其它组件(最简单,优先考虑)
  通过拼装其它组件来组合成一个新的组件。
  例如Container就是一个组合组件,它是由DecoratedBox、ConstrainedBox、Transform、Padding、Align等组件组成。


2. 自绘
  如果遇到无法通过现有的组件来实现需要的UI时,可以通过自绘组件的方式来实现。
  可以通过Flutter中提供的CustomPaint和Canvas来实现UI自绘。
  例如需要一个颜色渐变的圆形进度条,而Flutter提供的CircularProgressIndicator并不支持在显示精确进度时对进度条应用渐变色(其valueColor 属性只支持执行旋转动画时变化Indicator的颜色),这时最好的方法就是自绘。
  

3. 实现RenderObject。
  Flutter提供的自身具有UI外观的组件,如文本Text、Image都是通过相应的RenderObject渲染出来的,如Text是由RenderParagraph渲染;而Image是由RenderImage渲染。
  RenderObject是一个抽象类,它定义了一个抽象方法paint(...):
    void paint(PaintingContext context, Offset offset)
  PaintingContext代表组件的绘制上下文,通过PaintingContext.canvas可以获得Canvas,而绘制逻辑主要是通过Canvas API来实现。子类需要重写此方法以实现自身的绘制逻辑,如RenderParagraph需要实现文本绘制逻辑,而RenderImage需要实现图片绘制逻辑。
  可以发现,RenderObject中最终也是通过Canvas API来绘制的。CustomPaint只是为了方便开发者封装的一个代理类,它直接继承自SingleChildRenderObjectWidget,通过RenderCustomPaint的paint方法将Canvas和画笔Painter连接起来实现了最终的绘制(绘制逻辑在Painter中)。

自绘和实现RenderObject本质一样:都需要开发者调用Canvas API手动去绘制UI,优点是强大灵活,理论上可以实现任何外观的UI,而缺点是必须了解Canvas API细节,并且得自己去实现绘制逻辑。

1. 组合其它Widget(最简单,优先考虑)

当需要封装一些通用组件时,应该首先考虑是否可以通过组合其它组件来实现,如果可以,则应优先使用组合,因为直接通过现有组件拼装会非常简单、灵活、高效。
在抽离出单独的组件时要考虑代码规范性,如必要参数要用@required 标注,对于可选参数在特定场景需要判空或设置默认值等。这是由于使用者大多时候可能不了解组件的内部细节,所以为了保证代码健壮性,需要在用户错误地使用组件时能够兼容或报错提示(使用assert断言函数)。

例(组合)

Flutter Material组件库中的按钮默认不支持渐变背景,为了实现渐变背景按钮,自定义一个GradientButton组件,它需要支持一下功能:
    背景支持渐变色
    手指按下时有涟漪效果
    可以支持圆角
DecoratedBox可以支持背景色渐变和圆角,InkWell在手指按下有涟漪效果,所以可以通过组合DecoratedBox和InkWell来实现GradientButton。


import 'package:flutter/material.dart';

class GradientButton extends StatelessWidget {
  GradientButton({
    this.colors,
    this.width,
    this.height,
    this.onPressed,
    this.borderRadius,
    @required this.child,
  });

  // 渐变色数组
  final List<Color> colors;

  // 按钮宽高
  final double width;
  final double height;

  final Widget child;
  final BorderRadius borderRadius;

  // 点击回调
  final GestureTapCallback onPressed;

  @override
  Widget build(BuildContext context) {
    ThemeData theme = Theme.of(context);

    // 确保colors数组不空
    List<Color> _colors = colors ??
        [theme.primaryColor, theme.primaryColorDark ?? theme.primaryColor];

    return DecoratedBox(
      decoration: BoxDecoration(
        gradient: LinearGradient(colors: _colors),
        borderRadius: borderRadius,
      ),
      child: Material(
        type: MaterialType.transparency,
        child: InkWell(
          splashColor: _colors.last,
          highlightColor: Colors.transparent,
          borderRadius: borderRadius,
          onTap: onPressed,
          child: ConstrainedBox(
            constraints: BoxConstraints.tightFor(height: height, width: width),
            child: Center(
              child: Padding(
                padding: const EdgeInsets.all(8.0),
                child: DefaultTextStyle(
                  style: TextStyle(fontWeight: FontWeight.bold),
                  child: child,
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}


===========================
使用


import 'package:flutter/material.dart';
import '../widgets/index.dart';

class GradientButtonRoute extends StatefulWidget {
  @override
  _GradientButtonRouteState createState() => _GradientButtonRouteState();
}

class _GradientButtonRouteState extends State<GradientButtonRoute> {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Column(
        children: <Widget>[
          GradientButton(
            colors: [Colors.orange, Colors.red],
            height: 50.0,
            child: Text("Submit"),
            onPressed: onTap,
          ),
          GradientButton(
            height: 50.0,
            colors: [Colors.lightGreen, Colors.green[700]],
            child: Text("Submit"),
            onPressed: onTap,
          ),
          GradientButton(
            height: 50.0,
            colors: [Colors.lightBlue[300], Colors.blueAccent],
            child: Text("Submit"),
            onPressed: onTap,
          ),
        ],
      ),
    );
  }
  onTap() {
    print("button click");
  }
}

例(组合)

以任意角度来旋转其子节点,而且可以在角度发生变化时执行一个动画以过渡到新状态,同时可以手动指定动画速度。

import 'package:flutter/widgets.dart';

class TurnBox extends StatefulWidget {
  const TurnBox({
    Key key,
    this.turns = .0, //旋转的“圈”数,一圈为360度,如0.25圈即90度
    this.speed = 200, //过渡动画执行的总时长
    this.child
  }) :super(key: key);

  final double turns;
  final int speed;
  final Widget child;

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

class _TurnBoxState extends State<TurnBox>
    with SingleTickerProviderStateMixin {
  AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = new AnimationController(
        vsync: this,
        lowerBound: -double.infinity,
        upperBound: double.infinity
    );
    _controller.value = widget.turns;
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return RotationTransition(
      turns: _controller,
      child: widget.child,
    );
  }

  @override
  void didUpdateWidget(TurnBox oldWidget) {
    super.didUpdateWidget(oldWidget);
    // 旋转角度发生变化时执行过渡动画  
    if (oldWidget.turns != widget.turns) {
      _controller.animateTo(
        widget.turns,
        duration: Duration(milliseconds: widget.speed??200),
        curve: Curves.easeOut,
      );
    }
  }
}
import 'package:flutter/material.dart';
import '../widgets/index.dart';

class TurnBoxRoute extends StatefulWidget {
  @override
  _TurnBoxRouteState createState() => new _TurnBoxRouteState();
}

class _TurnBoxRouteState extends State<TurnBoxRoute> {
  double _turns = .0;

  @override
  Widget build(BuildContext context) {

    return Center(
      child: Column(
        children: <Widget>[
          TurnBox(
            turns: _turns,
            speed: 500,
            child: Icon(Icons.refresh, size: 50,),
          ),
          TurnBox(
            turns: _turns,
            speed: 1000,
            child: Icon(Icons.refresh, size: 150.0,),
          ),
          RaisedButton(
            child: Text("顺时针旋转1/5圈"),
            onPressed: () {
              setState(() {
                _turns += .2;
              });
            },
          ),
          RaisedButton(
            child: Text("逆时针旋转1/5圈"),
            onPressed: () {
              setState(() {
                _turns -= .2;
              });
            },
          )
        ],
      ),
    );
  }
}

当点击旋转按钮时,两个图标的旋转都会旋转1/5圈,但旋转的速度是不同的

例(组合)

如果封装的是StatefulWidget,那么一定要注意在组件更新时是否需要同步状态。
比如要封装一个富文本展示组件MyRichText ,它可以自动处理url链接,定义如下:

class MyRichText extends StatefulWidget {
  MyRichText({
    Key key,
    this.text, // 文本字符串
    this.linkStyle, // url链接样式
  }) : super(key: key);

  final String text;
  final TextStyle linkStyle;

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

接下来在_MyRichTextState中要实现的功能有两个:
    解析文本字符串“text”,生成TextSpan缓存起来;
    在build中返回最终的富文本样式;

class _MyRichTextState extends State<MyRichText> {

  TextSpan _textSpan;

  @override
  Widget build(BuildContext context) {
    return RichText(
      text: _textSpan,
    );
  }

  TextSpan parseText(String text) {
    // 耗时操作:解析文本字符串,构建出TextSpan。
    // 省略具体实现。
  }

  @override
  void initState() {
    _textSpan = parseText(widget.text)
    super.initState();
  }
}


由于解析文本字符串,构建出TextSpan是一个耗时操作,为了不在每次build的时候都解析一次,所以在initState中对解析的结果进行了缓存,然后再build中直接使用解析的结果_textSpan。这看起来很不错,但是上面的代码有一个严重的问题,就是父组件传入的text发生变化时(组件树结构不变),那么MyRichText显示的内容不会更新,原因就是initState只会在State创建时被调用,所以在text发生变化时,parseText没有重新执行,导致_textSpan任然是旧的解析值。要解决这个问题也很简单,只需添加一个didUpdateWidget回调,然后再里面重新调用parseText即可:

@override
void didUpdateWidget(MyRichText oldWidget) {
  if (widget.text != oldWidget.text) {
    _textSpan = parseText(widget.text);
  }
  super.didUpdateWidget(oldWidget);
}

2. 自绘组件 (CustomPaint与Canvas)

对于一些复杂或不规则的UI,可能无法通过组合其它组件的方式来实现,比如需要一个正六边形、一个渐变的圆形进度条、一个棋盘等。当然,有时候可以使用图片来实现,但在一些需要动态交互的场景静态图片也是实现不了的。

自绘控件非常强大,理论上可以实现任何2D图形外观,实际上Flutter提供的所有组件最终都是通过调用Canvas绘制出来的,只不过绘制的逻辑被封装起来了。可以查看具有外观样式的组件源码,找到其对应的RenderObject对象,如Text对应的RenderParagraph对象最终会通过Canvas实现文本绘制逻辑。

绘制是比较昂贵的操作,所以在实现自绘控件时应该考虑到性能开销,下面是两条关于性能优化的建议:
    1. 尽可能的利用好shouldRepaint返回值;在UI树重新build时,控件在绘制前都会先调用该方法以确定是否有必要重绘;假如绘制的UI不依赖外部状态,那么就应该始终返回false,因为外部状态改变导致重新build时不会影响UI外观;如果绘制依赖外部状态,那么就应该在shouldRepaint中判断依赖的状态是否改变,如果已改变则应返回true来重绘,反之则应返回false不需要重绘。
    2. 绘制尽可能多的分层;

重要的几个类

  1. CustomPaint
CustomPaint({
  Key key,
  this.painter, 
  this.foregroundPainter,
  this.size = Size.zero, 
  this.isComplex = false, 
  this.willChange = false, 
  Widget child, //子节点,可以为空
})
说明:
    1. painter: 背景画笔,会显示在子节点后面
    2. foregroundPainter: 前景画笔,会显示在子节点前面
    3. size:当child为null时,代表默认绘制区域大小,如果有child则忽略此参数,画布尺寸则为child尺寸。如果有child但是想指定画布为特定大小,可以使用SizeBox包裹CustomPaint实现。
    4. isComplex:是否复杂的绘制,如果是,Flutter会应用一些缓存策略来减少重复渲染的开销。
    5. willChange:和isComplex配合使用,当启用缓存时,该属性代表在下一帧中绘制是否会改变。


如果CustomPaint有子节点,为了避免子节点不必要的重绘并提高性能,通常情况下都会将子节点包裹在RepaintBoundary组件中,这样会在绘制时就会创建一个新的绘制层(Layer),其子组件将在新的Layer上绘制,而父组件将在原来Layer上绘制,也就是说RepaintBoundary 子组件的绘制将独立于父组件的绘制,RepaintBoundary会隔离其子节点和CustomPaint本身的绘制边界。示例如下:
CustomPaint(
  size: Size(300, 300), //指定画布大小
  painter: MyPainter(),
  child: RepaintBoundary(child:...)), 
)
  1. CustomPainter
画笔需要继承CustomPainter类,在画笔类中实现真正的绘制逻辑。

void paint(Canvas canvas, Size size);
说明:
    1. Canvas:一个画布,包括各种绘制方法
      drawLine 画线 
      drawPoint 画点  
      drawPath 画路径 
      drawImage 画图像 
      drawRect 画矩形 
      drawCircle 画圆 
      drawOval 画椭圆 
      drawArc 画圆弧
    2. Size:当前绘制区域大小。
  1. 画笔Paint
Flutter提供了Paint类来实现画笔。

在Paint中,可以配置画笔的各种属性如粗细、颜色、样式等。
var paint = Paint() //创建一个画笔并配置其属性
  ..isAntiAlias = true //是否抗锯齿
  ..style = PaintingStyle.fill //画笔样式:填充
  ..color=Color(0x77cdb175);//画笔颜色

示例

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

class CustomPaintRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: CustomPaint(
        size: Size(300, 300), // 指定画布大小
        painter: MyPainter(),
      ),
    );
  }
}

class MyPainter extends CustomPainter {  // 画笔
  @override
  void paint(Canvas canvas, Size size) {
    double eWidth = size.width / 15;
    double eHeight = size.height / 15;

    // 画棋盘背景
    var paint = Paint()
      ..isAntiAlias = true
      ..style = PaintingStyle.fill //填充
      ..color = Color(0x77cdb175); //背景为纸黄色
    canvas.drawRect(Offset.zero & size, paint);

    // 画棋盘网格
    paint
      ..style = PaintingStyle.stroke //线
      ..color = Colors.black87
      ..strokeWidth = 1.0;

    for (int i = 0; i <= 15; ++i) {
      double dy = eHeight * i;
      canvas.drawLine(Offset(0, dy), Offset(size.width, dy), paint);
    }

    for (int i = 0; i <= 15; ++i) {
      double dx = eWidth * i;
      canvas.drawLine(Offset(dx, 0), Offset(dx, size.height), paint);
    }

    // 画一个黑子
    paint
      ..style = PaintingStyle.fill
      ..color = Colors.black;
    canvas.drawCircle(
      Offset(size.width / 2 - eWidth / 2, size.height / 2 - eHeight / 2),
      min(eWidth / 2, eHeight / 2) - 2,
      paint,
    );

    // 画一个白子
    paint.color = Colors.white;
    canvas.drawCircle(
      Offset(size.width / 2 + eWidth / 2, size.height / 2 - eHeight / 2),
      min(eWidth / 2, eHeight / 2) - 2,
      paint,
    );
  }

  // 在实际场景中正确利用此回调可以避免重绘开销,这里简单的返回true
  @override
  bool shouldRepaint(CustomPainter oldDelegate) => true;
}

在上面五子棋的示例中,将棋盘和棋子的绘制放在了一起,这样会有一个问题:由于棋盘始终是不变的,用户每次落子时变的只是棋子,但是如果按照上面的代码来实现,每次绘制棋子时都要重新绘制一次棋盘,这是没必要的。优化的方法就是将棋盘单独抽为一个组件,并设置其shouldRepaint回调值为false,然后将棋盘组件作为背景。然后将棋子的绘制放到另一个组件中,这样每次落子时只需要绘制棋子。

例(自绘)

实现一个圆形背景渐变进度条,它支持:
    支持多种背景渐变色。
    任意弧度;进度条可以不是整圆。
    可以自定义粗细、两端是否圆角等样式。
要实现这样的一个进度条是无法通过现有组件组合而成的,所以通过自绘方式实现,代码如下

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

class GradientCircularProgressIndicator extends StatelessWidget {
  GradientCircularProgressIndicator({
    this.strokeWidth = 2.0,
    @required this.radius,
    @required this.colors,
    this.stops,
    this.strokeCapRound = false,
    this.backgroundColor = const Color(0xFFEEEEEE),
    this.totalAngle = 2 * pi,
    this.value
  });

  /// 粗细
  final double strokeWidth;

  /// 圆的半径
  final double radius;

  /// 两端是否为圆角
  final bool strokeCapRound;

  /// 当前进度,取值范围 [0.0-1.0]
  final double value;

  /// 进度条背景色
  final Color backgroundColor;

  /// 进度条的总弧度,2*PI为整圆,小于2*PI则不是整圆
  final double totalAngle;

  /// 渐变色数组
  final List<Color> colors;

  /// 渐变色的终止点,对应colors属性
  final List<double> stops;

  @override
  Widget build(BuildContext context) {
    double _offset = .0;
    // 如果两端为圆角,则需要对起始位置进行调整,否则圆角部分会偏离起始位置
    // 下面调整的角度的计算公式是通过数学几何知识得出
    if (strokeCapRound) {
      _offset = asin(strokeWidth / (radius * 2 - strokeWidth));
    }
    var _colors = colors;
    if (_colors == null) {
      Color color = Theme
          .of(context)
          .accentColor;
      _colors = [color, color];
    }
    return Transform.rotate(
      angle: -pi / 2.0 - _offset, 
      child: CustomPaint(
          size: Size.fromRadius(radius),
          painter: _GradientCircularProgressPainter(
            strokeWidth: strokeWidth,
            strokeCapRound: strokeCapRound,
            backgroundColor: backgroundColor,
            value: value,
            total: totalAngle,
            radius: radius,
            colors: _colors,
          )
      ),
    );
  }
}

// 实现画笔
class _GradientCircularProgressPainter extends CustomPainter {
  _GradientCircularProgressPainter({
    this.strokeWidth: 10.0,
    this.strokeCapRound: false,
    this.backgroundColor = const Color(0xFFEEEEEE),
    this.radius,
    this.total = 2 * pi,
    @required this.colors,
    this.stops,
    this.value
  });

  final double strokeWidth;
  final bool strokeCapRound;
  final double value;
  final Color backgroundColor;
  final List<Color> colors;
  final double total;
  final double radius;
  final List<double> stops;

  @override
  void paint(Canvas canvas, Size size) {
    if (radius != null) {
      size = Size.fromRadius(radius);
    }
    double _offset = strokeWidth / 2.0;
    double _value = (value ?? .0);
    _value = _value.clamp(.0, 1.0) * total;
    double _start = .0;

    if (strokeCapRound) {
      _start = asin(strokeWidth/ (size.width - strokeWidth));
    }

    Rect rect = Offset(_offset, _offset) & Size(
        size.width - strokeWidth,
        size.height - strokeWidth
    );

    var paint = Paint()
      ..strokeCap = strokeCapRound ? StrokeCap.round : StrokeCap.butt
      ..style = PaintingStyle.stroke
      ..isAntiAlias = true
      ..strokeWidth = strokeWidth;

    // 先画背景
    if (backgroundColor != Colors.transparent) {
      paint.color = backgroundColor;
      canvas.drawArc(
          rect,
          _start,
          total,
          false,
          paint
      );
    }

    // 再画前景,应用渐变
    if (_value > 0) {
      paint.shader = SweepGradient(
        startAngle: 0.0,
        endAngle: _value,
        colors: colors,
        stops: stops,
      ).createShader(rect);

      canvas.drawArc(
          rect,
          _start,
          _value,
          false,
          paint
      );
    }
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => true;

}
import 'dart:math';
import 'package:flutter/material.dart';
import '../widgets/index.dart';

class GradientCircularProgressRoute extends StatefulWidget {
  @override
  GradientCircularProgressRouteState createState() {
    return new GradientCircularProgressRouteState();
  }
}

class GradientCircularProgressRouteState
    extends State<GradientCircularProgressRoute> with TickerProviderStateMixin {
  AnimationController _animationController;

  @override
  void initState() {
    super.initState();
    _animationController =
        new AnimationController(vsync: this, duration: Duration(seconds: 3));
    bool isForward = true;
    _animationController.addStatusListener((status) {
      if (status == AnimationStatus.forward) {
        isForward = true;
      } else if (status == AnimationStatus.completed ||
          status == AnimationStatus.dismissed) {
        if (isForward) {
          _animationController.reverse();
        } else {
          _animationController.forward();
        }
      } else if (status == AnimationStatus.reverse) {
        isForward = false;
      }
    });
    _animationController.forward();
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Center(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            AnimatedBuilder(
              animation: _animationController,
              builder: (BuildContext context, Widget child) {
                return Padding(
                  padding: const EdgeInsets.symmetric(vertical: 16.0),
                  child: Column(
                    children: <Widget>[
                      Wrap(
                        spacing: 10.0,
                        runSpacing: 16.0,
                        children: <Widget>[
                          GradientCircularProgressIndicator(
                            // No gradient
                            colors: [Colors.blue, Colors.blue],
                            radius: 50.0,
                            strokeWidth: 3.0,
                            value: _animationController.value,
                          ),
                          GradientCircularProgressIndicator(
                            colors: [Colors.red, Colors.orange],
                            radius: 50.0,
                            strokeWidth: 3.0,
                            value: _animationController.value,
                          ),
                          GradientCircularProgressIndicator(
                            colors: [Colors.red, Colors.orange, Colors.red],
                            radius: 50.0,
                            strokeWidth: 5.0,
                            value: _animationController.value,
                          ),
                          GradientCircularProgressIndicator(
                            colors: [Colors.teal, Colors.cyan],
                            radius: 50.0,
                            strokeWidth: 5.0,
                            strokeCapRound: true,
                            value: CurvedAnimation(
                                    parent: _animationController,
                                    curve: Curves.decelerate)
                                .value,
                          ),
                          TurnBox(
                            turns: 1 / 8,
                            child: GradientCircularProgressIndicator(
                                colors: [Colors.red, Colors.orange, Colors.red],
                                radius: 50.0,
                                strokeWidth: 5.0,
                                strokeCapRound: true,
                                backgroundColor: Colors.red[50],
                                totalAngle: 1.5 * pi,
                                value: CurvedAnimation(
                                        parent: _animationController,
                                        curve: Curves.ease)
                                    .value),
                          ),
                          RotatedBox(
                            quarterTurns: 1,
                            child: GradientCircularProgressIndicator(
                                colors: [Colors.blue[700], Colors.blue[200]],
                                radius: 50.0,
                                strokeWidth: 3.0,
                                strokeCapRound: true,
                                backgroundColor: Colors.transparent,
                                value: _animationController.value),
                          ),
                          GradientCircularProgressIndicator(
                            colors: [
                              Colors.red,
                              Colors.amber,
                              Colors.cyan,
                              Colors.green[200],
                              Colors.blue,
                              Colors.red
                            ],
                            radius: 50.0,
                            strokeWidth: 5.0,
                            strokeCapRound: true,
                            value: _animationController.value,
                          ),
                        ],
                      ),
                      GradientCircularProgressIndicator(
                        colors: [Colors.blue[700], Colors.blue[200]],
                        radius: 100.0,
                        strokeWidth: 20.0,
                        value: _animationController.value,
                      ),

                      Padding(
                        padding: const EdgeInsets.symmetric(vertical: 16.0),
                        child: GradientCircularProgressIndicator(
                          colors: [Colors.blue[700], Colors.blue[300]],
                          radius: 100.0,
                          strokeWidth: 20.0,
                          value: _animationController.value,
                          strokeCapRound: true,
                        ),
                      ),
                      // 剪裁半圆
                      ClipRect(
                        child: Align(
                          alignment: Alignment.topCenter,
                          heightFactor: .5,
                          child: Padding(
                            padding: const EdgeInsets.only(bottom: 8.0),
                            child: SizedBox(
                              //width: 100.0,
                              child: TurnBox(
                                turns: .75,
                                child: GradientCircularProgressIndicator(
                                  colors: [Colors.teal, Colors.cyan[500]],
                                  radius: 100.0,
                                  strokeWidth: 8.0,
                                  value: _animationController.value,
                                  totalAngle: pi,
                                  strokeCapRound: true,
                                ),
                              ),
                            ),
                          ),
                        ),
                      ),
                      SizedBox(
                        height: 104.0,
                        width: 200.0,
                        child: Stack(
                          alignment: Alignment.center,
                          children: <Widget>[
                            Positioned(
                              height: 200.0,
                              top: .0,
                              child: TurnBox(
                                turns: .75,
                                child: GradientCircularProgressIndicator(
                                  colors: [Colors.teal, Colors.cyan[500]],
                                  radius: 100.0,
                                  strokeWidth: 8.0,
                                  value: _animationController.value,
                                  totalAngle: pi,
                                  strokeCapRound: true,
                                ),
                              ),
                            ),
                            Padding(
                              padding: const EdgeInsets.only(top: 10.0),
                              child: Text(
                                "${(_animationController.value * 100).toInt()}%",
                                style: TextStyle(
                                  fontSize: 25.0,
                                  color: Colors.blueGrey,
                                ),
                              ),
                            )
                          ],
                        ),
                      ),
                    ],
                  ),
                );
              },
            ),
          ],
        ),
      ),
    );
  }
}
上一篇 下一篇

猜你喜欢

热点阅读