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

相关文章
|
2月前
|
Android开发
Flutter控件的显示与隐藏
Flutter控件的显示与隐藏
108 3
|
21天前
|
前端开发 搜索推荐
Flutter中自定义气泡框效果的实现
Flutter中自定义气泡框效果的实现
27 3
|
2月前
|
前端开发
Flutter快速实现自定义折线图,支持数据改变过渡动画
Flutter快速实现自定义折线图,支持数据改变过渡动画
43 4
Flutter快速实现自定义折线图,支持数据改变过渡动画
|
2月前
|
开发者 监控 开发工具
如何将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应用。
40 0
|
2月前
|
开发者 容器 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应用提供全面支持,助力开发者拓展技术视野与实践机会。
13 0
|
2月前
|
开发框架 API 开发者
Flutter表单控件深度解析:从基本构建到高级自定义,全方位打造既美观又实用的移动端数据输入体验,让应用交互更上一层楼
【8月更文挑战第31天】在构建美观且功能强大的移动应用时,表单是不可或缺的部分。Flutter 作为热门的跨平台开发框架,提供了丰富的表单控件和 API,使开发者能轻松创建高质量表单。本文通过问题解答形式,深入解读 Flutter 表单控件,并通过具体示例代码展示如何构建优秀的移动应用表单。涵盖创建基本表单、处理表单提交、自定义控件样式、焦点管理和异步验证等内容,适合各水平开发者学习和参考。
28 0
|
3月前
flutter 导航组件 AppBar (含顶部选项卡TabBar,抽屉菜单 drawer ,自定义导航图标)
flutter 导航组件 AppBar (含顶部选项卡TabBar,抽屉菜单 drawer ,自定义导航图标)
37 1
|
3月前
|
Dart Android开发
Flutter-自定义短信验证码
Flutter-自定义短信验证码
31 1
|
3月前
Flutter-自定义画板
Flutter-自定义画板
26 0
Flutter-自定义画板
|
3月前
|
移动开发 UED 容器
Flutter-自定义可展开文本控件
Flutter-自定义可展开文本控件
69 0
下一篇
无影云桌面