效果圖
序言
在移動(dòng)應(yīng)用開(kāi)發(fā)中牢裳,顯示數(shù)據(jù)的方式多種多樣逢防,直觀的圖形展示常常能帶給用戶(hù)更好的體驗(yàn)。本文將介紹如何使用Flutter創(chuàng)建一個(gè)自定義三角形緯度評(píng)分控件蒲讯,該控件可以通過(guò)動(dòng)畫(huà)展示評(píng)分的變化忘朝,讓?xiě)?yīng)用界面更加生動(dòng)。
實(shí)現(xiàn)思路及步驟
思路
- 定義控件屬性:首先需要定義控件的基本屬性判帮,如寬度局嘁、高度溉箕、最大評(píng)分以及每個(gè)頂點(diǎn)的評(píng)分值。
-
實(shí)現(xiàn)動(dòng)畫(huà)效果:使用
AnimationController
和CurvedAnimation
來(lái)控制評(píng)分動(dòng)畫(huà)悦昵,使每個(gè)頂點(diǎn)的評(píng)分從0逐漸增加到對(duì)應(yīng)的評(píng)分值肴茄。 -
自定義繪制:使用
CustomPainter
繪制三角形和評(píng)分三角形,并在頂點(diǎn)處繪制空心圓點(diǎn)但指。
步驟
- 創(chuàng)建一個(gè)
TriangleRatingAnimView
小部件寡痰。 - 定義動(dòng)畫(huà)控制器和動(dòng)畫(huà)曲線。
- 在
CustomPainter
中繪制三角形及評(píng)分三角形棋凳。 - 使用
AnimatedBuilder
實(shí)現(xiàn)動(dòng)畫(huà)效果拦坠。
4. 代碼實(shí)現(xiàn)
以下是完整的代碼實(shí)現(xiàn):
import 'package:flutter/material.dart';
/// 三角形等級(jí)評(píng)分的控件
class TriangleRatingAnimView extends StatefulWidget {
final double width; // 控件寬度
final double height; // 控件高度
final int maxRating; // 最大評(píng)分
final int upRating; // 上頂點(diǎn)評(píng)分
final int leftRating; // 左頂點(diǎn)評(píng)分
final int rightRating; // 右頂點(diǎn)評(píng)分
final Color strokeColor; // 三角形邊框顏色
final double strokeWidth; // 三角形邊框?qū)挾? final Color ratingStrokeColor; // 評(píng)分三角形邊框顏色
final double ratingStrokeWidth; // 評(píng)分三角形邊框?qū)挾?
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;
// 計(jì)算三角形頂點(diǎn)坐標(biāo)
final p1 = Offset(size.width / 2, 0); // 頂部頂點(diǎn)
final p2 = Offset(0, size.height); // 左下頂點(diǎn)
final p3 = Offset(size.width, size.height); // 右下頂點(diǎn)
// 繪制外部三角形
final path = Path()
..moveTo(p1.dx, p1.dy)
..lineTo(p2.dx, p2.dy)
..lineTo(p3.dx, p3.dy)
..close();
canvas.drawPath(path, paint);
// 計(jì)算重心
final centroid = Offset(
(p1.dx + p2.dx + p3.dx) / 3,
(p1.dy + p2.dy + p3.dy) / 3,
);
// 繪制頂點(diǎn)到重心的連線
canvas.drawLine(p1, centroid, paint);
canvas.drawLine(p2, centroid, paint);
canvas.drawLine(p3, centroid, paint);
// 根據(jù)評(píng)分計(jì)算動(dòng)態(tài)頂點(diǎn)
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),
);
// 繪制內(nèi)部動(dòng)態(tài)三角形
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);
// 繪制動(dòng)態(tài)點(diǎn)上的空心圓
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: "三角形評(píng)分控件",
onBack: () {
Navigator.pop(context);
},
),
body: Container(
alignment: Alignment.center,
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"時(shí)間管理",
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(
"質(zhì)量保證",
style: TextStyle(
fontSize: 12.sp,
),
),
SizedBox(width: 10.w),
],
),
SizedBox(height: 50.w),
ElevatedButton(
onPressed: () {
updateRatingData();
},
child: const Text("更改數(shù)據(jù)"),
)
],
),
));
}
/// 更新星數(shù)指標(biāo)數(shù)據(jù)
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(() {});
}
}
通過(guò)以上步驟和代碼,我們可以創(chuàng)建一個(gè)帶動(dòng)畫(huà)效果的三角形緯度評(píng)分控件剩岳,使評(píng)分展示更加生動(dòng)和直觀贞滨。