dart 创建带有 Flutter 的温度计构件

cngwdvgl  于 2023-04-27  发布在  Flutter
关注(0)|答案(1)|浏览(159)

我有一个设计任务,在图中的图像。我需要创建一个小部件相同的图像。
颜色可以从最后一行移到顶部。
此外,它还兼容多种屏幕尺寸。

class ThermometerWidget extends StatelessWidget {
  const ThermometerWidget({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100,
      height: 300,
      decoration: const BoxDecoration(
          color: Colors.white,
          shape: BoxShape.rectangle,
          borderRadius: BorderRadius.all(Radius.circular(16)),
          boxShadow: [
            BoxShadow(
              color: Color(0xFFBFC9D7),
              offset: Offset(4, 4),
              blurRadius: 10,
            )
          ]),
      child: Stack(
        children: [
          Container(
            width: 14,
            height: 4,
            margin: const EdgeInsets.only(top: 30),
            decoration: const BoxDecoration(
              color: Color(0xFFB0B0B0),
              shape: BoxShape.rectangle,
              borderRadius: BorderRadius.only(
                topRight: Radius.circular(16),
                bottomRight: Radius.circular(16),
              ),
            ),
          ),
          Container(
            width: 14,
            height: 4,
            margin: const EdgeInsets.only(top: 60),
            decoration: const BoxDecoration(
              color: Color(0xFFB0B0B0),
              shape: BoxShape.rectangle,
              borderRadius: BorderRadius.only(
                topRight: Radius.circular(16),
                bottomRight: Radius.circular(16),
              ),
            ),
          ),
          Container(
            width: 14,
            height: 4,
            margin: const EdgeInsets.only(top: 90),
            decoration: const BoxDecoration(
              color: Color(0xFFB0B0B0),
              shape: BoxShape.rectangle,
              borderRadius: BorderRadius.only(
                topRight: Radius.circular(16),
                bottomRight: Radius.circular(16),
              ),
            ),
          ),
          Container(
            width: 14,
            height: 4,
            margin: const EdgeInsets.only(top: 120),
            decoration: const BoxDecoration(
              color: Color(0xFFB0B0B0),
              shape: BoxShape.rectangle,
              borderRadius: BorderRadius.only(
                topRight: Radius.circular(16),
                bottomRight: Radius.circular(16),
              ),
            ),
          ),
          Container(
            width: 14,
            height: 4,
            margin: const EdgeInsets.only(top: 150),
            decoration: const BoxDecoration(
              color: Color(0xFFB0B0B0),
              shape: BoxShape.rectangle,
              borderRadius: BorderRadius.only(
                topRight: Radius.circular(16),
                bottomRight: Radius.circular(16),
              ),
            ),
          ),
          Align(
            alignment: Alignment.topRight,
            child: Container(
              width: 14,
              height: 4,
              margin: const EdgeInsets.only(top: 38),
              decoration: const BoxDecoration(
                color: Color(0xFFB0B0B0),
                shape: BoxShape.rectangle,
                borderRadius: BorderRadius.only(
                  topLeft: Radius.circular(16),
                  bottomLeft: Radius.circular(16),
                ),
              ),
            ),
          ),
          Align(
            alignment: Alignment.topRight,
            child: Container(
              width: 14,
              height: 4,
              margin: const EdgeInsets.only(top: 68),
              decoration: const BoxDecoration(
                color: Color(0xFFB0B0B0),
                shape: BoxShape.rectangle,
                borderRadius: BorderRadius.only(
                  topLeft: Radius.circular(16),
                  bottomLeft: Radius.circular(16),
                ),
              ),
            ),
          ),
          Align(
            alignment: Alignment.topRight,
            child: Container(
              width: 14,
              height: 4,
              margin: const EdgeInsets.only(top: 98),
              decoration: const BoxDecoration(
                color: Color(0xFFB0B0B0),
                shape: BoxShape.rectangle,
                borderRadius: BorderRadius.only(
                  topLeft: Radius.circular(16),
                  bottomLeft: Radius.circular(16),
                ),
              ),
            ),
          ),
          Align(
            alignment: Alignment.topRight,
            child: Container(
              width: 14,
              height: 4,
              margin: const EdgeInsets.only(top: 128),
              decoration: const BoxDecoration(
                color: Color(0xFFB0B0B0),
                shape: BoxShape.rectangle,
                borderRadius: BorderRadius.only(
                  topLeft: Radius.circular(16),
                  bottomLeft: Radius.circular(16),
                ),
              ),
            ),
          ),
          Align(
            alignment: Alignment.topRight,
            child: Container(
              width: 14,
              height: 4,
              margin: const EdgeInsets.only(top: 158),
              decoration: const BoxDecoration(
                color: Color(0xFFB0B0B0),
                shape: BoxShape.rectangle,
                borderRadius: BorderRadius.only(
                  topLeft: Radius.circular(16),
                  bottomLeft: Radius.circular(16),
                ),
              ),
            ),
          ),
          Align(
            alignment: Alignment.center,
            child: Stack(
              alignment: Alignment.topCenter,
              children: [
                Container(
                  width: 80,
                  margin: const EdgeInsets.only(top: 160),
                  decoration: const BoxDecoration(
                    color: Colors.white,
                    shape: BoxShape.circle,
                    boxShadow: [
                      BoxShadow(
                        color: Color(0xFFBFC9D7),
                        blurRadius: 10,
                        offset: Offset(2, 3),
                      )
                    ],
                  ),
                ),
                Container(
                  width: 40,
                  height: 200,
                  margin: const EdgeInsets.only(top: 20),
                  decoration: const BoxDecoration(
                    color: Colors.white,
                    shape: BoxShape.rectangle,
                    boxShadow: [
                      BoxShadow(
                        color: Color(0xFFBFC9D7),
                        blurRadius: 10,
                        offset: Offset(2, 3),
                      )
                    ],
                    borderRadius: BorderRadius.only(
                      topLeft: Radius.circular(30),
                      topRight: Radius.circular(30),
                    ),
                  ),
                ),
                Container(
                  width: 60,
                  margin: const EdgeInsets.only(top: 160),
                  decoration: const BoxDecoration(
                    shape: BoxShape.circle,
                    color: Colors.white,
                  ),
                ),
                Container(
                  width: 30,
                  height: 160,
                  margin: const EdgeInsets.only(top: 80),
                  decoration: const BoxDecoration(
                    color: Color(0xFFF5F5F5),
                    shape: BoxShape.rectangle,
                    borderRadius: BorderRadius.only(
                      topLeft: Radius.circular(16),
                      topRight: Radius.circular(16),
                    ),
                  ),
                ),
                Container(
                  width: 60,
                  margin: const EdgeInsets.only(top: 160),
                  decoration: const BoxDecoration(
                    shape: BoxShape.circle,
                    color: Color(0xFFF5F5F5),
                  ),
                ),
              ],
            ),
          )
        ],
      ),
    );
  }
}

这里我的代码。我的代码也设置硬大小。如何优化它的多个屏幕大小和正确的设计。

请帮帮我谢谢你的帮助

vwkv1x7d

vwkv1x7d1#

你可以只使用CustomPaint小部件就可以做到这一点,但是我让它更“动态”一点,我使用了一个自定义的ImplicitlyAnimatedWidget,现在如果你用不同的参数重建Thermo小部件,你可以看到它如何动画value(和/或color)属性。

class Thermo extends ImplicitlyAnimatedWidget {
  const Thermo({
    super.key,
    super.curve,
    required this.color,
    required this.value,
    required super.duration,
    super.onEnd,
  });

  final Color color;
  final double value;

  @override
  AnimatedWidgetBaseState<Thermo> createState() => _ThermoState();
}

class _ThermoState extends AnimatedWidgetBaseState<Thermo> {
  ColorTween? _color;
  Tween<double>? _value;

  @override
  Widget build(BuildContext context) {
    return SizedBox.expand(
      child: FittedBox(
        child: CustomPaint(
          size: const Size(18, 63),
          painter: _ThermoPainter(
            color: _color!.evaluate(animation)!,
            value: _value!.evaluate(animation),
          ),
        ),
      ),
    );
  }

  @override
  void forEachTween(TweenVisitor<dynamic> visitor) {
    _color = visitor(_color, widget.color, (dynamic v) => ColorTween(begin: v)) as ColorTween?;
    _value = visitor(_value, widget.value, (dynamic v) => Tween<double>(begin: v)) as Tween<double>?;
  }
}

class _ThermoPainter extends CustomPainter {
  _ThermoPainter({
    required this.color,
    required this.value,
  });

  final Color color;
  final double value;

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

    const bulbRadius = 6.0;
    const smallRadius = 3.0;
    const border = 1.0;
    final rect = (Offset.zero & size);
    final innerRect = rect.deflate(size.width / 2 - bulbRadius);
    final r1 = Alignment.bottomCenter.inscribe(const Size(2 * smallRadius, bulbRadius * 2), innerRect);
    final r2 = Alignment.center.inscribe(Size(2 * smallRadius, innerRect.height), innerRect);

    final bulb = Path()..addOval(Alignment.bottomCenter.inscribe(Size.square(innerRect.width), innerRect));
    final outerPath = Path()
      ..addOval(Alignment.bottomCenter.inscribe(Size.square(innerRect.width), innerRect).inflate(border))
      ..addRRect(RRect.fromRectAndRadius(r2, const Radius.circular(smallRadius)).inflate(border));

    final scaleRect = Rect.fromPoints(innerRect.topLeft, innerRect.bottomRight - const Offset(0, 2 * bulbRadius));
    Iterable<Offset> generatePoints() sync* {
      for (int i = 0; i < 11; i++) {
        final t = i / 10;
        final point = i.isOdd?
          Offset.lerp(scaleRect.bottomLeft, scaleRect.topLeft, t)! :
          Offset.lerp(scaleRect.bottomRight, scaleRect.topRight, t)!;
        yield point;
        yield point.translate(i.isOdd? 2 : -2, 0);
      }
    }

    final valueRect = Rect.lerp(r1, r2, value)!;
    final valuePaint = Paint()..color = color;

    canvas
      ..save()
      // draw scale
      ..drawPoints(PointMode.lines, generatePoints().toList(), Paint()
        ..color = Colors.black45
        ..style = PaintingStyle.stroke
        ..strokeWidth = 1
      )
      // draw shadow
      ..drawPath(outerPath.shift(const Offset(1, 1)), Paint()
        ..color = Colors.black54
        ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 1)
      )
      ..clipPath(outerPath)
      // draw background
      ..drawPaint(Paint()..color = Color.alphaBlend(Colors.white60, color))
      // draw foreground
      ..drawPath(bulb, valuePaint)
      ..drawRRect(RRect.fromRectAndRadius(valueRect, const Radius.circular(smallRadius)), valuePaint)
      ..restore();

    // debug only:

    // canvas.drawRect(rect, Paint()..color = Colors.black38);
    // canvas.drawRect(innerRect, Paint()..color = Colors.black38);
    // canvas.drawRect(valueRect, Paint()..color = Colors.black38);
    // canvas.drawRect(scaleRect, Paint()..color = Colors.black38);
  }

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

你可以用这个ThermoTest小部件来测试它:

class ThermoTest extends StatefulWidget {
  @override
  State<ThermoTest> createState() => _ThermoTestState();
}

class _ThermoTestState extends State<ThermoTest> {
  int i = 0;
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.black12,
      child: Column(
        children: [
          Expanded(
            child: Stack(
              children: [
                Positioned(
                  left: 25,
                  top: 50,
                  width: 125,
                  height: 400,
                  child: Card(
                    elevation: 6,
                    child: Thermo(
                      duration: const Duration(milliseconds: 1250),
                      color: i.isOdd? Colors.red : Colors.green,
                      value: i.isOdd? 0.9 : 0.1,
                      curve: Curves.easeInOut,
                    ),
                  ),
                ),
                Positioned(
                  left: 150,
                  top: 75,
                  width: 50,
                  height: 150,
                  child: Card(
                    elevation: 6,
                    child: Thermo(
                      duration: const Duration(milliseconds: 1250),
                      color: i.isOdd? Colors.indigo : Colors.teal,
                      value: i.isOdd? 0.2 : 0.7,
                      curve: Curves.elasticOut,
                    ),
                  ),
                ),
                Positioned(
                  left: 175,
                  top: 225,
                  width: 100,
                  height: 200,
                  child: Card(
                    elevation: 6,
                    child: Thermo(
                      duration: const Duration(milliseconds: 750),
                      color: i.isOdd? Colors.orange : Colors.deepPurple,
                      value: i.isOdd? 0.3 : 1.0,
                      curve: Curves.bounceOut,
                    ),
                  ),
                ),
              ],
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(32.0),
            child: ElevatedButton(
              onPressed: () {
                setState(() => i++);
              },
              child: const Text('click me'),
            ),
          ),
        ],
      ),
    );
  }
}

这就是结果

编辑

自定义的OutlinedBorder似乎比自定义的CustomPainter类更好:

class ThermoShape extends OutlinedBorder {
  const ThermoShape({
    required super.side,
    required this.color,
    required this.value,
    required this.scalePainter,
  });

  final Color color;
  final double value;
  final Function(Canvas, Rect)? scalePainter;

  @override
  OutlinedBorder copyWith({BorderSide? side}) {
    return ThermoShape(
      side: side ?? this.side,
      color: color,
      value: value,
      scalePainter: scalePainter,
    );
  }

  @override
  Path getInnerPath(Rect rect, {TextDirection? textDirection}) => getOuterPath(rect);

  static const kWidthFactor = 0.55;

  @override
  Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
    final bulbRect = Alignment.bottomCenter.inscribe(Size.square(rect.width), rect);
    final maxValueRect = Alignment.topCenter.inscribe(Size(rect.width * kWidthFactor, rect.height), rect);
    return Path()
      ..addRRect(RRect.fromRectAndRadius(maxValueRect, Radius.circular(maxValueRect.width / 2)))
      ..addOval(bulbRect);
  }

  @override
  void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {
    canvas.drawPath(getOuterPath(rect), Paint()..color = side.color);
    final bulbRect = Alignment.bottomCenter.inscribe(Size.square(rect.width), rect).deflate(side.width);
    final size = Size(rect.width * kWidthFactor - 2 * side.width, rect.height - 2 * side.width);
    final b = Alignment.bottomCenter.inscribe(size, bulbRect);
    final a = b.intersect(bulbRect);
    final valueRect = Rect.lerp(a, b, value)!;
    final path = Path()
      ..addRRect(RRect.fromRectAndRadius(valueRect, Radius.circular(valueRect.width / 2)))
      ..addOval(bulbRect);
    canvas.drawPath(path, Paint()..color = color);
    scalePainter?.call(canvas, Rect.fromLTRB(rect.left, rect.top, rect.right, bulbRect.top));
  }

  @override
  ShapeBorder scale(double t) => this;
}

class Thermo2 extends ImplicitlyAnimatedWidget {
  const Thermo2({
    super.key,
    super.curve,
    required super.duration,
    required this.color,
    required this.value,
    required this.side,
    this.shadow,
    this.padding = const EdgeInsets.all(8),
    this.scalePainter,
    this.child,
    super.onEnd,
  });

  final Color color;
  final double value;
  final BorderSide side;
  final BoxShadow? shadow;
  final EdgeInsetsGeometry padding;
  final Function(Canvas, Rect)? scalePainter;
  final Widget? child;

  @override
  AnimatedWidgetBaseState<Thermo2> createState() => _Thermo2State();
}

class _Thermo2State extends AnimatedWidgetBaseState<Thermo2> {
  ColorTween? _color;
  Tween<double>? _value;

  @override
  Widget build(BuildContext context) {
    final shape = ThermoShape(
      side: widget.side,
      color: _color!.evaluate(animation)!,
      value: _value!.evaluate(animation),
      scalePainter: widget.scalePainter,
    );
    final child = widget.child != null? ClipPath(
      clipBehavior: Clip.antiAlias,
      clipper: ShapeBorderClipper(shape: shape),
      child: widget.child,
    ) : null;

    final box = DecoratedBox(
      decoration: ShapeDecoration(
        shape: shape,
        shadows: widget.shadow != null? [widget.shadow!] : null,
      ),
      child: child,
    );

    return widget.padding != EdgeInsets.zero?
      Padding(padding: widget.padding, child: box) : box;
  }

  @override
  void forEachTween(TweenVisitor<dynamic> visitor) {
    _color = visitor(_color, widget.color, (dynamic v) => ColorTween(begin: v)) as ColorTween?;
    _value = visitor(_value, widget.value, (dynamic v) => Tween<double>(begin: v)) as Tween<double>?;
  }
}

你可以用以下方法测试它:

class Thermo2Test extends StatefulWidget {
  @override
  State<Thermo2Test> createState() => _Thermo2TestState();
}

class _Thermo2TestState extends State<Thermo2Test> {
  int i = 0;
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.black12,
      child: Column(
        children: [
          Expanded(
            child: Stack(
              children: [
                Positioned.fromRect(
                  rect: const Rect.fromLTWH(25, 50, 125, 400),
                  child: Card(
                    elevation: 8,
                    child: Column(
                      children: [
                        Expanded(
                          child: Thermo2(
                            duration: const Duration(milliseconds: 1000),
                            color: i.isOdd? Colors.red : Colors.green,
                            value: i.isOdd? 0.9 : 0.4,
                            side: const BorderSide(color: Colors.white, width: 6),
                            padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 8),
                            shadow: const BoxShadow(blurRadius: 6, offset: Offset(2, 2), color: Colors.black87),
                            curve: Curves.easeOutBack,
                            child: Material(
                              type: MaterialType.transparency,
                              child: InkWell(
                                highlightColor: Colors.transparent,
                                splashColor: (i.isOdd? Colors.red : Colors.green).shade900,
                                onTap: () => setState(() => i++),
                              ),
                            ),
                          ),
                        ),
                        const Padding(
                          padding: EdgeInsets.all(8.0),
                          child: Text('click the bulb above'),
                        ),
                      ],
                    ),
                  ),
                ),
                Positioned.fromRect(
                  rect: const Rect.fromLTWH(175, 50, 75, 150),
                  child: Transform.rotate(
                    angle: 0.1,
                    child: Card(
                      elevation: 6,
                      child: Thermo2(
                        duration: const Duration(milliseconds: 1250),
                        color: i.isOdd? Colors.green.shade400 : Colors.orange,
                        value: i.isOdd? 0.2 : 0.7,
                        side: BorderSide(color: Colors.black.withOpacity(0.75), width: 5),
                        curve: Curves.elasticOut,
                      ),
                    ),
                  ),
                ),
                Positioned.fromRect(
                  rect: const Rect.fromLTWH(175, 225, 60, 200),
                  child: Card(
                    elevation: 6,
                    child: Thermo2(
                      duration: const Duration(milliseconds: 750),
                      color: i.isOdd? Colors.orange : Colors.deepPurple,
                      value: i.isOdd? 0.3 : 1.0,
                      side: const BorderSide(color: Colors.white, width: 3),
                      shadow: const BoxShadow(spreadRadius: 2, color: Colors.black26),
                      curve: Curves.bounceOut,
                    ),
                  ),
                ),
              ],
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(32.0),
            child: ElevatedButton(
              onPressed: () {
                setState(() => i++);
              },
              child: const Text('click me'),
            ),
          ),
        ],
      ),
    );
  }
}

结果是:

相关问题