Flutter-自定义组件

101 阅读5分钟

组合多个Widget

通过拼装多个组件来组合成一个新的组件。例如Container就是一个组合组件,由DecoratedBox、ConstrainedBox、Transform、Padding、Align等组件组成。

通过CuntomPaint自绘

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

通过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来绘制,通过实现RenderObject的方式和上面介绍的通过CustomPaint和Canvas自绘的方式的区别是:
CustomPaint只是为了方便开发者封装的一个代理,它直接继承自SingleChildRenderObjectWidget,通过RenderCustomPaint的paint方法将Canvas和画笔Painter连接起来实现了最终的绘制(绘制逻辑在Painter中)。

总结

组合是自定义组件最简单的方法,在任何需要自定义组件的场景下,都应该优先考虑是否能够通过组合来实现。而通过CustomPaint和RenderObject自绘的方式本质上是一样的,需要调用Canvas API手动去绘制UI,优点是强大灵活,理论上可以实现任何外观UI,缺点是必须了解Canvas API的细节,并且的自己去实现绘制逻辑。

附上时针自绘的一个示例:

class DialPainter extends CustomPainter{
  late double width;
  late double height;
  late double radius;
  late final Paint _paint = _initPaint();
  late double unit;
  Paint _initPaint(){
    return Paint()
        ..isAntiAlias = true
        ..color = Colors.white;
  }
  @override
  void paint(Canvas canvas, Size size){
    initSize(size);
    drawDial(canvas);
    drawCalibration(canvas);
    drawText(canvas);
    drawCenter(canvas);
    var date = DateTime.now();
    drawHour(canvas, date);
    drawMinutes(canvas, date);
    drawSeconds(canvas, date);
  }
  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate){
    return true;
  }
  void initSize(Size size){
    width = size.width;
    height = size.height;
    radius = min(width, height)/2;
    unit = radius / 15;
  }
  void drawMinutes(Canvas canvas, DateTime dateTime){
    double hourHalfHeight = 0.4 * unit;
    double minutesLeft = -1.33 * unit;
    double minutesTop = -hourHalfHeight;
    double minutesRight = 11 * unit;
    double minutesBottom = hourHalfHeight;

    canvas.save();
    canvas.translate(width/2, height/2);
    canvas.rotate(2*pi/60 * (dateTime.minute - 15 + dateTime.second / 60));

    ///绘制分针
    var rRect = RRect.fromLTRBR(minutesLeft, minutesTop, minutesRight, minutesBottom, Radius.circular(0.42 * unit));
    _paint.color = const Color(0xFF343536);
    canvas.drawRRect(rRect, _paint);

    canvas.restore();
  }

  void drawSeconds(Canvas canvas, DateTime dateTime){
    double hourHalfHeight = 0.4 * unit;
    double secondsLeft = -4.5 * unit;
    double secondsTop = -hourHalfHeight;
    double secondsRight = 12.5 * unit;
    double secondsBottom = hourHalfHeight;
    Path secondsPath = Path();
    secondsPath.moveTo(secondsLeft, secondsTop);
    ///尾部弧形
    var rect = Rect.fromLTWH(secondsLeft, secondsTop, 2.5 * unit, hourHalfHeight*2);
    secondsPath.addArc(rect, pi/2, pi);
    ///尾部圆角矩形
    var rRect = RRect.fromLTRBR(secondsLeft + 1 * unit, secondsTop, - 2 * unit, secondsBottom, Radius.circular(0.25*unit));
    secondsPath.addRRect(rRect);
    ///指针
    secondsPath.moveTo(-2*unit, - 0.125*unit);
    secondsPath.lineTo(secondsRight, 0);
    secondsPath.lineTo(-2*unit, 0.125*unit);
    ///中心圆
    var ovalRect = Rect.fromLTWH(-0.67 * unit, -0.67*unit, 1.33*unit, 1.33*unit);
    secondsPath.addOval(ovalRect);
    canvas.save();
    canvas.translate(width/2, height/2);
    canvas.rotate(2*pi/60 * (dateTime.second - 15));
    ///绘制阴影
    canvas.drawShadow(secondsPath, const Color(0xffcc0000), 0.17 * unit, true);
    ///绘制秒针
    _paint.color = const Color(0xFFcc0000);
    canvas.drawPath(secondsPath, _paint);
    canvas.restore();
  }


  void drawHour(Canvas canvas, DateTime dateTime){
    double hourHalfHeight = 0.4 * unit;
    double hourRectRight = 7 * unit;

    Path hourPath = Path();
    ///添加矩形 时针主体
    hourPath.moveTo(0 - hourHalfHeight, 0 - hourHalfHeight);
    hourPath.lineTo(hourRectRight, 0 - hourHalfHeight);
    hourPath.lineTo(hourRectRight, 0 + hourHalfHeight);
    hourPath.lineTo(0 - hourHalfHeight, 0 + hourHalfHeight);

    ///时针箭头尾部弧形
    double offsetTop = 0.5 * unit;
    double arcWidth = 1.5 * unit;
    double arrowWidth = 2.17 * unit;
    double offset = 0.42 * unit;
    var rect = Rect.fromLTWH(hourRectRight - offset, 0 - hourHalfHeight - offsetTop, arcWidth, hourHalfHeight * 2 + offsetTop * 2);
    hourPath.addArc(rect, pi/2, pi);

    ///时针箭头
    hourPath.moveTo(hourRectRight - offset + arcWidth/2, 0 - hourHalfHeight - offsetTop);
    hourPath.lineTo(hourRectRight - offset + arcWidth/2 + arrowWidth, 0);
    hourPath.lineTo(hourRectRight - offset + arcWidth/2, 0 + hourHalfHeight + offsetTop);
    hourPath.close();

    canvas.save();
    canvas.translate(width/2, height/2);
    canvas.rotate(2*pi/60*((dateTime.hour - 3 + dateTime.minute/ 60 + dateTime.second/60/60)* 5));

    ///绘制
    _paint.color = const Color(0xff232425);
    canvas.drawPath(hourPath, _paint);
    canvas.restore();
  }

  void drawCenter(Canvas canvas){
    /// 绘制一个线性渐变的圆
    var radialGradient = RadialGradient(colors: const[
      Color.fromARGB(255, 200, 200, 200),
      Color.fromARGB(255, 190, 190, 190),
      Color.fromARGB(255,130, 130, 130),],
        radius: width/2,
        stops: const[0, 0.9, 1.0]).createShader(Rect.fromCenter(center: Offset(width/2, height/2), width: width, height: height));

    ///底部背景
    _paint.shader = radialGradient;
    _paint.style = PaintingStyle.fill;
    canvas.drawCircle(Offset(width/2, height/2), 2 * unit, _paint);

    ///顶部圆点
    _paint.shader = null;
    _paint.style = PaintingStyle.fill;
    _paint.color = const Color(0xFF121314);
    canvas.drawCircle(Offset(width/2, height/2), 0.8 * unit, _paint);
  }
  void drawDial(Canvas canvas){
    ///绘制一个线性渐变的圆
    var gradient = const LinearGradient(colors:
    [Color(0xFFF9F9F9), Color(0xFF666666)],
        begin: Alignment.topLeft, end: Alignment.bottomRight)
        .createShader(Rect.fromCenter(center: Offset(width/2, height/2), width: width, height: height));
    _paint.shader = gradient;
    _paint.color = Colors.white;
    canvas.drawCircle(Offset(width/2, height/2), radius, _paint);

    ///绘制一层径向渐变的圆

    var radialGradient = RadialGradient(colors: const[
      Color.fromARGB(216, 246, 248, 249),
      Color.fromARGB(216, 229, 235, 238),
      Color.fromARGB(216,205, 212, 217),
      Color.fromARGB(216,245, 247, 249),],
        radius: width/2, stops: const[0, 0.92, 0.93, 1.0]).createShader(Rect.fromCenter(center: Offset(width/2, height/2), width: width, height: height));
    
    _paint.shader = radialGradient;
    canvas.drawCircle(Offset(width/2, height/2), radius - 0.3 * unit, _paint);

    ///绘制一层border
    var shadowRadius = radius - 0.8 * unit;
    _paint.color = const Color.fromARGB(33, 0, 0, 0);
    _paint.shader = null;
    _paint.style = PaintingStyle.stroke;
    _paint.strokeWidth = 0.1 * unit;
    canvas.drawCircle(Offset(width/2, height/2), shadowRadius - 0.2 * unit, _paint);

    ///绘制阴影
    Path path = Path();
    path.moveTo(width/2, height/2);
    var rect = Rect.fromLTRB(width/2 - shadowRadius, height / 2 - shadowRadius, width/2 + shadowRadius, height/2 + shadowRadius);
    path.addOval(rect);
    canvas.drawShadow(path, const Color.fromARGB(51, 0, 0, 0), 1 * unit, true);
  }
  ///绘制刻度
  void drawCalibration(Canvas canvas){
    double dialCanvasRadius = radius - 0.8 * unit;
    canvas.save();
    canvas.translate(width/2, height/2);
    
    var y = 0.0;
    var x1 = 0.0;
    var x2 = 0.0;
    
    _paint.shader = null;
    _paint.color = const Color(0xFF929394);
    for (int i = 0;i < 60; i++){
      x1 = dialCanvasRadius - (i % 5 == 0 ? 0.85 * unit : 1 * unit);
      x2 = dialCanvasRadius - (i % 5 == 0 ? 2 * unit : 1.67 * unit);
      _paint.strokeWidth = i % 5 == 0 ? 0.38 * unit : 0.2 * unit;
      canvas.drawLine(Offset(x1, y), Offset(x2, y), _paint);
      canvas.rotate(2*pi/60);
    }
    canvas.restore();
  }
  ///绘制刻度上的值3 6 9 12
  void drawText(Canvas canvas){
    double dialCanvasRadius = radius - 0.8 * unit;
    var textPainter = TextPainter(
      text: const TextSpan(
        text: "3",
        style: TextStyle(color: Colors.black, fontSize: 20, fontWeight: FontWeight.bold, height:  1.0),
      ),
      textDirection: TextDirection.rtl,
      textWidthBasis: TextWidthBasis.longestLine,
      maxLines: 1,
      )..layout();
    
    var offset = 2.25 * unit;
    var points = [
      Offset(width / 2 + dialCanvasRadius - offset - textPainter.width, height / 2 - textPainter.height / 2),
      Offset(width / 2 - textPainter.width / 2, height / 2 + dialCanvasRadius - offset - textPainter.height),
      Offset(width / 2 - dialCanvasRadius + offset, height / 2 -textPainter.height / 2),
      Offset(width / 2 - textPainter.width, height / 2 - dialCanvasRadius + offset),
    ];
    
    for (int i=0; i < 4; i++){
      textPainter = TextPainter(
        text: TextSpan(
          text: "${(i + 1) * 3}",
          style: const TextStyle(color: Colors.black, fontSize: 20, fontWeight: FontWeight.bold, height: 1.0),
        ),
        textDirection: TextDirection.rtl,
        textWidthBasis: TextWidthBasis.longestLine,
        maxLines: 1,
      )..layout();
      textPainter.paint(canvas, points[i]);
    }
  }