Initial project import
This commit is contained in:
@@ -0,0 +1,769 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:miaoji_zhang/shared/theme/app_theme.dart';
|
||||
import 'package:miaoji_zhang/shared/services/shanghai_time.dart';
|
||||
|
||||
class JzSheetHeader extends StatelessWidget {
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
const JzSheetHeader({super.key, required this.title, this.subtitle});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 38,
|
||||
height: 4,
|
||||
margin: const EdgeInsets.only(top: 10, bottom: 18),
|
||||
decoration: BoxDecoration(
|
||||
color: context.jz.line,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w800),
|
||||
),
|
||||
if (subtitle != null) ...[
|
||||
SizedBox(height: 6),
|
||||
Text(
|
||||
subtitle!,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: context.jz.text2,
|
||||
fontSize: 12.5,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class JzActionButton extends StatelessWidget {
|
||||
final String label;
|
||||
final VoidCallback? onPressed;
|
||||
final bool secondary;
|
||||
final bool destructive;
|
||||
final bool loading;
|
||||
final Widget? icon;
|
||||
|
||||
const JzActionButton({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.onPressed,
|
||||
this.secondary = false,
|
||||
this.destructive = false,
|
||||
this.loading = false,
|
||||
this.icon,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final foreground = destructive ? AppTheme.red : AppTheme.primaryDeep;
|
||||
final indicator = loading
|
||||
? SizedBox(
|
||||
width: 17,
|
||||
height: 17,
|
||||
child: CircularProgressIndicator(strokeWidth: 2, color: foreground),
|
||||
)
|
||||
: icon ?? const SizedBox.shrink();
|
||||
if (secondary) {
|
||||
return OutlinedButton.icon(
|
||||
onPressed: loading ? null : onPressed,
|
||||
icon: indicator,
|
||||
label: Text(label),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: foreground,
|
||||
backgroundColor: destructive
|
||||
? context.jz.expenseBackground
|
||||
: context.jz.card,
|
||||
side: BorderSide(
|
||||
color: destructive
|
||||
? AppTheme.red.withValues(alpha: 0.25)
|
||||
: context.jz.line,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return FilledButton.icon(
|
||||
onPressed: loading ? null : onPressed,
|
||||
icon: loading
|
||||
? SizedBox(
|
||||
width: 17,
|
||||
height: 17,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: indicator,
|
||||
label: Text(label),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: destructive ? AppTheme.red : AppTheme.primary,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class JzOption<T> {
|
||||
final T value;
|
||||
final String label;
|
||||
final String? subtitle;
|
||||
final Widget? leading;
|
||||
|
||||
const JzOption({
|
||||
required this.value,
|
||||
required this.label,
|
||||
this.subtitle,
|
||||
this.leading,
|
||||
});
|
||||
}
|
||||
|
||||
Future<T?> showJzOptionSheet<T>(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
String? subtitle,
|
||||
required List<JzOption<T>> options,
|
||||
T? selected,
|
||||
}) {
|
||||
return showModalBottomSheet<T>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (sheetContext) => SafeArea(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.sizeOf(sheetContext).height * 0.72,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: context.jz.card,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: JzSheetHeader(title: title, subtitle: subtitle),
|
||||
),
|
||||
Flexible(
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 20),
|
||||
itemCount: options.length,
|
||||
separatorBuilder: (_, __) => SizedBox(height: 7),
|
||||
itemBuilder: (_, index) {
|
||||
final option = options[index];
|
||||
final active = option.value == selected;
|
||||
return Semantics(
|
||||
selected: active,
|
||||
button: true,
|
||||
child: InkWell(
|
||||
onTap: () => Navigator.pop(sheetContext, option.value),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 14,
|
||||
vertical: 12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: active
|
||||
? context.jz.primaryBackground
|
||||
: context.jz.background,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(
|
||||
color: active ? AppTheme.primary : context.jz.line,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
if (option.leading != null) ...[
|
||||
option.leading!,
|
||||
SizedBox(width: 11),
|
||||
],
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
option.label,
|
||||
style: TextStyle(
|
||||
fontSize: 13.5,
|
||||
fontWeight: active
|
||||
? FontWeight.w700
|
||||
: FontWeight.w600,
|
||||
color: active
|
||||
? AppTheme.primaryDeep
|
||||
: context.jz.text,
|
||||
),
|
||||
),
|
||||
if (option.subtitle != null) ...[
|
||||
SizedBox(height: 3),
|
||||
Text(
|
||||
option.subtitle!,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: context.jz.text3,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (active)
|
||||
Icon(
|
||||
Icons.check_rounded,
|
||||
color: AppTheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> showJzConfirmSheet(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
required String message,
|
||||
String confirmLabel = '确定',
|
||||
String cancelLabel = '取消',
|
||||
bool destructive = false,
|
||||
Widget? content,
|
||||
}) async {
|
||||
final result = await showModalBottomSheet<bool>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (sheetContext) => SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.viewInsetsOf(sheetContext).bottom,
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 18),
|
||||
decoration: BoxDecoration(
|
||||
color: context.jz.card,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
JzSheetHeader(title: title, subtitle: message),
|
||||
if (content != null) ...[SizedBox(height: 14), content],
|
||||
SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: JzActionButton(
|
||||
label: cancelLabel,
|
||||
secondary: true,
|
||||
onPressed: () => Navigator.pop(sheetContext, false),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: JzActionButton(
|
||||
label: confirmLabel,
|
||||
destructive: destructive,
|
||||
onPressed: () => Navigator.pop(sheetContext, true),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
return result == true;
|
||||
}
|
||||
|
||||
Future<String?> showJzTextInputSheet(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
required String label,
|
||||
String? subtitle,
|
||||
String? initialValue,
|
||||
bool obscureText = false,
|
||||
int? maxLength,
|
||||
String confirmLabel = '确定',
|
||||
}) async {
|
||||
final controller = TextEditingController(text: initialValue);
|
||||
final result = await showModalBottomSheet<String>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (sheetContext) => SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.viewInsetsOf(sheetContext).bottom,
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 18),
|
||||
decoration: BoxDecoration(
|
||||
color: context.jz.card,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
JzSheetHeader(title: title, subtitle: subtitle),
|
||||
SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
obscureText: obscureText,
|
||||
maxLength: maxLength,
|
||||
textInputAction: TextInputAction.done,
|
||||
onSubmitted: (_) =>
|
||||
Navigator.pop(sheetContext, controller.text.trim()),
|
||||
decoration: InputDecoration(labelText: label),
|
||||
),
|
||||
SizedBox(height: 14),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: JzActionButton(
|
||||
label: confirmLabel,
|
||||
onPressed: () =>
|
||||
Navigator.pop(sheetContext, controller.text.trim()),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
controller.dispose();
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<DateTime?> showJzDateTimeSheet(
|
||||
BuildContext context, {
|
||||
required DateTime initial,
|
||||
DateTime? firstDate,
|
||||
DateTime? lastDate,
|
||||
String title = '选择日期和时间',
|
||||
}) {
|
||||
final minimum = firstDate ?? DateTime(2000);
|
||||
final maximum = lastDate ?? ShanghaiTime.now.add(const Duration(days: 365));
|
||||
return showModalBottomSheet<DateTime>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (sheetContext) {
|
||||
var date = DateUtils.dateOnly(initial);
|
||||
var hour = initial.hour;
|
||||
var minute = initial.minute;
|
||||
return StatefulBuilder(
|
||||
builder: (context, setSheetState) => SafeArea(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.sizeOf(context).height * 0.86,
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
decoration: BoxDecoration(
|
||||
color: context.jz.card,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
JzSheetHeader(title: title),
|
||||
Flexible(
|
||||
child: CalendarDatePicker(
|
||||
initialDate: date.isBefore(minimum)
|
||||
? minimum
|
||||
: date.isAfter(maximum)
|
||||
? maximum
|
||||
: date,
|
||||
firstDate: minimum,
|
||||
lastDate: maximum,
|
||||
onDateChanged: (value) => setSheetState(() => date = value),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 92,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
decoration: BoxDecoration(
|
||||
color: context.jz.background,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: context.jz.line),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'时间',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
_TimeWheel(
|
||||
value: hour,
|
||||
count: 24,
|
||||
onChanged: (value) => setSheetState(() => hour = value),
|
||||
),
|
||||
Text(
|
||||
':',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
_TimeWheel(
|
||||
value: minute,
|
||||
count: 60,
|
||||
onChanged: (value) =>
|
||||
setSheetState(() => minute = value),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 14),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: JzActionButton(
|
||||
label: '取消',
|
||||
secondary: true,
|
||||
onPressed: () => Navigator.pop(sheetContext),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: JzActionButton(
|
||||
label: '完成',
|
||||
onPressed: () => Navigator.pop(
|
||||
sheetContext,
|
||||
DateTime(
|
||||
date.year,
|
||||
date.month,
|
||||
date.day,
|
||||
hour,
|
||||
minute,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class JzSegmentedControl<T> extends StatelessWidget {
|
||||
final T value;
|
||||
final List<JzOption<T>> options;
|
||||
final ValueChanged<T>? onChanged;
|
||||
|
||||
const JzSegmentedControl({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.options,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(3),
|
||||
decoration: BoxDecoration(
|
||||
color: context.jz.line,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: options.map((option) {
|
||||
final selected = option.value == value;
|
||||
return Expanded(
|
||||
child: Semantics(
|
||||
selected: selected,
|
||||
button: true,
|
||||
child: InkWell(
|
||||
onTap: onChanged == null
|
||||
? null
|
||||
: () => onChanged!(option.value),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
curve: Curves.easeOutCubic,
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: selected ? context.jz.card : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: selected ? context.jz.line : Colors.transparent,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
option.label,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 13.5,
|
||||
fontWeight: selected ? FontWeight.w800 : FontWeight.w500,
|
||||
color: selected
|
||||
? value == 'income'
|
||||
? AppTheme.primaryDeep
|
||||
: context.jz.text
|
||||
: context.jz.text2,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class JzSwitchTile extends StatelessWidget {
|
||||
final bool value;
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
final ValueChanged<bool>? onChanged;
|
||||
|
||||
const JzSwitchTile({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Semantics(
|
||||
toggled: value,
|
||||
button: true,
|
||||
label: title,
|
||||
child: InkWell(
|
||||
onTap: onChanged == null ? null : () => onChanged!(!value),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
if (subtitle != null) ...[
|
||||
SizedBox(height: 3),
|
||||
Text(
|
||||
subtitle!,
|
||||
style: TextStyle(
|
||||
fontSize: 11.5,
|
||||
color: context.jz.text3,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
width: 46,
|
||||
height: 27,
|
||||
padding: const EdgeInsets.all(3),
|
||||
decoration: BoxDecoration(
|
||||
color: value ? context.jz.primaryBackground : context.jz.line,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(
|
||||
color: value ? AppTheme.primary : context.jz.text3,
|
||||
),
|
||||
),
|
||||
child: AnimatedAlign(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
curve: Curves.easeOutCubic,
|
||||
alignment: value
|
||||
? Alignment.centerRight
|
||||
: Alignment.centerLeft,
|
||||
child: Container(
|
||||
width: 19,
|
||||
height: 19,
|
||||
decoration: BoxDecoration(
|
||||
color: value ? AppTheme.primary : context.jz.card,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class JzSlider extends StatelessWidget {
|
||||
final double value;
|
||||
final double min;
|
||||
final double max;
|
||||
final Color color;
|
||||
final ValueChanged<double>? onChanged;
|
||||
|
||||
const JzSlider({
|
||||
super.key,
|
||||
required this.value,
|
||||
this.min = 0,
|
||||
this.max = 100,
|
||||
this.color = AppTheme.primary,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
double get _fraction =>
|
||||
max <= min ? 0 : ((value - min) / (max - min)).clamp(0.0, 1.0);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
void update(double x, double width) {
|
||||
if (onChanged == null || width <= 0) return;
|
||||
final fraction = (x / width).clamp(0.0, 1.0);
|
||||
onChanged!(min + (max - min) * fraction);
|
||||
}
|
||||
|
||||
return Semantics(
|
||||
slider: true,
|
||||
value: value.round().toString(),
|
||||
increasedValue: (value + 5).clamp(min, max).round().toString(),
|
||||
decreasedValue: (value - 5).clamp(min, max).round().toString(),
|
||||
onIncrease: onChanged == null
|
||||
? null
|
||||
: () => onChanged!((value + 5).clamp(min, max)),
|
||||
onDecrease: onChanged == null
|
||||
? null
|
||||
: () => onChanged!((value - 5).clamp(min, max)),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final width = constraints.maxWidth;
|
||||
final thumbLeft = (width - 24) * _fraction;
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTapDown: onChanged == null
|
||||
? null
|
||||
: (details) => update(details.localPosition.dx, width),
|
||||
onHorizontalDragUpdate: onChanged == null
|
||||
? null
|
||||
: (details) => update(details.localPosition.dx, width),
|
||||
child: SizedBox(
|
||||
height: 36,
|
||||
child: Stack(
|
||||
alignment: Alignment.centerLeft,
|
||||
children: [
|
||||
Container(
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
color: context.jz.line,
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
),
|
||||
FractionallySizedBox(
|
||||
widthFactor: _fraction,
|
||||
child: Container(
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: thumbLeft,
|
||||
child: Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
color: context.jz.card,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: color, width: 2),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x1A191F26),
|
||||
blurRadius: 5,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TimeWheel extends StatelessWidget {
|
||||
final int value;
|
||||
final int count;
|
||||
final ValueChanged<int> onChanged;
|
||||
const _TimeWheel({
|
||||
required this.value,
|
||||
required this.count,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 52,
|
||||
child: ListWheelScrollView.useDelegate(
|
||||
controller: FixedExtentScrollController(initialItem: value),
|
||||
itemExtent: 34,
|
||||
physics: const FixedExtentScrollPhysics(),
|
||||
onSelectedItemChanged: (index) {
|
||||
HapticFeedback.selectionClick();
|
||||
onChanged(index);
|
||||
},
|
||||
childDelegate: ListWheelChildBuilderDelegate(
|
||||
childCount: count,
|
||||
builder: (_, index) => Center(
|
||||
child: Text(
|
||||
index.toString().padLeft(2, '0'),
|
||||
style: TextStyle(
|
||||
fontSize: index == value ? 18 : 13,
|
||||
fontWeight: index == value ? FontWeight.w800 : FontWeight.w500,
|
||||
color: index == value ? AppTheme.primaryDeep : context.jz.text3,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user