Flutter: Drag Draggable Stack item inside a Draggable Stack Item - flutter

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.

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.

ScrollController attached to multiple scroll views. flutter

I try to rebuild instagram stories by using GetX. I always receive this issue.
Can anyone help me solve this problem?
ScrollController attached to multiple scroll views.
'package:flutter/src/widgets/scroll_controller.dart':
Failed assertion: line 109 pos 12: '_positions.length == 1'
I try to rebuild instagram stories by using GetX. I always receive this issue.
Can anyone help me solve this problem?
ScrollController attached to multiple scroll views.
'package:flutter/src/widgets/scroll_controller.dart':
Failed assertion: line 109 pos 12: '_positions.length == 1'
import 'package:flamingo/Business_Logic/GetXControllers/Pages_Controllers/Stories_Controller.dart';
import 'package:flamingo/Data/DataProviders/StoriesList.dart';
import 'package:flamingo/Data/Models/All_Models.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class Stories extends StatelessWidget {
#override
Widget build(BuildContext context) {
return GetBuilder<StoriesController>(
init: StoriesController(),
builder: (storiesCtrl) {
return Scaffold(
backgroundColor: Colors.black,
body: GestureDetector(
onTapDown: (details) => storiesCtrl.onTapDown(
details, story[storiesCtrl.currentIndex.value]),
child: Stack(
children: <Widget>[
PageView.builder(
controller: storiesCtrl.pageC,
physics: NeverScrollableScrollPhysics(),
itemBuilder: (context, i) {
final StoryModel s = story[i];
switch (s.media) {
case MediaType.image:
return Image.network(
s.image,
fit: BoxFit.cover,
);
}
return const SizedBox.shrink();
},
),
Positioned(
top: 15.0,
left: 10.0,
right: 10.0,
child: Row(
children: story
.asMap()
.map((i, e) {
return MapEntry(
i,
Flexible(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 1.5),
child: LayoutBuilder(
builder: (context, constraints) {
return Stack(
children: <Widget>[
_buildContainer(
double.infinity,
i < storiesCtrl.currentIndex.value
? Colors.white
: Colors.white.withOpacity(0.5),
),
i == storiesCtrl.currentIndex.value
? AnimatedBuilder(
animation: storiesCtrl.animC,
builder: (context, child) {
return _buildContainer(
constraints.maxWidth *
storiesCtrl.animC.value,
Colors.white,
);
},
)
: const SizedBox.shrink(),
],
);
}),
),
));
})
.values
.toList(),
),
),
],
),
),
);
},
);
}
Container _buildContainer(double width, Color color) {
return Container(
height: 5.0,
width: width,
decoration: BoxDecoration(
color: color,
border: Border.all(
color: Colors.black26,
width: 0.8,
),
borderRadius: BorderRadius.circular(3.0),
),
);
}
}
import 'package:flamingo/Data/Models/StoryModel.dart';
import 'package:flamingo/Utils/AllUtils.dart';
import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';
import 'package:flamingo/Data/DataProviders/StoriesList.dart';
import 'package:get/get.dart';
class StoriesController extends GetxController
with SingleGetTickerProviderMixin {
List<StoryModel> stories;
AnimationController animC;
var pageC;
var currentIndex = 0.obs;
#override
void onInit() {
stories = story;
super.onInit();
pageC = PageController();
animC = AnimationController(vsync: this);
final StoryModel firstStory = stories.first;
loadStory(story: firstStory, animateToPage: false);
animC.addStatusListener((status) {
if (status == AnimationStatus.completed) {
animC.stop();
animC.reset();
if (currentIndex.value + 1 < stories.length) {
currentIndex.value += 1;
loadStory(story: stories[currentIndex.value]);
update();
} else {
currentIndex.value = 0;
loadStory(story: stories[currentIndex.value]);
update();
}
}
});
}
void onTapDown(TapDownDetails details, StoryModel s) {
final double dx = details.globalPosition.dx;
if (dx < GlobalSize.screenWidth / 3) {
if (currentIndex.value - 1 >= 0) {
currentIndex.value -= 1;
loadStory(story: story[currentIndex.value]);
update();
}
} else if (dx > 2 * GlobalSize.screenWidth / 3) {
if (currentIndex.value + 1 < story.length) {
currentIndex.value += 1;
loadStory(story: story[currentIndex.value]);
} else {
currentIndex.value = 0;
loadStory(story: story[currentIndex.value]);
}
}
}
void loadStory({StoryModel story, bool animateToPage = true}) {
animC.stop();
animC.reset();
switch (story.media) {
case MediaType.image:
animC.duration = story.duration;
animC.forward();
break;
}
if (animateToPage) {
pageC.animateToPage(
currentIndex.value,
duration: const Duration(microseconds: 1),
curve: Curves.easeInOut,
);
}
}
#override
void onClose() {
pageC.value.dispose();
animC.dispose();
super.onClose();
}
}
Okay so I had this same issue but for me it was not actually related to the code where I used my controller. Typically this issue shows up if you access the same controller from multiple pages. However, it also occurs if you have multiple instances of the same page on the router stack. In my case I was using Get.to() elsewhere in my application to navigate back to my home page which created another instance of my home page on the stack, hence the multiple controllers error.
So to solve it, I changed Get.to() to Get.offAll(). If you are using named routes, switch Get.toNamed() to Get.offAllNamed().
If you get errors after doing this that say your controllers are no longer found, in your bindings, set permanent: true when using Get.put() to instantiate your controller: Get.put(Home(), permanent: true);
Looking at your code, it does not look like the error is coming from using the same controller twice and may instead occur when you route back to this page from somewhere in your application. Took me forever to figure out but essentially Flutter makes another instance of the "shared" controller when you add the same page onto the stack so you have to remove the original page from the stack when navigating.
If anyone gets this error from navigating and is not using GetX for state management, use Navigation.pushNamedAndRemoveUntil() to navigate back to the page with the controller.

Flutter - How to reload the entire page(reload the widget with its initstate again) on the call of an action button?

I have a Stateful Widget containing a custom tab view.
At the initialisation of the widget, category data(All, Science, Trending, Health & Fitness here) is fetched from the firestore and accordingly corresponding widgets are added to the tab view.
To add a new category, it can be selected from the last tab('+' here). Inside the corresponding widget, one can select the categories of his interest.
On the tap of "Add Category" button I am adding the selected category to firestore but after that I want to reload the widget and load its initstate(because the category data is fetched in initstate).
Can somone please explain that how can i achieve the same here?
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_advanced_networkimage/provider.dart';
import 'package:flutter_advanced_networkimage/transition.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:journey_togather/pages/home.dart';
import 'package:journey_togather/pages/invite_friends.dart';
import 'package:journey_togather/pages/journey/create_journey.dart';
import 'package:journey_togather/pages/journey/create_paid_journey.dart';
import 'package:journey_togather/pages/journey/journey_home.dart';
import 'package:journey_togather/pages/locator.dart';
import 'package:journey_togather/services/analytics.dart';
import 'package:journey_togather/settings/appbuilder.dart';
import 'package:journey_togather/settings/sizeconfig.dart';
import 'package:journey_togather/settings/textstyle.dart';
import 'package:journey_togather/widgets/all_user_journey.dart';
import 'package:journey_togather/widgets/pillButton.dart';
import 'package:journey_togather/widgets/progress.dart';
import 'package:uuid/uuid.dart';
class ExploreFeed extends StatefulWidget {
#override
_ExploreFeedState createState() => _ExploreFeedState();
}
class _ExploreFeedState extends State<ExploreFeed>
with TickerProviderStateMixin {
bool isLoading = false;
final AnalyticsService _analyticsService = locator<AnalyticsService>();
TabController tabController;
final _scaffoldKey = GlobalKey<ScaffoldState>();
// this will control the button clicks and tab changing
TabController _controller;
// this will control the animation when a button changes from an off state to an on state
AnimationController _animationControllerOn;
// this will control the animation when a button changes from an on state to an off state
AnimationController _animationControllerOff;
// this will give the background color values of a button when it changes to an on state
Animation _colorTweenBackgroundOn;
Animation _colorTweenBackgroundOff;
// this will give the foreground color values of a button when it changes to an on state
Animation _colorTweenForegroundOn;
Animation _colorTweenForegroundOff;
// when swiping, the _controller.index value only changes after the animation, therefore, we need this to trigger the animations and save the current index
int _currentIndex = 0;
// saves the previous active tab
int _prevControllerIndex = 0;
// saves the value of the tab animation. For example, if one is between the 1st and the 2nd tab, this value will be 0.5
double _aniValue = 0.0;
// saves the previous value of the tab animation. It's used to figure the direction of the animation
double _prevAniValue = 0.0;
List<dynamic> _userInterestCategory = ['All'];
List<dynamic> _userInterestCategoryId = ['All'];
List<Widget> _interestFeedBuild = [];
//saves the newly added interest data of a user from add category tab
List _interestDataId = [];
List _interestDataName = [];
// store the pill buttons for categories in add category page
List<Widget> buttons;
Color _foregroundOn = Colors.white;
Color _foregroundOff = Colors.grey;
// active button's background color
Color _backgroundOn = Colors.green;
// Color _backgroundOff = Colors.black;
Color _backgroundOff = Colors.grey.withOpacity(0.1);
// scroll controller for the TabBar
ScrollController _tabScrollController = new ScrollController();
// this will save the keys for each Tab in the Tab Bar, so we can retrieve their position and size for the scroll controller
List _keys = [];
bool _buttonTap = false;
// ScrollController _scrollController = ScrollController();
#override
void initState() {
setState(() {
isLoading = true;
});
_initialiseData();
// WidgetsBinding.instance.addPostFrameCallback(_initialiseData());
super.initState();
}
_initialiseData()async{
_getInterestData().whenComplete(() {
for (int index = 0; index < _userInterestCategory.length; index++) {
// create a GlobalKey for each Tab
_keys.add(new GlobalKey());
}
// this creates the controller with 6 tabs (in our case)
_controller =
TabController(vsync: this, length: _userInterestCategory.length);
// this will execute the function every time there's a swipe animation
_controller.animation.addListener(_handleTabAnimation);
// this will execute the function every time the _controller.index value changes
_controller.addListener(_handleTabChange);
_animationControllerOff = AnimationController(
vsync: this, duration: Duration(milliseconds: 75));
// so the inactive buttons start in their "final" state (color)
_animationControllerOff.value = 1.0;
_colorTweenBackgroundOff =
ColorTween(begin: _backgroundOn, end: _backgroundOff)
.animate(_animationControllerOff);
_colorTweenForegroundOff =
ColorTween(begin: _foregroundOn, end: _foregroundOff)
.animate(_animationControllerOff);
_animationControllerOn = AnimationController(
vsync: this, duration: Duration(milliseconds: 150));
// so the inactive buttons start in their "final" state (color)
_animationControllerOn.value = 1.0;
_colorTweenBackgroundOn =
ColorTween(begin: _backgroundOff, end: _backgroundOn)
.animate(_animationControllerOn);
_colorTweenForegroundOn =
ColorTween(begin: _foregroundOff, end: _foregroundOn)
.animate(_animationControllerOn);
_userInterestCategoryId.forEach((element) {
if (element != '+') {
_interestFeedBuild.add(
BuildInterestFeed(
categoryId: element,
),
);
} else {
buildButton();
_interestFeedBuild.add(
_buildAddInterest(),
);
}
});
setState(() {
isLoading = false;
});
});
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
_getInterestData() async {
DocumentSnapshot _doc =
await userInterestsRef.document(currentUser.id).get();
List _interestsFetched = _doc.data['categories'];
_interestsFetched = _interestsFetched.toSet().toList();
Map _userInterestData = {};
for (var val in _interestsFetched) {
Map _filteredData =
globalInterest.firstWhere((element) => element['categoryId'] == val);
_userInterestData[_filteredData['categoryId']] =
(_filteredData['categoryName']);
}
setState(() {
_userInterestCategory =
_userInterestCategory + _userInterestData.values.toList() + ['+'];
_userInterestCategoryId =
_userInterestCategoryId + _userInterestData.keys.toList() + ['+'];
});
}
//tab bar functions
// runs during the switching tabs animation
_handleTabAnimation() {
// gets the value of the animation. For example, if one is between the 1st and the 2nd tab, this value will be 0.5
_aniValue = _controller.animation.value;
// if the button wasn't pressed, which means the user is swiping, and the amount swipped is less than 1 (this means that we're swiping through neighbor Tab Views)
if (!_buttonTap && ((_aniValue - _prevAniValue).abs() < 1)) {
// set the current tab index
_setCurrentIndex(_aniValue.round());
}
// save the previous Animation Value
_prevAniValue = _aniValue;
}
// runs when the displayed tab changes
_handleTabChange() {
// if a button was tapped, change the current index
if (_buttonTap) _setCurrentIndex(_controller.index);
// this resets the button tap
if ((_controller.index == _prevControllerIndex) ||
(_controller.index == _aniValue.round())) _buttonTap = false;
// save the previous controller index
_prevControllerIndex = _controller.index;
}
_setCurrentIndex(int index) {
// if we're actually changing the index
if (index != _currentIndex) {
setState(() {
// change the index
_currentIndex = index;
});
// trigger the button animation
_triggerAnimation();
// scroll the TabBar to the correct position (if we have a scrollable bar)
_scrollTo(index);
}
}
_triggerAnimation() {
// reset the animations so they're ready to go
_animationControllerOn.reset();
_animationControllerOff.reset();
// run the animations!
_animationControllerOn.forward();
_animationControllerOff.forward();
}
_scrollTo(int index) {
// get the screen width. This is used to check if we have an element off screen
double screenWidth = MediaQuery.of(context).size.width;
// get the button we want to scroll to
RenderBox renderBox = _keys[index].currentContext.findRenderObject();
// get its size
double size = renderBox.size.width;
// and position
double position = renderBox.localToGlobal(Offset.zero).dx;
// this is how much the button is away from the center of the screen and how much we must scroll to get it into place
double offset = (position + size / 2) - screenWidth / 2;
// if the button is to the left of the middle
if (offset < 0) {
// get the first button
renderBox = index-1 < 0 ? _keys[0].currentContext.findRenderObject() : _keys[index - 1].currentContext.findRenderObject();
// get the position of the first button of the TabBar
position = renderBox.localToGlobal(Offset.zero).dx;
// if the offset pulls the first button away from the left side, we limit that movement so the first button is stuck to the left side
if (position > offset) offset = position;
} else {
// if the button is to the right of the middle
// get the last button
renderBox = index+1 == _userInterestCategory.length ? _keys[_userInterestCategory.length-1]
.currentContext
.findRenderObject(): _keys[index + 1]
.currentContext
.findRenderObject();
// get its position
position = renderBox.localToGlobal(Offset.zero).dx;
// and size
size = renderBox.size.width;
// if the last button doesn't reach the right side, use it's right side as the limit of the screen for the TabBar
if (position + size < screenWidth) screenWidth = position + size;
// if the offset pulls the last button away from the right side limit, we reduce that movement so the last button is stuck to the right side limit
if (position + size - offset < screenWidth) {
offset = position + size - screenWidth;
}
}
// scroll the calculated amount
_tabScrollController.animateTo(offset + _tabScrollController.offset,
duration: new Duration(milliseconds: 150), curve: Curves.easeInOut);
}
_getBackgroundColor(int index) {
if (index == _currentIndex) {
// if it's active button
return _colorTweenBackgroundOn.value;
} else if (index == _prevControllerIndex) {
// if it's the previous active button
return _colorTweenBackgroundOff.value;
} else {
// if the button is inactive
return _backgroundOff;
}
}
_getForegroundColor(int index) {
// the same as the above
if (index == _currentIndex) {
return _colorTweenForegroundOn.value;
} else if (index == _prevControllerIndex) {
return _colorTweenForegroundOff.value;
} else {
return _foregroundOff;
}
}
callback(Map data) {
if (data['method'] == 'delete') {
setState(() {
_interestDataId.removeWhere((element) => element == data['id']);
_interestDataName.removeWhere((element) => element == data['name']);
});
} else {
setState(() {
_interestDataId.add(data['id']);
_interestDataName.add(data['name']);
});
}
}
addInterest(context) async {
if(_interestDataId.length > 0 && _interestDataName.length > 0) {
await userInterestsRef
.document(currentUser.id)
.updateData({'categories': FieldValue.arrayUnion(_interestDataId)});
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) {
return Home();
},
),
);
}
else{
SnackBar snackbar = SnackBar(
content: Text("Please select atleast 1 interest before submitting"),
);
_scaffoldKey.currentState.showSnackBar(snackbar);
}
}
buildButton() {
List<Widget> _buttons = [];
globalInterest.forEach((element) {
if (_userInterestCategoryId.contains(element['categoryId'])) {
} else {
_buttons.add(PillButton(
interestId: element['categoryId'],
interestTitle: element['categoryName'],
callback: callback,
));
}
});
setState(() {
buttons = _buttons;
});
}
Widget _buildAddInterest() {
return Scaffold(
bottomNavigationBar: buttons.length != 0 ?
Container(
margin: EdgeInsets.only(bottom: 16.0),
padding: EdgeInsets.symmetric(horizontal: 16.0),
height: 40,
child: FlatButton(
onPressed: () => addInterest(context),
color: Colors.black,
child: Text(
'Add category',
style: Theme.of(context)
.textTheme
.subtitle2
.copyWith(color: Theme.of(context).primaryColor),
),
),
) : Container(),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: buttons.length != 0
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Wrap(
spacing: 12.0,
children: buttons,
),
],
)
: Center(
child: Text(
'Voila! You\'ve selected all the available interests.',
style: Theme.of(context).textTheme.subhead.copyWith(
color:
Theme.of(context).primaryColorDark.withOpacity(0.40),),
// Colors.black,)
),
),
),
);
}
#override
Widget build(BuildContext context) {
return isLoading
? circularProgress(context)
: AppBuilder(
builder: (context){
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
automaticallyImplyLeading: false,
backgroundColor: Theme.of(context).primaryColor,
elevation: 0,
title: IconButton(
iconSize: SizeConfig.safeBlockHorizontal * 21,
icon: Image.asset(
Theme.of(context).brightness == Brightness.light
? 'assets/images/logo.png'
: 'assets/images/journey_exploreFeed_dark.png',
),
),
titleSpacing: 8.0,
actions: <Widget>[
IconButton(
padding: EdgeInsets.only(right: 24),
icon: new Image.asset(
Theme.of(context).brightness == Brightness.light
? 'assets/icons/create_journey_black.png'
: 'assets/icons/create_journey_white.png'),
onPressed: () {
_analyticsService.logCreateJourneyExploreFeed();
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CreateJourney()),
);
},
),
IconButton(
padding: EdgeInsets.only(right: 16),
icon:
new Image.asset('assets/icons/invite_friends_icon.png'),
onPressed: () {
_analyticsService.logShareApp();
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => InviteFriends()),
);
},
),
]),
backgroundColor: Colors.transparent,
body: Column(children: <Widget>[
// this is the TabBar
Container(
height: 52.0,
color: Theme.of(context).primaryColor,
// this generates our tabs buttons
child: ListView.builder(
// this gives the TabBar a bounce effect when scrolling farther than it's size
physics: BouncingScrollPhysics(),
controller: _tabScrollController,
// make the list horizontal
scrollDirection: Axis.horizontal,
// number of tabs
itemCount: _userInterestCategory.length,
itemBuilder: (BuildContext context, int index) {
return Padding(
// each button's key
key: _keys[index],
// padding for the buttons
padding: EdgeInsets.fromLTRB(8.0, 4.0, 0.0, 8.0),
child: ButtonTheme(
child: AnimatedBuilder(
animation: _colorTweenBackgroundOn,
builder: (context, child) => FlatButton(
// get the color of the button's background (dependent of its state)
color: _getBackgroundColor(index),
padding:
EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 8.0),
// make the button a rectangle with round corners
shape: RoundedRectangleBorder(
borderRadius:
new BorderRadius.circular(24.0)),
onPressed: () {
setState(() {
_buttonTap = true;
// trigger the controller to change between Tab Views
_controller.animateTo(index);
// set the current index
_setCurrentIndex(index);
// scroll to the tapped button (needed if we tap the active button and it's not on its position)
_scrollTo(index);
});
},
child: index !=
_userInterestCategory.length - 1
? Text(
// get the icon
_userInterestCategory[index],
// get the color of the icon (dependent of its state)
style: (TextStyle(
color: _getForegroundColor(index),
)),
)
: Icon(
Icons.add,
color: _getForegroundColor(index),
)),
)));
})),
Flexible(
// this will host our Tab Views
child: TabBarView(
// and it is controlled by the controller
controller: _controller,
children: _interestFeedBuild,
)),
]));
},
);
}
}
class BuildInterestFeed extends StatefulWidget {
final String categoryId;
BuildInterestFeed({this.categoryId});
#override
_BuildInterestFeedState createState() => _BuildInterestFeedState();
}
class _BuildInterestFeedState extends State<BuildInterestFeed>
with AutomaticKeepAliveClientMixin {
bool isLoading = false;
List<Journey> journeys = [];
final AnalyticsService _analyticsService = locator<AnalyticsService>();
String docId = Uuid().v4();
List<DocumentSnapshot> journeysFetched = []; // stores fetched products
bool hasMore = true; // flag for more products available or not
int documentLimit = 10; // documents to be fetched per request
DocumentSnapshot
lastDocument; // flag for last document from where next 10 records to be fetched
ScrollController _verticalScrollController = ScrollController();
#override
void initState() {
super.initState();
if (widget.categoryId != '+') {
_getExploreFeedData();
_verticalScrollController.addListener(() {
double maxScroll = _verticalScrollController.position.maxScrollExtent;
double currentScroll = _verticalScrollController.position.pixels;
double delta = MediaQuery.of(context).size.height * 0.10;
if (maxScroll - currentScroll <= delta) {
_getExploreFeedData();
}
});
}
}
_getExploreFeedData() async {
if (!hasMore) {
print('No More Journeys');
return;
}
if (isLoading) {
return;
}
setState(() {
isLoading = true;
});
if (widget.categoryId == 'All') {
await buildAllDataFeed();
} else {
QuerySnapshot querySnapshot;
if (lastDocument == null) {
querySnapshot = await journeyRef
.orderBy('createdAt', descending: true)
.where('category', arrayContains: widget.categoryId)
.limit(documentLimit)
.getDocuments();
} else {
querySnapshot = await journeyRef
.orderBy('createdAt', descending: true)
.where('category', arrayContains: widget.categoryId)
.startAfterDocument(lastDocument)
.limit(documentLimit)
.getDocuments();
}
if (querySnapshot.documents.length != 0) {
if (querySnapshot.documents.length < documentLimit) {
hasMore = false;
}
if (querySnapshot.documents.length != 0) {
lastDocument =
querySnapshot.documents[querySnapshot.documents.length - 1];
} else {
lastDocument = null;
}
journeysFetched.addAll(querySnapshot.documents);
}
}
setState(() {
isLoading = false;
});
}
buildAllDataFeed() async {
QuerySnapshot querySnapshot;
if (lastDocument == null) {
querySnapshot = await journeyRef
.orderBy('createdAt', descending: true)
.limit(documentLimit)
.getDocuments();
} else {
querySnapshot = await journeyRef
.orderBy('createdAt', descending: true)
.startAfterDocument(lastDocument)
.limit(documentLimit)
.getDocuments();
}
if (querySnapshot.documents.length < documentLimit) {
hasMore = false;
}
if (querySnapshot.documents.length != 0) {
lastDocument =
querySnapshot.documents[querySnapshot.documents.length - 1];
} else {
lastDocument = null;
}
// lastDocument = querySnapshot.documents[querySnapshot.documents.length - 1];
journeysFetched.addAll(querySnapshot.documents);
}
addInterestData({tags}) async{
userInterestsRef.document(currentUser.id).updateData({
'tags': FieldValue.arrayUnion(tags),
});
}
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
return
RefreshIndicator(
onRefresh: () => _getExploreFeedData(),
child:
Scaffold(
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Column(children: [
Expanded(
child: journeysFetched.length == 0
? Center(
child: Text(
'There\'s nothing new here right now',
style: Theme.of(context).textTheme.subhead.copyWith(
color: Theme.of(context)
.primaryColorDark
.withOpacity(0.40)),
),
)
: StaggeredGridView.countBuilder(
controller: _verticalScrollController,
crossAxisCount: 2,
itemCount: journeysFetched.length,
itemBuilder: (BuildContext context, int index) {
Map journeyData = journeysFetched.elementAt(index).data;
String journeyId =
journeysFetched.elementAt(index).documentID;
return GestureDetector(
child: Container(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
TransitionToImage(
transitionType: TransitionType.fade,
borderRadius: BorderRadius.circular(8.0),
loadingWidget: Container(
height: 242,
decoration: (BoxDecoration(
borderRadius:
BorderRadius.circular(8.0),
color: Theme.of(context)
.primaryColorLight
.withOpacity(0.05),
))),
image: AdvancedNetworkImage(
journeyData['coverImage'],
useDiskCache: true,
cacheRule: CacheRule(
maxAge: Duration(hours: 9999))),
),
Padding(
padding: EdgeInsets.only(
top: SizeConfig.safeBlockVertical * 0.5,
left:
SizeConfig.safeBlockVertical * 0.5),
child: Text(
journeyData['title'],
style: Theme.of(context)
.textTheme
.bodyText2
.copyWith(
fontWeight: FontWeight.w500),
))
],
),
),
onTap: () {
addInterestData(
tags: journeyData['community']['hashtags']);
Navigator.push(
context,
MaterialPageRoute(
settings:
RouteSettings(name: 'journey_home'),
builder: (context) =>
JourneyHome(journeyId: journeyId)));
});
},
staggeredTileBuilder: (int index) => StaggeredTile.fit(1),
mainAxisSpacing: SizeConfig.safeBlockVertical * 2,
crossAxisSpacing: 8.0,
),
),
isLoading ? circularProgress(context) : Container()
]),
),
),
);
}
}
There is also a pub package named states_rebuilder
https://pub.dev/packages/states_rebuilder
or
our Widget should have a setState() method, this method is called, the widget is redrawn

Flutter TextField covered by keyboard, I can't use resizeToAvoidBottomInset = true

I'm trying to make a an autocomplete Textfield that decide whether to show the list of suggestions up or down respect to the textfield depending on where there is more space in the screen. Like this:
AutocompleteTextFieldExample
To decide in which direction visualize it I need to know how much space is covered by the keyboard, so I use: "MediaQuery.of(context).viewPadding.bottom"
However "MediaQuery.of(context).viewPadding.bottom" returns always 0.0 if I set resizeToAvoidBottomInset: true.
So I need to set it false. But doing like that the textfields are covered by the keyboard when on focus, as you can see in this two images:
Screen
Screen with focus on the bottom textfield
This is my textFieldCode so far:
class _AutoCompleteTextFieldState extends State<AutoCompleteTextField> {
final double _maxSuggestionBoxDimension = 300;
OverlayEntry _overlayEntry;
final LayerLink _layerLink = LayerLink();
final FocusNode _focusNode = FocusNode();
final TextEditingController _textEditingController =
new TextEditingController();
// it contains the listTiles utilized by the overlay
// the items are changed inside the
List<ListTile> listTiles = [];
#override
void initState() {
super.initState();
// it inserts or remove the suggestions container depending on the focus of the textfield
_focusNode.addListener(() {
if (_focusNode.hasFocus) {
this._overlayEntry = this._createOverlayEntry();
Overlay.of(context).insert(this._overlayEntry);
} else {
this._overlayEntry.remove();
}
});
// if text is not empty it updates the suggestion in the suggestions box
_textEditingController.addListener(() {
// move this logic inside updateSuggestinoContainer
if (_textEditingController.text.isNotEmpty) {
updateSuggestionContainer(_textEditingController.text);
} else {
listTiles = [];
_overlayEntry.markNeedsBuild();
}
});
}
// it modify the suggestion container depending on the input text (in the textfield),
// it works in 3 part
// - defining the List of string which rappresent the suggestions that will be in the suggestions container
// - create and assign the new List of ListTile that will be used to build the suggestions container
// - mark _overlayEntry as dirty to rebuild and
void updateSuggestionContainer(String text) {
List<String> stringSuggestions = getStringSuggestions(text);
listTiles = getListTileSuggestion(stringSuggestions);
_overlayEntry.markNeedsBuild();
}
// it defines what suggestion to visualize
List<String> getStringSuggestions(String text) {
return widget.suggestions
.where((possibleSuggestion) =>
possibleSuggestion.substring(0, text.length) == text)
.toList();
}
// it define how to visualize each single suggestion
List<ListTile> getListTileSuggestion(List<String> suggestions) {
List<ListTile> listTiles = [];
for (String suggestion in suggestions) {
listTiles.add(ListTile(title: Text(suggestion)));
}
return listTiles;
}
OverlayEntry _createOverlayEntry() {
RenderBox renderBox = context.findRenderObject();
var size = renderBox.size;
Offset position = renderBox.localToGlobal(Offset.zero);
Future.delayed(Duration(seconds: 3)).then((_) {
print("${_showBottom(position.dy)}");
});
return OverlayEntry(
builder: (context) => Positioned(
width: size.width,
child: CompositedTransformFollower(
link: this._layerLink,
showWhenUnlinked: false,
offset: Offset(
0.0,
_showBottom(position.dy)
? min(size.height + 5.0, _maxSuggestionBoxDimension)
: max(-listTiles.length * 56.0 - size.height + 40.0,
-_maxSuggestionBoxDimension)),
child: Material(
child: Container(
height: min(
listTiles.length * 56.0, _maxSuggestionBoxDimension),
//duration: Duration(milliseconds: 150),
color: Colors.white,
child: MediaQuery.removePadding(
context: context,
removeTop: true,
child: ListView(
reverse: _showBottom(position.dy) ? false : true,
children: listTiles.isNotEmpty
? ListTile.divideTiles(
context: context, tiles: listTiles)
.toList()
: List()),
),
),
),
),
));
}
// controll whetever the suggestion should be shown above or bottom the textField
bool _showBottom(double textFieldCordinateY) {
print("textFieldCordinateY = $textFieldCordinateY");
print(
"MediaQuery.of(context).size.height = ${MediaQuery.of(context).size.height}");
print(
"MediaQuery.of(context).viewInsets.bottom = ${MediaQuery.of(context).viewInsets.bottom}");
print(
"MediaQuery.of(context).viewPadding.bottom = ${MediaQuery.of(context).viewPadding.bottom}");
// TODO
// consider also the top viewInsets
if ((MediaQuery.of(context).size.height -
MediaQuery.of(context).viewInsets.bottom) /
2 >
textFieldCordinateY)
return true;
else
return false;
}
#override
Widget build(BuildContext context) {
return CompositedTransformTarget(
link: this._layerLink,
child: TextFormField(
//scrollPadding: EdgeInsets.all(500),
controller: _textEditingController,
focusNode: this._focusNode,
decoration: InputDecoration(suffixIcon: Icon(Icons.arrow_drop_down)),
),
);
}
}
This is the build in my screen code:
#override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(title: Text("Titolo")),
body: ListView(children: [
SizedBox(height: 15),
Container(
padding: EdgeInsets.all(20),
child: AutoCompleteTextField(["Ape", "Areoplano", "Austronauta"])),
SizedBox(height: 200),
Container(
padding: EdgeInsets.all(20),
child: AutoCompleteTextField([
"Biliardo",
"Bufu",
"Bamba",
"Bici",
"Bambolina",
"Busta",
"Bella",
"Basta",
"Balza"
])),
SizedBox(height: 200),
Container(
padding: EdgeInsets.all(20),
child: AutoCompleteTextField(["Ciru", "Capanna", "Casa"]),
),
]),
);
}
Thank you in advance.

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;
}
}