You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter中如何通过Custom Paint或Custom Clipper实现适配多设备的指定UI?

实现适配多设备的自定义仪表盘UI组件(基于CustomPaint)

要实现图中的环形仪表盘组件,使用CustomPaint是最佳方案,同时通过相对尺寸计算和LayoutBuilder保证在不同设备上的适配性。下面是完整的实现步骤和代码示例:

核心思路

  • 利用CustomPaint的CustomPainter自定义绘制环形背景、刻度线和指针
  • 通过LayoutBuilder获取父组件的可用空间,基于空间尺寸的比例计算所有绘制元素的大小,避免固定像素导致的适配问题
  • 使用角度计算定位刻度和指针的位置,确保视觉一致性

完整代码实现

import 'package:flutter/material.dart';

class CustomDashboard extends StatelessWidget {
  final double value; // 0-1之间的数值,对应指针的位置
  final double size; // 可选:指定组件大小,不指定则自适应父容器

  const CustomDashboard({
    super.key,
    required this.value,
    this.size = double.infinity,
  });

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        // 取父容器可用宽高中的较小值,保证组件是正方形
        final double actualSize = size == double.infinity
            ? constraints.biggest.shortestSide
            : size;
        return SizedBox(
          width: actualSize,
          height: actualSize,
          child: CustomPaint(
            painter: _DashboardPainter(
              value: value,
              radius: actualSize / 2,
            ),
            child: Center(
              // 可选:添加中心数值显示
              child: Text(
                '${(value * 100).toInt()}%',
                style: const TextStyle(
                  fontSize: 24,
                  fontWeight: FontWeight.bold,
                ),
              ),
            ),
          ),
        );
      },
    );
  }
}

class _DashboardPainter extends CustomPainter {
  final double value;
  final double radius;

  _DashboardPainter({
    required this.value,
    required this.radius,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final center = Offset(size.width / 2, size.height / 2);
    final outerRadius = radius * 0.9; // 外圈环形的半径
    final innerRadius = radius * 0.7; // 刻度线内侧的半径

    // 1. 绘制环形背景
    final backgroundPaint = Paint()
      ..color = Colors.grey.shade200
      ..style = PaintingStyle.stroke
      ..strokeWidth = radius * 0.15;
    canvas.drawCircle(center, outerRadius - backgroundPaint.strokeWidth / 2, backgroundPaint);

    // 2. 绘制刻度线
    const int totalTicks = 20; // 总刻度数
    const int longTickInterval = 5; // 每5个刻度一个长刻度
    final tickLengthLong = radius * 0.12;
    final tickLengthShort = radius * 0.06;
    final tickPaint = Paint()
      ..color = Colors.black
      ..style = PaintingStyle.stroke
      ..strokeWidth = radius * 0.015;

    for (int i = 0; i < totalTicks; i++) {
      final angle = (i / totalTicks) * 2 * 3.14159 - 3.14159 / 2; // 从顶部开始计算角度
      final isLongTick = (i % longTickInterval) == 0;
      final tickLength = isLongTick ? tickLengthLong : tickLengthShort;

      final startX = center.dx + innerRadius * cos(angle);
      final startY = center.dy + innerRadius * sin(angle);
      final endX = center.dx + (innerRadius + tickLength) * cos(angle);
      final endY = center.dy + (innerRadius + tickLength) * sin(angle);

      canvas.drawLine(Offset(startX, startY), Offset(endX, endY), tickPaint);
    }

    // 3. 绘制指针
    final pointerAngle = value * 2 * 3.14159 - 3.14159 / 2; // 0对应顶部,1对应底部
    final pointerPaint = Paint()
      ..color = Colors.red
      ..style = PaintingStyle.stroke
      ..strokeWidth = radius * 0.02
      ..strokeCap = StrokeCap.round;

    final pointerEndX = center.dx + innerRadius * cos(pointerAngle);
    final pointerEndY = center.dy + innerRadius * sin(pointerAngle);
    canvas.drawLine(center, Offset(pointerEndX, pointerEndY), pointerPaint);

    // 4. 绘制中心圆点
    final centerDotPaint = Paint()..color = Colors.red;
    canvas.drawCircle(center, radius * 0.03, centerDotPaint);
  }

  @override
  bool shouldRepaint(covariant _DashboardPainter oldDelegate) {
    return oldDelegate.value != value || oldDelegate.radius != radius;
  }
}

// 使用示例
void main() {
  runApp(const MaterialApp(
    home: Scaffold(
      body: Center(
        child: CustomDashboard(value: 0.6),
      ),
    ),
  ));
}

适配关键点说明

  • 相对尺寸计算:所有绘制元素的大小(如半径、刻度长度、线宽)都基于组件的实际半径比例计算,而非固定像素,确保在不同尺寸的设备上视觉比例一致
  • LayoutBuilder的使用:通过LayoutBuilder获取父容器的约束,自动调整组件大小,避免超出父容器范围
  • 角度计算:刻度和指针的位置通过角度计算,保证环形布局的一致性,不受尺寸变化影响

自定义扩展建议

  • 通过参数自定义环形颜色、刻度颜色、指针颜色等样式
  • 添加动画效果,让指针平滑过渡到目标值
  • 支持不同的刻度数量和间隔设置

内容的提问来源于stack exchange,提问作者Sinnoor C

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 19:12:34