Files
venera/lib/components/select.dart
2024-10-11 21:47:50 +08:00

285 lines
7.1 KiB
Dart

part of 'components.dart';
class Select extends StatelessWidget {
const Select({
super.key,
required this.current,
required this.values,
this.onTap,
});
final String current;
final List<String> values;
final void Function(int index)? onTap;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
border: Border.all(color: context.colorScheme.outlineVariant),
borderRadius: BorderRadius.circular(4),
),
child: InkWell(
onTap: () {
var renderBox = context.findRenderObject() as RenderBox;
var offset = renderBox.localToGlobal(Offset.zero);
var size = renderBox.size;
showMenu(
elevation: 3,
color: context.colorScheme.surface,
surfaceTintColor: Colors.transparent,
context: context,
useRootNavigator: true,
constraints: BoxConstraints(
minWidth: size.width,
maxWidth: size.width,
),
position: RelativeRect.fromLTRB(
offset.dx,
offset.dy + size.height,
offset.dx + size.height,
offset.dy,
),
items: values
.map((e) => PopupMenuItem(
height: App.isMobile ? 46 : 40,
value: e,
child: Text(e),
))
.toList(),
).then((value) {
if (value != null) {
onTap?.call(values.indexOf(value));
}
});
},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(current, style: ts.s14),
const SizedBox(width: 8),
Icon(Icons.arrow_drop_down, color: context.colorScheme.primary),
],
).padding(const EdgeInsets.symmetric(horizontal: 12, vertical: 4)),
),
);
}
}
class FilterChipFixedWidth extends StatefulWidget {
const FilterChipFixedWidth(
{required this.label,
required this.selected,
required this.onSelected,
super.key});
final Widget label;
final bool selected;
final void Function(bool) onSelected;
@override
State<FilterChipFixedWidth> createState() => _FilterChipFixedWidthState();
}
class _FilterChipFixedWidthState extends State<FilterChipFixedWidth> {
get selected => widget.selected;
double? labelWidth;
double? labelHeight;
var key = GlobalKey();
@override
void initState() {
Future.microtask(measureSize);
super.initState();
}
void measureSize() {
final RenderBox renderBox =
key.currentContext!.findRenderObject() as RenderBox;
labelWidth = renderBox.size.width;
labelHeight = renderBox.size.height;
setState(() {});
}
@override
Widget build(BuildContext context) {
return Material(
textStyle: Theme.of(context).textTheme.labelLarge,
child: InkWell(
onTap: () => widget.onSelected(true),
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: AnimatedContainer(
duration: _fastAnimationDuration,
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).colorScheme.outline),
borderRadius: const BorderRadius.all(Radius.circular(8)),
color: selected
? Theme.of(context).colorScheme.primaryContainer
: null,
),
padding: const EdgeInsets.fromLTRB(12, 8, 12, 8),
child: labelWidth == null ? firstBuild() : buildContent(),
),
),
);
}
Widget firstBuild() {
return Center(
child: SizedBox(
key: key,
child: widget.label,
),
);
}
Widget buildContent() {
const iconSize = 18.0;
const gap = 4.0;
return SizedBox(
width: iconSize + labelWidth! + gap,
height: math.max(iconSize, labelHeight!),
child: Stack(
children: [
AnimatedPositioned(
duration: _fastAnimationDuration,
left: selected ? (iconSize + gap) : (iconSize + gap) / 2,
child: widget.label,
),
if (selected)
Positioned(
left: 0,
top: 0,
bottom: 0,
right: labelWidth! + gap,
child: const AnimatedCheckIcon(size: iconSize).toCenter(),
)
],
),
);
}
}
class AnimatedCheckWidget extends AnimatedWidget {
const AnimatedCheckWidget({
super.key,
required Animation<double> animation,
this.size,
}) : super(listenable: animation);
final double? size;
@override
Widget build(BuildContext context) {
var iconSize = size ?? IconTheme.of(context).size ?? 25;
final animation = listenable as Animation<double>;
return SizedBox(
width: iconSize,
height: iconSize,
child: Align(
alignment: Alignment.centerLeft,
child: FractionallySizedBox(
widthFactor: animation.value,
child: ClipRRect(
child: Icon(
Icons.check,
size: iconSize,
color: Theme.of(context).colorScheme.primary,
),
),
),
),
);
}
}
class AnimatedCheckIcon extends StatefulWidget {
const AnimatedCheckIcon({this.size, super.key});
final double? size;
@override
State<AnimatedCheckIcon> createState() => _AnimatedCheckIconState();
}
class _AnimatedCheckIconState extends State<AnimatedCheckIcon>
with SingleTickerProviderStateMixin {
late Animation<double> animation;
late AnimationController controller;
@override
void initState() {
controller = AnimationController(
vsync: this,
duration: _fastAnimationDuration,
);
animation = Tween<double>(begin: 0, end: 1).animate(controller)
..addListener(() {
setState(() {});
});
controller.forward();
super.initState();
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedCheckWidget(
animation: animation,
size: widget.size,
);
}
}
class OptionChip extends StatelessWidget {
const OptionChip(
{super.key,
required this.text,
required this.isSelected,
required this.onTap});
final String text;
final bool isSelected;
final void Function() onTap;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: isSelected
? context.colorScheme.primaryContainer
: context.colorScheme.surface,
border: isSelected
? Border.all(color: context.colorScheme.primaryContainer)
: Border.all(color: context.colorScheme.outline),
borderRadius: BorderRadius.circular(8),
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
child: Text(text),
),
),
),
);
}
}