Forked from boeledi/OverlayableContainerOnLongPress_sample.dart
Created
May 28, 2020 15:03
-
-
Save minikin/1b4e61cddf5cd12f40809705da31da05 to your computer and use it in GitHub Desktop.
How to display an overlay on top of a particular item, present in a Scroll Area, on longPress?
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'; | |
import 'dart:math'; | |
void main() { | |
/// | |
/// Launch the application | |
/// | |
runApp(Application()); | |
} | |
class Application extends StatelessWidget { | |
@override | |
Widget build(BuildContext context){ | |
return MaterialApp( | |
title: 'Application', | |
debugShowCheckedModeBanner: false, | |
home: Page(), | |
); | |
} | |
} | |
const int _kNumberOfItems = 50; | |
const int _kNumberOfItemsPerRow = 2; | |
class Page extends StatelessWidget { | |
@override | |
Widget build(BuildContext context){ | |
return SafeArea( | |
child: Scaffold( | |
appBar: AppBar( | |
title: Text('Application title'), | |
), | |
bottomNavigationBar: BottomNavigationBar( | |
currentIndex: 0, // this will be set when a new tab is tapped | |
items: [ | |
BottomNavigationBarItem( | |
icon: new Icon(Icons.home), | |
title: new Text('Home'), | |
), | |
BottomNavigationBarItem( | |
icon: new Icon(Icons.mail), | |
title: new Text('Messages'), | |
), | |
BottomNavigationBarItem( | |
icon: Icon(Icons.person), | |
title: Text('Profile') | |
) | |
], | |
), | |
body: Container( | |
child: ClipRect( // Forces the OverlayEntry not to overflow this container | |
child: Overlay( // The Overlay that allows us to control the positioning | |
initialEntries: <OverlayEntry>[ | |
OverlayEntry( | |
builder: (BuildContext context) { | |
return GridView.builder( | |
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( | |
crossAxisCount: _kNumberOfItemsPerRow, | |
childAspectRatio: 1.0, | |
), | |
itemCount: _kNumberOfItems, | |
itemBuilder: (BuildContext context, int index) { | |
return ItemWidget( | |
id: index, | |
color: Color((Random().nextDouble() * 0xFFFFFF).toInt() << 0) | |
.withOpacity(1.0), | |
); | |
}, | |
); | |
}, | |
), | |
], | |
), | |
), | |
), | |
), | |
); | |
} | |
} | |
/// | |
/// Simple Widget to demonstrate the use | |
/// of the OverlayableContainerOnLongPress | |
/// | |
class ItemWidget extends StatelessWidget { | |
ItemWidget({ | |
Key key, | |
this.id, | |
this.color, | |
}): super(key: key); | |
final int id; | |
final Color color; | |
@override | |
Widget build(BuildContext context){ | |
return OverlayableContainerOnLongPress( | |
child: GridTile( | |
child: Card( | |
child: Container( | |
color: color, | |
child: Center( | |
child: Text('item_$id', style: TextStyle(color: Colors.black,)), | |
), | |
), | |
), | |
), | |
overlayContentBuilder: (BuildContext context, VoidCallback onHideOverlay) { | |
return Container( | |
height: double.infinity, | |
color: Colors.black38, | |
child: Row( | |
crossAxisAlignment: CrossAxisAlignment.center, | |
mainAxisAlignment: MainAxisAlignment.spaceAround, | |
children: <Widget>[ | |
IconButton( | |
icon: const Icon( | |
Icons.edit, | |
color: Colors.white, | |
), | |
onPressed: () { | |
onHideOverlay(); | |
_onEditItem(); | |
}, | |
), | |
IconButton( | |
icon: const Icon( | |
Icons.delete, | |
color: Colors.white, | |
), | |
onPressed: () { | |
onHideOverlay(); | |
_onDeleteItem(); | |
}, | |
), | |
], | |
), | |
); | |
}, | |
onTap: () { | |
_onViewItem(); | |
}, | |
); | |
} | |
void _onViewItem(){ | |
print('view item: $id'); | |
} | |
void _onEditItem(){ | |
print('edit item: $id'); | |
} | |
void _onDeleteItem(){ | |
print('delete item: $id'); | |
} | |
} | |
/// ----------------------------------------------------------------- | |
/// Widget that accepts an overlay to be displayed on top of itself | |
/// when a LongPress gesture is detected. | |
/// | |
/// Required a specific Overlay higher in the hierarchy to be used | |
/// as a parent | |
/// ----------------------------------------------------------------- | |
typedef OverlayableContainerOnLongPressBuilder(BuildContext context, VoidCallback hideOverlay); | |
class OverlayableContainerOnLongPress extends StatefulWidget { | |
OverlayableContainerOnLongPress({ | |
Key key, | |
@required this.child, | |
@required this.overlayContentBuilder, | |
this.onTap, | |
}): super(key: key); | |
final Widget child; | |
final OverlayableContainerOnLongPressBuilder overlayContentBuilder; | |
final VoidCallback onTap; | |
@override | |
_OverlayableContainerOnLongPressState createState() => _OverlayableContainerOnLongPressState(); | |
} | |
class _OverlayableContainerOnLongPressState extends State<OverlayableContainerOnLongPress> { | |
OverlayEntry _overlayEntry; | |
@override | |
void dispose() { | |
_removeOverlayEntry(); | |
super.dispose(); | |
} | |
void _removeOverlayEntry() { | |
_overlayEntry?.remove(); | |
_overlayEntry = null; | |
} | |
/// | |
/// Returns the position (as a Rect) of an item | |
/// identified by its BuildContext | |
/// | |
Rect _getPosition(BuildContext context) { | |
final RenderBox box = context.findRenderObject() as RenderBox; | |
final Offset topLeft = box.size.topLeft(box.localToGlobal(Offset.zero)); | |
final Offset bottomRight = | |
box.size.bottomRight(box.localToGlobal(Offset.zero)); | |
return Rect.fromLTRB( | |
topLeft.dx, topLeft.dy, bottomRight.dx, bottomRight.dy); | |
} | |
/// | |
/// Displays an OverlayEntry on top of the selected item | |
/// This overlay disappears if we click outside or, on demand | |
/// | |
void _showOverlayOnTopOfItem(BuildContext context) { | |
OverlayState overlayState = Overlay.of(context); | |
final Rect overlayPosition = _getPosition(overlayState.context); | |
// Get the coordinates of the item | |
final Rect widgetPosition = _getPosition(context).translate( | |
-overlayPosition.left, | |
-overlayPosition.top, | |
); | |
// Generate the overlay entry | |
_overlayEntry = OverlayEntry(builder: (BuildContext context) { | |
return GestureDetector( | |
behavior: HitTestBehavior.deferToChild, | |
onTap: () { | |
/// | |
/// Remove the overlay when we tap outside | |
/// | |
_removeOverlayEntry(); | |
}, | |
child: Material( | |
color: Colors.black12, | |
child: CustomSingleChildLayout( | |
delegate: _OverlayableContainerLayout(widgetPosition), | |
child: widget.overlayContentBuilder(context, _removeOverlayEntry), | |
), | |
), | |
); | |
}); | |
// Insert the overlayEntry on the screen | |
overlayState.insert( | |
_overlayEntry, | |
); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return GestureDetector( | |
onTap: () { | |
if (widget.onTap != null){ | |
widget.onTap(); | |
} | |
}, | |
onLongPress: () { | |
_showOverlayOnTopOfItem(context); | |
}, | |
child: widget.child, | |
); | |
} | |
} | |
class _OverlayableContainerLayout extends SingleChildLayoutDelegate { | |
_OverlayableContainerLayout(this.position); | |
final Rect position; | |
@override | |
BoxConstraints getConstraintsForChild(BoxConstraints constraints) { | |
return BoxConstraints.loose(Size(position.width, position.height)); | |
} | |
@override | |
Offset getPositionForChild(Size size, Size childSize) { | |
return Offset(position.left, position.top); | |
} | |
@override | |
bool shouldRelayout(_OverlayableContainerLayout oldDelegate) { | |
return position != oldDelegate.position; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment