Skip to content

Commit

Permalink
fix: Adjust Slider animation (Fixes #1006)
Browse files Browse the repository at this point in the history
  • Loading branch information
bdlukaa committed Jan 13, 2024
1 parent 57ce1f4 commit 9d22136
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 18 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## [next]

* **MINOR BREAKING** Renamed `NavigationPaneThemeData.standard` to `NavigationPaneThemeData.fromResources`, and removed the `backgroundColor` and `inactiveColor` properties ([#1008](https://github.com/bdlukaa/fluent_ui/issues/1008))
* fix: Adjust `Slider` animation ([#1006](https://github.com/bdlukaa/fluent_ui/issues/1006))

## 4.8.4

Expand Down
1 change: 1 addition & 0 deletions example/devtools_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
extensions:
91 changes: 73 additions & 18 deletions lib/src/controls/inputs/slider.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:math' as math;
import 'dart:ui';

Expand All @@ -21,7 +22,7 @@ import 'package:flutter/rendering.dart';
/// * [RatingBar], that allows users to view and set ratings
/// * <https://docs.microsoft.com/en-us/windows/apps/design/controls/slider>
class Slider extends StatefulWidget {
/// Creates a Slider
/// Creates a fluent-styled slider.
const Slider({
super.key,
required this.value,
Expand Down Expand Up @@ -174,11 +175,23 @@ class Slider extends StatefulWidget {
/// If null, the slider is continuous.
final int? divisions;

/// The style used in this slider. It's mescled with [FluentThemeData.sliderThemeData]
/// The style used in this slider.
///
/// If provided, it's merged with [FluentThemeData.sliderTheme]. If not,
/// the theme slider theme is used.
///
/// See also:
///
/// * [SliderTheme] and [SliderThemeData], which define the style for a
/// slider.
final SliderThemeData? style;

/// A label to show above the slider, or at the left
/// of the slider if [vertical] is `true` when the slider is active.
/// A label to show close to the slider.
///
/// It is displayed above the slider if [vertical] is false, and at the left
/// of the slider if [vertical] is true.
///
/// It is only shown if the slider is active.
final String? label;

/// {@macro flutter.widgets.Focus.focusNode}
Expand All @@ -187,11 +200,10 @@ class Slider extends StatefulWidget {
/// {@macro flutter.widgets.Focus.autofocus}
final bool autofocus;

/// Whether the slider is vertical or not
/// Whether the slider is vertical or not.
///
/// Use a vertical slider if the slider represents a
/// real-world value that is normally shown vertically
/// (such as temperature).
/// Use a vertical slider if the slider represents a real-world value that is
/// normally shown vertically (such as temperature).
final bool vertical;

/// {@macro fluent_ui.controls.inputs.HoverButton.mouseCursor}
Expand All @@ -215,11 +227,15 @@ class Slider extends StatefulWidget {
}
}

class _SliderState extends m.State<Slider> {
bool _showFocusHighlight = false;
class _SliderState extends State<Slider> {
final materialSliderKey = GlobalKey<State<m.Slider>>();

late FocusNode _focusNode;
bool _sliding = false;
bool _showFocusHighlight = false;

static const showLabelDuration = Duration(milliseconds: 400);
Timer? _overlayTimer;

@override
void initState() {
Expand All @@ -237,13 +253,28 @@ class _SliderState extends m.State<Slider> {
_focusNode.removeListener(_handleFocusChanged);
// Only dispose the focus node manually created
if (widget.focusNode == null) _focusNode.dispose();
_overlayTimer?.cancel();
super.dispose();
}

void _showLabelOverlay() {
final sliderState = materialSliderKey.currentState as dynamic;
sliderState.showValueIndicator();
(sliderState.overlayController as AnimationController).forward();
(sliderState.valueIndicatorController as AnimationController).forward();
}

void _hideLabelOverlay() {
final sliderState = materialSliderKey.currentState as dynamic;
(sliderState.overlayController as AnimationController).reverse();
(sliderState.valueIndicatorController as AnimationController).reverse();
}

@override
Widget build(BuildContext context) {
assert(debugCheckHasFluentTheme(context));
assert(debugCheckHasDirectionality(context));
final theme = FluentTheme.of(context);
final style = SliderTheme.of(context).merge(widget.style);
final direction = Directionality.of(context);

Expand All @@ -255,7 +286,7 @@ class _SliderState extends m.State<Slider> {
builder: (context, states) => m.Material(
type: m.MaterialType.transparency,
child: TweenAnimationBuilder<double>(
duration: FluentTheme.of(context).fastAnimationDuration,
duration: theme.fastAnimationDuration,
tween: Tween<double>(
begin: 1.0,
end: states.isPressing || _sliding
Expand All @@ -276,9 +307,7 @@ class _SliderState extends m.State<Slider> {
pressedElevation: 1.0,
useBall: style.useThumbBall ?? true,
innerFactor: innerFactor,
borderColor: FluentTheme.of(context)
.resources
.controlSolidFillColorDefault,
borderColor: theme.resources.controlSolidFillColorDefault,
enabledThumbRadius: style.thumbRadius?.resolve(states) ?? 10.0,
disabledThumbRadius: style.thumbRadius?.resolve(states),
),
Expand All @@ -299,6 +328,7 @@ class _SliderState extends m.State<Slider> {
child: child!,
),
child: m.Slider(
key: materialSliderKey,
value: widget.value,
max: widget.max,
min: widget.min,
Expand Down Expand Up @@ -329,6 +359,17 @@ class _SliderState extends m.State<Slider> {
child: child,
),
);
child = MouseRegion(
onEnter: (event) {
_overlayTimer = Timer(showLabelDuration, _showLabelOverlay);
},
onExit: (event) {
_overlayTimer?.cancel();
_overlayTimer = null;
_hideLabelOverlay();
},
child: child,
);
if (widget.vertical) {
return RotatedBox(
quarterTurns: direction == TextDirection.ltr ? 3 : 5,
Expand Down Expand Up @@ -478,7 +519,7 @@ class SliderThumbShape extends m.SliderComponentShape {
center - const Offset(0, 6),
center + const Offset(0, 6),
Paint()
..color = color
..color = color.withOpacity(activationAnimation.value)
..style = PaintingStyle.stroke
..strokeJoin = StrokeJoin.round
..strokeCap = StrokeCap.round
Expand Down Expand Up @@ -832,6 +873,10 @@ class _RectangularSliderValueIndicatorPathPainter {
}
assert(!sizeWithOverflow.isEmpty);

final opacity = scale;
// the animation should not scale, only fade
scale = 1.0;

final rectangleWidth = _upperRectangleWidth(
labelPainter,
scale,
Expand All @@ -855,7 +900,8 @@ class _RectangularSliderValueIndicatorPathPainter {
);

final trianglePath = Path()..close();
final fillPaint = Paint()..color = backgroundPaintColor;
final fillPaint = Paint()
..color = backgroundPaintColor.withOpacity(opacity);
final upperRRect = RRect.fromRectAndRadius(
upperRect,
const Radius.circular(_upperRectRadius),
Expand Down Expand Up @@ -886,7 +932,7 @@ class _RectangularSliderValueIndicatorPathPainter {
if (vertical) canvas.rotate((ltr ? 1 : -1) * math.pi / 2);
if (strokePaintColor != null) {
final strokePaint = Paint()
..color = strokePaintColor
..color = strokePaintColor.withOpacity(opacity)
..strokeWidth = 1.0
..style = PaintingStyle.stroke;
canvas.drawPath(trianglePath, strokePaint);
Expand All @@ -901,7 +947,16 @@ class _RectangularSliderValueIndicatorPathPainter {
final halfLabelPainterOffset =
Offset(labelPainter.width / 2, labelPainter.height / 2);
final labelOffset = boxCenter - halfLabelPainterOffset;
labelPainter.paint(canvas, labelOffset);

final span = labelPainter.text as TextSpan;
labelPainter
..text = TextSpan(
text: span.text,
style: span.style
?.copyWith(color: span.style?.color?.withOpacity(opacity)),
)
..paint(canvas, labelOffset);

canvas.restore();
}
}

0 comments on commit 9d22136

Please sign in to comment.