效果图
序言
在移动应用开发中,显示数据的方式多种多样,直观的图形展示常常能带给用户更好的体验。本文将介绍如何使用Flutter创建一个自定义三角形纬度评分控件,该控件可以通过动画展示评分的变化,让应用界面更加生动。
实现思路及步骤
思路
- 定义控件属性:首先需要定义控件的基本属性,如宽度、高度、最大评分以及每个顶点的评分值。
- 实现动画效果:使用
AnimationController
和CurvedAnimation
来控制评分动画,使每个顶点的评分从0逐渐增加到对应的评分值。 - 自定义绘制:使用
CustomPainter
绘制三角形和评分三角形,并在顶点处绘制空心圆点。
步骤
- 创建一个
TriangleRatingAnimView
小部件。 - 定义动画控制器和动画曲线。
- 在
CustomPainter
中绘制三角形及评分三角形。 - 使用
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