Skip to content
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

feat: Implement Slider #506

Closed
wants to merge 22 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .fvmrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"flutter": "3.19.0",
"flutter": "stable",
"flavors": {
"prod": "stable",
"mincompat": "3.19.0"
Expand Down
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"source.fixAll": "explicit",
"source.dcm.fixAll": "explicit"
},
"dart.flutterSdkPath": ".fvm/versions/3.19.0",
"dart.flutterSdkPath": ".fvm/versions/stable",
"dart.lineLength": 80,
"search.exclude": {
"**/.fvm/versions": true
Expand Down
2 changes: 2 additions & 0 deletions packages/mix/lib/mix.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export 'src/attributes/modifiers/widget_modifiers_data_dto.dart';
export 'src/attributes/modifiers/widget_modifiers_util.dart';
export 'src/attributes/nested_style/nested_style_attribute.dart';
export 'src/attributes/nested_style/nested_style_util.dart';
export 'src/attributes/scalars/curves.dart';
export 'src/attributes/scalars/scalar_util.dart';
export 'src/attributes/shadow/shadow_dto.dart';
export 'src/attributes/shadow/shadow_util.dart';
Expand Down Expand Up @@ -124,4 +125,5 @@ export 'src/variants/context_variant_util/on_util.dart';
export 'src/variants/variant_attribute.dart';
export 'src/variants/widget_state_variant.dart';
/// WIDGETS
export 'src/widgets/gesture_detector_widget.dart';
export 'src/widgets/pressable_widget.dart';
33 changes: 33 additions & 0 deletions packages/mix/lib/src/attributes/scalars/curves.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import 'dart:math';

import 'package:flutter/physics.dart';
import 'package:flutter/widgets.dart';

class SpringCurve extends Curve {
late final SpringSimulation _sim;
late final double _val;

SpringCurve({required Duration duration, double bounce = 0.0}) {
assert(
(bounce >= -1.0 && bounce <= 1.0),
'"bounce" value must be between -1.0 and 1.0.',
);

final double dur = max(1.0, (duration.inMilliseconds / 1000));
final double stiffness = ((2 * pi) / dur) * ((2 * pi) / dur) * 3.5;
final double dampingRatio = 1.0 - bounce;

final SpringDescription desc = SpringDescription.withDampingRatio(
mass: 1.0,
stiffness: stiffness,
ratio: dampingRatio,
);

_sim = SpringSimulation(desc, 0.0, 1.0, 0.0);

_val = (1 - _sim.x(1.0));
}

@override
double transform(double t) => _sim.x(t) + t * _val;
}
8 changes: 8 additions & 0 deletions packages/mix/lib/src/attributes/scalars/scalar_util.dart
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ final class TextDecorationUtility<T extends Attribute>
final class CurveUtility<T extends Attribute> extends MixUtility<T, Curve>
with _$CurveUtility {
const CurveUtility(super.builder);

T as(Curve curve) => builder(curve);

T spring({
Duration duration = const Duration(milliseconds: 500),
double bounce = 0.5,
}) =>
builder(SpringCurve(duration: duration, bounce: bounce));
}

@MixableClassUtility(generateCallMethod: false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,12 @@ class _GestureMixStateWidgetState extends State<GestureMixStateWidget> {
}

void _onPanUpdate(DragUpdateDetails event) {
_controller.dragged = true;
widget.onPanUpdate?.call(event);
}

void _onPanDown(DragDownDetails details) {
_controller.dragged = true;
widget.onPanDown?.call(details);
}

Expand All @@ -112,7 +114,7 @@ class _GestureMixStateWidgetState extends State<GestureMixStateWidget> {
}

void _onPanEnd(DragEndDetails details) {
_handlePress(true);
_controller.dragged = false;
widget.onPanEnd?.call(details);
}

Expand Down Expand Up @@ -142,10 +144,12 @@ class _GestureMixStateWidgetState extends State<GestureMixStateWidget> {
}

void _onPanCancel() {
_controller.dragged = false;
widget.onPanCancel?.call();
}

void _onPanStart(DragStartDetails details) {
_controller.dragged = true;
widget.onPanStart?.call(details);
}

Expand Down
228 changes: 228 additions & 0 deletions packages/mix/lib/src/widgets/gesture_detector_widget.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart';

import '../core/widget_state/internal/gesture_mix_state.dart';
import '../core/widget_state/internal/interactive_mix_state.dart';
import '../core/widget_state/internal/mix_widget_state_builder.dart';
import '../core/widget_state/internal/mouse_region_mix_state.dart';
import '../core/widget_state/widget_state_controller.dart';
import '../internal/constants.dart';

class MixGestureDetector extends StatefulWidget {
const MixGestureDetector({
super.key,
required this.child,
this.enabled = true,
this.enableFeedback = false,
this.onPress,
this.hitTestBehavior = HitTestBehavior.opaque,
this.onLongPress,
this.onFocusChange,
this.autofocus = false,
this.focusNode,
this.mouseCursor,
this.onKey,
this.canRequestFocus = true,
this.excludeFromSemantics = false,
this.semanticButtonLabel,
this.onKeyEvent,
this.unpressDelay = kDefaultAnimationDuration,
this.controller,
this.actions,
this.onTapUp,
this.onTapCancel,
this.onLongPressStart,
this.onLongPressEnd,
this.onLongPressCancel,
this.onPanDown,
this.onPanUpdate,
this.onPanEnd,
this.onPanCancel,
this.onPanStart,
});

final Widget child;

final bool enabled;

final MouseCursor? mouseCursor;

final String? semanticButtonLabel;

final bool excludeFromSemantics;

final bool canRequestFocus;

/// Should gestures provide audible and/or haptic feedback
///
/// On platforms like Android, enabling feedback will result in audible and tactile
/// responses to certain actions. For example, a tap may produce a clicking sound,
/// while a long-press may trigger a short vibration.
final bool enableFeedback;

/// The callback that is called when the box is tapped or otherwise activated.
///
/// If this callback and [onLongPress] are null, then it will be disabled automatically.
final VoidCallback? onPress;

/// The callback that is called when long-pressed.
///
/// If this callback and [onPress] are null, then `PressableBox` will be disabled automatically.
final VoidCallback? onLongPress;

/// Called when a pointer that will trigger a tap has stopped contacting the screen.
final GestureTapUpCallback? onTapUp;

/// Called when the pointer that previously triggered [onTapDown] will not end up causing a tap.
final GestureTapCancelCallback? onTapCancel;

/// Called when a long press gesture has been recognized.
final GestureLongPressStartCallback? onLongPressStart;

/// Called when a long press gesture that had been recognized is ended.
final GestureLongPressEndCallback? onLongPressEnd;

/// Called when a long press gesture that had been recognized is canceled.
final GestureLongPressCancelCallback? onLongPressCancel;

/// Called when a pointer has contacted the screen and might begin to move.
final GestureDragDownCallback? onPanDown;

/// Called when a pointer that is in contact with the screen and moving has moved again.
final GestureDragUpdateCallback? onPanUpdate;

/// Called when a pointer that was previously in contact with the screen and moving is no longer in contact with the screen.
final GestureDragEndCallback? onPanEnd;

/// Called when the pointer that previously triggered [onPanDown] did not complete.
final GestureDragCancelCallback? onPanCancel;

/// Called when a pointer has contacted the screen and has begun to move.
final GestureDragStartCallback? onPanStart;

/// Called when the focus state of the [Focus] changes.
///
/// Called with true when the [Focus] node gains focus
/// and false when the [Focus] node loses focus.
final ValueChanged<bool>? onFocusChange;

/// {@macro flutter.widgets.Focus.autofocus}
final bool autofocus;

/// {@macro flutter.widgets.Focus.focusNode}
final FocusNode? focusNode;

/// {@macro flutter.widgets.Focus.onKey}
final FocusOnKeyEventCallback? onKey;

/// {@macro flutter.widgets.Focus.onKeyEvent}
final FocusOnKeyEventCallback? onKeyEvent;

/// {@macro flutter.widgets.GestureDetector.hitTestBehavior}
final HitTestBehavior hitTestBehavior;

/// Actions to be bound to the widget
final Map<Type, Action<Intent>>? actions;

final MixWidgetStateController? controller;

/// The duration to wait after the press is released before the state of pressed is removed
final Duration unpressDelay;

@override
State createState() => MixGestureDetectorState();
}

@visibleForTesting
class MixGestureDetectorState extends State<MixGestureDetector> {
late final MixWidgetStateController _controller;

@override
void initState() {
super.initState();
_controller = widget.controller ?? MixWidgetStateController();
}

@override
void dispose() {
if (widget.controller == null) _controller.dispose();
super.dispose();
}

bool get hasOnPress => widget.onPress != null;

MouseCursor get mouseCursor {
if (widget.mouseCursor != null) {
return widget.mouseCursor!;
}

if (!widget.enabled) {
return SystemMouseCursors.forbidden;
}

return hasOnPress ? SystemMouseCursors.click : MouseCursor.defer;
}

/// Binds the [ActivateIntent] from the Flutter SDK to the onPressed callback by default.
/// This enables SPACE and ENTER key activation on most platforms.
/// Additional actions can be provided externally to extend functionality.
Map<Type, Action<Intent>> get actions {
return {
ActivateIntent:
CallbackAction<Intent>(onInvoke: (_) => widget.onPress?.call()),
...?widget.actions,
};
}

@override
Widget build(BuildContext context) {
Widget current = GestureMixStateWidget(
enableFeedback: widget.enableFeedback,
controller: _controller,
onTap: widget.enabled ? widget.onPress : null,
onLongPress: widget.enabled ? widget.onLongPress : null,
onTapUp: widget.enabled ? widget.onTapUp : null,
onTapCancel: widget.enabled ? widget.onTapCancel : null,
onLongPressStart: widget.enabled ? widget.onLongPressStart : null,
onLongPressEnd: widget.enabled ? widget.onLongPressEnd : null,
onLongPressCancel: widget.enabled ? widget.onLongPressCancel : null,
onPanDown: widget.enabled ? widget.onPanDown : null,
onPanUpdate: widget.enabled ? widget.onPanUpdate : null,
onPanEnd: widget.enabled ? widget.onPanEnd : null,
onPanCancel: widget.enabled ? widget.onPanCancel : null,
onPanStart: widget.enabled ? widget.onPanStart : null,
excludeFromSemantics: widget.excludeFromSemantics,
hitTestBehavior: widget.hitTestBehavior,
unpressDelay: widget.unpressDelay,
child: InteractiveMixStateWidget(
enabled: widget.enabled,
onFocusChange: widget.onFocusChange,
autofocus: widget.autofocus,
focusNode: widget.focusNode,
onKey: widget.onKey,
onKeyEvent: widget.onKeyEvent,
canRequestFocus: widget.canRequestFocus,
mouseCursor: mouseCursor,
controller: _controller,
actions: actions,
child: MouseRegionMixStateWidget(
child: MixWidgetStateBuilder(
controller: _controller,
builder: (_) => widget.child,
),
),
),
);

if (!widget.excludeFromSemantics) {
current = Semantics(
button: true,
label: widget.semanticButtonLabel,
onTap: widget.onPress,
child: current,
);
}

return current;
}
}
Loading
Loading