feat(control-panel): custom animated device dropdown
This commit is contained in:
@@ -120,27 +120,10 @@ class _Header extends ConsumerWidget {
|
||||
height: SizeUtils.getByScreen(small: 18, big: 18),
|
||||
),
|
||||
SizedBox(width: SizeUtils.getByScreen(small: 8, big: 4)),
|
||||
SizedBox(
|
||||
width: SizeUtils.getByScreen(small: 100, big: 110),
|
||||
height: 32,
|
||||
child: CustomDropdown(
|
||||
items: state.devices.map((DeviceEntity device) {
|
||||
return Text(
|
||||
device.carrierName ?? '',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
);
|
||||
}).toList(),
|
||||
values: state.devices,
|
||||
value: state.devices.where((d) => d.id == state.selectedDevice?.id).firstOrNull,
|
||||
onChanged: (device) {
|
||||
vm.setSelectedDevice(device);
|
||||
},
|
||||
height: 32,
|
||||
color: Colors.transparent,
|
||||
padding: EdgeInsets.zero,
|
||||
showIcon: false,
|
||||
),
|
||||
_DeviceDropdown(
|
||||
devices: state.devices,
|
||||
selectedDevice: state.selectedDevice,
|
||||
onChanged: vm.setSelectedDevice,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -316,3 +299,196 @@ class _MapSection extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DeviceDropdown extends StatefulWidget {
|
||||
final List<DeviceEntity> devices;
|
||||
final DeviceEntity? selectedDevice;
|
||||
final ValueChanged<DeviceEntity> onChanged;
|
||||
|
||||
const _DeviceDropdown({
|
||||
required this.devices,
|
||||
required this.selectedDevice,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_DeviceDropdown> createState() => _DeviceDropdownState();
|
||||
}
|
||||
|
||||
class _DeviceDropdownState extends State<_DeviceDropdown>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final _overlayController = OverlayPortalController();
|
||||
final _link = LayerLink();
|
||||
late final AnimationController _animation;
|
||||
late final Animation<double> _curve;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animation = AnimationController(
|
||||
duration: const Duration(milliseconds: 800),
|
||||
vsync: this,
|
||||
);
|
||||
_curve = CurvedAnimation(
|
||||
parent: _animation,
|
||||
curve: Curves.easeOutCubic,
|
||||
reverseCurve: Curves.easeInCubic,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animation.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _toggle() {
|
||||
if (_overlayController.isShowing) {
|
||||
_animation.reverse().then((_) {
|
||||
if (mounted) _overlayController.hide();
|
||||
});
|
||||
} else {
|
||||
_overlayController.show();
|
||||
_animation.forward();
|
||||
}
|
||||
}
|
||||
|
||||
void _select(DeviceEntity device) {
|
||||
widget.onChanged(device);
|
||||
_animation.reverse().then((_) {
|
||||
if (mounted) _overlayController.hide();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final primaryColor = Theme.of(context).primaryColor;
|
||||
final selectedName = widget.selectedDevice?.carrierName ?? '';
|
||||
|
||||
return CompositedTransformTarget(
|
||||
link: _link,
|
||||
child: OverlayPortal(
|
||||
controller: _overlayController,
|
||||
overlayChildBuilder: (_) => GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: _toggle,
|
||||
child: SizedBox.expand(
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: CompositedTransformFollower(
|
||||
link: _link,
|
||||
targetAnchor: Alignment.bottomLeft,
|
||||
followerAnchor: Alignment.topLeft,
|
||||
offset: const Offset(0, 4),
|
||||
child: FadeTransition(
|
||||
opacity: _curve,
|
||||
child: ScaleTransition(
|
||||
scale: Tween<double>(begin: 0.92, end: 1.0)
|
||||
.animate(_curve),
|
||||
alignment: Alignment.topLeft,
|
||||
child: Material(
|
||||
elevation: 4,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: IntrinsicWidth(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: widget.devices.map((device) {
|
||||
final isSelected =
|
||||
device.id == widget.selectedDevice?.id;
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: () => _select(device),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 14,
|
||||
vertical: 10,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.watch,
|
||||
size: 16,
|
||||
color: isSelected
|
||||
? primaryColor
|
||||
: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withValues(alpha: 0.4),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
device.carrierName ?? '',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: isSelected
|
||||
? FontWeight.w600
|
||||
: FontWeight.w400,
|
||||
color: isSelected
|
||||
? primaryColor
|
||||
: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: GestureDetector(
|
||||
onTap: widget.devices.length > 1 ? _toggle : null,
|
||||
child: SizedBox(
|
||||
height: 32,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: SizeUtils.getByScreen(small: 100, big: 110),
|
||||
),
|
||||
child: Text(
|
||||
selectedName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 12, big: 13),
|
||||
fontWeight: FontWeight.w600,
|
||||
color: primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.devices.length > 1)
|
||||
AnimatedBuilder(
|
||||
animation: _animation,
|
||||
builder: (context, child) => Transform.rotate(
|
||||
angle: _curve.value * 3.14159,
|
||||
child: child,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
size: 18,
|
||||
color: primaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user