Flutter: How to set boundaries for a Draggable widget? - flutter

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

Related

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

implementing custom scrolling like Chanel app in flutter?

Recently I installed a new app called Chanel Fashion, on it's home page there is a very strange type of scrolling, which you can see it from below GIF, I highly doubt it's a customized scroller of anytype, I think it's a pageview, any hints on how can I implement such a thing in flutter?
P.s this blog tried to make something like that in android but it's different in many ways.
P.s 2 this SO question tried to implement it on IOS.
This is my demo
demo chanel scroll
library in demo: interpolate: ^1.0.2+2
main.dart
import 'package:chanel_scroll_animation/chanel1/chanel1_page.dart';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
),
home: Chanel1Page(),
);
}
}
chanel1_page.dart
import 'package:chanel_scroll_animation/chanel1/item.dart';
import 'package:chanel_scroll_animation/chanel1/snapping_list_view.dart';
import 'package:chanel_scroll_animation/models/model.dart';
import 'package:flutter/material.dart';
class Chanel1Page extends StatefulWidget {
#override
_Chanel1PageState createState() => _Chanel1PageState();
}
class _Chanel1PageState extends State<Chanel1Page> {
ScrollController _scrollController;
double y=0;
double maxHeight=0;
#override
void initState() {
// TODO: implement initState
super.initState();
_scrollController=new ScrollController();
_scrollController.addListener(() {
print("_scrollController.offset.toString() "+_scrollController.offset.toString());
setState(() {
y=_scrollController.offset;
});
});
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final Size size=MediaQuery.of(context).size;
setState(() {
maxHeight=size.height/2;
});
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: maxHeight!=0?SnappingListView(
controller: _scrollController,
snapToInterval: maxHeight,
scrollDirection: Axis.vertical,
children: [
Container(
height: ( models.length +1) * maxHeight,
child: Column(
children: [
for (int i = 0; i < models.length; i++)
Item(item: models[i],index: i,y: y,)
],
),
)
],
):Container(),
),
);
}
}
item.dart
import 'package:chanel_scroll_animation/models/model.dart';
import 'package:flutter/material.dart';
import 'package:interpolate/interpolate.dart';
const double MIN_HEIGHT = 128;
class Item extends StatefulWidget {
final Model item;
final int index;
final double y;
Item({this.item,this.index,this.y});
#override
_ItemState createState() => _ItemState();
}
class _ItemState extends State<Item> {
Interpolate ipHeight;
double maxHeight=0;
#override
void initState() {
// TODO: implement initState
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final Size size=MediaQuery.of(context).size;
maxHeight=size.height/2;
initInterpolate();
});
}
initInterpolate()
{
ipHeight=Interpolate(
inputRange: [(widget.index-1)*maxHeight,widget.index*maxHeight],
outputRange: [MIN_HEIGHT,maxHeight],
extrapolate: Extrapolate.clamp,
);
}
#override
Widget build(BuildContext context) {
final Size size=MediaQuery.of(context).size;
double height=ipHeight!=null? ipHeight.eval(widget.y):MIN_HEIGHT;
print("height "+height.toString());
return Container(
height: height,
child: Stack(
children: [
Positioned.fill(
child: Image.asset(
widget.item.picture,
fit: BoxFit.cover,
),
),
Positioned(
bottom:40,
left: 30,
right: 30,
child: Column(
children: [
Text(
widget.item.subtitle,
style: TextStyle(fontSize: 16, color: Colors.white),
),
SizedBox(
height: 10,
),
Text(
widget.item.title.toUpperCase(),
style: TextStyle(fontSize: 24, color: Colors.white),
textAlign: TextAlign.center,
),
],
),
)
],
),
);
}
}
snapping_list_view.dart
import "package:flutter/widgets.dart";
import "dart:math";
class SnappingListView extends StatefulWidget {
final Axis scrollDirection;
final ScrollController controller;
final IndexedWidgetBuilder itemBuilder;
final List<Widget> children;
final int itemCount;
final double snapToInterval;
final ValueChanged<int> onItemChanged;
final EdgeInsets padding;
SnappingListView(
{this.scrollDirection,
this.controller,
#required this.children,
#required this.snapToInterval,
this.onItemChanged,
this.padding = const EdgeInsets.all(0.0)})
: assert(snapToInterval > 0),
itemCount = null,
itemBuilder = null;
SnappingListView.builder(
{this.scrollDirection,
this.controller,
#required this.itemBuilder,
this.itemCount,
#required this.snapToInterval,
this.onItemChanged,
this.padding = const EdgeInsets.all(0.0)})
: assert(snapToInterval > 0),
children = null;
#override
createState() => _SnappingListViewState();
}
class _SnappingListViewState extends State<SnappingListView> {
int _lastItem = 0;
#override
Widget build(BuildContext context) {
final startPadding = widget.scrollDirection == Axis.horizontal
? widget.padding.left
: widget.padding.top;
final scrollPhysics = SnappingListScrollPhysics(
mainAxisStartPadding: startPadding, itemExtent: widget.snapToInterval);
final listView = widget.children != null
? ListView(
scrollDirection: widget.scrollDirection,
controller: widget.controller,
children: widget.children,
physics: scrollPhysics,
padding: widget.padding)
: ListView.builder(
scrollDirection: widget.scrollDirection,
controller: widget.controller,
itemBuilder: widget.itemBuilder,
itemCount: widget.itemCount,
physics: scrollPhysics,
padding: widget.padding);
return NotificationListener<ScrollNotification>(
child: listView,
onNotification: (notif) {
if (notif.depth == 0 &&
widget.onItemChanged != null &&
notif is ScrollUpdateNotification) {
final currItem =
(notif.metrics.pixels - startPadding) ~/ widget.snapToInterval;
if (currItem != _lastItem) {
_lastItem = currItem;
widget.onItemChanged(currItem);
}
}
return false;
});
}
}
class SnappingListScrollPhysics extends ScrollPhysics {
final double mainAxisStartPadding;
final double itemExtent;
const SnappingListScrollPhysics(
{ScrollPhysics parent,
this.mainAxisStartPadding = 0.0,
#required this.itemExtent})
: super(parent: parent);
#override
SnappingListScrollPhysics applyTo(ScrollPhysics ancestor) {
return SnappingListScrollPhysics(
parent: buildParent(ancestor),
mainAxisStartPadding: mainAxisStartPadding,
itemExtent: itemExtent);
}
double _getItem(ScrollPosition position) {
return (position.pixels - mainAxisStartPadding) / itemExtent;
}
double _getPixels(ScrollPosition position, double item) {
return min(item * itemExtent, position.maxScrollExtent);
}
double _getTargetPixels(
ScrollPosition position, Tolerance tolerance, double velocity) {
double item = _getItem(position);
if (velocity < -tolerance.velocity)
item -= 0.5;
else if (velocity > tolerance.velocity) item += 0.5;
return _getPixels(position, item.roundToDouble());
}
#override
Simulation createBallisticSimulation(
ScrollMetrics position, double velocity) {
// If we're out of range and not headed back in range, defer to the parent
// ballistics, which should put us back in range at a page boundary.
if ((velocity <= 0.0 && position.pixels <= position.minScrollExtent) ||
(velocity >= 0.0 && position.pixels >= position.maxScrollExtent))
return super.createBallisticSimulation(position, velocity);
final Tolerance tolerance = this.tolerance;
final double target = _getTargetPixels(position, tolerance, velocity);
if (target != position.pixels)
return ScrollSpringSimulation(spring, position.pixels, target, velocity,
tolerance: tolerance);
return null;
}
#override
bool get allowImplicitScrolling => false;
}
Use a with a SingleChildScrollView with a column as it's child. In order to make the picture small when it's a header, use a FittedBox. Wrap the FittedBox with a SizedBox to control the size of the inside widgets. Use a scroll notifier to cause updates when it is scrolling and track how far the user scrolls. Divide the scroll amount by the max height that you want in order to know the current widget that needs resizing. Resize that widget by finding the remainder and dividing it by the max height and multiplying by the difference of the min and max size then add min size. This will ensure a smooth transition. Then make any widgets above in the column max sized and below minimum sized to make sure lag doesn't ruin the scroller.
Use AnimatedOpacity to allow the description of the header to fade in and out or make a customized animation of how you think it should look.
The following code should work though customize the text widgets with what style you'd like. Enter the custom TitleWithImage(contains widget and two strings) items to be in the list, the maxHeight and minHeight into the custom widget. It likely isn't completely optimized and probably has lots of bugs although I fixed some:
import 'package:flutter/material.dart';
class CoolListView extends StatefulWidget {
final List<TitleWithImage> items;
final double minHeight;
final double maxHeight;
const CoolListView({Key key, this.items, this.minHeight, this.maxHeight}) : super(key: key);
#override
_CoolListViewState createState() => _CoolListViewState();
}
class _CoolListViewState extends State<CoolListView> {
List<Widget> widgets=[];
ScrollController _scrollController = new ScrollController();
#override
Widget build(BuildContext context) {
if(widgets.length == 0){
for(int i = 0; i<widget.items.length; i++){
if(i==0){
widgets.add(ListItem(height: widget.maxHeight, item: widget.items[0],descriptionTransparent: false));
}
else{
widgets.add(
ListItem(height: widget.minHeight, item: widget.items[i], descriptionTransparent: true,)
);
}
}
}
return new NotificationListener<ScrollUpdateNotification>(
child: SingleChildScrollView(
controller: _scrollController,
child: Column(
children: widgets,
)
),
onNotification: (t) {
if (t!= null && t is ScrollUpdateNotification) {
int currentWidget = (_scrollController.position.pixels/widget.maxHeight).ceil();
currentWidget = currentWidget==-1?0:currentWidget;
setState(() {
if(currentWidget != widgets.length-1){//makes higher index min
for(int i = currentWidget+1; i<=widgets.length-1; i++){
print(i);
widgets[i] = ListItem(height: widget.minHeight, item: widget.items[i],descriptionTransparent: true,);
}
}
if(currentWidget!=0){
widgets[currentWidget] = ListItem(
height: _scrollController.position.pixels%widget.maxHeight/widget.maxHeight*(widget.maxHeight-widget.minHeight)+widget.minHeight,
item: widget.items[currentWidget],
descriptionTransparent: true,
);
for(int i = currentWidget-1; i>=0; i--){
widgets[i] = ListItem(height: widget.maxHeight,
item: widget.items[i],
descriptionTransparent: false,
);
}
}
else{
widgets[0] = ListItem(
height: widget.maxHeight,
item: widget.items[0],
descriptionTransparent: false
);
}
});
}
},
);
}
}
class TitleWithImage
{
final Widget image;
final String title;
final String description;
TitleWithImage(this.image, this.title, this.description);
}
class ListItem extends StatelessWidget {
final double height;
final TitleWithImage item;
final bool descriptionTransparent;
const ListItem({Key key, this.height, this.item, this.descriptionTransparent}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
child:Stack(
children: [
SizedBox(
height: height,
width: MediaQuery.of(context).size.width,
child: FittedBox(
fit: BoxFit.none,
child:Align(
alignment: Alignment.center,
child: item.image
)
),
),
SizedBox(
height: height,
width: MediaQuery.of(context).size.width,
child: Column(
children: [
Spacer(),
Text(item.title,),
AnimatedOpacity(
child: Text(
item.description,
style: TextStyle(
color: Colors.black
),
),
opacity: descriptionTransparent? 0.0 : 1.0,
duration: Duration(milliseconds: 500),
),
],
),
),
],
),
);
}
}
Edit here is my main.dart:
import 'package:cool_list_view/CoolListView.dart';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Collapsing List Demo')),
body: CoolListView(
items: [
new TitleWithImage(
Container(
height: 1000,
width:1000,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end:
Alignment(0.8, 0.0), // 10% of the width, so there are ten blinds.
colors: [
const Color(0xffee0000),
const Color(0xffeeee00)
], // red to yellow
tileMode: TileMode.repeated, // repeats the gradient over the canvas
),
),
),
'title',
'description',
),
new TitleWithImage(
Container(
height: 1000,
width:1000,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end:
Alignment(0.8, 0.0), // 10% of the width, so there are ten blinds.
colors: [
Colors.orange,
Colors.blue,
], // red to yellow
tileMode: TileMode.repeated, // repeats the gradient over the canvas
),
),
),
'title',
'description',
),
new TitleWithImage(
Container(
height: 1000,
width:1000,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end:
Alignment(0.8, 0.0), // 10% of the width, so there are ten blinds.
colors: [
const Color(0xffee0000),
const Color(0xffeeee00)
], // red to yellow
tileMode: TileMode.repeated, // repeats the gradient over the canvas
),
),
),
'title',
'description',
),
new TitleWithImage(
Container(
height: 1000,
width:1000,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end:
Alignment(0.8, 0.0), // 10% of the width, so there are ten blinds.
colors: [
const Color(0xffee0000),
const Color(0xffeeee00)
], // red to yellow
tileMode: TileMode.repeated, // repeats the gradient over the canvas
),
),
),
'title',
'description',
),
new TitleWithImage(
Container(
height: 1000,
width:1000,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end:
Alignment(0.8, 0.0), // 10% of the width, so there are ten blinds.
colors: [
const Color(0xffee0000),
const Color(0xffeeee00)
], // red to yellow
tileMode: TileMode.repeated, // repeats the gradient over the canvas
),
),
),
'title',
'description',
),
new TitleWithImage(
Container(
height: 1000,
width:1000,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end:
Alignment(0.8, 0.0), // 10% of the width, so there are ten blinds.
colors: [
const Color(0xffee0000),
const Color(0xffeeee00)
], // red to yellow
tileMode: TileMode.repeated, // repeats the gradient over the canvas
),
),
),
'title',
'description',
),
new TitleWithImage(
Container(
height: 1000,
width:1000,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end:
Alignment(0.8, 0.0), // 10% of the width, so there are ten blinds.
colors: [
const Color(0xffee0000),
const Color(0xffeeee00)
], // red to yellow
tileMode: TileMode.repeated, // repeats the gradient over the canvas
),
),
),
'title',
'description',
),
new TitleWithImage(Container(height: 1000,width:1000,color: Colors.blue), 'title', 'description'),
new TitleWithImage(Container(height: 1000,width:1000, color: Colors.orange), 'title', 'description'),
],
minHeight: 50,
maxHeight: 300,
),
),
);
}
}
You can do that using ScrollController value to change the size of the widget or it's children's, sorry I can't write the code because it's time consuming and requires some computation but watch this video:https://www.youtube.com/watch?v=Cn6VCTaHB-k&t=558s it will gave you the basic idea and help you keep going.
try to use Sliver.
This is an example of what I mean:
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
backgroundColor: Color(0xFF0084C9),
leading: IconButton(
icon: Icon(
Icons.blur_on,
color: Colors.white70,
),
onPressed: () {
Scaffold.of(context).openDrawer();
},
),
expandedHeight: bannerHigh,
floating: true,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: Text("Your title",
style: TextStyle(
fontSize: 18,
color: Colors.white,
fontWeight: FontWeight.w600)),
background: Image.network(
'image url',
fit: BoxFit.cover,
),
),
),
SliverList(
delegate: SliverChildListDelegate(
<Widget>[
],
),
),
],
),
);

Flutter Slide Transition To a Specific Location

I'm making a grammar quiz app using flutter, I have a question and a couple of choices, I want to make the choice slides to the empty space part of the question with a slide animation
For example:
How is _ new School?
(You) (Your) (It)
and when I press on (Your) the choice widget slides to the _ leaving an empty container
How is (Your) new School?
(You) ( ) (It)
I Made it with Draggable and DragTarget and you can see it in these images
image 1
image 2
but I want it to slide when I press on it without dragging and dropping
here is some of the code
class QuestionsScreen extends StatefulWidget {
QuestionsScreen({Key key}) : super(key: key);
#override
_QuestionsScreenState createState() => _QuestionsScreenState();
}
class _QuestionsScreenState extends State<QuestionsScreen> {
String userAnswer = "_";
int indexOfDragPlace = QuestionBrain.getQuesitonText().indexOf("_");
#override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
return Scaffold(
body: SafeArea(
child: Column(
children: [
Container(
padding: EdgeInsets.all(10),
color: Colors.white,
child: Center(
child: Scrollbar(
child: ListView(
children: [
Center(
child: Wrap(
children: [
...QuestionBrain.getQuesitonText()
.substring(0, indexOfDragPlace)
.split(" ")
.map((e) => QuestionHolder(
question: e + " ",
)),
_buildDragTarget(),
...QuestionBrain.getQuesitonText()
.substring(indexOfDragPlace + 1)
.split(" ")
.map((e) => QuestionHolder(
question: e + " ",
)),
],
),
)
],
),
),
),
),
Wrap(
children: [
...QuestionBrain.choices.map((choice) {
if (choice == userAnswer) {
return ChoiceHolder(
choice: "",
backGroundColor: Colors.black12,
);
}
return DraggableChoiceBox(
choice: choice,
userAnswer: userAnswer,
onDragStarted: () {
setState(() {
dragedAnswerResult = "";
});
},
onDragCompleted: () {
setState(() {
userAnswer = choice;
setState(() {
answerColor = Colors.orange;
});
print("Called");
});
},
);
}).toList()
],
),
],
),
),
);
}
Widget _buildDragTarget() {
return DragTarget<String>(
builder: (context, icoming, rejected) {
return Material(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
width: MediaQuery.of(context).size.width * 0.20,
height: MediaQuery.of(context).size.height * 0.05,
color: answerColor,
child: FittedBox(
child: Text(
userAnswer,
style: TextStyle(
fontSize: 12,
color: Colors.white,
fontWeight: FontWeight.bold),
),
),
),
);
},
onAccept: (data) {
userAnswer = data;
answerColor = Colors.orange;
},
);
}
}
class DraggableChoiceBox extends StatelessWidget {
const DraggableChoiceBox({
Key key,
this.choice,
this.userAnswer,
this.onDragCompleted,
this.onDragStarted,
}) : super(key: key);
final String choice;
final String userAnswer;
final Function onDragCompleted;
final Function onDragStarted;
#override
Widget build(BuildContext context) {
return Draggable(
onDragCompleted: onDragCompleted,
data: choice,
child: ChoiceHolder(choice: choice),
feedback: Material(
elevation: 20,
child: ChoiceHolder(
choice: choice,
margin: 0,
),
),
childWhenDragging: ChoiceHolder(
choice: "",
backGroundColor: Colors.black12,
),
onDragStarted: onDragStarted,
);
}
}
You can use overlays similar to the way Hero widgets work, here is an "unpolished" example:
import 'package:flutter/material.dart';
class SlideToPosition extends StatefulWidget {
#override
_SlideToPositionState createState() => _SlideToPositionState();
}
class _SlideToPositionState extends State<SlideToPosition> {
GlobalKey target = GlobalKey();
GlobalKey toMove = GlobalKey();
double dx = 0.0, dy = 0.0, dxStart = 0.0, dyStart = 0.0;
String choosedAnswer = '', answer = 'answer', finalAnswer = '';
OverlayEntry overlayEntry;
#override
void initState() {
overlayEntry = OverlayEntry(
builder: (context) => TweenAnimationBuilder(
duration: Duration(milliseconds: 500),
tween:
Tween<Offset>(begin: Offset(dxStart, dyStart), end: Offset(dx, dy)),
builder: (context, offset, widget) {
return Positioned(
child: Material(
child: Container(
color: Colors.transparent,
height: 29,
width: 100,
child: Center(child: Text(choosedAnswer)))),
left: offset.dx,
top: offset.dy,
);
},
),
);
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: [
SizedBox(
height: 20,
),
Row(
children: [
Text('text'),
Container(
key: target,
height: 30,
width: 100,
child: Center(child: Text(finalAnswer)),
decoration:
BoxDecoration(border: Border(bottom: BorderSide())),
),
Text('text')
],
),
SizedBox(
height: 20,
),
GestureDetector(
child: Container(
height: 30,
width: 100,
color: Colors.blue[200],
child: Center(child: Text(answer, key: toMove))),
onTap: () async {
setState(() {
answer = '';
});
RenderBox box1 = target.currentContext.findRenderObject();
Offset targetPosition = box1.localToGlobal(Offset.zero);
RenderBox box2 = toMove.currentContext.findRenderObject();
Offset toMovePosition = box2.localToGlobal(Offset.zero);
setState(() {
answer = '';
choosedAnswer = 'answer';
});
dxStart = toMovePosition.dx;
dyStart = toMovePosition.dy;
dx = targetPosition.dx;
dy = targetPosition.dy;
Overlay.of(context).insert(overlayEntry);
setState(() {});
await Future.delayed(Duration(milliseconds: 500));
overlayEntry.remove();
setState(() {
finalAnswer = 'answer';
});
},
),
],
),
),
);
}
}
Sorry for the poor naming of the variables :)

Flutter GestureDector used to drag widget: Coordinates to not match second drag

I want to be able to drag an widget to any place on the screen. The first time the widget is dragged and place in a section of the screen. There is no issue. On the second attempt to drag of the widget container, the coordinates to not match and the widget jumps to the top of the screen. I want to have multiple widgets that can be dragged.
Any suggestions welcomed.
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
// visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'HomePage'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
double ytop = 50.0;
double xleft = 50.0;
GlobalKey blueKey = GlobalKey();
String position = '';
#override
void initState() {
// _getPositions();
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_getPositions();
});
}
#override
Widget build(BuildContext context) {
// _getPositions();
print('$xleft <---> $ytop');
return Scaffold(
// appBar: AppBar(
// title: Text(widget.title),
// ),
body: Container(
child: Stack(
children: <Widget>[
Positioned(
top: 30,
left: 10,
child: Text(
'Filler Text',
),
),
Positioned(
top: ytop,
left: xleft,
child: GestureDetector(
dragStartBehavior: DragStartBehavior.down,
onVerticalDragStart: _onVerticalDragStartHandler,
onVerticalDragUpdate: _onDragUpdateHandler,
onVerticalDragEnd: (details) => {print('On END $details')},
onTap: () {
print('HELLO');
_getPositions();
},
child: Container(
key: blueKey,
width: 100,
height: 100,
decoration: myBlueBoxDecoration,
),
// ),
),
),
],
),
),
// This trailing comma makes auto-formatting nicer for build methods.
);
}
/// Track starting point of a vertical gesture
void _onVerticalDragStartHandler(DragStartDetails details) {
setState(() {
ytop = details.globalPosition.dy;
xleft = details.globalPosition.dx;
// final dd = details.globalPosition.dy;
print('Vertical Start Local Vertical Left: $xleft Top: $ytop');
});
}
void _onDragUpdateHandler(DragUpdateDetails details) {
setState(
() {
ytop = details.localPosition.dy;
xleft = details.localPosition.dx;
xleft = xleft < 0 ? 0 : xleft;
ytop = ytop < 10 ? 10 : ytop;
// xleft = xleft > 259 ? 259 : xleft;
// ytop = ytop > 618 ? 618 : ytop;
print('Drage Update:--> Left: $xleft Top: $ytop\n');
print('');
},
);
}
_getPositions() {
final RenderBox renderBoxRed = blueKey.currentContext.findRenderObject();
final positionRed = renderBoxRed.localToGlobal(Offset.zero);
position = 'POSITION of Red: $positionRed';
print(position);
}
}
BoxDecoration myBlueBoxDecoration = BoxDecoration(
color: const Color(0xff7c94b6),
border: Border.all(color: Colors.green, width: 1),
borderRadius: BorderRadius.circular(12),
);
I fixed the problem by moving to LongPressDraggable. It would be good to reduce the time that makes the widget able to drag. See the code below. Any suggestions on the original implementation welcomed.
Positioned(
top: ytop,
left: xleft,
child: LongPressDraggable(
childWhenDragging: Container(
// key: blueKey,
width: w,
height: h,
decoration: myBlueBoxDecoration,
),
onDragStarted: genericCall,
onDragEnd: (d) {
setState(() {
xleft = d.offset.dx;
ytop = d.offset.dy;
print(d.offset.dx);
});
},
feedback: Container(
// key: blueKey,
width: 100,
height: 100,
decoration: myGreenBoxDecoration,
),
child: Container(
key: blueKey,
width: 100,
height: 100,
decoration: myBlueBoxDecoration,
),
),
),
I guess the old man is thinking to slow. Here is fix I needed.
Positioned(
top: ytop,
left: xleft,
child: Draggable(
childWhenDragging: Container(
// key: blueKey,
width: w,
height: h,
decoration: myBlueBoxDecoration,
),
onDragStarted: genericCall,
onDragEnd: (d) {
setState(() {
xleft = d.offset.dx;
ytop = d.offset.dy;
print(d.offset.dx);
});
},
feedback: Container(
// key: blueKey,
width: 100,
height: 100,
decoration: myGreenBoxDecoration,
),
child: Container(
key: blueKey,
width: 100,
height: 100,
decoration: myBlueBoxDecoration,
),
),
),

Multiple GestureDetector onPan event in Stack not working

I have to implement a custom analog clock where the clock hands indicate the breakfast,lunch and dinner time and user is able to reset the time by rotating the hands. I was able to draw the clock and hands with the help of canvas and CustomPaint and i haved stacked up in Stack Widget.I have added GestureDetector for each hand and using onPan events am able to rotate the hands also.But the issue is that when multiple hands are put in Stack only the last added clock hand is moving with the gesture others are not detecting the gesture.How to pass down the event to the stack so that all the gesture detectors detects it?
For drawing clock hands
class ClockHandComponent extends StatefulWidget {
final String title;
final int time;
final Color color;
const ClockHandComponent({Key key, this.title, this.time, this.color})
: super(key: key);
#override
ClockHandComponentState createState() {
return new ClockHandComponentState();
}
}
class ClockHandComponentState extends State<ClockHandComponent> {
int time;
double angle;
ClockHandPainter _clockHandPainter;
#override
void initState() {
time = widget.time;
angle = 2 * pi / 24 * time;
initialize();
super.initState();
}
#override
void didUpdateWidget(ClockHandComponent oldWidget) {
initialize();
super.didUpdateWidget(oldWidget);
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onPanDown: _onPanDown,
onPanUpdate: _onPanUpdate,
onPanEnd: _onPanEnd,
child: new Container(
width: double.infinity,
height: double.infinity,
child: new CustomPaint(
painter: _clockHandPainter,
)),
);
}
_onPanUpdate(DragUpdateDetails details) {
print(details);
RenderBox renderBox = context.findRenderObject();
var position = renderBox.globalToLocal(details.globalPosition);
angle = -coordinatesToRadians(_clockHandPainter.center, position);
print(angle);
initialize();
setState(() {});
}
_onPanEnd(_) {}
_onPanDown(DragDownDetails details) {
print(details);
print(angle);
}
initialize() {
_clockHandPainter =
new ClockHandPainter(widget.title, time, widget.color, angle);
}
double coordinatesToRadians(Offset center, Offset coords) {
var a = coords.dx - center.dx;
var b = center.dy - coords.dy;
return atan2(b, a);
}
}
For drawing clock face
class ClockFaceComponent extends StatefulWidget {
#override
ClockFaceComponentState createState() {
return new ClockFaceComponentState();
}
}
class ClockFaceComponentState extends State<ClockFaceComponent> {
#override
Widget build(BuildContext context) {
return new Padding(
padding: const EdgeInsets.all(10.0),
child: new AspectRatio(
aspectRatio: 1.0,
child: new Container(
width: double.infinity,
decoration: new BoxDecoration(
shape: BoxShape.circle,
color: ColorResource.clockBackgroundColor,
),
child: new Stack(
fit: StackFit.expand,
children: <Widget>[
//dial and numbers
new Container(
width: double.infinity,
height: double.infinity,
child: new CustomPaint(
painter: new ClockDialPainter(),
),
),
new Center(
child: new Container(
width: 40.0,
height: 40.0,
decoration: new BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
),
)),
//centerpoint
new Center(
child: new Container(
width: 15.0,
height: 15.0,
decoration: new BoxDecoration(
shape: BoxShape.circle,
color: ColorResource.clockCenterColor,
),
),
),
getClockHands()
],
),
),
),
);
}
getClockHands() {
return new AspectRatio(
aspectRatio: 1.0,
child: new Stack(
fit: StackFit.expand,
children: getHands(),
));
}
getHands() {
List<Widget> widgets = new List();
List<Hands> hands = Hands.getHands();
for (Hands hand in hands) {
widgets.add(ClockHandComponent(
title: hand.title,
time: hand.time,
color: hand.color,
));
}
return widgets;
}
}