No Animation with AnimatedSwitcher or AnimatedOpacity - flutter

I'm learning flutter and I'm having behavior with the animation system.
I created a Radio Button which is a circle that should get filled when it gets clicked.
I created a stateful widget class and a state class.
In the state class, I build a :
GestureDetector -> Container -> AnimatedSwitcher -> _animatedWidget
_animatedWidget is a Widget that changes when I click (in the GestureDetector onTap I do _changeSelect)
void _changeSelect(){
_isSelected = !_isSelected;
if(_isSelected){
setState(() {
_animatedWidget = Container(width: double.infinity,height: double.infinity,decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.black));
});
}
else{
setState(() {
_animatedWidget = Container();
});
}
}
And this code is not working properly, it should fade in the full container and fade out the empty container but instead, it just pops in and pops out (like a classic change would do)
Here is the full code of my state class :
class _RadioButtonState extends State<RadioButton> {
Widget _animatedWidget = Container();
bool _isSelected = false;
bool isSelected(){
return _isSelected;
}
void _changeSelect(){
_isSelected = !_isSelected;
if(_isSelected){
setState(() {
_animatedWidget = Container(width: double.infinity,height: double.infinity,decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.black));
});
}
else{
setState(() {
_animatedWidget = Container();
});
}
}
#override
Widget build(BuildContext context){
return GestureDetector(
onTap: _changeSelect,
child:
Container(
width: 16.0,
height: 16.0,
padding: EdgeInsets.all(2.0),
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(width: 2.0, color: Colors.black)
),
child: AnimatedSwitcher(
duration: Duration(seconds: 1),
child: _animatedWidget,
)
),
);
}
}
Note: I also tried with AnimatedOpacity instead of AnimatedSwitcher (with the full Container with a starting opacity of 0 increased to 1 when clicked) but it doesn't even change the view, however, the javascript looks to be working during the duration time

Is this what you're looking for?
Widget _animatedWidget = Container();
bool _isSelected = false;
bool isSelected() {
return _isSelected;
}
void _changeSelect() {
_isSelected = !_isSelected;
if (_isSelected) {
setState(() {
_animatedWidget = Container(
key: ValueKey(1),
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.black),
);
});
} else {
setState(() {
_animatedWidget = Container(
key: ValueKey(2),
);
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: GestureDetector(
onTap: _changeSelect,
child: Container(
width: 66.0,
height: 66.0,
padding: EdgeInsets.all(2.0),
decoration: BoxDecoration(shape: BoxShape.circle, border: Border.all(width: 2.0, color: Colors.black)),
child: AnimatedSwitcher(
duration: Duration(seconds: 1),
child: _animatedWidget,
)),
),
),
);
}

Related

Toggle switch animation not working in flutter

`I am trying to implement a toggle switch in which when I toggle the switch I display two different things but I'm getting an error while I use setstate with my logic or something like that if I remove the setstate from my code the animation starts working again but the logic does not work and I don't get two different outcomes when i toggle between the switch
the code is :
import 'package:flutter/material.dart';
import 'package:sadapay_clone/screens/homepage.dart';
import 'package:sadapay_clone/widgets/physical_card_item.dart';
import 'package:sadapay_clone/widgets/virtual_card_item.dart';
import 'package:toggle_switch/toggle_switch.dart';
class CardScreen extends StatefulWidget {
const CardScreen({super.key});
#override
State<CardScreen> createState() => _CardScreenState();
}
class _CardScreenState extends State<CardScreen>{
// AnimationController _controller;
bool toggle = false;
// #override
// void initState() {
// _controller = AnimationController(vsync: this);
// super.initState();
// }
// void toggleSwitch(int index) {
// if (index == 0) {
// setState(() {
// toggle = true;
// });
// } else if (index == 1) {
// setState(() {
// toggle = false;
// });
// }
// }
void toggleSwitch(int index) {
if (index == 0) {
setState(() {
toggle = true;
});
} else if (index == 1) {
setState(() {
toggle = false;
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
const SizedBox(height: 75),
SizedBox(
width: double.infinity,
child: Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
onPressed: () {
Navigator.pop(
context,
MaterialPageRoute(
builder: (context) => const MyHomePage(),
),
);
},
icon: const Icon(Icons.arrow_back_ios),
),
Container(
width: 295,
alignment: Alignment.center,
child: const Text(
'My Cards',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
),
],
),
),
const SizedBox(
height: 20,
),
Container(
alignment: Alignment.center,
height: 40,
width: 365,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: Colors.grey[300],
),
child: ToggleSwitch(
minHeight: 30,
minWidth: 180.0,
cornerRadius: 20.0,
activeBgColors: const [
[Colors.white],
[Colors.white]
],
activeFgColor: Colors.black,
inactiveBgColor: Colors.grey[300],
inactiveFgColor: Colors.black54,
initialLabelIndex: 0,
totalSwitches: 2,
labels: const ['Virtual', 'Physical'],
radiusStyle: true,
onToggle: (index) {
toggleSwitch(index!);
},
// onToggle: (index) {
// setState(() {
// toggle = index == 0;
// });
// },
),
),
toggle ? VirtualCard() : PhysicalCard(),
],
),
);
}
}
I tried using setstate logic inside the function rather than using it inside the onchanged property but still, the logic was working I was seeing two different outcomes when I pressed the switch but the animation was not working`
The issue is, while we are calling setState, the build method is trigger and setting initialLabelIndex to 0 again, you can do a check here,
class _CardScreenState extends State<CardScreen> {
bool toggle = false;
void toggleSwitch(int index) {
if (index == 0) {
setState(() {
toggle = true;
});
} else if (index == 1) {
setState(() {
toggle = false;
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Container(
alignment: Alignment.center,
height: 40,
width: 365,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: Colors.grey[300],
),
child: ToggleSwitch(
minHeight: 30,
minWidth: 180.0,
cornerRadius: 20.0,
activeBgColors: const [
[Colors.white],
[Colors.white]
],
activeFgColor: Colors.black,
inactiveBgColor: Colors.grey[300],
inactiveFgColor: Colors.black54,
initialLabelIndex: toggle ? 0 : 1,
totalSwitches: 2,
labels: ['Virtual', 'Physical'],
radiusStyle: true,
onToggle: (index) {
toggleSwitch(index!);
},
),
),
toggle ? Text("VirtualCard") : Text("PhysicalCard"),
],
),
);
}
}

Flutter remove OverlayEntry if touch outside

I have a CustomDropDown, done with a OverlayEntry. The problem is that I have a StatefulWidget for that, which I place in my Screen simply like that:
CustomDropDownButton(
buttonLabel: 'Aus Vorauswahl wählen',
options: [
'1',
'2',
'3',
'4',
],
),
Now inside that CustomDropDownButton I can simply call floatingDropdown.remove(); where ever I want but how can I call that from a Parent-Widget?? I hope you understand my problem. Right now the only way to remove the overlay is by pressing the DropDownButton again, but it should be removed everytime the user taps outside the actual overlay.
I am quite lost here so happy for every help! Let me know if you need any more details!
This is the code for my CustomDropDownButton if that helps:
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../../../constants/styles/colors.dart';
import '../../../constants/styles/text_styles.dart';
import '../../../services/size_service.dart';
import 'drop_down.dart';
class CustomDropDownButton extends StatefulWidget {
String buttonLabel;
final List<String> options;
CustomDropDownButton({
required this.buttonLabel,
required this.options,
});
#override
_CustomDropdownState createState() => _CustomDropdownState();
}
class _CustomDropdownState extends State<CustomDropDownButton> {
late GlobalKey actionKey;
late double height, width, xPosition, yPosition;
bool _isDropdownOpened = false;
int _selectedIndex = -1;
late OverlayEntry floatingDropdown;
#override
void initState() {
actionKey = LabeledGlobalKey(widget.buttonLabel);
super.initState();
}
#override
Widget build(BuildContext context) {
return GestureDetector(
key: actionKey,
onTap: () {
setState(() {
if (_isDropdownOpened) {
floatingDropdown.remove();
} else {
findDropdownData();
floatingDropdown = _createFloatingDropdown();
Overlay.of(context)!.insert(floatingDropdown);
}
_isDropdownOpened = !_isDropdownOpened;
});
},
child: Container(
height: scaleWidth(50),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(width: 1.0, color: AppColors.black),
),
color: AppColors.white,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: scaleWidth(10),
),
Text(
widget.buttonLabel,
style: AppTextStyles.h5Light,
),
Spacer(),
_isDropdownOpened
? SvgPicture.asset(
'images/icons/arrow_down_primary.svg',
width: scaleWidth(21),
)
: SvgPicture.asset(
'images/icons/arrow_up_primary.svg',
width: scaleWidth(21),
),
SizedBox(
width: scaleWidth(10),
),
],
),
),
);
}
void findDropdownData() {
RenderBox renderBox =
actionKey.currentContext!.findRenderObject()! as RenderBox;
height = renderBox.size.height;
width = renderBox.size.width;
Offset? offset = renderBox.localToGlobal(Offset.zero);
xPosition = offset.dx;
yPosition = offset.dy;
}
OverlayEntry _createFloatingDropdown() {
return OverlayEntry(builder: (context) {
return Positioned(
left: xPosition,
width: width,
top: yPosition + height,
height: widget.options.length * height + scaleWidth(5),
child: DropDown(
itemHeight: height,
options: widget.options,
onOptionTap: (selectedIndex) {
setState(() {
widget.buttonLabel = widget.options[selectedIndex];
_selectedIndex = selectedIndex;
floatingDropdown.remove();
_isDropdownOpened = !_isDropdownOpened;
});
},
selectedIndex: _selectedIndex,
),
);
});
}
}
1. Return a ListView instead GestureDetector
2. Under Listview use that GestureDetector containing DropDown as one of the children.
3. Add another children(widgets) as GestureDetector and set onTap of each one as:
GestureDetector(
onTap: () {
if(isDropdownOpened){
floatingDropDown!.remove();
isDropdownOpened = false;
}
},
child: Container(
height: 200,
color: Colors.black,
),
)
In short you have to add GestureDetector to the part wherever you want the tapping should close overlay entry
** Full Code **
//This is to close overlay when you navigate to another screen
#override
void dispose() {
// TODO: implement dispose
floatingDropDown!.remove();
super.dispose();
}
Widget build(BuildContext context) {
return ListView(
children: [
Padding(
padding: EdgeInsets.all(20),
child: GestureDetector(
key: _actionKey,
onTap: () {
setState(() {
if (isDropdownOpened) {
floatingDropDown!.remove();
} else {
findDropDownData();
floatingDropDown = _createFloatingDropDown();
Overlay.of(context)!.insert(floatingDropDown!);
}
isDropdownOpened = !isDropdownOpened;
});
},
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8), color: Colors.orangeAccent),
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Row(
children: <Widget>[
Text(
widget.text,
style: TextStyle(color: Colors.white, fontSize: 20),
),
Spacer(),
Icon(
Icons.arrow_drop_down,
color: Colors.white,
),
],
),
),
),
),
GestureDetector(
onTap: () {
if(isDropdownOpened){
floatingDropDown!.remove();
isDropdownOpened = false;
}
},
child: Container(
height: 200,
color: Colors.black,
),
)
],
);
}
Let me know whether it helped or not
Listen to full screen onTapDown gesture and navigation event.
The screen' s gesture event:
#override
Widget build(BuildContext context) {
return RawGestureDetector(
gestures: {
PenetrableTapRecognizer: GestureRecognizerFactoryWithHandlers<PenetrableTapRecognizer>(
() => PenetrableTapRecognizer(),
(instance) {
instance.onTapDown = (_) => _handleGlobalGesture();
},
),
},
behavior: HitTestBehavior.opaque,
child: Scaffold(
),
);
}
void _handleGlobalGesture {
// insert or remove the popup menu
// a bool flag maybe helpful
}
class PenetrableTapRecognizer extends TapGestureRecognizer {
#override
void rejectGesture(int pointer) {
acceptGesture(pointer);
}
}

Flutter extended_image: setState() or markNeedsBuild() called during build

I am using the package extended_image to load images from the network and display a shimmer on loading or on error.
I am getting this error setState() or markNeedsBuild() called during build when I am trying to call setState inside the loadStateChanged
In fact, I have two widgets, one VideoThumbnail responsible for loading a thumbnail from the network, and another one VideoDesc that should display the thumbnail description.
But I would like the description to display a shimmer when the image fails to load or is taking longer to load.
I created two states variables, on the VideoThumbnail widget, that should be passed to the VideoDesc widget
videoLoading = true;
videoError = false;
Here is my code following the repo example:
VideoThumbnail State
class _VideoThumbnailState extends State<VideoThumbnail>
with SingleTickerProviderStateMixin {
bool videoLoading;
bool videoError;
AnimationController _controller;
#override
void initState() {
videoLoading = true;
videoError = false;
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 3),
);
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
print("Build Process Complete");
});
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Container(
width: widget.width,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
ClipRRect(
borderRadius: BorderRadius.circular(4.0),
child: ExtendedImage.network(
widget.videoUrl,
width: widget.width,
height: (widget.width) * 3 / 4,
loadStateChanged: (ExtendedImageState state) {
switch (state.extendedImageLoadState) {
case LoadState.loading:
_controller.reset();
setState(() {
videoError = false;
videoLoading = true;
});
return Shimmer.fromColors(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4.0),
),
),
baseColor: Colors.black12,
highlightColor: Colors.white24,
);
break;
case LoadState.completed:
_controller.forward();
setState(() {
videoError = false;
videoLoading = false;
});
return FadeTransition(
opacity: _controller,
child: ExtendedRawImage(
image: state.extendedImageInfo?.image,
width: widget.width,
height: (widget.width) * 3 / 4,
),
);
break;
case LoadState.failed:
_controller.reset();
state.imageProvider.evict();
setState(() {
videoError = true;
videoLoading = false;
});
return Container(
width: widget.width,
height: (widget.width) * 3 / 4,
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage("assets/img/not-found.png"),
fit: BoxFit.fill,
),
),
);
break;
default:
return Container();
}
},
),
),
VideoDesc(
desc: widget.desc,
videoError: videoError,
videoLoading: videoLoading,
)
],
),
);
}
}
Video widget
class VideoDesc extends StatelessWidget {
final String desc;
final bool videoLoading;
final bool videoError;
const VideoDesc({
Key key,
#required this.desc,
this.videoLoading = true,
this.videoError = false,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
child: videoError || videoLoading
? Shimmer.fromColors(
baseColor: Colors.grey[700],
highlightColor: Colors.white24,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
SizedBox(height: 12.0),
Container(
width: double.infinity,
height: 8.0,
decoration: BoxDecoration(
color: Colors.grey[900],
borderRadius: BorderRadius.circular(2.0),
),
),
SizedBox(height: 12.0),
Container(
width: 80.0,
height: 8.0,
decoration: BoxDecoration(
color: Colors.grey[900],
borderRadius: BorderRadius.circular(2.0),
),
),
],
),
)
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
SizedBox(height: 12.0),
Text(
desc,
style: TextStyle(
color: Colors.white,
fontSize: 11.0,
),
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 5.0),
Text(
"361,143,203 views",
style: TextStyle(
color: Colors.white54,
fontSize: 12.0,
),
),
],
),
);
}
}
Can anyone help me with this problem? Or if there is a better way to get the extendedImageLoadState value and pass it to another widget without calling the setState inside loadStateChanged
You can't call setState during build process.
If you actually need to, you can do so by using instead:
WidgetsBinding.instance.addPostFrameCallback(() => setState((){}));
However, have in mind, that having this on your switch-case will schedule an infinite loop of rebuilds which you don't want as well.
I suggest you to re-structure your UI logic or at least make it conditional:
if(!videoLoading) {
WidgetsBinding.instance.addPostFrameCallback(() => setState((){
videoError = false;
videoLoading = true;
}));
}

How to return a Widget to a previous state

I'm changing the color of a custom menu icon, by using the setState in my Custom Icon Widget
Although I'd like to change the previous pressed Icon to the previous state (before the color was Indigo)
Edit 2:
Live demo: https://alexakis97.github.io/webappjs/
By changing the _color variable in onTap method I manage to change the color.
To get a better understanding this an Custom Icon Widget:
class _CustomBottomIconState extends State<CustomBottomIcon> {
final Function changeMenu;
final int index;
_CustomBottomIconState({this.changeMenu, this.index});
Color _color = Colors.indigo;
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
//
changeMenu(index);
setState(() {
_color = Colors.green;
});
},
child: Container(
decoration: BoxDecoration(
color: _color,
borderRadius: BorderRadius.circular(100),
),
height: MediaQuery.of(context).size.height / 20,
width: MediaQuery.of(context).size.height / 20,
child: Icon(
widget.icon,
color: Colors.white,
),
),
);
}
}
(Middle Icon pressed)
How can I change the first one back to Indigo ?
Edit 1:
Sharing bottom menu widget
class BottomMenu extends StatelessWidget {
final Function changeMenu;
final Key key;
BottomMenu({this.changeMenu, this.key});
#override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.bottomCenter,
child: Container(
height: MediaQuery.of(context).size.height / 15,
width: MediaQuery.of(context).size.width / 1.5,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: const Radius.circular(20.0),
topRight: const Radius.circular(20.0),
)),
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CustomBottomIcon(
icon: Icons.wallet_travel,
index: 0,
changeMenu: changeMenu),
SizedBox(
width: 10,
),
CustomBottomIcon(
icon: Icons.person, index: 1, changeMenu: changeMenu),
SizedBox(
width: 10,
),
CustomBottomIcon(
icon: Icons.email, index: 2, changeMenu: changeMenu),
SizedBox(
width: 10,
),
],
),
)),
);
}
}
and this is what the ChangeMenu function does:
final List<Widget> list = [
Text("Work 1"),
Text("Work 2"),
Text("Work 3"),
];
int i = 0;
void changeMenu(index) {
setState(() {
i = index;
});
}
It's placed on another parent widget
In a case like yours it is best to use a state management library such as Provider or BloC. But if you want to stick to your approach, then you will have to modify your code to be the following:
Custom Icon Widget
class _CustomBottomIconState extends State<CustomBottomIcon> {
final Function changeMenu;
final int index;
final int selectedIndex;
_CustomBottomIconState({this.changeMenu, this.index, this.selectedIndex});
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => changeMenu(index),
child: Container(
decoration: BoxDecoration(
color: selectedIndex == index ? Colors.green : Colors.indigo,
borderRadius: BorderRadius.circular(100),
),
height: MediaQuery.of(context).size.height / 20,
width: MediaQuery.of(context).size.height / 20,
child: Icon(
widget.icon,
color: Colors.white,
),
),
);
}
}
Bottom Menu
class BottomMenu extends StatelessWidget {
final Function changeMenu;
final Key key;
final int selectedIndex;
BottomMenu({this.changeMenu, this.key, this.selectedIndex});
#override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.bottomCenter,
child: Container(
height: MediaQuery.of(context).size.height / 15,
width: MediaQuery.of(context).size.width / 1.5,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: const Radius.circular(20.0),
topRight: const Radius.circular(20.0),
),
),
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CustomBottomIcon(
index: 0,
icon: Icons.wallet_travel,
selectedIndex: selectedIndex,
changeMenu: changeMenu,
),
SizedBox(width: 10),
CustomBottomIcon(
index: 1,
icon: Icons.person,
selectedIndex: selectedIndex,
changeMenu: changeMenu,
),
SizedBox(width: 10),
CustomBottomIcon(
index: 2,
icon: Icons.email,
selectedIndex: selectedIndex,
changeMenu: changeMenu,
),
SizedBox(width: 10),
],
),
),
),
);
}
}
Change Menu Function
void changeMenu(int _index) {
setState(() {
selectedIndex = _index;
});
}
Explanation
Now for the explanation, what I am doing is that I am doing a technique called state lifting, this technique is highly discouraged since it can get very messy very fast and hard to properly maintain. But for a small portion of the app it is possible to get away with it. 😉
Now what is happening is that we have a main or any class as the parent and is the one managing the state.
Below is the tree representation of your widget structure.
Now when the icon is clicked, the method in the main (changeMenu) is called which takes the index of the icon in the BottomBar and set the selectedIndex to be it. This will cause a rebuild in Flutter since a state was changed. Therefore the BottomBar will be rebuilt, and it will pass the selectedIndex to the icons. Inside the icons, there exist a check which checks if the selectedIndex is equal to the icon index, depending on this check, the color is set.
Note: The initial value of selectedIndex, will determine which icon is selected, set it to -1 to make no icon selected.
Try this one,
class _CustomBottomIconState extends State<CustomBottomIcon> {
final Function changeMenu;
final int index;
int _selectedIndex = -1; // by default all button color will be green, if _selectedIndex = 0 the first index color will be indigo
_CustomBottomIconState({this.changeMenu, this.index});
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
//
changeMenu(index);
setState(() {
_selectedIndex = index;
});
},
child: Container(
decoration: BoxDecoration(
color: getColor(index),
borderRadius: BorderRadius.circular(100),
),
height: MediaQuery.of(context).size.height / 20,
width: MediaQuery.of(context).size.height / 20,
child: Icon(
widget.icon,
color: Colors.white,
),
),
);
}
Color getColor(int index) {
if (index == _selectedIndex) {
return Colors.indigo;
} else {
return Colors.green;
}
}
}
This is a very similar idea to a radio button, which includes two parameters, value and groupValue. You can implement yours in a similar way: change your BottomMenu to a StatefulWidget, add a int groupValue = -1 in your state, and pass this to your CustomIconButton, as well as index.
You can the choose the colour of your button based on whether index == groupIndex inside your CustomIconButton and update groupIndex in your BottomMenu class every time a button is pressed. You can do this by wrapping your changeMenu parameter with something like
changeMenu: (index) {
setState(() {
groupIndex = index;
});
changeMenu(index);
},
or move this to a separate function.
If you want to turn all of them off when you press one of them a second time, just add this a check to the wrapper function and set groupIndex to -1 if it's the same as index.

Flutter: How to set boundaries for a Draggable widget?

I'm trying to create a drag and drop game. I would like to make sure that the Draggable widgets don't get out of the screen when they are dragged around.
I couldn't find an answer to this specific question. Someone asked something similar about constraining draggable area Constraining Draggable area but the answer doesn't actually make use of Draggable.
To start with I tried to implement a limit on the left-hand side.
I tried to use a Listener with onPointerMove. I've associated this event with a limitBoundaries method to detect when the Draggable exits from the left side of the screen. This part is working as it does print in the console the Offset value when the Draggable is going out (position.dx < 0). I also associated a setState to this method to set the position of the draggable to Offset(0.0, position.dy) but this doesn't work.
Could anybody help me with this?
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Draggable Test',
home: GamePlay(),
);
}
}
class GamePlay extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: <Widget>[
Row(
children: [
Container(
width: 360,
height: 400,
decoration: BoxDecoration(
color: Colors.lightGreen,
border: Border.all(
color: Colors.green,
width: 2.0,
),
),
),
Container(
width: 190,
height: 400,
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(
color: Colors.purple,
width: 2.0,
),
),
),
],
),
DragObject(
key: GlobalKey(),
initPos: Offset(365, 0.0),
id: 'Item 1',
itmColor: Colors.orange),
DragObject(
key: GlobalKey(),
initPos: Offset(450, 0.0),
id: 'Item 2',
itmColor: Colors.pink,
),
],
),
);
}
}
class DragObject extends StatefulWidget {
final String id;
final Offset initPos;
final Color itmColor;
DragObject({Key key, this.id, this.initPos, this.itmColor}) : super(key: key);
#override
_DragObjectState createState() => _DragObjectState();
}
class _DragObjectState extends State<DragObject> {
GlobalKey _key;
Offset position;
Offset posOffset = Offset(0.0, 0.0);
#override
void initState() {
WidgetsBinding.instance.addPostFrameCallback(_afterLayout);
_key = widget.key;
position = widget.initPos;
super.initState();
}
void _getRenderOffsets() {
final RenderBox renderBoxWidget = _key.currentContext.findRenderObject();
final offset = renderBoxWidget.localToGlobal(Offset.zero);
posOffset = offset - position;
}
void _afterLayout(_) {
_getRenderOffsets();
}
void limitBoundaries(PointerEvent details) {
if (details.position.dx < 0) {
print(details.position);
setState(() {
position = Offset(0.0, position.dy);
});
}
}
#override
Widget build(BuildContext context) {
return Positioned(
left: position.dx,
top: position.dy,
child: Listener(
onPointerMove: limitBoundaries,
child: Draggable(
child: Container(
width: 80,
height: 80,
color: widget.itmColor,
),
feedback: Container(
width: 82,
height: 82,
color: widget.itmColor,
),
childWhenDragging: Container(),
onDragEnd: (drag) {
setState(() {
position = drag.offset - posOffset;
});
},
),
),
);
}
}
Try this. I tweaked this from: Constraining Draggable area .
ValueNotifier<List<double>> posValueListener = ValueNotifier([0.0, 0.0]);
ValueChanged<List<double>> posValueChanged;
double _horizontalPos = 0.0;
double _verticalPos = 0.0;
#override
void initState() {
super.initState();
posValueListener.addListener(() {
if (posValueChanged != null) {
posValueChanged(posValueListener.value);
}
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: <Widget>[
_buildDraggable(),
]));
}
_buildDraggable() {
return SafeArea(
child: Container(
margin: EdgeInsets.only(bottom: 100),
color: Colors.green,
child: Builder(
builder: (context) {
final handle = GestureDetector(
onPanUpdate: (details) {
_verticalPos =
(_verticalPos + details.delta.dy / (context.size.height))
.clamp(.0, 1.0);
_horizontalPos =
(_horizontalPos + details.delta.dx / (context.size.width))
.clamp(.0, 1.0);
posValueListener.value = [_horizontalPos, _verticalPos];
},
child: Container(
child: Container(
margin: EdgeInsets.all(12),
width: 110.0,
height: 170.0,
child: Container(
color: Colors.black87,
),
decoration: BoxDecoration(color: Colors.black54),
),
));
return ValueListenableBuilder<List<double>>(
valueListenable: posValueListener,
builder:
(BuildContext context, List<double> value, Widget child) {
return Align(
alignment: Alignment(value[0] * 2 - 1, value[1] * 2 - 1),
child: handle,
);
},
);
},
),
),
);
}
I've found a workaround for this issue. It's not exactly the output I was looking for but I thought this could be useful to somebody else.
Instead of trying to control the drag object during dragging, I just let it go outside of my screen and I placed it back to its original position in case it goes outside of the screen.
Just a quick note if someone tries my code, I forgot to mention that I'm trying to develop a game for the web. The output on a mobile device might be a little bit odd!
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Draggable Test',
home: GamePlay(),
);
}
}
class GamePlay extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: <Widget>[
Row(
children: [
Container(
width: 360,
height: 400,
decoration: BoxDecoration(
color: Colors.lightGreen,
border: Border.all(
color: Colors.green,
width: 2.0,
),
),
),
Container(
width: 190,
height: 400,
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(
color: Colors.purple,
width: 2.0,
),
),
),
],
),
DragObject(
key: GlobalKey(),
initPos: Offset(365, 0.0),
id: 'Item 1',
itmColor: Colors.orange),
DragObject(
key: GlobalKey(),
initPos: Offset(450, 0.0),
id: 'Item 2',
itmColor: Colors.pink,
),
],
),
);
}
}
class DragObject extends StatefulWidget {
final String id;
final Offset initPos;
final Color itmColor;
DragObject({Key key, this.id, this.initPos, this.itmColor}) : super(key: key);
#override
_DragObjectState createState() => _DragObjectState();
}
class _DragObjectState extends State<DragObject> {
GlobalKey _key;
Offset position;
Offset posOffset = Offset(0.0, 0.0);
#override
void initState() {
WidgetsBinding.instance.addPostFrameCallback(_afterLayout);
_key = widget.key;
position = widget.initPos;
super.initState();
}
void _getRenderOffsets() {
final RenderBox renderBoxWidget = _key.currentContext.findRenderObject();
final offset = renderBoxWidget.localToGlobal(Offset.zero);
posOffset = offset - position;
}
void _afterLayout(_) {
_getRenderOffsets();
}
#override
Widget build(BuildContext context) {
return Positioned(
left: position.dx,
top: position.dy,
child: Listener(
child: Draggable(
child: Container(
width: 80,
height: 80,
color: widget.itmColor,
),
feedback: Container(
width: 82,
height: 82,
color: widget.itmColor,
),
childWhenDragging: Container(),
onDragEnd: (drag) {
setState(() {
if (drag.offset.dx > 0) {
position = drag.offset - posOffset;
} else {
position = widget.initPos;
}
});
},
),
),
);
}
}
I'm still interested if someone can find a proper solution to the initial issue :-)
you could use the property onDragEnd: of the widget Draggable and before setting the new position compare it with the height or width of your device using MediaQuery and update only if you didn't pass the limits of your screen, else set the new position to the initial one.
Example bellow :
Positioned(
left: position.dx,
top: position.dy,
child: Draggable(
maxSimultaneousDrags: 1,
childWhenDragging:
Opacity(opacity: .2, child: rangeEvent(context)),
feedback: rangeEvent(context),
axis: Axis.vertical,
affinity: Axis.vertical,
onDragEnd: (details) => updatePosition(details.offset),
child: Transform.scale(
scale: scale,
child: rangeEvent(context),
),
),
)
In the method updatePosition, you verify the new position before updating:
void updatePosition(Offset newPosition) => setState(() {
if (newPosition.dy > 10 &&
newPosition.dy < MediaQuery.of(context).size.height * 0.9) {
position = newPosition;
} else {
position = const Offset(0, 0);// initial possition
}
});