I want to nest PageViews in flutter, with a PageView inside a Scaffold inside a PageView. In the outer PageView I will have the logo and contact informations, as well as secundary infos. As a child, I will have a scaffold with the inner PageView and a BottomNavigationBar as the main user interaction screen. Here is the code I have so far:
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatefulWidget{
#override
State<StatefulWidget> createState() {
return _MyAppState();
}
}
class _MyAppState extends State<MyApp>{
int index = 0;
final PageController pageController = PageController();
final Curve _curve = Curves.ease;
final Duration _duration = Duration(milliseconds: 300);
_navigateToPage(value){
pageController.animateToPage(
value,
duration: _duration,
curve: _curve
);
setState((){
index = value;
});
}
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'PageViewCeption',
home: PageView(
children: <Widget>[
Container(
color: Colors.blue,
),
Scaffold(
body: PageView(
controller: pageController,
onPageChanged: (page){
setState(() {
index = page;
});
},
children: <Widget>[
Container(
child: Center(
child: Text('1', style: TextStyle(color: Colors.white))
)
),
Container(
child: Center(
child: Text('2', style: TextStyle(color: Colors.white))
)
),
Container(
child: Center(
child: Text('3', style: TextStyle(color: Colors.white))
)
),
],
),
backgroundColor: Colors.green,
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
onTap: (value) =>_navigateToPage(value),
currentIndex: index,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.cake),
title: Text('1')
),
BottomNavigationBarItem(
icon: Icon(Icons.cake),
title: Text('2')
),
BottomNavigationBarItem(
icon: Icon(Icons.cake),
title: Text('3')
)
],
),
),
Container(
color: Colors.blue
)
],
),
);
}
}
Here is the result:
Problem is: When I am in the inner PageView, I can't get away from it to the outer one scrolling left on the first page, or scrolling right on the last page of the inner PageView. The only way to go back to the outer PageView in scrolling (swiping) on the BottomNavigationBar.
In the docs of the Scroll Physics Class we find this in the description:
For example, determines how the Scrollable will behave when the user reaches the maximum scroll extent or when the user stops scrolling.
But I haven't been able to come up with a solution yet. Any thoughts?
Update 1
I had progress working with a CustomScrollPhysics class:
class CustomScrollPhysics extends ScrollPhysics{
final PageController _controller;
const CustomScrollPhysics(this._controller, {ScrollPhysics parent }) : super(parent: parent);
#override
CustomScrollPhysics applyTo(ScrollPhysics ancestor) {
return CustomScrollPhysics(_controller, parent: buildParent(ancestor));
}
#override
double applyBoundaryConditions(ScrollMetrics position, double value) {
assert(() {
if (value == position.pixels) {
throw new FlutterError(
'$runtimeType.applyBoundaryConditions() was called redundantly.\n'
'The proposed new position, $value, is exactly equal to the current position of the '
'given ${position.runtimeType}, ${position.pixels}.\n'
'The applyBoundaryConditions method should only be called when the value is '
'going to actually change the pixels, otherwise it is redundant.\n'
'The physics object in question was:\n'
' $this\n'
'The position object in question was:\n'
' $position\n'
);
}
return true;
}());
if (value < position.pixels && position.pixels <= position.minScrollExtent){ // underscroll
_controller.jumpTo(position.viewportDimension + value);
return 0.0;
}
if (position.maxScrollExtent <= position.pixels && position.pixels < value) {// overscroll
_controller.jumpTo(position.viewportDimension + (value - position.viewportDimension*2));
return 0.0;
}
if (value < position.minScrollExtent && position.minScrollExtent < position.pixels) // hit top edge
return value - position.minScrollExtent;
if (position.pixels < position.maxScrollExtent && position.maxScrollExtent < value) // hit bottom edge
return value - position.maxScrollExtent;
return 0.0;
}
}
Which is a modification of the ClampingScrollPhysics applyBoundaryConditions. It kinda works but because of the pageSnapping it is really buggy. It happens, because according to the docs:
Any active animation is canceled. If the user is currently scrolling, that
action is canceled.
When the action is canceled, the PageView starts to snap back to the Scafold page, if the user stop draggin the screen, and this messes things up. Any ideas on how to avoid the page snapping in this case, or for better implementation for that matter?
I was able to replicate the issue on the nested PageView. It seems that the inner PageView overrides the detected gestures. This explains why we're unable to navigate to other pages of the outer PageView, but the BottomNavigationBar can. More details of this behavior is explained in this thread.
As a workaround, you can use a single PageView and just hide the BottomNavigationBar on the outer pages. I've modified your code a bit.
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _MyAppState();
}
}
class _MyAppState extends State<MyApp> {
var index = 0;
final PageController pageController = PageController();
final Curve _curve = Curves.ease;
final Duration _duration = Duration(milliseconds: 300);
var isBottomBarVisible = false;
_navigateToPage(value) {
// When BottomNavigationBar button is clicked, navigate to assigned page
switch (value) {
case 0:
value = 1;
break;
case 1:
value = 2;
break;
case 2:
value = 3;
break;
}
pageController.animateToPage(value, duration: _duration, curve: _curve);
setState(() {
index = value;
});
}
// Set BottomNavigationBar indicator only on pages allowed
_getNavBarIndex(index) {
if (index <= 1)
return 0;
else if (index == 2)
return 1;
else if (index >= 3)
return 2;
else
return 0;
}
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'PageViewCeption',
home: Scaffold(
body: Container(
child: PageView(
controller: pageController,
onPageChanged: (page) {
setState(() {
// BottomNavigationBar only appears on page 1 to 3
isBottomBarVisible = page > 0 && page < 4;
print('page: $page bottom bar: $isBottomBarVisible');
index = page;
});
},
children: <Widget>[
Container(
color: Colors.red,
),
Container(
color: Colors.orange,
),
Container(
color: Colors.yellow,
),
Container(
color: Colors.green,
),
Container(color: Colors.lightBlue)
],
),
),
bottomNavigationBar: isBottomBarVisible // if true, generate BottomNavigationBar
? new BottomNavigationBar(
type: BottomNavigationBarType.fixed,
onTap: (value) => _navigateToPage(value),
currentIndex: _getNavBarIndex(index),
items: [
BottomNavigationBarItem(icon: Icon(Icons.cake), label: '1'),
BottomNavigationBarItem(icon: Icon(Icons.cake), label: '2'),
BottomNavigationBarItem(icon: Icon(Icons.cake), label: '3')
],
)
//else, create an empty container to hide the BottomNavigationBar
: Container(
height: 0,
),
),
);
}
}
Related
In my app I'm using CustomScrollView, which should load more data when scroll position reaches the almost-end of scroll view (currently it's position.pixels - 100.0). The mechanism works fine, data loads as I scroll, but depending on connection speed (more precisely, on api response speed) and initial "touch down and pan up" gesture speed this bug appears. Don't know how to correctly explain it, but of the speed of scrolling was large at the moment when app starts and then finishes loading next portion of data the scroll view kind-of scrolls back in opposite direction. It looks like the scroll view "shoots off" or resists. Here's the gif with problem representation:
I've created simulation of what is happening on gif:
import 'dart:async';
import 'package:flutter/cupertino.dart';
class ItemService {
final _itemsController = StreamController<List<String>>();
final _loadNextController = StreamController<bool>();
get items => _itemsController.stream;
get isLoadingNext => _loadNextController.stream;
List<String> _current = [];
loadNext() async {
_loadNextController.add(true);
await Future.delayed(Duration(milliseconds: 500));
_current.addAll(List.generate(10, (idx) => 'Item ${_current.length + idx}'));
_itemsController.add(_current);
_loadNextController.add(false);
}
void dispose() {
_itemsController.close();
_loadNextController.close();
}
}
class ItemsScreen extends StatefulWidget {
#override
_ItemsScreenState createState() => _ItemsScreenState();
}
class _ItemsScreenState extends State<ItemsScreen> {
final _scrollController = ScrollController();
double _prevScrollPos = 0.0;
final _service = ItemService();
_onScroll() {
double maxScroll = _scrollController.position.maxScrollExtent;
double currentScrollPos = _scrollController.position.pixels;
double delta = 100.0;
if (maxScroll - currentScrollPos <= delta && _prevScrollPos - currentScrollPos < 0) {
_service.loadNext();
}
_prevScrollPos = currentScrollPos;
}
#override
void initState() {
_scrollController.addListener(_onScroll);
_service.loadNext();
super.initState();
}
#override
void dispose() {
_scrollController.dispose();
_service.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
child: CustomScrollView(
controller: _scrollController,
slivers: <Widget>[
/// navbar
CupertinoSliverNavigationBar(
largeTitle: Text('Items'),
automaticallyImplyTitle: false,
previousPageTitle: 'Back',
transitionBetweenRoutes: false,
),
/// items list
StreamBuilder(
stream: _service.items,
builder: (_, snapshot) {
if (snapshot.hasData) {
final items = snapshot.data;
return SliverList(
delegate: SliverChildBuilderDelegate(
(_, int index) {
return Container(
height: 300.0,
margin: const EdgeInsets.only(bottom: 20.0),
color: index % 2 == 0
? CupertinoColors.activeGreen
: CupertinoColors.activeOrange,
child: Text(items[index]),
);
},
childCount: items.length,
),
);
} else {
return SliverFillRemaining(
child: Center(
child: CupertinoActivityIndicator(),
),
);
}
},
),
StreamBuilder(
stream: _service.isLoadingNext,
builder: (_, snapshot) {
if (snapshot.data == true) {
return SliverToBoxAdapter(
child: Container(
// color: Colors.primary,
padding: const EdgeInsets.symmetric(vertical: 10.0),
child: Center(
child: CupertinoActivityIndicator(),
),
),
);
} else {
return SliverToBoxAdapter(
child: Container(height: 0),
);
}
},
),
SliverToBoxAdapter(
child: SafeArea(
child: Container(),
bottom: true,
top: false,
),
)
],
),
);
}
}
main() {
runApp(CupertinoApp(
home: ItemsScreen(),
));
}
UPDATE: kind-of resolved this issue by calling _scrollController.jumpTo(_scrollController.position.pixels); after _service.loadNext()
Just add ClampingScrollPhysics() on CustomScrollView to remove the bounce physics.
CustomScrollView(
physics: const ClampingScrollPhysics(), // Remove bounce physics
controller: _scrollController,
slivers: <Widget>[
/// navbar
const CupertinoSliverNavigationBar(...)
],
),
I'm creating a text field like Text or RichText. And after that, I want to zoom in/out the size of text using pinching. For now, I tried implementing GestureDetector but it zooms in/out with one finger too. And it is really hard to aim pinching detection. Sometimes is freezing. I added a video that shows when after pinching it freezes and suddenly get bigger. The second video is with the case that image zoom in only when I tap on the text with one finger and move to up left corner. The ideal implementation is to detect pinch and zoom in/out all text area. And disable zooming when I use only one finger. Could you send me some hints, link or code how to solve or where to find the solution?
body: GestureDetector(
onScaleUpdate: (details) {
setState(() {
_textSize =
_initTextSize + (_initTextSize * (details.scale * .35));
});
},
onScaleEnd: (ScaleEndDetails details) {
setState(() {
_initTextSize = _textSize;
});
},
child: Center(
child: SizedBox(
height: _textSize,
child: FittedBox(
child: Text("Test"),
),
))),
In Stateful widget with these configuration
double _scaleFactor = 1.0;
double _baseScaleFactor = 1.0;
And use setState only on update, using the scaleFactor on textScaleFactor property of RichText.
Only one setState to rebuild widget and store the initial factor when scale starts
GestureDetector(
onScaleStart: (details) {
_baseScaleFactor = _scaleFactor;
},
onScaleUpdate: (details) {
setState(() {
_scaleFactor = _baseScaleFactor * details.scale;
});
},
child: Container(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
color: Colors.red,
child: Center(
child: Text(
'Test',
textScaleFactor: _scaleFactor,
),
),
),
);
The height and width I put just to expand and simulate area of gesture detector.
Google software engineers Gary Qian and Chris Yang demonstrated this in their Google Developer Days talk. The video is viewable here:
Text in Flutter: Building a fancy chat bubble at GDD China
There code is similar to some of the other answers here, but they notably add a clamp so that it doesn't get too big or small.
Here is a summary of their scalable text bubble:
Because scaling still gets called even for a single finger touch, I added a check for scaleUpdateDetails.scale == 1.0. That means the UI won't be updated if there was no change in scale.
class Bubble extends StatefulWidget {
#override
_BubbleState createState() => _BubbleState();
}
class _BubbleState extends State<Bubble> {
double _fontSize = 20;
final double _baseFontSize = 20;
double _fontScale = 1;
double _baseFontScale = 1;
#override
Widget build(BuildContext context) {
return GestureDetector(
onScaleStart: (ScaleStartDetails scaleStartDetails) {
_baseFontScale = _fontScale;
},
onScaleUpdate: (ScaleUpdateDetails scaleUpdateDetails) {
// don't update the UI if the scale didn't change
if (scaleUpdateDetails.scale == 1.0) {
return;
}
setState(() {
_fontScale = (_baseFontScale * scaleUpdateDetails.scale).clamp(0.5, 5.0);
_fontSize = _fontScale * _baseFontSize;
});
},
child: ...
// descendant with a Text widget that uses the _fontSize
);
}
}
Notes:
Use a StatefulWidget so that you can store the current font size and scale at all times
Use two additional variables to remember the original font size and also the scale at the start of the pinch
Wrap the Text widget in a GestureDetector
Save the original scale in onScaleStart
Calculate the new font size onScaleUpdate
Use setState to rebuild the widget with the new size
Solution: Two finger zoom-in and zoom-out.
import 'package:flutter/material.dart';
import 'package:matrix_gesture_detector/matrix_gesture_detector.dart';
class TransformText extends StatefulWidget {
TransformText({Key key}) : super(key: key); // changed
#override
_TransformTextState createState() => _TransformTextState();
}
class _TransformTextState extends State<TransformText> {
double scale = 0.0;
#override
Widget build(BuildContext context) {
final ValueNotifier<Matrix4> notifier = ValueNotifier(Matrix4.identity());
return Scaffold(
appBar: AppBar(
title: Text('Single finger Rotate text'), // changed
),
body: Center(
child: MatrixGestureDetector(
onMatrixUpdate: (m, tm, sm, rm) {
notifier.value = m;
},
child: AnimatedBuilder(
animation: notifier,
builder: (ctx, child) {
return Transform(
transform: notifier.value,
child: Center(
child: Stack(
children: <Widget>[
Container(
color: Colors.red,
padding: EdgeInsets.all(10),
margin: EdgeInsets.only(top: 50),
child: Transform.scale(
scale:
1, // make this dynamic to change the scaling as in the basic demo
origin: Offset(0.0, 0.0),
child: Container(
height: 100,
child: Text(
"Two finger to zoom!!",
style:
TextStyle(fontSize: 26, color: Colors.white),
),
),
),
),
],
),
),
);
},
),
),
),
);
}
}
Full code. Hope it helps.
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
final appTitle = 'Demo';
return MaterialApp(
title: appTitle,
home: MyHomePage(title: appTitle),
);
}
}
class MyHomePage extends StatelessWidget {
final String title;
MyHomePage({Key key, this.title}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: TransformText());
}
}
class TransformText extends StatefulWidget {
TransformText({Key key}) : super(key: key); // changed
#override
_TransformTextState createState() => _TransformTextState();
}
class _TransformTextState extends State<TransformText> {
double scale = 0.0;
double _scaleFactor = 1.0;
double _baseScaleFactor = 1.0;
double _savedVal = 1.0;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('GestureDetector Test'), // changed
),
body: Column(
children: <Widget>[
RaisedButton(
child: Text('get'),
onPressed: () {
_savedVal = _scaleFactor;
}),
RaisedButton(
child: Text('set'),
onPressed: () {
setState(() {
_scaleFactor = _savedVal;
});
}),
Expanded(
child: Center(
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onScaleStart: (details) {
_baseScaleFactor = _scaleFactor;
},
onScaleUpdate: (details) {
setState(() {
_scaleFactor = _baseScaleFactor * details.scale;
});
},
child: Container(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: Center(
child: Text(
'Test',
textScaleFactor: _scaleFactor,
),
),
),
)),
),
],
),
);
}
}
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 need to refresh my UI when data changes. I have a ListView to display Cards that contain my events, and these events are sorted with a datepicker. When I change the date with the datepicker I need to reload the page to display the correct pages.
I try to pass the datepicker data as a parameter of the ListView to sort the events in the ListView, I also tried to sort the data before ListView is built with a parameter containing the list of sorted data.
Widget of my HomePage class :
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
leading: Image.asset('assets/logo2.PNG', fit: BoxFit.contain),
title: Text(widget.title,style: TextStyle(fontFamily: 'IndieFlower',fontSize: 30,fontWeight: FontWeight.bold),),
actions: <Widget>[ // Add 3 lines from here...
new IconButton(icon: const Icon(Icons.account_circle, color: Color(0xFFf50057)), onPressed: _pushSaved, iconSize: 35,),
], // ... to here.
centerTitle: true,
backgroundColor: new Color(0xFF263238),
),
body: FutureBuilder<List<Event>>(
future: fetchPosts(http.Client()),
builder: (context, snapshot) {
//print(convertIntoMap(snapshot.data));
if (snapshot.hasError) print(snapshot.error);
return snapshot.hasData
? ListViewEvents(posts: sortEvents(snapshot.data), pickerDate: '${dobKey.currentState.dobDate} ' +dobKey.currentState.dobStrMonth +' ${dobKey.currentState.dobYear}')
: Center(child: CircularProgressIndicator(backgroundColor: Color(0xFFf50057),));
},
),
bottomNavigationBar : BottomAppBar(
child: Container(height: 100.0,
alignment: Alignment.topCenter,
child:
DatePicker(
key: dobKey,
setDate: _setDateOfBirth,
customItemColor: Color(0xFFf50057),
customGradient:
LinearGradient(begin: Alignment(-0.5, 2.8), colors: [
Color(0xFFf50057),
Color(0xFFffcece),
Color(0xFFf50057),
]),
),
),
),
);
}
}
This is my map:
List<Event> sortEvents(List<Event> data) {
List<Event> eventsSelected = new List<Event>();
for(var index = 0; index < data.length; index++){
if (data[index].date ==
//callback of datepicker
'${dobKey.currentState.dobYear}-${dobKey.currentState.month}-
${dobKey.currentState.dobDate}') {
eventsSelected.add(data[index]);
}
}
return eventsSelected;
}
And this is how I render my cards:
class ListViewEvents extends StatefulWidget {
ListViewEvents({Key key, this.posts, this.pickerDate}) : super(key: key);
final posts;
final pickerDate;
#override
_ListViewEventsState createState() => _ListViewEventsState();
}
class _ListViewEventsState extends State<ListViewEvents> with
SingleTickerProviderStateMixin {
#override
Widget build(BuildContext context) {
if(widget.posts.isEmpty) {
return Center(
child: Text(
'No events for this date'
),
);
}
return ListView.builder(
itemCount: widget.posts.length,
padding: const EdgeInsets.all(15.0),
itemBuilder: (context, index) {
return Center(
child: Text(
'Title : ${widget.posts[index].title}'
),
);
},
);
}
}
I actually have a system to display my events's Cards that works but it's not in real-time, I would like to refresh the UI when the data of the datepicker changes.
You need to call setState() when list data changes.
for(var index = 0; index < data.length; index++){
if (data[index].date ==
//callback of datepicker
'${dobKey.currentState.dobYear}-${dobKey.currentState.month}-
${dobKey.currentState.dobDate}') {
setState(() { eventsSelected.add(data[index]); } ); <--- add it here.
}
}
you can use setState() for this problem so setState() call when anything change in the screen
I have a PageView used with a BottomNavigationBar so that I can have swipeable and tappable tabs with a bottom bar rather than the normal navigation bar. Then I have two tabs/pages you can swipe between. One has a form with 4 UI elements and the other has no UI elements yet. Everything works fine but the performance of the PageView is very bad.
When I swipe between pages it is extremely slow and jumpy at first, definitely not the 60 frames per second promised by Flutter. Probably not even 30. After swiping several times though the performance gets better and better until its almost like a normal native app.
Below is my page class that includes the PageView, BottomNavigationBar, and logic connecting them. does anyone know how I can improve the performance of the PageView?
class _TabbarPageState extends State<TabbarPage> {
int _index = 0;
final controller = PageController(
initialPage: 0,
keepPage: true,
);
final _tabPages = <Widget>[
StartPage(),
OtherPage(),
];
final _tabs = <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.play_arrow),
title: Text('Start'),
),
BottomNavigationBarItem(
icon: Icon(Icons.accessibility_new),
title: Text('Other'),
)
];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: PageView(
controller: controller,
children: _tabPages,
onPageChanged: _onPageChanged,
),
bottomNavigationBar: BottomNavigationBar(
items: _tabs,
onTap: _onTabTapped,
currentIndex: _index,
),
floatingActionButton: _index != 1
? null
: FloatingActionButton(
onPressed: () {},
tooltip: 'Test',
child: Icon(Icons.add),
),
);
}
void _onTabTapped(int index) {
controller.animateToPage(
index,
duration: Duration(milliseconds: 300),
curve: Curves.ease,
);
setState(() {
_index = index;
});
}
void _onPageChanged(int index) {
setState(() {
_index = index;
});
}
}
Ensure you performance test with profile or release builds only. Evaluating performance with debug builds is completely meaningless.
https://flutter.io/docs/testing/ui-performance
https://flutter.io/docs/cookbook/testing/integration/profiling
Sorry but Günter's answer didn't helped me! You have to set physics: AlwaysScrollableScrollPhysics() And your performance increases.
Worked for me 👍
I am new to flutter, please tell me if this is wrong.
Have the same problem, here is my effort. Wors for me.
class HomePage extends StatefulWidget {
#override
State createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
var _currentIndex = 1;
var _pageController = PageController(initialPage: 1);
var _todoPage, _inProgressPage, _donePage, _userPage;
#override
void initState() {
this._currentIndex = 1;
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: PageView.builder(
controller: this._pageController,
onPageChanged: (index) {
setState(() {
this._currentIndex = index.clamp(0, 3);
});
},
itemCount: 4,
itemBuilder: (context, index) {
if (index == 0) return this.todoPage();
if (index == 1) return this.inProgressPage();
if (index == 2) return this.donePage();
if (index == 3) return this.userPage();
return null;
},
),
bottomNavigationBar: buildBottomNavigationBar(),
);
}
Widget buildBottomNavigationBar() {
return BottomNavigationBar(
showUnselectedLabels: false,
items: [
BottomNavigationBarItem(title: Text("待办"), icon: Icon(Icons.assignment)),
BottomNavigationBarItem(title: Text("进行"), icon: Icon(Icons.blur_on)),
BottomNavigationBarItem(title: Text("完成"), icon: Icon(Icons.date_range)),
BottomNavigationBarItem(title: Text("我的"), icon: Icon(Icons.account_circle)),
],
currentIndex: this._currentIndex,
onTap: (index) {
setState(() {
this._currentIndex = index.clamp(0, 3);
});
_pageController.jumpToPage(this._currentIndex);
},
);
}
Widget todoPage() {
if (this._todoPage == null) this._todoPage = TodoPage();
return this._todoPage;
}
Widget inProgressPage() {
if (this._inProgressPage == null) this._inProgressPage = InProgressPage();
return this._inProgressPage;
}
Widget donePage() {
if (this._donePage == null) this._donePage = DonePage();
return this._donePage;
}
Widget userPage() {
if (this._userPage == null) this._userPage = UserPage();
return this._userPage;
}
}
I just cache the pages that pageview hold. this REALLY smooth my pageview a lot, like native. but would prevent hotreload (ref: How to deal with unwanted widget build?),.
Try this hack,
Apply viewportFraction to your controller with value 0.99(it can be 0.999 or 0.9999 hit and try until you get desired result)
final controller = PageController(
viewportFraction: 0.99 );