Progressive Menu in Flutter (Hide Overflowing Elements in Dropdown) - flutter

I'm building a resizable Flutter desktop app and I'm wondering how it would be possible to automatically hide overflowing items in a menu (e.g. a Row) and making them visible in a "More" dropdown instead.
Conceptual Sample Images (Source: https://css-tricks.com/container-adapting-tabs-with-more-button/)
A lot of width available (shows all items):
Less width available (hides items in dropdown):
Thanks in advance!
Update: I had the idea of using a Wrap-element for it, but then I've had the following problems:
How do I limit the Wrap to only show one line of children? (related to https://github.com/flutter/flutter/issues/65331)
How do I get the info on which of the elements are on line 1 and which are hidden.

Maybe you can try creating a MultiChildRenderObjectWidget in which you can calculate the children size before painting. It's more complicated though because your like making a custom Row class.
I created a sample but it may still contain bugs and need some improvements.
Sample...
threshold
collapsible_menu_bar.dart
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class MenuBar extends StatelessWidget {
MenuBar({
required this.onItemPressed,
required this.children,
this.minItemWidth = 110,
this.minMoreItemsWidth = 70,
Key? key,
}) : super(key: key);
final ValueChanged<int> onItemPressed;
final List<MenuBarItem> children;
final double minItemWidth;
final double minMoreItemsWidth;
static int tmpStartIndex = 0;
#override
Widget build(BuildContext context) {
return CollapsibleMenuBar(
onCollapseIndex: (int startIndex) {
if (tmpStartIndex == startIndex) {
return;
}
tmpStartIndex = startIndex;
},
minItemWidth: minItemWidth,
minMoreItemsWidth: minMoreItemsWidth,
children: [
...children,
PopupMenuButton(
offset: const Offset(0, 40),
color: Colors.red,
child: Container(
height: 40,
padding: const EdgeInsets.symmetric(horizontal: 10),
alignment: Alignment.center,
color: Colors.amber,
child: const Text('More'),
),
itemBuilder: (_) => children
.sublist(tmpStartIndex)
.map((e) => PopupMenuItem(child: e))
.toList(),
),
],
);
}
}
///
///
///
class MenuBarItem extends StatelessWidget {
const MenuBarItem({
required this.onPressed,
required this.text,
Key? key,
}) : super(key: key);
final VoidCallback? onPressed;
final String text;
#override
Widget build(BuildContext context) {
return TextButton(
onPressed: onPressed,
style: TextButton.styleFrom(
backgroundColor: Colors.red,
padding: const EdgeInsets.all(20),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.zero,
),
),
child: Text(
text,
style: const TextStyle(color: Colors.white),
),
);
}
}
///
///
///
class CollapsibleMenuBar extends MultiChildRenderObjectWidget {
CollapsibleMenuBar({
required this.onCollapseIndex,
required List<Widget> children,
required this.minItemWidth,
required this.minMoreItemsWidth,
Key? key,
}) : super(key: key, children: children);
final ValueChanged<int> onCollapseIndex;
final double minItemWidth;
final double minMoreItemsWidth;
#override
RenderObject createRenderObject(BuildContext context) {
return RenderCollapsibleMenuBar(
onCollapseIndex,
minItemWidth,
minMoreItemsWidth,
);
}
}
///
///
///
class CollapsibleMenuBarParentData extends ContainerBoxParentData<RenderBox> {}
///
///
///
class RenderCollapsibleMenuBar extends RenderBox
with
ContainerRenderObjectMixin<RenderBox, CollapsibleMenuBarParentData>,
RenderBoxContainerDefaultsMixin<RenderBox,
CollapsibleMenuBarParentData> {
RenderCollapsibleMenuBar(
this.onCollapseIndex,
this.minItemWidth,
this.minMoreItemsWidth,
);
final ValueChanged<int> onCollapseIndex;
final double minItemWidth;
final double minMoreItemsWidth;
#override
void setupParentData(covariant RenderObject child) {
if (child.parentData is! CollapsibleMenuBarParentData) {
child.parentData = CollapsibleMenuBarParentData();
}
}
#override
void performLayout() {
// Make width of children equal.
final double childWidth = max(
constraints.maxWidth / (childCount - 1),
minItemWidth,
);
double totalWidth = 0;
double totalHeight = 0;
RenderBox? child = firstChild;
Offset childOffset = Offset(0, 0);
int childIdx = 0;
while (child != null && child != lastChild) {
CollapsibleMenuBarParentData childParentData =
child.parentData as CollapsibleMenuBarParentData;
// Set child's dimension.
child.layout(
BoxConstraints(
minWidth: childWidth,
maxWidth: childWidth,
maxHeight: constraints.maxHeight,
),
parentUsesSize: true,
);
// If the total width exceeds the max screen width,
// display "more" item.
if (totalWidth + child.size.width > constraints.maxWidth) {
// Set overflow item dimension to 0.
child.layout(
BoxConstraints(
minWidth: 0,
maxWidth: 0,
maxHeight: constraints.maxHeight,
),
parentUsesSize: true,
);
// Get popup menu item.
child = lastChild!;
childParentData = child.parentData as CollapsibleMenuBarParentData;
// Set popup menu item's dimension. Will cover the remaining width.
child.layout(
BoxConstraints(
minWidth: constraints.maxWidth - totalWidth,
maxWidth: constraints.maxWidth - totalWidth,
maxHeight: constraints.maxHeight,
),
parentUsesSize: true,
);
}
if (child == lastChild) {
// If "more" item's width is below threshold, hide left item.
if (child.size.width <= minMoreItemsWidth) {
childIdx--;
RenderBox nthChild = getChildrenAsList()[childIdx];
// Hide left item of "more" item.
totalWidth -= nthChild.size.width;
childOffset -= Offset(nthChild.size.width, 0);
nthChild.layout(
BoxConstraints(
minWidth: 0,
maxWidth: 0,
maxHeight: constraints.maxHeight,
),
parentUsesSize: true,
);
// Resize "more" item.
child.layout(
BoxConstraints(
minWidth: constraints.maxWidth - totalWidth,
maxWidth: constraints.maxWidth - totalWidth,
maxHeight: constraints.maxHeight,
),
parentUsesSize: true,
);
}
// Update the start index of children to be displayed
// in "more" items.
onCollapseIndex(childIdx);
}
totalWidth += child.size.width;
totalHeight = max(totalHeight, child.size.height);
childParentData.offset = Offset(childOffset.dx, 0);
childOffset += Offset(child.size.width, 0);
if (child != lastChild) {
childIdx++;
}
child = childParentData.nextSibling;
}
// If all children is displayed except for "more" item.
if (childIdx == childCount - 1) {
// Set the layout of popup button to size 0.
lastChild!.layout(BoxConstraints(
minWidth: 0,
maxWidth: 0,
maxHeight: constraints.maxHeight,
));
}
size = Size(totalWidth, totalHeight);
}
#override
void paint(PaintingContext context, Offset offset) {
defaultPaint(context, offset);
}
#override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
return defaultHitTestChildren(result, position: position);
}
}
Usage:
class HomePage extends StatelessWidget {
final List<String> _data = const <String>[
'Falkenberg',
'Braga',
'Stockholm',
'Trnnava',
'Plodiv',
'Klaipeda',
'Punta Cana',
'Lisbon',
];
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(20),
child: MenuBar(
onItemPressed: (int i) {},
children: _data
.map((String data) => MenuBarItem(
onPressed: () => print(data),
text: data,
))
.toList(),
),
),
],
),
);
}
}
Try watching this video to learn more how it works.

Related

Flutter Drag and Drop

I'm facing a problem implementing drag and drop in my project. I want dynamically add and delete draggable elements. The problem is that I can't understand how to get the reference to the widget I'm currently moving so that I can understand which coordinates in the list I have to change.
Here is an example of the code where I use the static number of draggable widgets. They are assigned with coordinates from the list. But what if I have to dynamically add and delete those draggable widgets how can I understand which coordinates to change?
So, I have an array of draggble elements and array of coordinates. Widget with index 0 refers to coordinates with index 0. How can I understand which widget I'm currently moving so that I can get the index of it in array of widgets and then change proper coordinates in array of coordinates.
class DragAndDrop extends StatefulWidget {
const DragAndDrop({
Key? key,
this.width,
this.height,
}) : super(key: key);
final double? width;
final double? height;
#override
_DragAndDropState createState() => _DragAndDropState();
}
class _DragAndDropState extends State<DragAndDrop> {
List<double?> _x = [0.0, 20.0];
List<double?> _y = [0.0, 20.0];
int k = -1;
List<Widget> pel = [];
final GlobalKey stackKey = GlobalKey();
#override
Widget build(BuildContext context) {
pel.add(Container(color: Colors.blue));
k++;
Widget drag = Draggable<int>(
data: k,
child: Icon(
Icons.keyboard_arrow_down,
color: Color(0x95000000),
size: 40,
),
feedback: Icon(
Icons.keyboard_arrow_down,
color: Color.fromRGBO(212, 14, 14, 0.584),
size: 40,
),
childWhenDragging: Container(),
onDragStarted: () {},
onDragEnd: (dragDetails) {
setState(() {
final parentPos = stackKey.globalPaintBounds;
if (parentPos == null) return;
if (dragDetails.offset.dx - parentPos.left < 0)
_x[0] = 0;
else if (dragDetails.offset.dx - parentPos.left >
double.parse(widget.width.toString()) - 40)
_x[0] = double.parse(widget.width.toString()) - 40;
else
_x[0] = dragDetails.offset.dx - parentPos.left;
if (dragDetails.offset.dy - parentPos.top < 0)
_y[0] = 0;
else if (dragDetails.offset.dy - parentPos.top >
double.parse(widget.height.toString()) - 40)
_y[0] = double.parse(widget.height.toString()) - 40;
else
_y[0] = dragDetails.offset.dy - parentPos.top;
});
},
);
pel.add(Positioned(
left: _x[0],
top: _y[0],
child: drag,
));
pel.add(Positioned(
left: _x[1],
top: _y[1],
child: Draggable<int>(
data: k,
child: Icon(
Icons.keyboard_arrow_down,
color: Color(0x95000000),
size: 40,
),
feedback: Icon(
Icons.keyboard_arrow_down,
color: Color.fromRGBO(212, 14, 14, 0.584),
size: 40,
),
childWhenDragging: Container(),
onDragStarted: () {},
onDragEnd: (dragDetails) {
setState(() {
final parentPos = stackKey.globalPaintBounds;
if (parentPos == null) return;
_x[1] = dragDetails.offset.dx - parentPos.left; // 11.
_y[1] = dragDetails.offset.dy - parentPos.top;
});
},
),
));
return Stack(
key: stackKey,
fit: StackFit.expand,
children: pel,
);
}
}
extension GlobalKeyExtension on GlobalKey {
Rect? get globalPaintBounds {
final renderObject = currentContext?.findRenderObject();
var translation = renderObject?.getTransformTo(null).getTranslation();
if (translation != null && renderObject?.paintBounds != null) {
return renderObject!.paintBounds
.shift(Offset(translation.x, translation.y));
} else {
return null;
}
}
}
I tried to use a variable that I can assign to Dragble.data field but I'm not able to get it inside the widget.

Flutter - Show Row on extra scroll - Top of column (Like Whatsapp Archived Chats)

I want to put a row on top of a column, which will not be visible initially. It will only be visible when the scroll offset of the SingleChildScrollview is negative.
In other words, only if the user scrolls further than normal (downwards motion) will this Row show. This is an example in Whatsapp. The "Search Box" widget is not shown initially, only if you scroll up, and disappears once scroll is downwards.
UPDATE
Using #Lulupointu's answer, I was able to get to this:
The top widget shows and hides on scroll with a smooth animation.
#Hooshyar's answer also works but is less smooth and uses a different method.
Using singleChildScrollView here is what I came up with using s NotificationListener() widget, there are other solutions but this one is the simplest one:
have a bool to determine Container visibility:
bool shouldIShowTheUpperThing = false;
the have your SingleChildScrollView() wrapped with a NotificationListener() :
NotificationListener(
child: SingleChildScrollView(
child: Column(
children: [
shouldIShowTheUpperThing == false ? Row(
children: [
Container(height: 0,),
],
) : Row(
children: [
Expanded(child: Container(color: Colors.red , height: 100 , child: Text('the hidden box'),)),
],
),
Container(
padding: EdgeInsets.all(130),
child: Text('data'),
color: Colors.blueGrey,
),
Container(
padding: EdgeInsets.all(130),
child: Text('data'),
color: Colors.blueAccent,
),
Container(
padding: EdgeInsets.all(130),
child: Text('data'),
color: Colors.amber,
),
Container(
padding: EdgeInsets.all(130),
child: Text('data'),
color: Colors.black12,
),
Container(
padding: EdgeInsets.all(130),
child: Text('data'),
),
],
),
),
onNotification: (t) {
if (t is ScrollNotification) {
if (t.metrics.pixels < 1.0) {
setState(() {
shouldIShowTheUpperThing = true;
});
}
}
return true;
});
}
Since this is a unusual scroll effect, if you want it to look good I think you need to use slivers.
Implementation
What I did was copy paste SliverToBoxAdapter and modify it.
Features:
The child is hidden when the widget is first loaded
The child is snappy
The child hide when the user scroll down again
Limitations:
If there are not enough children in the CustomScrollView to over-scroll, the child will always be visible and a weird scroll effect will appear on startup
class SliverHidedHeader extends SingleChildRenderObjectWidget {
const SliverHidedHeader({
Key? key,
required Widget child,
}) : super(key: key, child: child);
#override
RenderObject createRenderObject(BuildContext context) {
return RenderSliverHidedHeader(context: context);
}
}
class RenderSliverHidedHeader extends RenderSliverSingleBoxAdapter {
RenderSliverHidedHeader({
required BuildContext context,
RenderBox? child,
}) : _context = context,
super(child: child);
/// Whether we need to apply a correction to the scroll
/// offset during the next layout
///
///
/// This is useful to avoid the viewport to jump when we
/// insert/remove the child.
///
/// If [showChild] is true, its an insert
/// If [showChild] is false, its a removal
bool _correctScrollOffsetNextLayout = true;
/// Whether [child] should be shown
///
///
/// This is used to hide the child when the user scrolls down
bool _showChild = true;
/// The context is used to get the [Scrollable]
BuildContext _context;
#override
void performLayout() {
if (child == null) {
geometry = SliverGeometry.zero;
return;
}
final SliverConstraints constraints = this.constraints;
child!.layout(constraints.asBoxConstraints(), parentUsesSize: true);
final double childExtent;
switch (constraints.axis) {
case Axis.horizontal:
childExtent = child!.size.width;
break;
case Axis.vertical:
childExtent = child!.size.height;
break;
}
final double paintedChildSize =
calculatePaintOffset(constraints, from: 0.0, to: childExtent);
final double cacheExtent = calculateCacheOffset(constraints, from: 0.0, to: childExtent);
assert(paintedChildSize.isFinite);
assert(paintedChildSize >= 0.0);
// Here are the few custom lines, which use [scrollOffsetCorrection]
// to remove the child size
//
// Note that this should only be called for correction linked with the
// insertion (NOT the removal)
if (_correctScrollOffsetNextLayout) {
geometry = SliverGeometry(scrollOffsetCorrection: childExtent);
_correctScrollOffsetNextLayout = false;
return;
}
// Subscribe a listener to the scroll notifier
// which will snap if needed
_manageSnapEffect(
childExtent: childExtent,
paintedChildSize: paintedChildSize,
);
// Subscribe a listener to the scroll notifier
// which hide the child if needed
_manageInsertChild(
childExtent: childExtent,
paintedChildSize: paintedChildSize,
);
geometry = SliverGeometry(
scrollExtent: childExtent,
paintExtent: paintedChildSize,
paintOrigin: _showChild ? 0 : -paintedChildSize,
layoutExtent: _showChild ? null : 0,
cacheExtent: cacheExtent,
maxPaintExtent: childExtent,
hitTestExtent: paintedChildSize,
hasVisualOverflow:
childExtent > constraints.remainingPaintExtent || constraints.scrollOffset > 0.0,
);
setChildParentData(child!, constraints, geometry!);
}
/// Override to remove the listeners if needed
#override
void dispose() {
final _scrollPosition = Scrollable.of(_context)!.position;
if (_subscribedSnapScrollNotifierListener != null) {
_scrollPosition.isScrollingNotifier
.removeListener(_subscribedSnapScrollNotifierListener!);
}
if (_subscribedInsertChildScrollNotifierListener != null) {
_scrollPosition.isScrollingNotifier
.removeListener(_subscribedInsertChildScrollNotifierListener!);
}
super.dispose();
}
/// The listener which will snap if needed
///
///
/// We store it to be able to remove it before subscribing
/// a new one
void Function()? _subscribedSnapScrollNotifierListener;
/// Handles the subscription and removal of subscription to
/// the scrollable position notifier which are responsible
/// for the snapping effect
///
///
/// This must be called at each [performLayout] to ensure that the
/// [childExtent] and [paintedChildSize] parameters are up to date
_manageSnapEffect({
required double childExtent,
required double paintedChildSize,
}) {
final _scrollPosition = Scrollable.of(_context)!.position;
// If we were subscribed with previous value, remove the subscription
if (_subscribedSnapScrollNotifierListener != null) {
_scrollPosition.isScrollingNotifier
.removeListener(_subscribedSnapScrollNotifierListener!);
}
// We store the subscription to be able to remove it
_subscribedSnapScrollNotifierListener = () => _snapScrollNotifierListener(
childExtent: childExtent,
paintedChildSize: paintedChildSize,
);
_scrollPosition.isScrollingNotifier.addListener(_subscribedSnapScrollNotifierListener!);
}
/// Snaps if the user just stopped scrolling and the child is
/// partially visible
void _snapScrollNotifierListener({
required double childExtent,
required double paintedChildSize,
}) {
final _scrollPosition = Scrollable.of(_context)!.position;
// Whether the user is currently idle (i.e not scrolling)
//
// We don't check _scrollPosition.activity.isScrolling or
// _scrollPosition.isScrollingNotifier.value because even if
// the user is holding still we don't want to start animating
//
// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
final isIdle = _scrollPosition.activity is IdleScrollActivity;
// Whether at least part of the child is visible
final isChildVisible = paintedChildSize > 0;
if (isIdle && isChildVisible) {
// If more than half is visible, snap to see everything
if (paintedChildSize >= childExtent / 2 && paintedChildSize != childExtent) {
_scrollPosition.animateTo(
0,
duration: Duration(milliseconds: 100),
curve: Curves.easeOut,
);
}
// If less than half is visible, snap to hide
else if (paintedChildSize < childExtent / 2 && paintedChildSize != 0) {
_scrollPosition.animateTo(
childExtent,
duration: Duration(milliseconds: 200),
curve: Curves.easeOut,
);
}
}
}
/// The listener which will hide the child if needed
///
///
/// We store it to be able to remove it before subscribing
/// a new one
void Function()? _subscribedInsertChildScrollNotifierListener;
/// Handles the subscription and removal of subscription to
/// the scrollable position notifier which are responsible
/// for inserting/removing the child if needed
///
///
/// This must be called at each [performLayout] to ensure that the
/// [childExtent] and [paintedChildSize] parameters are up to date
void _manageInsertChild({
required double childExtent,
required double paintedChildSize,
}) {
final _scrollPosition = Scrollable.of(_context)!.position;
// If we were subscribed with previous value, remove the subscription
if (_subscribedInsertChildScrollNotifierListener != null) {
_scrollPosition.isScrollingNotifier
.removeListener(_subscribedInsertChildScrollNotifierListener!);
}
// We store the subscription to be able to remove it
_subscribedInsertChildScrollNotifierListener = () => _insertChildScrollNotifierListener(
childExtent: childExtent,
paintedChildSize: paintedChildSize,
);
_scrollPosition.isScrollingNotifier
.addListener(_subscribedInsertChildScrollNotifierListener!);
}
/// When [ScrollPosition.isScrollingNotifier] fires:
/// - If the viewport is at the top and the child is not visible,
/// ^ insert the child
/// - If the viewport is NOT at the top and the child is NOT visible,
/// ^ remove the child
void _insertChildScrollNotifierListener({
required double childExtent,
required double paintedChildSize,
}) {
final _scrollPosition = Scrollable.of(_context)!.position;
final isScrolling = _scrollPosition.isScrollingNotifier.value;
// If the user is still scrolling, do nothing
if (isScrolling) {
return;
}
final scrollOffset = _scrollPosition.pixels;
// If the viewport is at the top and the child is not visible,
// insert the child
//
// We use 0.1 as a small value in case the user is nearly scrolled
// all the way up
if (!_showChild && scrollOffset <= 0.1) {
_showChild = true;
_correctScrollOffsetNextLayout = true;
markNeedsLayout();
}
// There is sometimes an issue with [ClampingScrollPhysics] where
// the child is NOT shown but the scroll offset still includes [childExtent]
//
// There is no why to detect it but we always insert the child when all
// this conditions are united.
// This means that if a user as [ClampingScrollPhysics] and stops scrolling
// exactly at [childExtent], the child will be wrongfully inserted. However
// this seems a small price to pay to avoid the issue.
if (_scrollPosition.physics.containsScrollPhysicsOfType<ClampingScrollPhysics>()) {
if (!_showChild && scrollOffset == childExtent) {
_showChild = true;
markNeedsLayout();
}
}
// If the viewport is NOT at the top and the child is NOT visible,
// remove the child
if (_showChild && scrollOffset > childExtent) {
_showChild = false;
markNeedsLayout();
// We don't have to correct the scroll offset here, no idea why
}
}
}
/// An extension on [ScrollPhysics] to check if it or its
/// parent are the given [ScrollPhysics]
extension _ScrollPhysicsExtension on ScrollPhysics {
/// Check the type of this [ScrollPhysics] and its parents and return
/// true if any is of type [T]
bool containsScrollPhysicsOfType<T extends ScrollPhysics>() {
return this is T || (parent?.containsScrollPhysicsOfType<T>() ?? false);
}
}
How to use it
Use it at the top of your list of slivers:
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(MaterialApp(home: MyStatefulWidget()));
}
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({Key? key}) : super(key: key);
#override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverHidedHeader(
child: Container(
child: Center(child: Text('SliverAppBar')),
height: 100,
color: Colors.redAccent,
),
),
const SliverToBoxAdapter(
child: SizedBox(
height: 20,
child: Center(
child: Text('Scroll to see the SliverAppBar in effect.'),
),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
color: index.isOdd ? Colors.white : Colors.black12,
height: 100.0,
child: Center(
child: Text('$index', textScaleFactor: 5),
),
);
},
childCount: 20,
),
),
],
),
),
);
}
}
Other resource
Check out the pull_to_refresh package if you want a different approach to solve this issue. Beware that their code is quite complex since they also have the auto-hide feature implemented, but it's worth a look if you have time.
Solve the limitation
I'm not sure the limitation can be solved using this approach. The issue is that, for performance reason, the sliver does not know anything about the one bellow it, meaning that it's quite hard to even knowing when we are in the problematic case, let alone handle it.
try SliverAppBar and set floating and snap named params to true
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({Key? key}) : super(key: key);
#override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
const SliverAppBar(
pinned: true,
snap: true,
floating: true,
expandedHeight: 160.0,
flexibleSpace: FlexibleSpaceBar(
title: Text('SliverAppBar'),
background: FlutterLogo(),
),
),
const SliverToBoxAdapter(
child: SizedBox(
height: 20,
child: Center(
child: Text('Scroll to see the SliverAppBar in effect.'),
),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
color: index.isOdd ? Colors.white : Colors.black12,
height: 100.0,
child: Center(
child: Text('$index', textScaleFactor: 5),
),
);
},
childCount: 20,
),
),
],
),
);
}
}

Mimic iOS contact form AppBar

I'm trying to mimic iOS contact form app bar.
expanded
collapsed
Here is where I get so far
Main Screen
class CompanyScreen extends StatefulWidget {
#override
_CompanyScreenState createState() => _CompanyScreenState();
}
class _CompanyScreenState extends State<CompanyScreen> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverPersistentHeader(
pinned: true,
floating: true,
delegate: SafeAreaPersistentHeaderDelegate(
expandedHeight: 200,
flexibleSpace:
SafeArea(child: Image.asset('assets/images/user.png'))),
),
SliverList(
delegate: SliverChildListDelegate([
TextField(),
]),
)
],
),
);
}
}
SliverHeader
class SafeAreaPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
final Widget title;
final Widget flexibleSpace;
final double expandedHeight;
SafeAreaPersistentHeaderDelegate(
{this.title, this.flexibleSpace, this.expandedHeight});
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
final Widget appBar = FlexibleSpaceBar.createSettings(
minExtent: minExtent,
maxExtent: maxExtent,
currentExtent: max(minExtent, maxExtent - shrinkOffset),
toolbarOpacity: 1,
child: AppBar(
actions: <Widget>[
Container(
height: 60,
child: FlatButton(
child: Text('Done'),
),
)
],
backgroundColor: Colors.blue,
automaticallyImplyLeading: false,
title: title,
flexibleSpace: (title == null && flexibleSpace != null)
? Semantics(child: flexibleSpace, header: true)
: flexibleSpace,
centerTitle: true,
toolbarOpacity: 1,
bottomOpacity: 1.0),
);
return appBar;
}
#override
double get maxExtent => expandedHeight;
#override
double get minExtent => 80;
#override
bool shouldRebuild(SafeAreaPersistentHeaderDelegate old) {
if (old.flexibleSpace != flexibleSpace) {
return true;
}
return false;
}
}
UPDATE: It all works but I have a problem add the text under the image (Add Photo) and make that text disappear when collapsed. With this solution, if I wrap the image into a column then image expands overflow and doesn't scale.
Requirements:
AppBar and the flex area must be in safe area
Widget with image must have text at the bottom which can be changed dynamically (Add image or Change image) and it must be clickable
The text under the image area must disappear when flex area is collapsed with some transition
Ability to add title in app bar lined up with action buttons
When title in app bar is provided then flex area should scale bellow the title, if not flex area should scale into the title area as on the above image
Any help with this greatly appreciated
I gave it a try.. I'm not an expert on slivers so this solution might not be perfect. I have taken your code as starting point. The column seems to deactivate all scaling so I scaled all manually.
here is your app bar
UPDATE I have tweaked it a little so it feels more like iOS app bar plus I've added extra feature
import 'dart:math';
import 'package:flutter/material.dart';
double _defaultTextHeight = 14;
double _defaultTextPadding = 5;
double _defaultAppBarHeight = 60;
double _defaultMinAppBarHeight = 40;
double _unknownTextValue = 1;
class AppBarSliverHeader extends SliverPersistentHeaderDelegate {
final String title;
final double expandedHeight;
final double safeAreaPadding;
final Widget flexibleImage;
final double flexibleSize;
final String flexibleTitle;
final double flexiblePadding;
final bool flexToTop;
final Function onTap;
final Widget rightButton;
final Widget leftButton;
AppBarSliverHeader(
{this.title,
this.onTap,
this.flexibleImage,
#required this.expandedHeight,
#required this.safeAreaPadding,
this.flexibleTitle = '',
this.flexToTop = false,
this.leftButton,
this.rightButton,
this.flexibleSize = 30,
this.flexiblePadding = 4});
double _textPadding(double shrinkOffset) {
return _defaultTextPadding * _scaleFactor(shrinkOffset);
}
double _widgetPadding(double shrinkOffset) {
double offset;
if (title == null) {
offset = _defaultMinAppBarHeight * _scaleFactor(shrinkOffset);
} else {
if (flexToTop) {
offset = _defaultAppBarHeight * _scaleFactor(shrinkOffset);
} else {
offset = (_defaultAppBarHeight - _defaultMinAppBarHeight) *
_scaleFactor(shrinkOffset) +
_defaultMinAppBarHeight;
}
}
return offset;
}
double _topOffset(double shrinkOffset) {
double offset;
if (title == null) {
offset = safeAreaPadding +
(_defaultMinAppBarHeight * _scaleFactor(shrinkOffset));
} else {
if (flexToTop) {
offset = safeAreaPadding +
(_defaultAppBarHeight * _scaleFactor(shrinkOffset));
} else {
offset = safeAreaPadding +
((_defaultAppBarHeight - _defaultMinAppBarHeight) *
_scaleFactor(shrinkOffset)) +
_defaultMinAppBarHeight;
}
}
return offset;
}
double _calculateWidgetHeight(double shrinkOffset) {
double actualTextHeight = _scaleFactor(shrinkOffset) * _defaultTextHeight +
_textPadding(shrinkOffset) +
_unknownTextValue;
final padding = title == null
? (2 * flexiblePadding)
: flexToTop ? (2 * flexiblePadding) : flexiblePadding;
final trueMinExtent = minExtent - _topOffset(shrinkOffset);
final trueMaxExtent = maxExtent - _topOffset(shrinkOffset);
double minWidgetSize =
trueMinExtent - padding;
double widgetHeight =
((trueMaxExtent - actualTextHeight) - shrinkOffset) - padding;
return widgetHeight >= minWidgetSize ? widgetHeight : minWidgetSize;
}
double _scaleFactor(double shrinkOffset) {
final ratio = (maxExtent - minExtent) / 100;
double percentageHeight = shrinkOffset / ratio;
double limitedPercentageHeight =
percentageHeight >= 100 ? 100 : percentageHeight;
return 1 - (limitedPercentageHeight / 100);
}
Widget _builtContent(BuildContext context, double shrinkOffset) {
_topOffset(shrinkOffset);
return SafeArea(
bottom: false,
child: Semantics(
child: Padding(
padding: title == null
? EdgeInsets.symmetric(vertical: flexiblePadding)
: flexToTop
? EdgeInsets.symmetric(vertical: flexiblePadding)
: EdgeInsets.only(bottom: flexiblePadding),
child: GestureDetector(
onTap: onTap,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
LimitedBox(
maxWidth: _calculateWidgetHeight(shrinkOffset),
maxHeight: _calculateWidgetHeight(shrinkOffset),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(
_calculateWidgetHeight(shrinkOffset))),
color: Colors.white),
child: ClipRRect(
borderRadius: BorderRadius.circular(
_calculateWidgetHeight(shrinkOffset)),
child: flexibleImage,
),
)),
Padding(
padding: EdgeInsets.only(top: _textPadding(shrinkOffset)),
child: Text(
flexibleTitle,
textScaleFactor: _scaleFactor(shrinkOffset),
style: TextStyle(
fontSize: _defaultTextHeight,
color: Colors.white
.withOpacity(_scaleFactor(shrinkOffset)), height: 1),
),
)
],
),
),
),
button: true,
),
);
}
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
final Widget appBar = FlexibleSpaceBar.createSettings(
minExtent: minExtent,
maxExtent: maxExtent,
currentExtent: max(minExtent, maxExtent - shrinkOffset),
toolbarOpacity: 1,
child: AppBar(
actions: <Widget>[rightButton == null ? Container() : rightButton],
leading: leftButton == null ? Container() : leftButton,
backgroundColor: Colors.blue,
automaticallyImplyLeading: false,
title: title != null
? Text(
title,
style: TextStyle(
color: flexToTop
? Colors.white.withOpacity(_scaleFactor(shrinkOffset))
: Colors.white),
)
: null,
flexibleSpace: Padding(
padding: EdgeInsets.only(top: _widgetPadding(shrinkOffset)),
child: _builtContent(context, shrinkOffset),
),
centerTitle: true,
toolbarOpacity: 1,
bottomOpacity: 1.0),
);
return appBar;
}
#override
double get maxExtent => expandedHeight + safeAreaPadding;
#override
double get minExtent => title == null
? _defaultAppBarHeight + safeAreaPadding
: flexToTop
? _defaultAppBarHeight + safeAreaPadding
: _defaultAppBarHeight + safeAreaPadding + flexibleSize;
#override
bool shouldRebuild(AppBarSliverHeader old) {
if (old.flexibleImage != flexibleImage) {
return true;
}
return false;
}
}
and here is usage
Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverPersistentHeader(
pinned: true,
floating: true,
delegate: AppBarSliverHeader(
expandedHeight: 250,
safeAreaPadding: MediaQuery.of(context).padding.top,
title: 'New Contact',
flexibleImage: Image.asset('assets/images/avatar.png'),
flexibleTitle: 'Add Image',
flexiblePadding: 6,
flexibleSize: 50,
flexToTop: true,
onTap: () {
print('hello');
},
leftButton: IconButton(
icon: Text('Cancel'),
iconSize: 60,
padding: EdgeInsets.zero,
onPressed: () {},
),
rightButton: IconButton(
icon: Text('Done'),
iconSize: 60,
padding: EdgeInsets.zero,
onPressed: () {},
)),
),
SliverList(
delegate: SliverChildListDelegate([
TextField(),
]),
)
],
),
);
There are some things which took me by surprise as well. First is text size. It seems like text size is not an actual text size so I've added _unknownTextValue there for compensation. Also even if text size is set to 0 then the Text widget has still 1px size so I've compensated that in commented code. Another thing is I wanted to use CircularAvatar for the image but apparently the CircularAvatar widget has built in animation when changing the size which interfere with app bar animation so I've built custom avatar.
UPDATE: To make actual text height same as font size, I have added height property 1 to TextStyle. It seems to work however there is still occasional overflow on the textfield of up to 1px so I've kept _unknownTextValue at 1px
As I said I'm not sliver expert so there might be a better solutions out there so I would suggest you to wait for other answers
NOTE: I only tested it on 2 iOS devices so you should test further to use it
With Title
Without Title
With Title and flexToTop activated

Flutter: Drag Draggable Stack item inside a Draggable Stack Item

I have a Draggable on a DragTarget as part of a Stack. Inside is another Stack with Draggables, again on DragTargets and so on... (Stack over Stack over Stack etc.).
The Draggable is a Positioned with a Listener telling where to be placed.
homeView.dart
body: Stack(children: [
DraggableWidget(parentKey, Offset(0, 0)),
]),
draggableWidget.dart
class DraggableWidget extends StatefulWidget {
final Key itemKey;
final Offset itemPosition;
DraggableWidget(this.itemKey, this.itemPosition);
#override
_DraggableWidgetState createState() => _DraggableWidgetState();
}
class _DraggableWidgetState extends State<DraggableWidget> {
Offset tempDelta = Offset(0, 0);
Window<List<Key>> item;
List<DraggableWidget> childList = [];
Map<Key, Window<List>> structureMap;
initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
structureMap = Provider.of<Data>(context).structureMap;
if (structureMap[widget.itemKey] != null) {
structureMap[widget.itemKey].childKeys.forEach(
(k) => childList.add(
DraggableWidget(k, item.position),
),
);
} else {
structureMap[widget.itemKey] = Window<List<Key>>(
title: 'App',
key: widget.itemKey,
size: Size(MediaQuery.of(context).size.width,
MediaQuery.of(context).size.height),
position: Offset(0, 0),
color: Colors.blue,
childKeys: []);
}
item = Provider.of<Data>(context).structureMap[widget.itemKey];
return Positioned(
top: item.position.dx,
left: item.position.dy,
child: DragTarget(
builder:
(buildContext, List<Window<List<Key>>> candidateData, rejectData) {
return Listener(
onPointerDown: (PointerDownEvent event) {},
onPointerUp: (PointerUpEvent event) {
setState(() {
item.position = Offset(item.position.dx + tempDelta.dx,
item.position.dy + tempDelta.dy);
tempDelta = Offset(0, 0);
});
},
onPointerMove: (PointerMoveEvent event) {
tempDelta = Offset((event.delta.dy + tempDelta.dx),
(event.delta.dx + tempDelta.dy));
},
child: Draggable(
childWhenDragging: Container(),
feedback: Container(
color: item.color,
height: item.size.height,
width: item.size.width,
),
child: Column(children: [
Text(item.title),
Container(
color: item.color,
height: item.size.height,
width: item.size.width,
child: ItemStackBuilder(widget.itemKey, item.position),
),
]),
data: item),
);
},
),
);
}
}
itemStackBuilder.dart
class ItemStackBuilder extends StatelessWidget {
final Key itemKey;
final Offset itemPosition;
ItemStackBuilder(this.itemKey, this.itemPosition);
#override
Widget build(BuildContext context) {
Map<Key, Window<List<Key>>> structureMap =
Provider.of<Data>(context).structureMap;
if (structureMap[itemKey] == null) {
structureMap[itemKey] = Window(size: Size(20, 20), childKeys: []);
}
return Stack(overflow: Overflow.visible, children: [
...stackItems(context),
Container(
height: structureMap[itemKey].size.height,
width: structureMap[itemKey].size.width,
color: Colors.transparent),
]);
}
List<Widget> stackItems(BuildContext context) {
List<Key> childKeyList =
Provider.of<Data>(context).structureMap[itemKey].childKeys;
var stackItemDraggable;
List<Widget> stackItemsList = [];
if (childKeyList == null || childKeyList.length < 1) {
stackItemsList = [Container()];
} else {
for (int i = 0; i < childKeyList.length; i++) {
stackItemDraggable = DraggableWidget(childKeyList[i], itemPosition);
stackItemsList.add(stackItemDraggable);
}
}
return stackItemsList;
}
}
When I want to move the Draggable item on top, the underlying Stack moves.
I tried it with a Listener widget and was able to detect all RenderBoxes inside the Stack.
But how can I select the specific Draggable and/or disable all the other layers? Is it a better idea to forget about Draggables and do it all with Positioned and GestureDetector?
Ok, it was my mistake not of the framework:
on itemStackBuilder.dart I used an additional Container to size the Stack. I was not able to recognise, because color was transparent:
Container(
height: structureMap[itemKey].size.height,
width: structureMap[itemKey].size.width,
color: Colors.transparent),
]);
}
After deleting this part, all works fine for now.

In Flutter, how can a positioned Widget feel taps outside of its parent Stack area?

A Stack contains MyWidget inside of a Positioned.
Stack(
overflow: Overflow.visible,
children: [
Positioned(
top: 0.0,
left: 0.0,
child: MyWidget(),
)],
);
Since overflow is Overflow.visible and MyWidget is larger than the Stack, it displays outside of the Stack, which is what I want.
However, I can't tap in the area of MyWidget which is outside of the Stack area. It simply ignores the tap there.
How can I make sure MyWidget accepts gestures there?
This behavior occurs because the stack checks whether the pointer is inside its bounds before checking whether a child got hit:
Class: RenderBox (which RenderStack extends)
bool hitTest(BoxHitTestResult result, { #required Offset position }) {
...
if (_size.contains(position)) {
if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
}
return false;
}
My workaround is deleting the
if (_size.contains(position))
check.
Unfortunately, this is not possible without copying code from the framework.
Here is what I did:
Copied the Stack class and named it Stack2
Copied RenderStack and named it RenderStack2
Made Stack2 reference RenderStack2
Added the hitTest method from above without the _size.contains check
Copied Positioned and named it Positioned2 and made it reference Stack2 as its generic parameter
Used Stack2 and Positioned2 in my code
This solution is by no means optimal, but it achieves the desired behavior.
I had a similar issue. Basically since the stack's children don't use the fully overflown box size for their hit testing, i used a nested stack and an arbitrary big height so that i can capture the clicks of the nested stack's overflown boxes. Not sure if it can work for you but here goes nothing :)
So in your example maybe you could try something like that
Stack(
clipBehavior: Clip.none,
children: [
Positioned(
top: 0.0,
left: 0.0,
height : 500.0 // biggest possible child size or just very big
child: Stack(
children: [MyWidget()]
),
)],
);
You can consider using inheritance to copy the hitTest method to break the hit rule, example
class Stack2 extends Stack {
Stack2({
Key key,
AlignmentGeometry alignment = AlignmentDirectional.topStart,
TextDirection textDirection,
StackFit fit = StackFit.loose,
Overflow overflow = Overflow.clip,
List<Widget> children = const <Widget>[],
}) : super(
key: key,
alignment: alignment,
textDirection: textDirection,
fit: fit,
overflow: overflow,
children: children,
);
#override
RenderStack createRenderObject(BuildContext context) {
return RenderStack2(
alignment: alignment,
textDirection: textDirection ?? Directionality.of(context),
fit: fit,
overflow: overflow,
);
}
}
class RenderStack2 extends RenderStack {
RenderStack2({
List<RenderBox> children,
AlignmentGeometry alignment = AlignmentDirectional.topStart,
TextDirection textDirection,
StackFit fit = StackFit.loose,
Overflow overflow = Overflow.clip,
}) : super(
children: children,
alignment: alignment,
textDirection: textDirection,
fit: fit,
overflow: overflow,
);
#override
bool hitTest(BoxHitTestResult result, {Offset position}) {
if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
return false;
}
}
Ok, I did a workaround about this, basically I added a GestureDetector on the parent and implemented the onTapDown.
Also you have to keep track your Widget using GlobalKey to get the current position.
When the Tap at the parent level is detected check if the tap position is inside your widget.
The code below:
final GlobalKey key = new GlobalKey();
void onTapDown(BuildContext context, TapDownDetails details) {
final RenderBox box = context.findRenderObject();
final Offset localOffset = box.globalToLocal(details.globalPosition);
final RenderBox containerBox = key.currentContext.findRenderObject();
final Offset containerOffset = containerBox.localToGlobal(localOffset);
final onTap = containerBox.paintBounds.contains(containerOffset);
if (onTap){
print("DO YOUR STUFF...");
}
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (TapDownDetails details) => onTapDown(context, details),
child: Container(
color: Colors.red,
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: 200.0,
height: 400.0,
child: Container(
color: Colors.black,
child: Stack(
overflow: Overflow.visible,
children: [
Positioned(
top: 0.0, left: 0.0,
child: Container(
key: key,
width: 500.0,
height: 200.0,
color: Colors.blue,
),
),
],
),
),
),
),
),
);
}
This limitation can be worked around by using an OverlayEntry widget as the Stack's parent (since OverlayEntry fills up the entire screen all children are also hit tested). Here is a proof of concept solution on DartPad.
Create a custom widget that returns a Future:
Widget build(BuildContext context) {
Future(showOverlay);
return Container();
}
This future should then remove any previous instance of OverlayEntry and insert the Stack with your custom widgets:
void showOverlay() {
hideOverlay();
RenderBox? renderBox = context.findAncestorRenderObjectOfType<RenderBox>();
var parentSize = renderBox!.size;
var parentPosition = renderBox.localToGlobal(Offset.zero);
overlay = _overlayEntryBuilder(parentPosition, parentSize);
Overlay.of(context)!.insert(overlay!);
}
void hideOverlay() {
overlay?.remove();
}
Use a builder function to generate the Stack:
OverlayEntry _overlayEntryBuilder(Offset parentPosition, Size parentSize) {
return OverlayEntry(
maintainState: false,
builder: (context) {
return Stack(
clipBehavior: Clip.none,
children: [
Positioned(
left: parentPosition.dx + parentSize.width,
top: parentPosition.dy + parentSize.height,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {},
child: Container(),
),
),
),
],
);
},
);
}
Column is another Stack
key point:
verticalDirection is up.
transform down the top widget.
Below is my code, you can copy and test:
Column(
verticalDirection: VerticalDirection.up,
children: [
Container(
width: 200,
height: 100,
color: Colors.red,
),
Transform.translate(
offset: const Offset(0, 30),
child: GestureDetector(
onTap: () {
print('tap orange view');
},
child: Container(
width: 60,
height: 60,
color: Colors.orange,
),
),
),
],
),
I write a container to resolve this problem, which not implements beautifully, but can be used and did code encapsulation for easily to use.
Here is the implement:
import 'package:flutter/widgets.dart';
/// Creates a widget that can check its' overflow children's hitTest
///
/// [overflowKeys] is must, and there should be used on overflow widget's outermost widget those' sizes cover the overflow child, because it will [hitTest] its' children, but not [hitTest] its' parents. And i cannot found a way to check RenderBox's parent in flutter.
///
/// The [OverflowWithHitTest]'s size must contains the overflow widgets, so you can use it as outer as possible.
///
/// This will not reduce rendering performance, because it only overcheck the given widgets marked by [overflowKeys].
///
/// Demo:
///
/// class MyPage extends State<UserCenterPage> {
///
/// var overflowKeys = <GlobalKey>[GlobalKey()];
///
/// Widget build(BuildContext context) {
/// return Scaffold(
/// body: OverflowWithHitTest(
///
/// overflowKeys: overflowKeys,
///
/// child: Container(
/// height: 50,
/// child: UnconstrainedBox(
/// child: Container(
/// width: 200,
/// height: 50,
/// color: Colors.red,
/// child: OverflowBox(
/// alignment: Alignment.topLeft,
/// minWidth: 100,
/// maxWidth: 200,
/// minHeight: 100,
/// maxHeight: 200,
/// child: GestureDetector(
/// key: overflowKeys[0],
/// behavior: HitTestBehavior.translucent,
/// onTap: () {
/// print('==== onTap;');
/// },
/// child: Container(
/// color: Colors.blue,
/// height: 200,
/// child: Text('aaaa'),
/// ),
/// ),
/// ),
/// ),
/// ),
/// ),
/// ),
/// );
/// }
/// }
///
///
class OverflowWithHitTest extends SingleChildRenderObjectWidget {
const OverflowWithHitTest({
required this.overflowKeys,
Widget? child,
Key? key,
}) : super(key: key, child: child);
final List<GlobalKey> overflowKeys;
#override
_OverflowWithHitTestBox createRenderObject(BuildContext context) {
return _OverflowWithHitTestBox(overflowKeys: overflowKeys);
}
#override
void updateRenderObject(
BuildContext context, _OverflowWithHitTestBox renderObject) {
renderObject.overflowKeys = overflowKeys;
}
#override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(
DiagnosticsProperty<List<GlobalKey>>('overflowKeys', overflowKeys));
}
}
class _OverflowWithHitTestBox extends RenderProxyBoxWithHitTestBehavior {
_OverflowWithHitTestBox({required List<GlobalKey> overflowKeys})
: _overflowKeys = overflowKeys,
super(behavior: HitTestBehavior.translucent);
/// Global keys of overflow children
List<GlobalKey> get overflowKeys => _overflowKeys;
List<GlobalKey> _overflowKeys;
set overflowKeys(List<GlobalKey> value) {
var changed = false;
if (value.length != _overflowKeys.length) {
changed = true;
} else {
for (var ind = 0; ind < value.length; ind++) {
if (value[ind] != _overflowKeys[ind]) {
changed = true;
}
}
}
if (!changed) {
return;
}
_overflowKeys = value;
markNeedsPaint();
}
#override
bool hitTest(BoxHitTestResult result, {required Offset position}) {
if (hitTestOverflowChildren(result, position: position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
bool hitTarget = false;
if (size.contains(position)) {
hitTarget =
hitTestChildren(result, position: position) || hitTestSelf(position);
if (hitTarget || behavior == HitTestBehavior.translucent)
result.add(BoxHitTestEntry(this, position));
}
return hitTarget;
}
bool hitTestOverflowChildren(BoxHitTestResult result,
{required Offset position}) {
if (overflowKeys.length == 0) {
return false;
}
var hitGlobalPosition = this.localToGlobal(position);
for (var child in overflowKeys) {
if (child.currentContext == null) {
continue;
}
var renderObj = child.currentContext!.findRenderObject();
if (renderObj == null || renderObj is! RenderBox) {
continue;
}
var localPosition = renderObj.globalToLocal(hitGlobalPosition);
if (renderObj.hitTest(result, position: localPosition)) {
return true;
}
}
return false;
}
}