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),
|
height: SizeUtils.getByScreen(small: 18, big: 18),
|
||||||
),
|
),
|
||||||
SizedBox(width: SizeUtils.getByScreen(small: 8, big: 4)),
|
SizedBox(width: SizeUtils.getByScreen(small: 8, big: 4)),
|
||||||
SizedBox(
|
_DeviceDropdown(
|
||||||
width: SizeUtils.getByScreen(small: 100, big: 110),
|
devices: state.devices,
|
||||||
height: 32,
|
selectedDevice: state.selectedDevice,
|
||||||
child: CustomDropdown(
|
onChanged: vm.setSelectedDevice,
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -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