I've made a page that contains several textfields and buttons in a column which is contained in a container that has a background image. And this container is itself the child of a scrollview widget.
So when a person clicks on one of the fields, their keyboard will pop up (taking a portion of the screen), which means some buttons/fields are offscreen, which is where the scrollview widget serves its purpose.
The problem here is that I want to limit how far the scroll view allows a user to scroll.
There are some blank space under the lowest button, and I don't want the user to be able to scroll all the way there. This is too keep the experience simple and not have the user "overscroll" past the fields he should be typing in.
But since the background image is part of the scroll view the view will allow a user to scroll as far down as the bottom of the image. I want to limit this.
As a follow-up I'm trying to figure out how to set an initial scroll position. (So that when clicking on a field the scroll view scrolls down to very first text field, so all fields are in view. without the user needing to scroll down to them. However I don't want this scroll position to be re-applied every time the user clicks on a field, of course.)
Here is the relevant (if any of my code looks really bad please say so, I'm new to programming in general and accept any advice to improve):
class LoginPageConstructor extends StatelessWidget {
#override
Widget build(BuildContext context) {
AssetImage loginBackgroundAsset =
new AssetImage("assets/loginscreen/backgroundrock.png");
// var _scrollController = new ScrollController(
// initialScrollOffset: 200.0,
// keepScrollOffset: true);
return new Scaffold(
body: new Container(
child: new ListView(key: new PageStorageKey("Divider 1"),
// controller: _scrollController,
children: <Widget>[
new Stack(children: <Widget>[
new Container(
constraints: new BoxConstraints.expand(height: 640.0),
decoration: new BoxDecoration(
image: new DecorationImage(
image: loginBackgroundAsset, fit: BoxFit.cover)),
child: new Column(
children: <Widget>[
new Divider(height: 300.0,),
new Center(child: new UsernameText(),),
new Divider(height: 8.0,),
new Center(child: new PasswordText(),),
new Divider(),
new LoginButton(),
new Divider(),
new SignUpButton(),
],
))
])
],
),
));
}
}
For auto-scrolling the fields into view, it sounds like you are wrestling with issue 10826. I posted a workaround on that issue. I adapted the workaround to your sample code; see below. (You may want to tweak it a little.)
If you want to prevent users from scrolling, you might want to just ensure that all the fields are visible using the same techniques below and then use a NeverScrollableScrollPhysics as the physics of the ListView. Or if you're feeling ambitious you could implement a custom scroll physics as shown in the Gallery example. If I were you I'd hold out for #10826 to be fixed, though.
import 'package:meta/meta.dart';
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(new MaterialApp(home: new LoginPage()));
}
/// A widget that ensures it is always visible when focused.
class EnsureVisibleWhenFocused extends StatefulWidget {
const EnsureVisibleWhenFocused({
Key key,
#required this.child,
#required this.focusNode,
this.curve: Curves.ease,
this.duration: const Duration(milliseconds: 100),
}) : super(key: key);
/// The node we will monitor to determine if the child is focused
final FocusNode focusNode;
/// The child widget that we are wrapping
final Widget child;
/// The curve we will use to scroll ourselves into view.
///
/// Defaults to Curves.ease.
final Curve curve;
/// The duration we will use to scroll ourselves into view
///
/// Defaults to 100 milliseconds.
final Duration duration;
EnsureVisibleWhenFocusedState createState() => new EnsureVisibleWhenFocusedState();
}
class EnsureVisibleWhenFocusedState extends State<EnsureVisibleWhenFocused> {
#override
void initState() {
super.initState();
widget.focusNode.addListener(_ensureVisible);
}
#override
void dispose() {
super.dispose();
widget.focusNode.removeListener(_ensureVisible);
}
Future<Null> _ensureVisible() async {
// Wait for the keyboard to come into view
// TODO: position doesn't seem to notify listeners when metrics change,
// perhaps a NotificationListener around the scrollable could avoid
// the need insert a delay here.
await new Future.delayed(const Duration(milliseconds: 600));
if (!widget.focusNode.hasFocus)
return;
final RenderObject object = context.findRenderObject();
final RenderAbstractViewport viewport = RenderAbstractViewport.of(object);
assert(viewport != null);
ScrollableState scrollableState = Scrollable.of(context);
assert(scrollableState != null);
ScrollPosition position = scrollableState.position;
double alignment;
if (position.pixels > viewport.getOffsetToReveal(object, 0.0)) {
// Move down to the top of the viewport
alignment = 0.0;
} else if (position.pixels < viewport.getOffsetToReveal(object, 1.0)) {
// Move up to the bottom of the viewport
alignment = 1.0;
} else {
// No scrolling is necessary to reveal the child
return;
}
position.ensureVisible(
object,
alignment: alignment,
duration: widget.duration,
curve: widget.curve,
);
}
Widget build(BuildContext context) => widget.child;
}
class LoginPage extends StatefulWidget {
LoginPageState createState() => new LoginPageState();
}
class LoginPageState extends State<LoginPage> {
FocusNode _usernameFocusNode = new FocusNode();
FocusNode _passwordFocusNode = new FocusNode();
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Example App'),
),
body: new Container(
child: new ListView(
physics: new NeverScrollableScrollPhysics(),
key: new PageStorageKey("Divider 1"),
children: <Widget>[
new Container(
constraints: new BoxConstraints.expand(height: 640.0),
decoration: new BoxDecoration(
image: new DecorationImage(
image: new NetworkImage(
'https://flutter.io/images/flutter-mark-square-100.png',
),
fit: BoxFit.cover,
),
),
child: new Column(
children: <Widget>[
new Container(
height: 300.0,
),
new Center(
child: new EnsureVisibleWhenFocused(
focusNode: _usernameFocusNode,
child: new TextFormField(
focusNode: _usernameFocusNode,
decoration: new InputDecoration(
labelText: 'Username',
),
),
),
),
new Container(height: 8.0),
new Center(
child: new EnsureVisibleWhenFocused(
focusNode: _passwordFocusNode,
child: new TextFormField(
focusNode: _passwordFocusNode,
obscureText: true,
decoration: new InputDecoration(
labelText: 'Password',
),
),
),
),
new Container(),
new RaisedButton(
onPressed: () {},
child: new Text('Log in'),
),
new Divider(),
new RaisedButton(
onPressed: () {},
child: new Text('Sign up'),
),
],
),
),
],
),
),
);
}
}
Related
Normal situation
When keyboard opens
It shows overflow error
When resizeToAvoidBottomInset included
TextField is hidden beneath keyboard
Code:
void main() => runApp(MaterialApp(home: HomePage()));
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
// resizeToAvoidBottomInset: false, // including this hides TextField beneath Keyboard
body: Column(
children: <Widget>[
FlutterLogo(size: 500),
TextField(decoration: InputDecoration(hintText: " Enter something here ...")),
Expanded(child: FlutterLogo(size: 300)),
],
),
);
}
}
Edit:
What I want is the TextField should scroll up automatically when it it is in focus, that also means FlutterLogo1 should scroll up too and FlutterLogo2 should be underneath keyboard. (This is very common behavior in Android natively, everything is handled for you)
I think that will work.
void main() => runApp(MaterialApp(home: HomePage()));
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
// resizeToAvoidBottomInset: false, // including this hides TextField beneath Keyboard
body: Stack(
children: <Widget>[
FlutterLogo(size: 500),
Column(children: <Widget>[
Container(
margin: EdgeInsets.fromLTRB(0.0, 200.0, 0.0, 0.0),
child: TextField( decoration: InputDecoration(hintText: " Enter something here ..."))),
Expanded(child: FlutterLogo(size: 300)),
],)
],
),
);
}
}
I'm making an image gallery and I need the user to be able to long-press an image to show a popup menu which will let him delete the image.
My code, so far:
return GestureDetector(
onLongPress: () {
showMenu(
items: <PopupMenuEntry>[
PopupMenuItem(
value: this._index,
child: Row(
children: <Widget>[
Icon(Icons.delete),
Text("Delete"),
],
),
)
],
context: context,
);
},
child: Image.memory(
this._asset.thumbData.buffer.asUint8List(),
fit: BoxFit.cover,
gaplessPlayback: true,
),
);
Which produces:
But also, I couldn't find out how to completely remove the image's widget when the longPress function is called. How to do so?
The OP and the First Answerer bypassed the original problem using PopupMenuButton, which worked fine in their case. But I think the more general question of how to position one's own menu and how to receive the user's response without using PopupMenuButton is worth answering, because sometimes we want a popup menu on a custom widget, and we want it to appear on some gestures other than a simple tap (e.g. the OP's original intention was to long-press).
I set out to make a simple app demonstrating the following:
Use a GestureDetector to capture long-press
Use the function showMenu() to display a popup menu, and position it near the finger's touch
How to receive the user's selection
(Bonus) How to make a PopupMenuEntry that represents multiple values (the oft-used PopupMenuItem can only represent a single value)
The result is, when you long-press on a big yellow area, a popup menu appears on which you can select +1 or -1, and the big number would increment or decrement accordingly:
Skip to the end for the entire body of code. Comments are sprinkled in there to explain what I am doing. Here are a few things to note:
showMenu()'s position parameter takes some effort to understand. It's a RelativeRect, which represents how a smaller rect is positioned inside a bigger rect. In our case, the bigger rect is the entire screen, the smaller rect is the area of touch. Flutter positions the popup menu according to these rules (in plain English):
if the smaller rect leans toward the left half of the bigger rect, the popup menu would align with the smaller rect's left edge
if the smaller rect leans toward the right half of the bigger rect, the popup menu would align with the smaller rect's right edge
if the smaller rect is in the middle, which edge wins depends on the language's text direction. Left edge wins if using English and other left-to-right languages, right edge wins otherwise.
It's always useful to reference PopupMenuButton's official implementation to see how it uses showMenu() to display the menu.
showMenu() returns a Future. Use Future.then() to register a callback to handle user selection. Another option is to use await.
Remember that PopupMenuEntry is a (subclass of) StatefulWidget. You can layout any number of sub-widgets inside it. This is how you represent multiple values in a PopupMenuEntry. If you want it to represent two values, just make it contain two buttons, however you want to lay them out.
To close the popup menu, use Navigator.pop(). Flutter treats popup menus like a smaller "page". When we display a popup menu, we are actually pushing a "page" to the navigator's stack. To close a popup menu, we pop it from the stack, thus completing the aforementioned Future.
Here is the full code:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Popup Menu Usage',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Popup Menu Usage'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
var _count = 0;
var _tapPosition;
void _showCustomMenu() {
final RenderBox overlay = Overlay.of(context).context.findRenderObject();
showMenu(
context: context,
items: <PopupMenuEntry<int>>[PlusMinusEntry()],
position: RelativeRect.fromRect(
_tapPosition & const Size(40, 40), // smaller rect, the touch area
Offset.zero & overlay.size // Bigger rect, the entire screen
)
)
// This is how you handle user selection
.then<void>((int delta) {
// delta would be null if user taps on outside the popup menu
// (causing it to close without making selection)
if (delta == null) return;
setState(() {
_count = _count + delta;
});
});
// Another option:
//
// final delta = await showMenu(...);
//
// Then process `delta` however you want.
// Remember to make the surrounding function `async`, that is:
//
// void _showCustomMenu() async { ... }
}
void _storePosition(TapDownDetails details) {
_tapPosition = details.globalPosition;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
GestureDetector(
// This does not give the tap position ...
onLongPress: _showCustomMenu,
// Have to remember it on tap-down.
onTapDown: _storePosition,
child: Container(
color: Colors.amberAccent,
padding: const EdgeInsets.all(100.0),
child: Text(
'$_count',
style: const TextStyle(
fontSize: 100, fontWeight: FontWeight.bold),
),
),
),
],
),
),
);
}
}
class PlusMinusEntry extends PopupMenuEntry<int> {
#override
double height = 100;
// height doesn't matter, as long as we are not giving
// initialValue to showMenu().
#override
bool represents(int n) => n == 1 || n == -1;
#override
PlusMinusEntryState createState() => PlusMinusEntryState();
}
class PlusMinusEntryState extends State<PlusMinusEntry> {
void _plus1() {
// This is how you close the popup menu and return user selection.
Navigator.pop<int>(context, 1);
}
void _minus1() {
Navigator.pop<int>(context, -1);
}
#override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Expanded(child: FlatButton(onPressed: _plus1, child: Text('+1'))),
Expanded(child: FlatButton(onPressed: _minus1, child: Text('-1'))),
],
);
}
}
If you are going to use a gridView or listview for laying out the images on the screen, you can wrap each item with a gesture detector then you should keep your images in a list somewhere, then simply remove the image from the list and call setState().
Something like the following. (This code will probably won't compile but it should give you the idea)
ListView.builder(
itemCount: imageList.length,
itemBuilder: (BuildContext context, int index) {
return GestureDetector(
onLongPress: () {
showMenu(
onSelected: () => setState(() => imageList.remove(index))}
items: <PopupMenuEntry>[
PopupMenuItem(
value: this._index,
child: Row(
children: <Widget>[
Icon(Icons.delete),
Text("Delete"),
],
),
)
],
context: context,
);
},
child: imageList[index],
);
}
)
Edit: You can use a popup menu too, like following
Container(
margin: EdgeInsets.symmetric(vertical: 10),
height: 100,
width: 100,
child: PopupMenuButton(
child: FlutterLogo(),
itemBuilder: (context) {
return <PopupMenuItem>[new PopupMenuItem(child: Text('Delete'))];
},
),
),
Building on the answers by Nick Lee and hacker1024, but instead of turning the solution into a mixin, you could simply just turn it into a widget:
class PopupMenuContainer<T> extends StatefulWidget {
final Widget child;
final List<PopupMenuEntry<T>> items;
final void Function(T) onItemSelected;
PopupMenuContainer({#required this.child, #required this.items, #required this.onItemSelected, Key key}) : super(key: key);
#override
State<StatefulWidget> createState() => PopupMenuContainerState<T>();
}
class PopupMenuContainerState<T> extends State<PopupMenuContainer<T>>{
Offset _tapDownPosition;
#override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (TapDownDetails details){
_tapDownPosition = details.globalPosition;
},
onLongPress: () async {
final RenderBox overlay = Overlay.of(context).context.findRenderObject();
T value = await showMenu<T>(
context: context,
items: widget.items,
position: RelativeRect.fromLTRB(
_tapDownPosition.dx,
_tapDownPosition.dy,
overlay.size.width - _tapDownPosition.dx,
overlay.size.height - _tapDownPosition.dy,
),
);
widget.onItemSelected(value);
},
child: widget.child
);
}
}
And then you'd use it like this:
child: PopupMenuContainer<String>(
child: Image.asset('assets/image.png'),
items: [
PopupMenuItem(value: 'delete', child: Text('Delete'))
],
onItemSelected: (value) async {
if( value == 'delete' ){
await showDialog(context: context, child: AlertDialog(
title: Text('Delete image'),
content: Text('Are you sure you want to delete the image?'),
actions: [
uiFlatButton(child: Text('NO'), onTap: (){ Navigator.of(context).pop(false); }),
uiFlatButton(child: Text('YES'), onTap: (){ Navigator.of(context).pop(true); }),
],
));
}
},
),
Adjust the code to fit your needs.
Nick Lee's answer can be turned into a mixin quite easily, which can then be used anywhere you want to use a popup menu.
The mixin:
import 'package:flutter/material.dart' hide showMenu;
import 'package:flutter/material.dart' as material show showMenu;
/// A mixin to provide convenience methods to record a tap position and show a popup menu.
mixin CustomPopupMenu<T extends StatefulWidget> on State<T> {
Offset _tapPosition;
/// Pass this method to an onTapDown parameter to record the tap position.
void storePosition(TapDownDetails details) => _tapPosition = details.globalPosition;
/// Use this method to show the menu.
Future<T> showMenu<T>({
#required BuildContext context,
#required List<PopupMenuEntry<T>> items,
T initialValue,
double elevation,
String semanticLabel,
ShapeBorder shape,
Color color,
bool captureInheritedThemes = true,
bool useRootNavigator = false,
}) {
final RenderBox overlay = Overlay.of(context).context.findRenderObject();
return material.showMenu<T>(
context: context,
position: RelativeRect.fromLTRB(
_tapPosition.dx,
_tapPosition.dy,
overlay.size.width - _tapPosition.dx,
overlay.size.height - _tapPosition.dy,
),
items: items,
initialValue: initialValue,
elevation: elevation,
semanticLabel: semanticLabel,
shape: shape,
color: color,
captureInheritedThemes: captureInheritedThemes,
useRootNavigator: useRootNavigator,
);
}
}
And then, to use it:
import 'package:flutter/material.dart';
import './custom_context_menu.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Popup Menu Usage',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Popup Menu Usage'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with CustomPopupMenu {
var _count = 0;
void _showCustomMenu() {
this.showMenu(
context: context,
items: <PopupMenuEntry<int>>[PlusMinusEntry()],
)
// This is how you handle user selection
.then<void>((int delta) {
// delta would be null if user taps on outside the popup menu
// (causing it to close without making selection)
if (delta == null) return;
setState(() {
_count = _count + delta;
});
});
// Another option:
//
// final delta = await showMenu(...);
//
// Then process `delta` however you want.
// Remember to make the surrounding function `async`, that is:
//
// void _showCustomMenu() async { ... }
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
GestureDetector(
// This does not give the tap position ...
onLongPress: _showCustomMenu,
// Have to remember it on tap-down.
onTapDown: storePosition,
child: Container(
color: Colors.amberAccent,
padding: const EdgeInsets.all(100.0),
child: Text(
'$_count',
style: const TextStyle(fontSize: 100, fontWeight: FontWeight.bold),
),
),
),
],
),
),
);
}
}
class PlusMinusEntry extends PopupMenuEntry<int> {
#override
double height = 100;
// height doesn't matter, as long as we are not giving
// initialValue to showMenu().
#override
bool represents(int n) => n == 1 || n == -1;
#override
PlusMinusEntryState createState() => PlusMinusEntryState();
}
class PlusMinusEntryState extends State<PlusMinusEntry> {
void _plus1() {
// This is how you close the popup menu and return user selection.
Navigator.pop<int>(context, 1);
}
void _minus1() {
Navigator.pop<int>(context, -1);
}
#override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Expanded(child: FlatButton(onPressed: _plus1, child: Text('+1'))),
Expanded(child: FlatButton(onPressed: _minus1, child: Text('-1'))),
],
);
}
}
Answer for 2023
In Flutter 3.7 there is now a ContextMenuRegion widget that you can wrap around any existing widget. When the user long presses or right-clicks (depending on the platform), the menu you give it will appear.
return Scaffold(
body: Center(
child: ContextMenuRegion(
contextMenuBuilder: (context, offset) {
return AdaptiveTextSelectionToolbar.buttonItems(
anchors: TextSelectionToolbarAnchors(
primaryAnchor: offset,
),
buttonItems: <ContextMenuButtonItem>[
ContextMenuButtonItem(
onPressed: () {
ContextMenuController.removeAny();
},
label: 'Save',
),
],
);
},
child: const SizedBox(
width: 200.0,
height: 200.0,
child: FlutterLogo(),
),
),
),
);
I have problems to get real local position on gesture detector widget using pan update.
new Center(
child new Container(
height: 150.0,
width: 150.0,
decoration: new BoxDecoration(
borderRadius: new BorderRadius.circular(5.0),
color: Colors.white,
),
child: new GestureDetector(
onPanUpdate: (details) => onPanUpdate(context, details),
child: new CustomPaint(
painter: new MyPainter(
points: points,
limitBottom:height - 125.0,
limitTop: 10.0,
limitLeft: 5.0,
limitRight: width - 55.0
),
),
),
),
)
when I print the the Offset of global position && local position, I've got same value for both of them. in Result it was painted outside of my Container widget. Is there anything I missed to get local position?
I assume you're using something like this to get the local position:
RenderBox getBox = context.findRenderObject();
var local = getBox.globalToLocal(start.globalPosition);
The reason you'd be getting the wrong offset after doing that has to do with the context you're using to find the local position.
If you're using the overall widget's context, essentially what you're doing is calculating the offset within the overall widget. i.e.
YourWidget <-- context
Center
Container
GestureDetector
...
Let's say that the screen looks something like this:
1__________________________
| | <--- YourWidget
| 2_________ |
| | gesture | |
| | detector| |
| | 3. | |
| |_________| |
| |
|__________________________|
With 1 being the top left of your overall widget (and the screen), 2 being the top left of your gesture detector, and 3 being the point where you tap.
By using the context of YourWidget, you're calculating the position of the tap based on the difference between 1 and 3. If 1 happens to be at the top left of the screen, the result will match the global coordinates.
By using the context for your gesture detector, you'd instead be measuring the distance between 2 and 3, which will give you the offset you want.
There's two ways to fix this - either you can wrap your GestureDetector in a Builder widget, or you can create a new Stateful/Stateless widget that encapsulates just the GestureDetector. I'd personally recommend creating a new widget.
I was having a problem with my "tap position" being offset. I believe it was the same issue mentioned above. I was able to solve this similar problem by reading about keys and finding the location/size of widgets here:
https://medium.com/#diegoveloper/flutter-widget-size-and-position-b0a9ffed9407
So I didn't have to create another widget or layout builder as mentioned above, but I was able to just add a global key and then grab the position of the tap by doing the following with "currentContext" that comes from the key. Very cool!
void _handleLongPress(LongPressStartDetails details) {
final RenderBox referenceBox = _keyRink.currentContext.findRenderObject();
var x = referenceBox.globalToLocal(details.globalPosition);
if (x == tapPosition) {
Vibration.vibrate(duration: 50);
setState(() {
_shots[_homeShotCount] = tapPosition;
});
}
}
Here was my main scaffold where you can see the spot I added the key.
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: GestureDetector(
key: _keyRink,
onTapDown: _getTapPosition,
onLongPressStart: _handleLongPress,
child: Stack(
children: <Widget>[
AnimatedOpacity(
duration: Duration(milliseconds: 2500),
opacity: _rinkOpacity,
child: Center(
child: Image.asset('assets/rink.png',
width: size.width, height: size.height, fit: BoxFit.fill),
),
),
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Transform.rotate(
angle: animationRotate.value,
child: Transform.scale(
scale: animationDrop.value,
child: FlatButton(
onPressed: null,
padding: EdgeInsets.all(0.0),
child: Image.asset('assets/puck.png'))),
)
],
),
),
CustomPaint(
painter: ShapesPainter(),
child: FractionallySizedBox(heightFactor: 1.0,widthFactor: 1.0,),
),
],
),
),
drawer: Drawer(
child: drawerItems,
),
);
}
Wrapping it with a StatelessWidget works fine for me, thanks to #rmtmckenzie
Here my code:
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(),
body: _Foo(),
);
}
}
class _Foo extends StatelessWidget {
#override
Widget build(BuildContext context) {
return GestureDetector(
onLongPressDragStart: (details) => _onLongPressDragStart(details, context),
onLongPressDragUpdate: (details) => _onLongPressDragUpdate(details, context),
onLongPressDragUp: (details) => _onLongPressDragUp(details, context),
child: FlareActor(
"assets/test.flr",
alignment: Alignment.center,
fit: BoxFit.contain,
),
);
}
void _onLongPressDragStart(GestureLongPressDragStartDetails details, BuildContext context) {
print('_onLongPressDragStart details: ${details.globalPosition}');
}
void _onLongPressDragUpdate(GestureLongPressDragUpdateDetails details, BuildContext context) {
var localTouchPosition = (context.findRenderObject() as RenderBox).globalToLocal(details.globalPosition);
print('_onLongPressDragUpdate details: ${details.globalPosition} - localTouchPosition: $localTouchPosition');
}
void _onLongPressDragUp(GestureLongPressDragUpDetails details, BuildContext context) {
print('_onLongPressDragUp details: ${details.globalPosition}');
}
}
The important things happen in the _Foo class.
You can get the local position of a GestureDetector using the DragUpdateDetails.localPosition
GestureDetector(
onPanUpdate: (DragUpdateDetails details) {
print('onPanUpdate');
print(details.localPosition);
},
)
I'm trying to create a widget that has a button and whenever that button is pressed, a list opens up underneath it filling in all of the space under the button. I implemented it with a simple Column, something like this:
class _MyCoolWidgetState extends State<MyCoolWidget> {
...
#override
Widget build(BuildContext context) {
return new Column(
children: <Widget>[
new MyButton(...),
isPressed ? new Expanded(
child: new SizedBox(
width: MediaQuery.of(context).size.width,
child: new MyList()
)
) : new Container()
]
)
}
}
This works totally fine in a lot of cases, but not all.
The problem I'm having with creating this widget is that if a MyCoolWidget is placed inside a Row for example with other widgets, lets say other MyCoolWidgets, the list is constrained by the width that the Row implies on it.
I tried fixing this with an OverflowBox, but with no luck unfortunately.
This widget is different from tabs in the sense that they can be placed anywhere in the widget tree and when the button is pressed, the list will fill up all the space under the button even if this means neglecting constraints.
The following image is a representation of what I'm trying to achieve in which "BUTTON1" and "BUTTON2" or both MyCoolWidgets in a Row:
Edit: Snippet of the actual code
class _MyCoolWidgetState extends State<MyCoolWidget> {
bool isTapped = false;
#override
Widget build(BuildContext context) {
return new Column(
children: <Widget>[
new SizedBox(
height: 20.0,
width: 55.0,
child: new Material(
color: Colors.red,
child: new InkWell(
onTap: () => setState(() => isTapped = !isTapped),
child: new Text("Surprise"),
),
),
),
bottomList()
],
);
}
Widget comboList() {
if (isTapped) {
return new Expanded(
child: new OverflowBox(
child: new Container(
color: Colors.orange,
width: MediaQuery.of(context).size.width,
child: new ListView( // Random list
children: <Widget>[
new Text("ok"),
new Text("ok"),
new Text("ok"),
new Text("ok"),
new Text("ok"),
new Text("ok"),
new Text("ok"),
new Text("ok"),
new Text("ok"),
new Text("ok"),
new Text("ok"),
new Text("ok"),
new Text("ok"),
],
)
)
),
);
} else {
return new Container();
}
}
}
I'm using it as follows:
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Row(
children: <Widget>[
new Expanded(child: new MyCoolWidget()),
new Expanded(child: new MyCoolWidget()),
]
)
}
}
Here is a screenshot of what the code is actually doing:
From the comments, it was clarified that what the OP wants is this:
Making a popup that covers everything and goes from wherever the button is on the screen to the bottom of the screen, while also filling it horizontally, regardless of where the button is on the screen. It would also toggle open/closed when the button is pressed.
There are a few options for how this could be done; the most basic would be to use a Dialog & showDialog, except that it has some issues around SafeArea that make that difficult. Also, the OP is asking for the button to toggle rather than pressing anywhere not the dialog (which is what dialog does - either that or blocks touches behind the dialog).
This is a working example of how to do something like this. Full disclaimer - I'm not stating that this is a good thing to do, or even a good way to do it... but it is a way to do it.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
// We're extending PopupRoute as it (and ModalRoute) do a lot of things
// that we don't want to have to re-create. Unfortunately ModalRoute also
// adds a modal barrier which we don't want, so we have to do a slightly messy
// workaround for that. And this has a few properties we don't really care about.
class NoBarrierPopupRoute<T> extends PopupRoute<T> {
NoBarrierPopupRoute({#required this.builder});
final WidgetBuilder builder;
#override
Color barrierColor;
#override
bool barrierDismissible = true;
#override
String barrierLabel;
#override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return new Builder(builder: builder);
}
#override
Duration get transitionDuration => const Duration(milliseconds: 100);
#override
Iterable<OverlayEntry> createOverlayEntries() sync* {
// modalRoute creates two overlays - the modal barrier, then the
// actual one we want that displays our page. We simply don't
// return the modal barrier.
// Note that if you want a tap anywhere that isn't the dialog (list)
// to close it, then you could delete this override.
yield super.createOverlayEntries().last;
}
#override
Widget buildTransitions(
BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
// if you don't want a transition, remove this and set transitionDuration to 0.
return new FadeTransition(opacity: new CurvedAnimation(parent: animation, curve: Curves.easeOut), child: child);
}
}
class PopupButton extends StatefulWidget {
final String text;
final WidgetBuilder popupBuilder;
PopupButton({#required this.text, #required this.popupBuilder});
#override
State<StatefulWidget> createState() => PopupButtonState();
}
class PopupButtonState extends State<PopupButton> {
bool _active = false;
#override
Widget build(BuildContext context) {
return new FlatButton(
onPressed: () {
if (_active) {
Navigator.of(context).pop();
} else {
RenderBox renderbox = context.findRenderObject();
Offset globalCoord = renderbox.localToGlobal(new Offset(0.0, context.size.height));
setState(() => _active = true);
Navigator
.of(context, rootNavigator: true)
.push(
new NoBarrierPopupRoute(
builder: (context) => new Padding(
padding: new EdgeInsets.only(top: globalCoord.dy),
child: new Builder(builder: widget.popupBuilder),
),
),
)
.then((val) => setState(() => _active = false));
}
},
child: new Text(widget.text),
);
}
}
class MyApp extends StatefulWidget {
#override
State<StatefulWidget> createState() => MyAppState();
}
class MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new SafeArea(
child: new Container(
color: Colors.white,
child: new Column(children: [
new PopupButton(
text: "one",
popupBuilder: (context) => new Container(
color: Colors.blue,
),
),
new PopupButton(
text: "two",
popupBuilder: (context) => new Container(color: Colors.red),
)
]),
),
),
);
}
}
For even more outlandish suggestions, you can take the finding the location part of this and look at this answer which describes how to create a child that isn't constrained by it's parent's position.
However you end up doing this, it's probably best that the list not to be a direct child of the button as a lot of things in flutter depend on a child's sizing and making it be able to expand to the full screen size could quite easily cause problems.
Our app is built on top of Scaffold and to this point we have been able to accommodate most of our routing and navigation requirements using the provided calls within NavigatorState (pushNamed(), pushReplacementNamed(), etc.). What we don't want though, is to have any kind of 'push' animation when a user selects an item from our drawer (nav) menu. We want the destination screen from a nav menu click to effectively become the new initial route of the stack. For the moment we are using pushReplacementNamed() for this to ensure no back arrow in the app bar. But, the slide-in-from-the-right animation implies a stack is building.
What is our best option for changing that initial route without animation, and, can we do that while also concurrently animating the drawer closed? Or are we looking at a situation here where we need to move away from Navigator over to just using a single Scaffold and updating the 'body' directly when the user wants to change screens?
We note there is a replace() call on NavigatorState which we assume might be the right place to start looking, but it's unclear how to access our various routes originally set up in new MaterialApp(). Something like replaceNamed() might be in order ;-)
What you're doing sounds somewhat like a BottomNavigationBar, so you might want to consider one of those instead of a Drawer.
However, having a single Scaffold and updating the body when the user taps a drawer item is a totally reasonable approach. You might consider a FadeTransition to change from one body to another.
Or, if you like using Navigator but don't want the default slide animation, you can customize (or disable) the animation by extending MaterialPageRoute. Here's an example of that:
import 'package:flutter/material.dart';
void main() {
runApp(new MyApp());
}
class MyCustomRoute<T> extends MaterialPageRoute<T> {
MyCustomRoute({ WidgetBuilder builder, RouteSettings settings })
: super(builder: builder, settings: settings);
#override
Widget buildTransitions(BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child) {
if (settings.isInitialRoute)
return child;
// Fades between routes. (If you don't want any animation,
// just return child.)
return new FadeTransition(opacity: animation, child: child);
}
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Navigation example',
onGenerateRoute: (RouteSettings settings) {
switch (settings.name) {
case '/': return new MyCustomRoute(
builder: (_) => new MyHomePage(),
settings: settings,
);
case '/somewhere': return new MyCustomRoute(
builder: (_) => new Somewhere(),
settings: settings,
);
}
assert(false);
}
);
}
}
class MyHomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Navigation example'),
),
drawer: new Drawer(
child: new ListView(
children: <Widget> [
new DrawerHeader(
child: new Container(
child: const Text('This is a header'),
),
),
new ListTile(
leading: const Icon(Icons.navigate_next),
title: const Text('Navigate somewhere'),
onTap: () {
Navigator.pushNamed(context, '/somewhere');
},
),
],
),
),
body: new Center(
child: new Text(
'This is a home page.',
),
),
);
}
}
class Somewhere extends StatelessWidget {
Widget build(BuildContext context) {
return new Scaffold(
body: new Center(
child: new Text(
'Congrats, you did it.',
),
),
appBar: new AppBar(
title: new Text('Somewhere'),
),
drawer: new Drawer(
child: new ListView(
children: <Widget>[
new DrawerHeader(
child: new Container(
child: const Text('This is a header'),
),
),
],
),
),
);
}
}
Use PageRouteBuilder like:
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (_, __, ___) => Screen2(),
transitionDuration: Duration.zero,
),
);
And if you want transition, simply add following property to above PageRouteBuilder, and change seconds to say 1.
transitionsBuilder: (_, a, __, c) => FadeTransition(opacity: a, child: c),