Last active
May 1, 2024 04:47
-
-
Save boeledi/bc852dea6ed7f65f99a5e121168eb40d 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(const Application()); | |
} | |
class Application extends StatelessWidget { | |
const Application({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return const MaterialApp( | |
title: 'Application', | |
debugShowCheckedModeBanner: false, | |
home: Page(), | |
); | |
} | |
} | |
const int _kNumberOfItems = 50; | |
const int _kNumberOfItemsPerRow = 2; | |
class Page extends StatelessWidget { | |
const Page({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return SafeArea( | |
child: Scaffold( | |
appBar: AppBar( | |
title: const Text('Application title'), | |
), | |
bottomNavigationBar: BottomNavigationBar( | |
currentIndex: 0, // this will be set when a tab is tapped | |
items: const [ | |
BottomNavigationBarItem( | |
icon: Icon(Icons.home), | |
label: 'Home', | |
), | |
BottomNavigationBarItem( | |
icon: Icon(Icons.mail), | |
label: 'Messages', | |
), | |
BottomNavigationBarItem( | |
icon: Icon(Icons.person), | |
label: 'Profile', | |
) | |
], | |
), | |
body: 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: | |
const 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 { | |
const ItemWidget({ | |
super.key, | |
required this.id, | |
required this.color, | |
}); | |
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: const 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() { | |
debugPrint('view item: $id'); | |
} | |
void _onEditItem() { | |
debugPrint('edit item: $id'); | |
} | |
void _onDeleteItem() { | |
debugPrint('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 = Function( | |
BuildContext context, VoidCallback hideOverlay); | |
class OverlayableContainerOnLongPress extends StatefulWidget { | |
const OverlayableContainerOnLongPress({ | |
super.key, | |
required this.child, | |
required this.overlayContentBuilder, | |
this.onTap, | |
}); | |
final Widget child; | |
final OverlayableContainerOnLongPressBuilder overlayContentBuilder; | |
final VoidCallback? onTap; | |
@override | |
State<OverlayableContainerOnLongPress> 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: () { | |
widget.onTap?.call(); | |
}, | |
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