170 lines
5.6 KiB
Dart
170 lines
5.6 KiB
Dart
import 'package:aves/theme/durations.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
class WheelSelector<T> extends StatefulWidget {
|
|
final ValueNotifier<T> valueNotifier;
|
|
final List<T> values;
|
|
final TextStyle textStyle;
|
|
final TextAlign textAlign;
|
|
|
|
const WheelSelector({
|
|
super.key,
|
|
required this.valueNotifier,
|
|
required this.values,
|
|
required this.textStyle,
|
|
required this.textAlign,
|
|
});
|
|
|
|
@override
|
|
State<WheelSelector<T>> createState() => _WheelSelectorState<T>();
|
|
}
|
|
|
|
class _WheelSelectorState<T> extends State<WheelSelector<T>> {
|
|
late final FixedExtentScrollController _controller;
|
|
final ValueNotifier<bool> _focusedNotifier = ValueNotifier(false);
|
|
|
|
ValueNotifier<T> get valueNotifier => widget.valueNotifier;
|
|
|
|
List<T> get values => widget.values;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller = FixedExtentScrollController(
|
|
initialItem: values.indexOf(valueNotifier.value),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
_focusedNotifier.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
|
|
const background = Colors.transparent;
|
|
final foreground = DefaultTextStyle.of(context).style.color!;
|
|
final transitionDuration = context.select<DurationsData, Duration>((v) => v.formTransition);
|
|
final itemSize = Size.square(40 * textScaleFactor);
|
|
|
|
return FocusableActionDetector(
|
|
shortcuts: const {
|
|
SingleActivator(LogicalKeyboardKey.arrowUp): _AdjustValueIntent.up(),
|
|
SingleActivator(LogicalKeyboardKey.arrowDown): _AdjustValueIntent.down(),
|
|
},
|
|
actions: {
|
|
_AdjustValueIntent: CallbackAction<_AdjustValueIntent>(onInvoke: _onAdjustValueIntent),
|
|
},
|
|
onShowFocusHighlight: (v) => _focusedNotifier.value = v,
|
|
child: NotificationListener<ScrollNotification>(
|
|
// cancel notification bubbling so that the dialog scroll bar
|
|
// does not misinterpret wheel scrolling for dialog content scrolling
|
|
onNotification: (notification) => true,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(8),
|
|
child: Stack(
|
|
children: [
|
|
Positioned.fill(
|
|
child: Center(
|
|
child: ValueListenableBuilder<bool>(
|
|
valueListenable: _focusedNotifier,
|
|
builder: (context, focused, child) {
|
|
return AnimatedContainer(
|
|
width: itemSize.width,
|
|
height: itemSize.height,
|
|
duration: transitionDuration,
|
|
decoration: BoxDecoration(
|
|
color: foreground.withOpacity(focused ? .2 : 0),
|
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
SizedBox(
|
|
width: itemSize.width,
|
|
height: itemSize.height * 3,
|
|
child: ShaderMask(
|
|
shaderCallback: LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [
|
|
background,
|
|
foreground,
|
|
foreground,
|
|
background,
|
|
],
|
|
).createShader,
|
|
child: Theme(
|
|
data: Theme.of(context).copyWith(
|
|
scrollbarTheme: ScrollbarThemeData(
|
|
thumbVisibility: MaterialStateProperty.all(false),
|
|
),
|
|
),
|
|
child: ListWheelScrollView(
|
|
controller: _controller,
|
|
physics: const FixedExtentScrollPhysics(parent: BouncingScrollPhysics()),
|
|
diameterRatio: 1.2,
|
|
itemExtent: itemSize.height,
|
|
squeeze: 1.3,
|
|
onSelectedItemChanged: (i) => valueNotifier.value = values[i],
|
|
children: values
|
|
.map((i) => SizedBox.fromSize(
|
|
size: itemSize,
|
|
child: Text(
|
|
'$i',
|
|
textAlign: widget.textAlign,
|
|
style: widget.textStyle,
|
|
),
|
|
))
|
|
.toList(),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _onAdjustValueIntent(_AdjustValueIntent intent) {
|
|
late int delta;
|
|
switch (intent.type) {
|
|
case _ValueAdjustmentType.up:
|
|
delta = -1;
|
|
case _ValueAdjustmentType.down:
|
|
delta = 1;
|
|
}
|
|
final targetItem = _controller.selectedItem + delta;
|
|
final duration = context.read<DurationsData>().formTransition;
|
|
if (duration > Duration.zero) {
|
|
_controller.animateToItem(targetItem, duration: duration, curve: Curves.easeInOutCubic);
|
|
} else {
|
|
_controller.jumpToItem(targetItem);
|
|
}
|
|
}
|
|
}
|
|
|
|
class _AdjustValueIntent extends Intent {
|
|
const _AdjustValueIntent({
|
|
required this.type,
|
|
});
|
|
|
|
const _AdjustValueIntent.up() : type = _ValueAdjustmentType.up;
|
|
|
|
const _AdjustValueIntent.down() : type = _ValueAdjustmentType.down;
|
|
|
|
final _ValueAdjustmentType type;
|
|
}
|
|
|
|
enum _ValueAdjustmentType {
|
|
up,
|
|
down,
|
|
}
|