Last active
December 17, 2024 14:27
-
-
Save hectorAguero/9907539303159f822907bace7147a7f4 to your computer and use it in GitHub Desktop.
Flutter CompositedTransform with Listenable
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
// Based on https://medium.com/snapp-x/creating-custom-dropdowns-with-overlayportal-in-flutter-4f09b217cfce | |
// And https://github.com/flutter/flutter/issues/138828 | |
void main() { | |
runApp(const MyApp()); | |
} | |
class MyApp extends StatelessWidget { | |
const MyApp({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return const MaterialApp( | |
debugShowCheckedModeBanner: false, | |
home: Scaffold( | |
body: Center( | |
child: SizedBox( | |
width: 300, | |
child: CustomDropDown(), | |
), | |
), | |
), | |
); | |
} | |
} | |
class ButtonWidget extends StatelessWidget { | |
const ButtonWidget({ | |
super.key, | |
this.height = 48, | |
this.width, | |
this.onTap, | |
this.child, | |
required this.isExpanded, | |
}); | |
final double? height; | |
final double? width; | |
final VoidCallback? onTap; | |
final Widget? child; | |
final bool isExpanded; | |
@override | |
Widget build(BuildContext context) { | |
return SizedBox( | |
height: height, | |
width: width, | |
child: Container( | |
decoration: BoxDecoration( | |
borderRadius: isExpanded | |
? BorderRadius.only( | |
topLeft: Radius.circular(10), | |
topRight: Radius.circular(10), | |
) | |
: BorderRadius.circular(10), | |
color: isExpanded ? Colors.red : Colors.grey, | |
border: isExpanded | |
? Border( | |
top: BorderSide(color: Colors.black), | |
right: BorderSide(color: Colors.black), | |
left: BorderSide(color: Colors.black), | |
) | |
: Border.all(color: Colors.black)), | |
child: InkWell( | |
onTap: onTap, | |
borderRadius: BorderRadius.circular(10), | |
child: Center( | |
child: child ?? const SizedBox(), | |
), | |
), | |
), | |
); | |
} | |
} | |
class MenuWidget extends StatelessWidget { | |
const MenuWidget({ | |
super.key, | |
this.width, | |
this.onTap, | |
}); | |
final double? width; | |
final ValueChanged<int>? onTap; | |
@override | |
Widget build(BuildContext context) { | |
return Container( | |
width: width ?? 200, | |
height: 300, | |
decoration: BoxDecoration( | |
color: Colors.red, | |
borderRadius: BorderRadius.only( | |
bottomLeft: Radius.circular(10), | |
bottomRight: Radius.circular(10), | |
), | |
border: Border( | |
right: BorderSide(color: Colors.black), | |
left: BorderSide(color: Colors.black), | |
bottom: BorderSide(color: Colors.black), | |
), | |
), | |
child: ListView.builder( | |
padding: const EdgeInsets.all(8), | |
itemCount: 100, | |
itemBuilder: (BuildContext context, int index) { | |
return InkWell( | |
onTap: () => onTap?.call(index), | |
child: Text('$index'), | |
); | |
}, | |
), | |
); | |
} | |
} | |
class CustomDropDown extends StatefulWidget { | |
const CustomDropDown({ | |
super.key, | |
}); | |
@override | |
State<StatefulWidget> createState() => CustomDropDownState(); | |
} | |
class CustomDropDownState extends State<CustomDropDown> { | |
final _tooltipController = ListenOverlayPortalController(); | |
final _link = LayerLink(); | |
/// width of the button after the widget rendered | |
double? _buttonWidth; | |
void onTap() { | |
_buttonWidth = context.size?.width; | |
_tooltipController.toggle(); | |
} | |
int? _selectedItem; | |
@override | |
Widget build(BuildContext context) { | |
return CompositedTransformTarget( | |
link: _link, | |
child: OverlayPortal( | |
controller: _tooltipController, | |
overlayChildBuilder: (BuildContext context) { | |
return CompositedTransformFollower( | |
link: _link, | |
targetAnchor: Alignment.bottomLeft, | |
child: Align( | |
alignment: AlignmentDirectional.topStart, | |
child: MenuWidget( | |
width: _buttonWidth, | |
onTap: (index) { | |
print('Selected element is $index'); | |
_tooltipController.toggle(); | |
setState(() => _selectedItem = index); | |
}, | |
), | |
), | |
); | |
}, | |
child: ListenableBuilder( | |
listenable: _tooltipController, | |
builder: (context, child) => ButtonWidget( | |
isExpanded: _tooltipController.isShowing, | |
onTap: onTap, | |
child: _tooltipController.isShowing | |
? const Text('Select an Item') | |
: Text(_selectedItem != null | |
? 'Item Selected is $_selectedItem' | |
: 'Tap to Select'), | |
), | |
), | |
), | |
); | |
} | |
} | |
class ListenOverlayPortalController extends OverlayPortalController | |
with ChangeNotifier { | |
@override | |
void show() { | |
super.show(); | |
notifyListeners(); | |
} | |
@override | |
void hide() { | |
super.hide(); | |
notifyListeners(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment