import 'package:flutter/material.dart'; import 'package:widgets/my_load_image.dart'; class RatingWidget extends StatefulWidget { /// 星星数量 final int count; /// 最大评分值 final double maxRating; /// 当前选中的评分值 final double value; /// 没一颗星的大小 final double size; /// 每个星之间的间距 final double padding; /// 空星的图片资源 final String nomalImage; /// 满星的图片资源 final String selectImage; /// 是否可以操作选择 final bool selectAble; /// 选中评分的回调 final ValueChanged onRatingUpdate; /// 是否只显示整数的分数 final bool integerOnly; RatingWidget({ required this.nomalImage, required this.selectImage, required this.onRatingUpdate, this.count = 5, this.value = 5.0, this.maxRating = 5.0, this.size = 20, this.padding = 0, this.selectAble = false, this.integerOnly = false, }); @override _RatingWidgetState createState() => _RatingWidgetState(); } class _RatingWidgetState extends State { late double value; @override Widget build(BuildContext context) { return Listener( child: buildRowRating(), onPointerDown: (PointerDownEvent event) { double x = event.localPosition.dx; if (x < 0) x = 0; pointValue(x); }, onPointerMove: (PointerMoveEvent event) { double x = event.localPosition.dx; if (x < 0) x = 0; pointValue(x); }, onPointerUp: (_) {}, behavior: HitTestBehavior.deferToChild, ); } pointValue(double dx) { if (!widget.selectAble) { return; } if (dx >= widget.size * widget.count + widget.padding * (widget.count - 1)) { value = widget.maxRating; } else { for (double i = 1; i < widget.count + 1; i++) { if (dx > widget.size * i + widget.padding * (i - 1) && dx < widget.size * i + widget.padding * i) { value = i * (widget.maxRating / widget.count); break; } else if (dx > widget.size * (i - 1) + widget.padding * (i - 1) && dx < widget.size * i + widget.padding * i) { value = (dx - widget.padding * (i - 1)) / (widget.size * widget.count) * widget.maxRating; break; } } } if (widget.integerOnly) { value = value.ceilToDouble(); // 直接向上取整 } setState(() { widget.onRatingUpdate(value.toStringAsFixed(1)); }); } int fullStars() { if (value != null) { return (value / (widget.maxRating / widget.count)).ceil(); // 使用向上取整 } return 0; } double star() { if (widget.integerOnly) { return 0; } if (value != null) { if (widget.count / fullStars() == widget.maxRating / value) { return 0; } return (value % (widget.maxRating / widget.count)) / (widget.maxRating / widget.count); } return 0; } List buildRow() { int full = fullStars(); List children = []; for (int i = 0; i < full; i++) { children.add(MyAssetImage( widget.selectImage, width: widget.size, height: widget.size, )); if (i < widget.count - 1) { children.add( SizedBox( width: widget.padding, ), ); } } if (full < widget.count) { children.add(ClipRect( clipper: SMClipper(rating: star() * widget.size), child: MyAssetImage( widget.selectImage, width: widget.size, height: widget.size, ), )); } return children; } List buildNomalRow() { List children = []; for (int i = 0; i < widget.count; i++) { children.add(MyAssetImage( widget.nomalImage, width: widget.size, height: widget.size, )); if (i < widget.count - 1) { children.add(SizedBox( width: widget.padding, )); } } return children; } Widget buildRowRating() { return Container( child: Stack( children: [ Row( children: buildNomalRow(), ), Row( children: buildRow(), ) ], ), ); } @override void initState() { super.initState(); value = widget.value; } } class SMClipper extends CustomClipper { final double rating; SMClipper({required this.rating}); @override Rect getClip(Size size) { return Rect.fromLTRB(0.0, 0.0, rating, size.height); } @override bool shouldReclip(SMClipper oldClipper) { return rating != oldClipper.rating; } }