Flutter-自定义三角形评分控件

简介: Flutter-自定义三角形评分控件

效果图

image.png

序言

移动应用开发中,显示数据的方式多种多样,直观的图形展示常常能带给用户更好的体验。本文将介绍如何使用Flutter创建一个自定义三角形纬度评分控件,该控件可以通过动画展示评分的变化,让应用界面更加生动。

实现思路及步骤

思路

  1. 定义控件属性:首先需要定义控件的基本属性,如宽度、高度、最大评分以及每个顶点的评分值。
  2. 实现动画效果:使用AnimationControllerCurvedAnimation来控制评分动画,使每个顶点的评分从0逐渐增加到对应的评分值。
  3. 自定义绘制:使用CustomPainter绘制三角形和评分三角形,并在顶点处绘制空心圆点。

步骤

  1. 创建一个TriangleRatingAnimView小部件。
  2. 定义动画控制器和动画曲线。
  3. CustomPainter中绘制三角形及评分三角形。
  4. 使用AnimatedBuilder实现动画效果。

代码实现

以下是完整的代码实现:

import 'package:flutter/material.dart';

/// 三角形等级评分的控件
/// https://github.com/yixiaolunhui/flutter_xy
class TriangleRatingAnimView extends StatefulWidget {
  final double width; // 控件宽度
  final double height; // 控件高度
  final int maxRating; // 最大评分
  final int upRating; // 上顶点评分
  final int leftRating; // 左顶点评分
  final int rightRating; // 右顶点评分
  final Color strokeColor; // 三角形边框颜色
  final double strokeWidth; // 三角形边框宽度
  final Color ratingStrokeColor; // 评分三角形边框颜色
  final double ratingStrokeWidth; // 评分三角形边框宽度

  const TriangleRatingAnimView({
    Key? key,
    required this.width,
    required this.height,
    this.maxRating = 5,
    this.upRating = 0,
    this.leftRating = 0,
    this.rightRating = 0,
    this.strokeColor = Colors.grey,
    this.strokeWidth = 1,
    this.ratingStrokeColor = Colors.red,
    this.ratingStrokeWidth = 2,
  }) : super(key: key);

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

class TriangleRatingAnimViewState extends State<TriangleRatingAnimView>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
    );
    _animation = CurvedAnimation(
      parent: _controller,
      curve: Curves.easeInOut,
    );
    _startAnimations();
  }

  @override
  void didUpdateWidget(TriangleRatingAnimView oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.upRating != widget.upRating ||
        oldWidget.leftRating != widget.leftRating ||
        oldWidget.rightRating != widget.rightRating) {
      _startAnimations();
    }
  }

  void _startAnimations() {
    _controller.reset();
    _controller.forward();
  }

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

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animation,
      builder: (context, child) {
        return CustomPaint(
          size: Size(widget.width, widget.height),
          painter: TrianglePainter(
            upRating: (widget.upRating * _animation.value).toInt(),
            rightRating: (widget.rightRating * _animation.value).toInt(),
            leftRating: (widget.leftRating * _animation.value).toInt(),
            strokeWidth: widget.strokeWidth,
            ratingStrokeWidth: widget.ratingStrokeWidth,
            strokeColor: widget.strokeColor,
            ratingStrokeColor: widget.ratingStrokeColor,
            maxRating: widget.maxRating,
          ),
        );
      },
    );
  }
}

class TrianglePainter extends CustomPainter {
  final int maxRating;
  final int upRating;
  final int leftRating;
  final int rightRating;
  final Color strokeColor;
  final double strokeWidth;
  final Color ratingStrokeColor;
  final double ratingStrokeWidth;

  TrianglePainter({
    required this.maxRating,
    required this.upRating,
    required this.leftRating,
    required this.rightRating,
    required this.strokeWidth,
    required this.ratingStrokeWidth,
    required this.strokeColor,
    required this.ratingStrokeColor,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = strokeColor
      ..style = PaintingStyle.stroke
      ..strokeWidth = strokeWidth;

    final outerPaint = Paint()
      ..color = ratingStrokeColor
      ..style = PaintingStyle.stroke
      ..strokeWidth = ratingStrokeWidth;

    final fillPaint = Paint()
      ..color = ratingStrokeColor.withOpacity(0.3)
      ..style = PaintingStyle.fill;

    final circlePaint = Paint()
      ..color = ratingStrokeColor
      ..style = PaintingStyle.stroke
      ..strokeWidth = 2.0;

    final circleFillPaint = Paint()
      ..color = Colors.white
      ..style = PaintingStyle.fill;

    // 计算三角形顶点坐标
    final p1 = Offset(size.width / 2, 0); // 顶部顶点
    final p2 = Offset(0, size.height); // 左下顶点
    final p3 = Offset(size.width, size.height); // 右下顶点

    // 绘制外部三角形
    final path = Path()
      ..moveTo(p1.dx, p1.dy)
      ..lineTo(p2.dx, p2.dy)
      ..lineTo(p3.dx, p3.dy)
      ..close();
    canvas.drawPath(path, paint);

    // 计算重心
    final centroid = Offset(
      (p1.dx + p2.dx + p3.dx) / 3,
      (p1.dy + p2.dy + p3.dy) / 3,
    );

    // 绘制顶点到重心的连线
    canvas.drawLine(p1, centroid, paint);
    canvas.drawLine(p2, centroid, paint);
    canvas.drawLine(p3, centroid, paint);

    // 根据评分计算动态顶点
    final dynamicP1 = Offset(
      centroid.dx + (p1.dx - centroid.dx) * (upRating / maxRating),
      centroid.dy + (p1.dy - centroid.dy) * (upRating / maxRating),
    );
    final dynamicP2 = Offset(
      centroid.dx + (p2.dx - centroid.dx) * (leftRating / maxRating),
      centroid.dy + (p2.dy - centroid.dy) * (leftRating / maxRating),
    );
    final dynamicP3 = Offset(
      centroid.dx + (p3.dx - centroid.dx) * (rightRating / maxRating),
      centroid.dy + (p3.dy - centroid.dy) * (rightRating / maxRating),
    );

    // 绘制内部动态三角形
    final ratingPath = Path()
      ..moveTo(dynamicP1.dx, dynamicP1.dy)
      ..lineTo(dynamicP2.dx, dynamicP2.dy)
      ..lineTo(dynamicP3.dx, dynamicP3.dy)
      ..close();
    canvas.drawPath(ratingPath, outerPaint);
    canvas.drawPath(ratingPath, fillPaint);

    // 绘制动态点上的空心圆
    const circleRadius = 5.0;
    canvas.drawCircle(dynamicP1, circleRadius, circlePaint);
    canvas.drawCircle(dynamicP1, circleRadius - 1.5, circleFillPaint); // 填充白色
    canvas.drawCircle(dynamicP2, circleRadius, circlePaint);
    canvas.drawCircle(dynamicP2, circleRadius - 1.5, circleFillPaint); // 填充白色
    canvas.drawCircle(dynamicP3, circleRadius, circlePaint);
    canvas.drawCircle(dynamicP3, circleRadius - 1.5, circleFillPaint); // 填充白色
  }

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

使用

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_xy/xydemo/rating/rating_anim_widget.dart';

import '../../widgets/xy_app_bar.dart';

class RatingPage extends StatefulWidget {
  const RatingPage({super.key});

  @override
  State<RatingPage> createState() => _RatingPageState();
}

class _RatingPageState extends State<RatingPage> {
  var upRating = 2;
  var leftRating = 3;
  var rightRating = 5;
  var maxRating = 5;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        backgroundColor: Colors.white,
        appBar: XYAppBar(
          title: "三角形评分控件",
          onBack: () {
            Navigator.pop(context);
          },
        ),
        body: Container(
          alignment: Alignment.center,
          child: Column(
            mainAxisSize: MainAxisSize.max,
            crossAxisAlignment: CrossAxisAlignment.center,
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text(
                "时间管理",
                style: TextStyle(
                  fontSize: 12.sp,
                ),
              ),
              SizedBox(height: 5.w),
              TriangleRatingAnimView(
                height: 200.w,
                width: 280.w,
                upRating: upRating,
                leftRating: leftRating,
                rightRating: rightRating,
                maxRating: maxRating,
                strokeWidth: 1.5.w,
                ratingStrokeWidth: 3.w,
              ),
              SizedBox(height: 5.w),
              Row(
                children: [
                  SizedBox(width: 10.w),
                  Text(
                    "成本控制",
                    style: TextStyle(
                      fontSize: 12.sp,
                    ),
                  ),
                  const Expanded(child: SizedBox.shrink()),
                  Text(
                    "质量保证",
                    style: TextStyle(
                      fontSize: 12.sp,
                    ),
                  ),
                  SizedBox(width: 10.w),
                ],
              ),
              SizedBox(height: 50.w),
              ElevatedButton(
                onPressed: () {
                  updateRatingData();
                },
                child: const Text("更改数据"),
              )
            ],
          ),
        ));
  }

  /// 更新星数指标数据
  void updateRatingData() {
    final random = Random();
    maxRating = 5 + random.nextInt(6);
    upRating = 1 + random.nextInt(maxRating);
    leftRating = 1 + random.nextInt(maxRating);
    rightRating = 1 + random.nextInt(maxRating);
    setState(() {});
  }
}

通过以上步骤和代码,我们可以创建一个带动画效果的三角形纬度评分控件,使评分展示更加生动和直观。

详情可见:github.com/yixiaolunhui/flutter_xy

相关文章
|
3月前
|
Android开发
Flutter控件的显示与隐藏
Flutter控件的显示与隐藏
151 3
|
15天前
|
UED 开发者 容器
Flutter&鸿蒙next 的 Sliver 实现自定义滚动效果
Flutter 提供了强大的滚动组件,如 ListView 和 GridView,但当需要更复杂的滚动效果时,Sliver 组件是一个强大的工具。本文介绍了如何使用 Sliver 实现自定义滚动效果,包括 SliverAppBar、SliverList 等常用组件的使用方法,以及通过 CustomScrollView 组合多个 Sliver 组件实现复杂布局的示例。通过具体代码示例,展示了如何实现带有可伸缩 AppBar 和可滚动列表的页面。
79 1
|
17天前
Flutter 自定义组件继承与调用的高级使用方式
本文深入探讨了 Flutter 中自定义组件的高级使用方式,包括创建基本自定义组件、继承现有组件、使用 Mixins 和组合模式等。通过这些方法,您可以构建灵活、可重用且易于维护的 UI 组件,从而提升开发效率和代码质量。
114 1
|
17天前
|
前端开发 开发者
深入探索 Flutter 鸿蒙版的画笔使用与高级自定义动画
本文深入探讨了 Flutter 中的绘图功能,重点介绍了 CustomPainter 和 Canvas 的使用方法。通过示例代码,详细讲解了如何绘制自定义图形、设置 Paint 对象的属性以及实现高级自定义动画。内容涵盖基本绘图、动画基础、渐变动画和路径动画,帮助读者掌握 Flutter 绘图与动画的核心技巧。
65 1
|
17天前
|
Dart UED 开发者
Flutter&鸿蒙next中的按钮封装:自定义样式与交互
在Flutter应用开发中,按钮是用户界面的重要组成部分。Flutter提供了多种内置按钮组件,但有时这些样式无法满足特定设计需求。因此,封装一个自定义按钮组件变得尤为重要。自定义按钮组件可以确保应用中所有按钮的一致性、可维护性和可扩展性,同时提供更高的灵活性,支持自定义颜色、形状和点击事件。本文介绍了如何创建一个名为CustomButton的自定义按钮组件,并详细说明了其样式、形状、颜色和点击事件的处理方法。
68 1
|
17天前
|
Dart 搜索推荐 API
Flutter & 鸿蒙next版本:自定义对话框与表单验证的动态反馈与错误处理
在现代移动应用开发中,用户体验至关重要。本文探讨了如何在 Flutter 与鸿蒙操作系统(HarmonyOS)中创建自定义对话框,并结合表单验证实现动态反馈与错误处理,提升用户体验。通过自定义对话框和表单验证,开发者可以提供更加丰富和友好的交互体验,同时利用鸿蒙next版本拓展应用的受众范围。
68 1
|
2月前
|
前端开发 搜索推荐
Flutter中自定义气泡框效果的实现
Flutter中自定义气泡框效果的实现
84 3
|
3月前
|
前端开发
Flutter快速实现自定义折线图,支持数据改变过渡动画
Flutter快速实现自定义折线图,支持数据改变过渡动画
93 4
Flutter快速实现自定义折线图,支持数据改变过渡动画
|
3月前
|
开发者 监控 开发工具
如何将JSF应用送上云端?揭秘在Google Cloud Platform上部署JSF应用的神秘步骤
【8月更文挑战第31天】本文详细介绍如何在Google Cloud Platform (GCP) 上部署JavaServer Faces (JSF) 应用。首先,确保已准备好JSF应用并通过Maven构建WAR包。接着,使用Google Cloud SDK登录并配置GCP环境。然后,创建`app.yaml`文件以配置Google App Engine,并使用`gcloud app deploy`命令完成部署。最后,通过`gcloud app browse`访问应用,并利用GCP的监控和日志服务进行管理和故障排查。整个过程简单高效,帮助开发者轻松部署和管理JSF应用。
60 0
|
3月前
|
开发者 容器 Java
Azure云之旅:JSF应用的神秘部署指南,揭开云原生的新篇章!
【8月更文挑战第31天】本文探讨了如何在Azure上部署JavaServer Faces (JSF) 应用,充分发挥其界面构建能力和云平台优势,实现高效安全的Web应用。Azure提供的多种服务如App Service、Kubernetes Service (AKS) 和DevOps简化了部署流程,并支持应用全生命周期管理。文章详细介绍了使用Azure Spring Cloud和App Service部署JSF应用的具体步骤,帮助开发者更好地利用Azure的强大功能。无论是在微服务架构下还是传统环境中,Azure都能为JSF应用提供全面支持,助力开发者拓展技术视野与实践机会。
18 0