-
Notifications
You must be signed in to change notification settings - Fork 79
Initial Implementation for Rating Widget and Change EmptyPage to InitialPage on Samples #503
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,183 @@ | ||||||
import 'package:flutter/foundation.dart'; | ||||||
import 'package:flutter/services.dart'; | ||||||
import 'package:flutter/widgets.dart'; | ||||||
import 'package:forui/forui.dart'; | ||||||
import 'package:meta/meta.dart'; | ||||||
|
||||||
part 'rating.style.dart'; | ||||||
part 'rating_content.dart'; | ||||||
|
||||||
/// A customizable rating widget that allows users to select a rating by tapping on icons. | ||||||
/// | ||||||
/// See: | ||||||
/// * https://forui.dev/docs/rating for working examples. | ||||||
/// * [FRatingStyle] for customizing a rating's appearance. | ||||||
class FRating extends StatefulWidget { | ||||||
/// The current rating value. | ||||||
final double value; | ||||||
|
||||||
/// The maximum rating value. | ||||||
final int count; | ||||||
|
||||||
/// Called when the rating value changes. | ||||||
final ValueChanged<double>? onStateChanged; | ||||||
|
||||||
/// Whether this rating widget is enabled. Defaults to true. | ||||||
final bool enabled; | ||||||
|
||||||
/// The style of the rating widget. | ||||||
final FRatingStyle? style; | ||||||
|
||||||
/// The icon to display for a filled (rated) state. | ||||||
final Widget filledIcon; | ||||||
|
||||||
/// The icon to display for an empty (unrated) state. | ||||||
final Widget emptyIcon; | ||||||
|
||||||
/// The icon to display for a half-filled state (when allowHalfRating is true). | ||||||
final Widget? halfFilledIcon; | ||||||
|
||||||
/// The semantic label for accessibility. | ||||||
final String? semanticsLabel; | ||||||
|
||||||
/// {@macro forui.foundation.doc_templates.autofocus} | ||||||
final bool autofocus; | ||||||
|
||||||
/// {@macro forui.foundation.doc_templates.focusNode} | ||||||
final FocusNode? focusNode; | ||||||
|
||||||
/// {@macro forui.foundation.doc_templates.onFocusChange} | ||||||
final ValueChanged<bool>? onFocusChange; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should probably add a |
||||||
|
||||||
/// The spacing between rating icons. | ||||||
final double spacing; | ||||||
Comment on lines
+52
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be in the style. |
||||||
|
||||||
/// Creates a [FRating] widget. | ||||||
const FRating({ | ||||||
super.key, | ||||||
this.value = 0.0, | ||||||
this.count = 5, | ||||||
this.onStateChanged, | ||||||
this.enabled = true, | ||||||
this.style, | ||||||
this.filledIcon = const Icon(FIcons.star, color: Color(0xFFFFD700)), // Gold | ||||||
this.emptyIcon = const Icon(FIcons.starOff, color: Color(0xFFBDBDBD)), // Gray | ||||||
Comment on lines
+63
to
+64
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should move the color to inside the style. Generally for icons, we wrap them in a |
||||||
this.halfFilledIcon, | ||||||
this.semanticsLabel, | ||||||
this.autofocus = false, | ||||||
this.focusNode, | ||||||
this.onFocusChange, | ||||||
this.spacing = 4.0, | ||||||
}); | ||||||
|
||||||
@override | ||||||
State<FRating> createState() => _FRatingState(); | ||||||
|
||||||
@override | ||||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) { | ||||||
super.debugFillProperties(properties); | ||||||
properties | ||||||
..add(DoubleProperty('value', value)) | ||||||
..add(IntProperty('count', count)) | ||||||
..add(FlagProperty('enabled', value: enabled, ifTrue: 'enabled')) | ||||||
..add(DiagnosticsProperty('style', style)) | ||||||
..add(ObjectFlagProperty<ValueChanged<double>?>.has('onStateChanged', onStateChanged)) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Generally we don't specify the generic type for diagnostic properties. |
||||||
..add(StringProperty('semanticsLabel', semanticsLabel)) | ||||||
..add(DiagnosticsProperty<bool>('autofocus', autofocus)) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
..add(DiagnosticsProperty<FocusNode?>('focusNode', focusNode)) | ||||||
..add(ObjectFlagProperty<ValueChanged<bool>?>.has('onFocusChange', onFocusChange)) | ||||||
..add(DoubleProperty('spacing', spacing)); | ||||||
} | ||||||
} | ||||||
|
||||||
class _FRatingState extends State<FRating> { | ||||||
double _hoverValue = 0.0; | ||||||
bool _isHovering = false; | ||||||
|
||||||
double _calculateRating(double dx, double totalWidth) { | ||||||
if (totalWidth <= 0) { | ||||||
return 0; | ||||||
} | ||||||
|
||||||
final itemWidth = totalWidth / widget.count; | ||||||
final x = dx.clamp(0.0, totalWidth); | ||||||
final index = x ~/ itemWidth; | ||||||
final localPosition = x - (index * itemWidth); | ||||||
|
||||||
double rating = index + 1.0; | ||||||
if (widget.halfFilledIcon != null && localPosition < itemWidth / 2) { | ||||||
rating -= 0.5; | ||||||
} | ||||||
|
||||||
return rating.clamp(0.0, widget.count.toDouble()); | ||||||
} | ||||||
|
||||||
void _handleHover(PointerHoverEvent event) { | ||||||
final box = context.findRenderObject() as RenderBox?; | ||||||
if (box == null) { | ||||||
return; | ||||||
} | ||||||
|
||||||
final localPosition = box.globalToLocal(event.position); | ||||||
setState(() => _hoverValue = _calculateRating(localPosition.dx, box.size.width)); | ||||||
} | ||||||
|
||||||
void _handleTap(TapUpDetails details) { | ||||||
final box = context.findRenderObject()! as RenderBox; | ||||||
final localPosition = box.globalToLocal(details.globalPosition); | ||||||
final rating = _calculateRating(localPosition.dx, box.size.width); | ||||||
|
||||||
widget.onStateChanged?.call(rating); | ||||||
setState(() => _isHovering = false); | ||||||
} | ||||||
Comment on lines
+97
to
+132
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Generally, I think we should try to avoid using retrieving the renderbox like this. Perhaps, you can use the I think there's something similar for tap gestures too. |
||||||
|
||||||
@override | ||||||
Widget build(BuildContext context) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you can use |
||||||
final theme = context.theme; | ||||||
final style = widget.style ?? const FRatingStyle(); | ||||||
final effectiveEnabled = widget.enabled && widget.onStateChanged != null; | ||||||
final effectiveValue = _isHovering ? _hoverValue : widget.value; | ||||||
|
||||||
return MouseRegion( | ||||||
onEnter: effectiveEnabled ? (_) => setState(() => _isHovering = true) : null, | ||||||
onExit: effectiveEnabled ? (_) => setState(() => _isHovering = false) : null, | ||||||
onHover: effectiveEnabled ? _handleHover : null, | ||||||
child: GestureDetector( | ||||||
onTapUp: effectiveEnabled ? _handleTap : null, | ||||||
child: _RatingContent( | ||||||
value: effectiveValue, | ||||||
count: widget.count, | ||||||
spacing: widget.spacing, | ||||||
filledIcon: widget.filledIcon, | ||||||
emptyIcon: widget.emptyIcon, | ||||||
halfFilledIcon: widget.halfFilledIcon, | ||||||
style: style, | ||||||
theme: theme, | ||||||
semanticsLabel: widget.semanticsLabel, | ||||||
focusNode: widget.focusNode, | ||||||
autofocus: widget.autofocus, | ||||||
onFocusChange: widget.onFocusChange, | ||||||
), | ||||||
), | ||||||
); | ||||||
} | ||||||
} | ||||||
|
||||||
/// Internal widget to display the rating content | ||||||
|
||||||
/// Defines the visual properties for [FRating]. | ||||||
/// | ||||||
/// See also: | ||||||
/// * [FRating], which uses this class for its visual styling. | ||||||
final class FRatingStyle with Diagnosticable, _$FRatingStyleFunctions { | ||||||
/// The color of the rating icons. | ||||||
@override | ||||||
final Color? color; | ||||||
|
||||||
/// The size of the rating icons. | ||||||
@override | ||||||
final double? size; | ||||||
|
||||||
/// Creates a [FRatingStyle]. | ||||||
const FRatingStyle({this.color, this.size}); | ||||||
} | ||||||
Comment on lines
+172
to
+183
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think these properties can be merged into a I think you can reference |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
part of 'rating.dart'; | ||
|
||
class _RatingContent extends StatelessWidget { | ||
final double value; | ||
final int count; | ||
final double spacing; | ||
final Widget filledIcon; | ||
final Widget emptyIcon; | ||
final Widget? halfFilledIcon; | ||
final FRatingStyle style; | ||
final FThemeData theme; | ||
final String? semanticsLabel; | ||
final FocusNode? focusNode; | ||
final bool autofocus; | ||
final ValueChanged<bool>? onFocusChange; | ||
|
||
const _RatingContent({ | ||
required this.value, | ||
required this.count, | ||
required this.spacing, | ||
required this.filledIcon, | ||
required this.emptyIcon, | ||
required this.style, | ||
required this.theme, | ||
required this.autofocus, | ||
this.halfFilledIcon, | ||
this.semanticsLabel, | ||
this.focusNode, | ||
this.onFocusChange, | ||
}); | ||
|
||
@override | ||
Widget build(BuildContext context) => Semantics( | ||
label: semanticsLabel ?? 'Rating: ${value.toStringAsFixed(1)} of $count', | ||
child: Focus( | ||
focusNode: focusNode, | ||
autofocus: autofocus, | ||
onFocusChange: onFocusChange, | ||
child: LayoutBuilder( | ||
builder: | ||
(context, constraints) => | ||
Row(mainAxisSize: MainAxisSize.min, children: List.generate(count, _buildRatingItem)), | ||
), | ||
), | ||
); | ||
|
||
Widget _buildRatingItem(int index) { | ||
final itemValue = index + 1.0; | ||
final Widget icon; | ||
|
||
if (itemValue <= value) { | ||
icon = filledIcon; | ||
} else if (halfFilledIcon != null && (itemValue - 0.5) <= value && value < itemValue) { | ||
icon = halfFilledIcon ?? filledIcon; | ||
} else { | ||
icon = emptyIcon; | ||
} | ||
|
||
return Padding( | ||
padding: EdgeInsets.only(right: index < count - 1 ? spacing : 0), | ||
child: IconTheme( | ||
data: IconThemeData(color: style.color ?? theme.colors.primary, size: style.size ?? 24.0), | ||
child: icon, | ||
), | ||
); | ||
} | ||
|
||
@override | ||
void debugFillProperties(DiagnosticPropertiesBuilder properties) { | ||
super.debugFillProperties(properties); | ||
properties | ||
..add(DoubleProperty('value', value)) | ||
..add(IntProperty('count', count)) | ||
..add(DoubleProperty('spacing', spacing)) | ||
..add(DiagnosticsProperty<FRatingStyle>('style', style)) | ||
..add(DiagnosticsProperty<FThemeData>('theme', theme)) | ||
..add(StringProperty('semanticsLabel', semanticsLabel)) | ||
..add(DiagnosticsProperty<FocusNode?>('focusNode', focusNode)) | ||
..add(DiagnosticsProperty<bool>('autofocus', autofocus)) | ||
..add(ObjectFlagProperty<ValueChanged<bool>?>.has('onFocusChange', onFocusChange)); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
/// {@category Widgets} | ||
/// | ||
/// A line calendar displays dates in a single horizontal, scrollable line. | ||
/// | ||
/// See https://forui.dev/docs/data/line-calendar for working examples. | ||
Comment on lines
+3
to
+5
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. docs need to be updated. |
||
library forui.widgets.rating; | ||
|
||
export '../src/widgets/rating/rating.dart'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be
onChange
,onStateChange
is meant forWidgetState
changes.