Flutter open popup menu above list item - flutter

I am making a chat app in Flutter with Firestore but I am stumped with one of the requirement: Making a reaction menu appear above a chat dialog. I need that when the user long press on a chat text the reaction menu would appear above the current chat position. Something like Facebook Messenger app:
I have look around and found https://medium.com/#duytq94/facebook-reactions-with-flutter-9019ce8b95b8 but it seems it is too much for just a simple function (making a popup menu appear above a list tile and don't need the animation). I also found this package: https://pub.dev/packages/flutter_reaction_button, but it does not allow to wrap the widget around another to open up the reacton menu. Is there a simple way that I can archive this? I have looked into PopupMenuButton but it only allows onPressed and not long press.

You can use Overlay widget. I make a simple example for you.
import 'package:flutter/material.dart';
class Temp extends StatefulWidget {
#override
_TempState createState() => _TempState();
}
class _TempState extends State<Temp> {
GlobalKey floatingKey = LabeledGlobalKey("Floating");
bool isFloatingOpen = false;
OverlayEntry floating;
OverlayEntry createFloating() {
RenderBox renderBox = floatingKey.currentContext.findRenderObject();
Offset offset = renderBox.localToGlobal(Offset.zero);
return OverlayEntry(
builder: (context) {
return Positioned(
left: offset.dx,
width: renderBox.size.width,
top: offset.dy - 50,
child: Material(
elevation: 20,
child: Container(
height: 50,
color: Colors.blue,
child: Text("I'm floating overlay")
)
)
);
}
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RaisedButton(
key: floatingKey,
color: Colors.red,
onPressed: (){
setState(() {
if(isFloatingOpen) floating.remove();
else {
floating = createFloating();
Overlay.of(context).insert(floating);
}
isFloatingOpen = !isFloatingOpen;
});
},
)
),
);
}
}

Related

Flutter overlay page, details account page

I'm trying to make an app and I need the help of the community for something: I'm trying to make an overlay (which would be a new page) coming on top of an other page. Look at these screenshots found on dribbble and I'll try to explain better then.
So, imaging you're on the page as shown on the first screenshot. What I want to do is, when you click, for exemple on the "contact page" button, a windows comes up from the bottom of the screen with a linear animation, and show the view, as on the screenshot on the right of the screen. But I don't want it to be a "real new page" because as you can see on the second screenshot, we can see in transparency the first page behind...
And of course, when you click the cross button, the window pop...
Ask me any question if I'm not clear enough.
Any help is welcome !
Thanks a lot, stackoverflow is an extraordinary community !
Here a min example how you can achieve this using AnimatedPositioned widget hoping it will help you get started.
class ExampleApp extends StatefulWidget {
#override
_ExampleAppState createState() => _ExampleAppState();
}
class _ExampleAppState extends State<ExampleApp> {
final double containerHeight = 200.0;
bool _showProfile;
#override
void initState() {
_showProfile = false;
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
color: Colors.teal,
child: Stack(
children: <Widget>[
Container(
color: Colors.redAccent,
child: Center(
child: FlatButton(
child: Text("Animate"),
onPressed: () {
setState(() {
_showProfile = true;
});
},
),
),
),
AnimatedPositioned(
bottom: _showProfile ? 0 : -containerHeight,
right: 0,
left: 0,
duration: Duration(milliseconds: 300),
child: Container(
color: Colors.white,
height: containerHeight,
))
],
),
),
);
}
}
you can achieve the same with Bottomsheet too.
Check out
https://api.flutter.dev/flutter/material/BottomSheet-class.html

Is there any solution to make my Flare Button work properly? Flutter

Basically, I have a button that I made in Flare and it should make an animation then I tap it! It works well but the animation starts everytime I tap anywhere within my app and not only the button.
Is there any way I can make animation only when I click in the Button Area? I`m using GestureDetector to handle tapping.
https://i.stack.imgur.com/ZBjmz.jpg
My code:
import 'package:flutter/material.dart';
import 'package:flare_flutter/flare_actor.dart';
import 'package:vibrate/vibrate.dart';
void main() => runApp(MaterialApp(
home: InvalidButton(),
));
class InvalidButton extends StatefulWidget {
#override
_InvalidButtonState createState() => _InvalidButtonState();
}
class _InvalidButtonState extends State<InvalidButton> {
var _type = FeedbackType.heavy;
final FlareControls controls = FlareControls();
void _playAnimation(){
controls.play('invalid');
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.pink[50],
body: Container(
child: GestureDetector(
onTap: () {
setState(() {
_playAnimation();
Vibrate.feedback(_type);
});
},
child: FlareActor(
'assets/invalid_button.flr',
animation: 'invalid',
alignment: Alignment.center,
fit: BoxFit.contain,
controller: controls,
),
),
),
);
}
}
I`m a newbie so will be glad to any help!
Do you have space around the button in the Rive editor ? If so, go in design mode, click Artboard and then "fit contents". That should remove the extra space. Then, wrap your FlareActor with a SizedBox and set its size, like so :
SizedBox(
width: 100,
height: 100,
child: FlareActor(),
)
That should do it.

Flutter - How to flip the previous card back using FlipCard

After days of search I'm getting help.
I work on a flutter application.
Context:
A grid view feeded with Json
-childs : GridTile with Flipcard in (https://pub.dev/packages/flip_card)
-On tap on GridTile there is a callback to get the selected Item and an animation because of the flipcard onTap
What I would:
When an item is aleready selected (flipcard flipped so we show the back of the card),
And I selected another item of the grid te(so flipcard of this itme also flipped)
I would like to flip back the old selected item Flipcard without rebuild the tree because I would lost the state of the new selected item.
I tried many thing. For example I tried to use GlobalKey on GridTiles to interract with after build but currentState is always null when I want to interact with.
I wonder what is the good practice in this case ?
I hope I was clear :) (I'm french)
Thank you the community!
.
Something to know...
It is possible to interract with the flipcard (child of gridtile) like this
(GlobalKey)
GlobalKey<FlipCardState> cardKey = GlobalKey<FlipCardState>();
#override
Widget build(BuildContext context) {
return FlipCard(
key: cardKey,
flipOnTouch: false,
front: Container(
child: RaisedButton(
onPressed: () => cardKey.currentState.toggleCard(),
child: Text('Toggle'),
),
),
back: Container(
child: Text('Back'),
),
);
}
I'm not sure if I understood your question, but here is an example of how you could use a GridView with FlipCards:
var cardKeys = Map<int, GlobalKey<FlipCardState>>();
GlobalKey<FlipCardState> lastFlipped;
Widget _buildFlipCard(String text, Color color, int index) {
return SizedBox(
height: 120.0,
child: Card(
color: color,
child: Center(
child:
Text(text, style: TextStyle(color: Colors.white, fontSize: 20.0)),
),
),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("FlipCards")),
body: GridView.builder(
itemCount: 20,
itemBuilder: (context, index) {
cardKeys.putIfAbsent(index, () => GlobalKey<FlipCardState>());
GlobalKey<FlipCardState> thisCard = cardKeys[index];
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlipCardWithKeepAlive(
child: FlipCard(
flipOnTouch: false,
key: thisCard,
front: _buildFlipCard("$index", Colors.blue, index),
back: _buildFlipCard("$index", Colors.green, index),
onFlip: () {
if (lastFlipped != thisCard) {
lastFlipped?.currentState?.toggleCard();
lastFlipped = thisCard;
}
},
),
),
RaisedButton(
child: Text("Flip Card"),
onPressed: () => cardKeys[index].currentState.toggleCard(),
)
],
);
},
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
),
);
}
class FlipCardWithKeepAlive extends StatefulWidget {
final FlipCard child;
FlipCardWithKeepAlive({Key key, this.child}) : super(key: key);
#override
State<StatefulWidget> createState() => FlipCardWithKeepAliveState();
}
class FlipCardWithKeepAliveState extends State<FlipCardWithKeepAlive>
with AutomaticKeepAliveClientMixin {
#override
Widget build(BuildContext context) {
super.build(context);
return widget.child;
}
#override
bool get wantKeepAlive => true;
}
You need to use a different key for each element of the list, I used a Map in this case.
I also wrapped the FlipCard with a custom FlipCardWithKeepAlive stateful widget that uses AutomaticKeepAliveClientMixin to keep alive the FlipCard while scrolling.
Edit: I updated the code so when you flip one card, the previous card flipped gets flipped back. Basically you need to save the last flipped card and when a new one is flipped, flip the last one and put the new one as last flipped.
The code will make both cards flip at the same time, if you want one card to wait the other use onFlipDone() instead of onFlip(), like this:
onFlipDone: (isFront) {
bool isFlipped = !isFront;
if (isFlipped && lastFlipped != thisCard) {
lastFlipped?.currentState?.toggleCard();
lastFlipped = thisCard;
}
}

Refresh widget or page in Flutter without ListView et al

I want refresh my page without having a scrollable content, i.e. without having a ListView et al.
When I want use RefreshIndicator, the documentation says it needs a scrollable widget like ListView.
But if I want to refresh and want to use the refresh animation of RefreshIndicator without using a ListView, GridView or any other scorllable widget, how can i do that?
You can simply wrap your content in a SingleChildScrollView, which will allow you to use a RefreshIndicator. In order to make the pull down to refresh interaction work, you will have to use AlwaysScrollableScrollPhysics as your content will most likely not cover more space than available without a scroll view:
RefreshIndicator(
onRefresh: () async {
// Handle refresh.
},
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: /* your content */,
),
);
You can just use GestureDetector, I have created a sample for you, but it's not perfect, you can customize it to your own needs, it just detects when you swipe from the top.
class Test extends StatefulWidget {
#override
_TestState createState() => _TestState();
}
class _TestState extends State<Test> {
var refresh=false;
void refreshData(){
if(!refresh){
refresh=true;
print("Refreshing");
Future.delayed(Duration(seconds: 4),(){
refresh =false;
print("Refreshed");
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Text("Test"),
centerTitle: true,
),
body: GestureDetector(
child: Container(
color: Colors.yellow,
height: double.infinity,
width: double.infinity,
child: Center(child: Text('TURN LIGHTS ON')),
),
onVerticalDragUpdate: (DragUpdateDetails details){
print("direction ${details.globalPosition.direction}");
print("distance ${details.globalPosition.distance}");
print("dy ${details.globalPosition.dy}");
if(details.globalPosition.direction < 1 && (details.globalPosition.dy >200 && details.globalPosition.dy < 250)){
refreshData();
}
},
));
}
}

How do I prevent onTapDown from being triggered on a parent widgets GestureDetector?

I have a Stack in which several widget can be dragged around. In addition, the container that the Stack is in has a GestureDetector to trigger on onTapDown and onTapUp. I want those onTap events only to be triggered when the user taps outside of the widget in the Stack. I've tried the following code:
class Gestures extends StatefulWidget {
#override
State<StatefulWidget> createState() => _GesturesState();
}
class _GesturesState extends State<Gestures> {
Color background;
Offset pos;
#override
void initState() {
super.initState();
pos = Offset(10.0, 10.0);
}
#override
Widget build(BuildContext context) => GestureDetector(
onTapDown: (_) => setState(() => background = Colors.green),
onTapUp: (_) => setState(() => background = Colors.grey),
onTapCancel: () => setState(() => background = Colors.grey),
child: Container(
color: background,
child: Stack(
children: <Widget>[
Positioned(
top: pos.dy,
left: pos.dx,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onPanUpdate: _onPanUpdate,
// onTapDown: (_) {}, Doesn't affect the problem
child: Container(
width: 30.0,
height: 30.0,
color: Colors.red,
),
),
)
],
),
),
);
void _onPanUpdate(DragUpdateDetails details) {
RenderBox renderBox = context.findRenderObject();
setState(() {
pos = renderBox.globalToLocal(details.globalPosition);
});
}
}
However, when starting to drag the widget, the onTap of the outermost container is triggered as well, making the background momentarily go green in this case. Settings HitTestBehavior.opaque doesn't seem to work like I'd expect. Neither does adding a handler for onTapDown to the widget in the Stack.
So, how do I prevent onTapDown from being triggered on the outermost GestureDetector when the user interacts with the widget inside of the Stack?
Update:
An even simpler example of the problem I'm encountering:
GestureDetector(
onTapDown: (_) {
print("Green");
},
child: Container(
color: Colors.green,
width: 300.0,
height: 300.0,
child: Center(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTapDown: (_) {
print("Red");
},
child: Container(
color: Colors.red,
width: 50.0,
height: 50.0,
),
),
),
),
);
When I tap and hold the red container, both "Red" and "Green" are printed even though the inner GestureDetector has HitTestBehavior.opaque.
In this answer, I'll solve the simpler example you have given. You are creating the following Widget hierarchy:
- GestureDetector // green
- Container
- Center
- GestureDetector // red
- Container
Therefore the red GestureDetector is a child Widget of the green GestureDetector. The green GestureDetector has the default HitTestBehavior: HitTestBehavior.deferToChild. That is why onTapDown is fired for both containers.
Targets that defer to their children receive events within their
bounds only if one of their children is hit by the hit test.
Instead, you can use a Stack to build your UI:
- Stack
- GestureDetector // green
- Container
- GestureDetector // red
- Container
This structure would result in the follwing sourcecode. It looks the same, but the behavior is the one you desired:
Stack(
alignment: Alignment.center,
children: <Widget>[
GestureDetector(
onTapDown: (_) {
print("Green");
},
child: Container(
color: Colors.green,
width: 300.0,
height: 300.0,
),
),
GestureDetector(
onTapDown: (_) {
print("Red");
},
child: Container(
color: Colors.red,
width: 50.0,
height: 50.0,
),
)
],
)
I found that I could get the behavior that I wanted (making a widget appear transparent to it parent, while still responding to pointer events) by creating a render object with hit test behavior like this:
#override
bool hitTest(BoxHitTestResult result, {#required Offset position}) {
// forward hits to our child:
final hit = super.hitTest(result, position: position);
// but report to our parent that we are not hit when `transparent` is true:
return false;
}
I've published a package with a widget having this behavior here: https://pub.dev/packages/transparent_pointer.
I use RiverPod, and have succeeded with these steps. This is a general process, so should work for all use cases (and with your state manager)
(The use case here is to prevent a ListView from scrolling, when I select text on a widget using the mouse).
Create a notifier
final textSelectionInProgress = StateProvider<bool>((ref) {
return false;
});
When there is an action (in my case onTap) on the widget, wrap widget with a Focus, or use the focusNode of the widget, and the following code in initState
#override
initState() {
super.initState();
focusN = FocusNode();
focusN.addListener(() {
if (focusN.hasFocus) {
widget.ref.read(textSelectionInProgress.notifier).state = true;
} else {
widget.ref.read(textSelectionInProgress.notifier).state = false;
}
});
}
Ensure you add this in the onDispose:
#override
void dispose() {
widget.ref.read(textSelectionInProgress.notifier).state = false;
super.Dispose();
}
Add listener in the build of the widget you want to stop scrolling
bool textSelectionOn = ref.watch(textSelectionInProgress);
Set ScrollPhysics appropriately
physics: textSelectionOn
? NeverScrollableScrollPhysics()
: <you choice>