Place widget outside of bounding box - flutter

My app shows various Container() Widget()s in several columns in a certain view.
I tried to place some icons inside the Container()s to provide operations like delete, minimize etc. Unfortunately, that doesn't look good on native targets.
Therefore I'd like to keep the visual appearance as is and show an actions menu above the actual Container() once the mouse pointer moves over the Container().
This menu would be above all other widgets, be non-modal and disappear, once the pointer leaves the bound box of the Container(). Containers() shouldn't change size and location.
Using MouseRegion(), I'd make the menu appear and disappear.
May I place some Widget() outside the bounding rectangle of a Container() [or other widgets)? Ideally, I'd like to place it relative to the other bounding box.
UPDATE 2022-03-24
Created an OverlayMenu() class which renders something like this:
Usage:
OverlayMenu(
actionWidget:
const Icon(Icons.settings, color: Colors.blue, size: 20),
callbacks: [
() {
// Click on icons 1 action
},
() {
// Click on icon 2 action
}
],
icons: [
Icons.delete, Icons.tv
],
leftOffset: StoreProvider.of<EModel>(context)
.state
.columnWidth
.toDouble())
And the implementation of OverlayMenu():
import 'package:flutter/material.dart';
class OverlayMenu {
List<IconData> icons;
List<Function> callbacks;
Widget actionWidget;
double leftOffset = 0.0;
bool mayShowMenu = true;
OverlayMenu({ required this.actionWidget, required this.icons, required this.callbacks, required this.leftOffset });
Widget insert( BuildContext context ) {
return MouseRegion(
onEnter: (_) {
if (mayShowMenu) {
_showOverlay( context );
}
},
onExit: (_) {
mayShowMenu = true;
},
child: const Icon(Icons.settings, color: Colors.blue, size: 20),
);
}
///
///
///
void _showOverlay(BuildContext outerContext ) async {
final renderBox = outerContext.findRenderObject() as RenderBox;
var size = renderBox.size;
var offset = renderBox.localToGlobal(Offset.zero);
assert( icons.length == callbacks.length, 'Need to provide as many icons as callbacks' );
List<Widget> actionElements = List.empty( growable: true );
for( var n=0; n<icons.length; n++ ) {
actionElements.add( GestureDetector(
onTap: () {
callbacks[ n ]();
},
child: Icon( icons[ n ], size: 22 ))
);
}
// Declaring and Initializing OverlayState
// and OverlayEntry objects
OverlayState? overlayState = Overlay.of(outerContext);
OverlayEntry? overlayEntry;
overlayEntry = OverlayEntry(builder: (context) {
// You can return any widget you like here
// to be displayed on the Overlay
return Stack(children: [
Positioned(
top: offset.dy,
left: offset.dx +
leftOffset -
40,
child: MouseRegion(
onExit: (_) {
overlayEntry?.remove();
print(DateTime.now().toString() + ' ovl: removed');
},
cursor: SystemMouseCursors.contextMenu,
child: Material(
child:Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: Colors.grey.shade300,
width: 1,
),
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
spreadRadius: 4,
blurRadius: 4,
offset: const Offset( 4,4 ),
blurStyle: BlurStyle.normal),
]
),
child: SizedBox(
width: 60,
height: 60,
child: Wrap(spacing: 4, children: actionElements
))))),
),
]);
});
// Inserting the OverlayEntry into the Overlay
overlayState?.insert(overlayEntry);
mayShowMenu = true;
print(DateTime.now().toString() + ' ovl: inserted');
}
}

One approach you can use to achieve what you want is Overlay widget since it's non-modal and also does't require layout/size changes to have hit testable items.
Based on your question I assume this flow is what want:
Insert an overlay entry once the pointer has entered the widget and remove it once it leaves
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Stack(
children: [
MouseRegion(
onEnter: (_) {
Overlay.of(context)!.insert(this._overlayEntry);
},
onExit: (_) => clear(),
child: FloatingActionButton(
key: buttonKey,
onPressed: () {},
),
),
],
),
),
);
}
This is how we remove the entry, a check whether we can remove or not and a delay (for smoothing) :
Future<void> clear() async {
if (!keepMenuVisible) {
await Future.delayed(Duration(milliseconds: 200));
if (!keepMenuVisible) {
_overlayEntry.remove();
}
}
}
The additional delays are used to ensure that the menu doesn't despair reactively but instead we make it smoother.
keepMenuVisible is used to lock the menu and keep it visible once the menu it self has been hovered.
Finally, we create the entry and place the items relative to the main widget (FAB in this case):
#override
void initState() {
super.initState();
WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
_overlayEntry = _createOverlayEntry();
});
}
OverlayEntry _createOverlayEntry() {
final renderBox = buttonKey.currentContext!.findRenderObject() as RenderBox;
var size = renderBox.size;
var offset = renderBox.localToGlobal(Offset.zero);
return OverlayEntry(
builder: (_) => Positioned(
left: offset.dx,
top: offset.dy + size.height + 5.0,
width: 200,
child: MouseRegion(
onEnter: (_) {
keepMenuVisible = true;
},
onHover: (_) {
keepMenuVisible = true;
},
onExit: (_) async {
keepMenuVisible = false;
clear();
},
child: Material(
elevation: 4.0,
child: ListView(
padding: EdgeInsets.zero,
shrinkWrap: true,
children: <Widget>[
ListTile(
onTap: () => print('tap action 1'),
title: Text('Action 1'),
),
ListTile(
onTap: () => print('tap action 2'),
title: Text('Action 2'),
)
],
),
),
),
),
);
}
check the full sample here

Related

Flutter - Want to show camera Tabbarview in full screen

I am building WhatsApp clone for demo. I want to hide AppBar and Tabbar when I click on Camera tab and I want CameraScreen in full screen. I hope, I could made very clear. I have also included whole code of the CameraScreen page thus you can understand(just edited to add whole code).
Sorry for uploading code late.
Thank you in advance.
Here is scree shot:Click here
class _CameraScreenState extends State<CameraScreen> {
CameraController? _cameraController;
// bool _isvideoRecording = false;
Future<void>? cameravalue;
bool isrecording = false;
String videopath = '';
XFile? videorecording;
bool flash = false;
bool isCameraFront = false;
double trasform = 0;
#override
void initState() {
// TODO: implement initState
super.initState();
_cameraController = CameraController(camera![0], ResolutionPreset.high);
cameravalue = _cameraController!.initialize();
}
#override
void dispose() {
// TODO: implement dispose
super.dispose();
_cameraController!.dispose();
}
#override
Widget build(BuildContext context) {
return Stack(
children: [
FutureBuilder(
future: cameravalue,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return CameraPreview(_cameraController!);
} else {
return const Center(
child: CircularProgressIndicator(),
);
}
},
),
Positioned(
bottom: 0,
child: Container(
padding: const EdgeInsets.only(
top: 5,
bottom: 5,
),
color: Colors.black,
width: MediaQuery.of(context).size.width,
child: Column(
children: [
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
onPressed: () {
setState(() {
flash = !flash;
});
flash
? _cameraController!.setFlashMode(FlashMode.torch)
: _cameraController!.setFlashMode(FlashMode.off);
},
icon: Icon(
flash ? Icons.flash_on : Icons.flash_off,
color: Colors.white,
size: 28,
),
),
GestureDetector(
onLongPress: () async {
await _cameraController!.startVideoRecording();
setState(() {
isrecording = true;
});
},
onLongPressUp: () async {
XFile videopath =
await _cameraController!.stopVideoRecording();
setState(() {
isrecording = false;
});
Navigator.push(
context,
MaterialPageRoute(
builder: (builder) => VideoView(
path: videopath.path,
)));
},
onTap: () {
if (!isrecording) {
takePhoto(context);
}
},
child: isrecording
? const Icon(
Icons.radio_button_on,
color: Colors.red,
size: 80,
)
: const Icon(
Icons.panorama_fish_eye,
color: Colors.white,
size: 70,
),
),
IconButton(
onPressed: () async {
setState(() {
isCameraFront = !isCameraFront;
trasform = trasform + pi;
});
int cameraPos = isCameraFront ? 0 : 1;
_cameraController = CameraController(
camera![cameraPos], ResolutionPreset.high);
cameravalue = _cameraController!.initialize();
},
icon: Transform.rotate(
angle: trasform,
child: const Icon(
Icons.flip_camera_ios,
color: Colors.white,
size: 30,
),
),
),
],
),
const SizedBox(
height: 4,
),
const Text(
'Hold for Video, tap for photo',
style: TextStyle(color: Colors.white),
textAlign: TextAlign.center,
),
],
),
),
)
],
);
}
void takePhoto(BuildContext context) async {
final path =
join((await getTemporaryDirectory()).path, "${DateTime.now()}.png");
XFile picture = await _cameraController!.takePicture();
picture.saveTo(path);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CameraView(
path: path,
),
),
);
}
You can implement this features by doing the following simple steps. There will be another possible solutions.
First of all, create a new route ( new widget ) that contains camera view in fullscreen.
When you tap the camera tab, go to that route with fade-in or scale animation with faster duration that will make UI more realistic.

Flutter remove OverlayEntry if touch outside

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

showMenu position Flutter

i have a GridView Widget that contain some GridTiles that are wrapped with GestureDetector trying to showUp a menu to delete the GridTile when i have longPress on it ,,, everything is fine except that i want that menu to be shown from the point that i have clicked into not at the top of the app
showMenu(
context: context,
position: ..........,// Here i want the solution
items: [
PopupMenuItem(
child: FlatButton.icon(
onPressed: () {
_notesProv.deleteNote(id);
Navigator.of(context).pop();
},
icon: Icon(
Icons.delete,
color: Colors.black,
),
label: Text(
'delete note',
style: TextStyle(color: Colors.black),
),
),
),
],
color: Colors.green[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
));
See comments in the code for guidance on what's going on.
Wherever there's a comment, that code was added to the example Gridview stolen from Flutter official cookbook in order to pop up the menu where you tapped.
class GridPositionPage extends StatefulWidget {
#override
_GridPositionPageState createState() => _GridPositionPageState();
}
class _GridPositionPageState extends State<GridPositionPage> {
// ↓ hold tap position, set during onTapDown, using getPosition() method
Offset tapXY;
// ↓ hold screen size, using first line in build() method
RenderBox overlay;
#override
Widget build(BuildContext context) {
// ↓ save screen size
overlay = Overlay.of(context).context.findRenderObject();
return BaseExamplePage(
title: 'Grid Position',
child: GridView.count(
crossAxisCount: 2,
children: List.generate(100, (index) {
return Center(
child: InkWell(
child: Text(
'Item $index',
style: Theme.of(context).textTheme.headline5,
),
// ↓ save screen tap position now
onTapDown: getPosition,
onLongPress: () => showMenu(
context: context,
position: relRectSize,
items: [
PopupMenuItem(
child: FlatButton.icon(
label: Text('Delete'),
icon: Icon(Icons.delete),
),
)
]
),
),
);
}),
),
);
}
// ↓ create the RelativeRect from size of screen and where you tapped
RelativeRect get relRectSize => RelativeRect.fromSize(tapXY & const Size(40,40), overlay.size);
// ↓ get the tap position Offset
void getPosition(TapDownDetails detail) {
tapXY = detail.globalPosition;
}
}
This function handles everything, you just need to use the findRenderObject method to get the RenderObject on each grid item:
void showGridMenu() async {
Rect? rect;
RenderBox? overlay = Overlay.of(context).context.findRenderObject() as RenderBox;
final renderObject = context.findRenderObject();
final translation = renderObject?.getTransformTo(null).getTranslation();
if (translation != null && renderObject?.paintBounds != null) {
final offset = Offset(translation.x, translation.y);
rect = renderObject!.paintBounds.shift(offset);
}
if (rect != null) {
var value = await showMenu<String>(
context: context,
position: RelativeRect.fromRect(
rect,
Offset.zero & overlay.size,
),
items: [
const PopupMenuItem(value: 'yes', child: Text('Yes')),
const PopupMenuItem(value: 'no', child: Text('No')),
],
);
if (value != null) {
debugPrint(value);
}
}
}
position: RelativeRect.fromSize(
Rect.fromCenter(
center: Offset.zero, width: 100, height: 100),
Size(100, 100),
),
Somthing like that

Flutter- per my code how to make material button invisible on swiping left and re-appear on swiping right

I am creating an APP which has a lot of emphasis on the image in the background as such, their is text in arabic on that image per line and I want to add "material buttons" on top of this text. I was able to do this ...but then I want the button to be invisible once I swipe left, and re-appear when I swipe to the right, I did use gesture Detector and it does print on the screen if I swipe right or swipe left ..I was trying to input the gesture detector within the material button but everytime I try this it sends an error that's why I have the gesture detector on the bottom of the whole code please help ...
please help
import 'dart:io';
import 'package:Quran_highlighter/main.dart';
import 'package:flutter/rendering.dart';
import 'package:system_shortcuts/system_shortcuts.dart';
import 'package:Quran_highlighter/Widgets/NavDrawer.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:zoom_widget/zoom_widget.dart';
import 'package:flutter/gestures.dart';
class Aliflaammeem extends StatefulWidget {
#override
_AliflaammeemState createState() => _AliflaammeemState();
}
class _AliflaammeemState extends State<Aliflaammeem> {
var nameList = new List<String>();
final items = List<String>.generate(20, (i) => "Item ${i + 1}");
List<MaterialButton> buttonsList = new List<MaterialButton>();
#override
void initState(){
super.initState();
nameList.add("I love");
nameList.add("my ALLAH");
nameList.add("SWT Very Much");
List<Widget> buildButtonswithName(){
int length = nameList.length;
for (int i=0; i<length; i++){
buttonsList.add(new MaterialButton(
height:40.0,
minWidth: 300.0,
color: Colors.blue,
textColor: Colors.white,
));
}
} }
List<String> labels = ['apple', 'banana', 'pineapple', 'kiwi'];
// List<VoidCallback> actions = [_buyApple, _doSomething, _downloadData, () => print('Hi')
// ];
bool _visible = true;
int _counter = 0;
double _initial = 0.0;
var textHolder = "";
changeTextEnglish() {
setState(() {
bool _visible = true;
_visible = _visible;
textHolder = "All Praise and Thanks is to Allah the lord of the worlds";
});
}
changeTextArabic() {
bool _visible = true;
setState(() {
_visible = _visible;
});
}
#override
Widget build(BuildContext context) {
final title = 'Dismissing Items';
// appBar: AppBar(
// title: Text('Para 1, Pg2'),
// backgroundColor: Colors.teal[400],
// SystemChrome.setPreferredOrientations(
// [DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]);
return Scaffold(
body: Center(
child: Stack(fit: StackFit.expand, children: <Widget>[
Stack(
children: <Widget>[
SingleChildScrollView(
scrollDirection: Axis.vertical,
child: SafeArea(
top: true,
bottom: true,
right: true,
left: true,
child: Image(
image: AssetImage('test/assets/quranpg0.png'),
fit: BoxFit.cover
),
),
),
],
),
Stack(
children: <Widget>[
// for(int i = 0; i< labels.length; i++)
// weather_app/lib/page/settings_page.dart -- line ~81
// ListView.builder(
// itemCount: items.length,
// itemBuilder: (context, index) {
// final item = items[index];
// return Dismissible(
// // Each Dismissible must contain a Key. Keys allow Flutter to
// // uniquely identify widgets.
// key: Key(item),
// // Provide a function that tells the app
// // what to do after an item has been swiped away.
// onDismissed: (direction) {
// // Remove the item from the data source.
// setState(() {
// items.removeAt(index);
// });
// // Then show a snackbar.
// Scaffold.of(context)
// .showSnackBar(SnackBar(content: Text("$item dismissed")));
// },
// // Show a red background as the item is swiped away.
// background: Container(color: Colors.green),
// secondaryBackground: Container(color: Colors.red),
// child: ListTile(title: Text('$item'))
// );
// },
// ),
Container(
child: Align(
alignment: Alignment(.27, 0.1
),
// child: Visibility(
// visible: _visible,
// maintainSize: true,
// maintainAnimation: true,
// maintainState: true,
child: MaterialButton(
height: 70.0,
// minWidth: 36.5,
minWidth: 85.0,
onPressed: () => changeTextArabic(),
onLongPress: () => changeTextEnglish(),
// child: Text(labels[i]),
child: Text('$textHolder'),
color: Colors.cyan[400],
// color: Colors.purple[300],
highlightColor: Colors.blue,
textColor: Colors.white,
padding: EdgeInsets.only(left: 10, top: 2, right: -1, bottom: 5
),
),
),
),
for(int i = 0; i< labels.length; i++)
Container(
child: Align(
alignment: Alignment(-.5, 0.1
),
// child: Text("The Most Loving",
// style: TextStyle(
// fontSize: 15.0,
// backgroundColor: Colors.cyan,
// height: 1.0,
// fontWeight: FontWeight.bold
// ),
child: MaterialButton(
height: 70.0,
minWidth: 36.5,
onPressed: () => changeTextArabic(),
onLongPress: () => changeTextEnglish(),
// Positioned(
// top: 21,
child: Text(labels[i]),
disabledTextColor: Colors.transparent,
color: Colors.cyan[300],
// color: Colors.purple[300],
highlightColor: Colors.blue,
textColor: Colors.white,
padding: EdgeInsets.only(left: 46, top: 2, right: -20, bottom: 5),
),
// ),
),
)
],
),
GestureDetector(onPanUpdate: (DragUpdateDetails details) {
if (details.delta.dx > 0) {
print("right swipe english");
changeTextEnglish();
setState(() {
});
} else if (details.delta.dx < 0) {
print("left swipe arabic");
changeTextArabic();
setState(() {
});
}
})
])));
}
}
I think I got want you want.
First I added a condition to display the MaterialButton like so:
(_visible) ? MaterialButton(...) : Container()
Then inside "changeTextEnglish" and "changeTextArabic":
I changed _visible to absolute value
I deleted your lines "bool _visible = ..." because here you where creating local variable inside the function and therefore could no longer access _visible as the attribute of _AliflaammeemState.
So "changeTextEnglish" and "changeTextArabic" became:
changeTextEnglish() {
setState(() {
_visible = true;
textHolder = "All Praise and Thanks is to Allah the lord of the worlds";
});
}
changeTextArabic() {
setState(() {
_visible = false;
});
}
Which fives me the following working code (I deleted your comment to be able to see the issue so maybe don't copy paste the entire code.
class Aliflaammeem extends StatefulWidget {
#override
_AliflaammeemState createState() => _AliflaammeemState();
}
class _AliflaammeemState extends State<Aliflaammeem> {
var nameList = new List<String>();
final items = List<String>.generate(20, (i) => "Item ${i + 1}");
List<MaterialButton> buttonsList = new List<MaterialButton>();
#override
void initState() {
super.initState();
nameList.add("I love");
nameList.add("my ALLAH");
nameList.add("SWT Very Much");
}
List<String> labels = ['apple', 'banana', 'pineapple', 'kiwi'];
bool _visible = true;
int _counter = 0;
double _initial = 0.0;
var textHolder = "";
changeTextEnglish() {
setState(() {
_visible = true;
textHolder = "All Praise and Thanks is to Allah the lord of the worlds";
});
}
changeTextArabic() {
setState(() {
_visible = false;
});
}
#override
Widget build(BuildContext context) {
final title = 'Dismissing Items';
return Scaffold(
body: Center(
child: Stack(fit: StackFit.expand, children: <Widget>[
Stack(
children: <Widget>[
SingleChildScrollView(
scrollDirection: Axis.vertical,
child: SafeArea(
top: true,
bottom: true,
right: true,
left: true,
child: Image(image: AssetImage('test/assets/quranpg0.png'), fit: BoxFit.cover),
),
),
],
),
Stack(
children: <Widget>[
Container(
child: Align(
alignment: Alignment(.27, 0.1),
child: _visible
? MaterialButton(
height: 70.0,
minWidth: 85.0,
onPressed: () => changeTextArabic(),
onLongPress: () => changeTextEnglish(),
child: Text('$textHolder'),
color: Colors.cyan[400],
highlightColor: Colors.blue,
textColor: Colors.white,
padding: EdgeInsets.only(left: 10, top: 2, right: -1, bottom: 5),
)
: Container(),
),
),
for (int i = 0; i < labels.length; i++)
Container(
child: Align(
alignment: Alignment(-.5, 0.1),
child: MaterialButton(
height: 70.0,
minWidth: 36.5,
onPressed: () => changeTextArabic(),
onLongPress: () => changeTextEnglish(),
child: Text(labels[i]),
disabledTextColor: Colors.transparent,
color: Colors.cyan[300],
highlightColor: Colors.blue,
textColor: Colors.white,
padding: EdgeInsets.only(left: 46, top: 2, right: -20, bottom: 5),
),
// ),
),
)
],
),
GestureDetector(onPanUpdate: (DragUpdateDetails details) {
if (details.delta.dx > 0) {
print("right swipe english");
changeTextEnglish();
setState(() {});
} else if (details.delta.dx < 0) {
print("left swipe arabic");
changeTextArabic();
setState(() {});
}
})
])));
}
}

How to save page state on revisit in flutter

I have 2 screens and I am trying to achieve understand how to achieve page state. For example, in the below screen, I have 4 options, and all of them take the user to the same screen the only difference is API getting called for each of them is different to build a list. I am trying to handle back arrow action, and that's where I am having issues.
Use Case -
When user is on screen 2 he is playing the song, now on back press song continues to play. Now when the user again chooses the same option from screen 1 I want to show the same list without reload and selection. If user selects any other option it should behave normal, which is working.
Solution -
I can hardcode loaded songs list and send back to screen 1 and then again get that back with the selection but this will take lot of resources.
AutomaticKeepAliveClientMixin i tried this tutorial but that didn't help or kept state active.
PageStorage is 3rd option i saw but i found majority of the time this is being used on app where we have a tab.
Screen 1 -
class Dashboard extends StatefulWidget {
int playingId;
Dashboard({this.playingId});
#override
_DashboardState createState() => _DashboardState(playingId);
}
class _DashboardState extends State<Dashboard> {
String appname;
int playingId = 0;
_DashboardState(this.playingId);
// print('${playingId}');
#override
void initState() {
appname="";
// playingId=0;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
elevation: 0,
backgroundColor: AppColors.darkBlue,
centerTitle: true,
title: Text("Tirthankar",
style: TextStyle(color: Colors.white),),
),
backgroundColor: AppColors.styleColor,
body: Column(
children: <Widget>[
// SizedBox(height: 5),
Padding(
padding: const EdgeInsets.all(20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
CustomGridWidget(
child: Icon(
Icons.file_download,
size: 100,
color: AppColors.styleColor,
),
// image: 'assets/bhaktambar.png',
sizew: MediaQuery.of(context).size.width * .4,
sizeh: MediaQuery.of(context).size.width * .5,
borderWidth: 2,
label: "Bhakti",
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => ListPage(appname: "Kids",playingId: playingId,),
),
);
},
),
CustomGridWidget(
child: Icon(
Icons.file_download,
size: 100,
color: AppColors.styleColor,
),
// image: 'assets/bhaktambar.png',
sizew: MediaQuery.of(context).size.width * .4,
sizeh: MediaQuery.of(context).size.width * .5,
borderWidth: 2,
label: "Kids",
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => ListPage(appname: "Kids",playingId: playingId,),
),
);
},
),
],
),
),
Padding(
padding: const EdgeInsets.only(
left: 20,
right: 20,
bottom: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
CustomGridWidget(
child: Icon(
Icons.favorite,
size: 100,
color: AppColors.styleColor,
),
// image: 'assets/kids.jpg',
sizew: MediaQuery.of(context).size.width * .4,
sizeh: MediaQuery.of(context).size.width * .5,
borderWidth: 2,
label: "Favorite",
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => ListPage(appname: "Songs"),
),
);
},
),
Align(
alignment: Alignment.center,
child: CustomGridWidget(
child: Icon(
Icons.book,
size: 100,
color: AppColors.styleColor,
),
// image: 'assets/vidyasagar.jpg',
sizew: MediaQuery.of(context).size.width * .4,
sizeh: MediaQuery.of(context).size.width * .5,
borderWidth: 2,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => ListPage(appname: "Bhajan"),
),
);
},
),
),
],
),
),
]
),
);
}
Material boxTiles(IconData icon, String name){
return Material(
color: AppColors.mainColor,
elevation: 14.0,
shadowColor: AppColors.styleColor,
borderRadius: BorderRadius.circular(24.0),
child: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child:Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
InkWell(
onTap: (){print("tapped"); /* or any action you want */ },
child: Container(
width: 130.0,
height: 10.0,
color : Colors.transparent,
), // container
), //
//Text
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(name,
style:TextStyle(
color: AppColors.styleColor,
fontSize: 20.0,
)
),
),
//Icon
Material(
color: AppColors.styleColor,
borderRadius: BorderRadius.circular(50.0),
child: Padding(padding: const EdgeInsets.all(10.0),
child: Icon(icon, color: AppColors.lightBlue,size: 30,),
),
),
],
)
],
)
),
)
);
}
}
Screen 2 -
// import 'dart:js';
class ListPage extends StatefulWidget {
String appname;
int playingId;
ListPage({this.appname,this.playingId});
#override
_ListPageState createState() => _ListPageState(appname,playingId);
}
class _ListPageState extends State<ListPage>
with SingleTickerProviderStateMixin,AutomaticKeepAliveClientMixin<ListPage> {
String appname;
int playingId;
bool isPlaying = false;
_ListPageState(this.appname,playingId);
// List<MusicModel> _list1;
List<MusicData> _list;
var _value;
int _playId;
int _songId;
String _playURL;
bool _isRepeat;
bool _isShuffle;
bool _isFavorite;
String _startTime;
String _endTime;
AnimationController _controller;
Duration _duration = new Duration();
Duration _position = new Duration();
final _random = new Random();
AudioPlayer _audioPlayer = AudioPlayer();
#override
void initState() {
_playId = 0;
// _list1 = MusicModel.list;
this._fileUpdate();
// _list = _list1;
_controller =
AnimationController(vsync: this, duration: Duration(microseconds: 250));
_value = 0.0;
_startTime = "0.0";
_endTime = "0.0";
_isRepeat = false;
_isShuffle = false;
_isFavorite = false;
_audioPlayer.onAudioPositionChanged.listen((Duration duration) {
setState(() {
// _startTime = duration.toString().split(".")[0];
_startTime = duration.toString().split(".")[0];
_duration = duration;
// _position = duration.toString().split(".")[0];
});
});
_audioPlayer.onDurationChanged.listen((Duration duration) {
setState(() {
_endTime = duration.toString().split(".")[0];
_position = duration;
});
});
_audioPlayer.onPlayerCompletion.listen((event) {
setState(() {
isPlaying = false;
_position = _duration;
if (_isRepeat) {
_songId = _songId;
} else {
if (_isShuffle) {
var element = _list[_random.nextInt(_list.length)];
_songId = element.id;
} else {
_songId = _songId + 1;
}
}
_player(_songId);
});
});
super.initState();
}
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
appBar: AppBar(
elevation: 0,
backgroundColor: AppColors.mainColor,
centerTitle: true,
leading: IconButton(
icon: Icon(Icons.arrow_back),
onPressed: (){
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => Dashboard(playingId: _songId),
),
);
},
),
title: Text(
appname,
style: TextStyle(color: AppColors.styleColor),
),
),
backgroundColor: AppColors.mainColor,
body: Stack(
children: <Widget>[
Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(24.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
CustomButtonWidget(
child: Icon(
Icons.favorite,
color: AppColors.styleColor,
),
size: 50,
onTap: () {},
),
CustomButtonWidget(
image: 'assets/logo.jpg',
size: 100,
borderWidth: 5,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => DetailPage(),
),
);
},
),
CustomButtonWidget(
child: Icon(
Icons.menu,
color: AppColors.styleColor,
),
size: 50,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => HomePage(),
),
);
},
)
],
),
),
slider(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
IconButton(
icon: Icon(
_isRepeat ? Icons.repeat_one : Icons.repeat,
color: _isRepeat ? Colors.brown : AppColors.styleColor,
),
onPressed: () {
if (_isRepeat) {
_isRepeat = false;
} else {
_isRepeat = true;
}
},
),
IconButton(
icon: Icon(
isPlaying ? Icons.pause : Icons.play_arrow,
color: AppColors.styleColor,
),
onPressed: () {
if (isPlaying) {
_audioPlayer.pause();
setState(() {
isPlaying = false;
});
} else {
if (!isPlaying){
_audioPlayer.resume();
setState(() {
isPlaying = true;
});
}
}
}),
IconButton(
icon: Icon(
Icons.stop,
color: AppColors.styleColor,
),
onPressed: () {
if (isPlaying){
_audioPlayer.stop();
setState(() {
isPlaying = false;
_duration = new Duration();
});
}
// isPlaying = false;
}),
IconButton(
icon: Icon(
Icons.shuffle,
color:
_isShuffle ? Colors.brown : AppColors.styleColor,
),
onPressed: () {
if (_isShuffle) {
_isShuffle = false;
} else {
_isShuffle = true;
}
}),
],
),
),
Expanded(
//This is added so we can see overlay else this will be over button
child: ListView.builder(
physics:
BouncingScrollPhysics(), //This line removes the dark flash when you are at the begining or end of list menu. Just uncomment for
// itemCount: _list.length,
itemCount: _list == null ? 0 : _list.length,
padding: EdgeInsets.all(12),
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
_songId = index;
_player(index);
},
child: AnimatedContainer(
duration: Duration(milliseconds: 500),
//This below code will change the color of sected area or song being played.
decoration: BoxDecoration(
color: _list[index].id == _playId
? AppColors.activeColor
: AppColors.mainColor,
borderRadius: BorderRadius.all(
Radius.circular(20),
),
),
//End of row color change
child: Padding(
padding: const EdgeInsets.all(
16), //This will all padding around all size
child: Row(
mainAxisAlignment: MainAxisAlignment
.spaceBetween, //This will allign button to left, else button will be infront of name
children: <Widget>[
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
_list[index].title,
style: TextStyle(
color: AppColors.styleColor,
fontSize: 16,
),
),
Text(
_list[index].album,
style: TextStyle(
color: AppColors.styleColor.withAlpha(90),
fontSize: 16,
),
),
],
),
IconButton(
icon: Icon(_isFavorite
? Icons.favorite
: Icons.favorite_border),
onPressed: () {
if (_isFavorite) {
_isFavorite = false;
} else {
_isFavorite = true;
}
})
//Diabled Play button and added fav button.
// CustomButtonWidget(
// //This is Play button functionality on list page.
// child: Icon(
// _list[index].id == _playId
// ? Icons.pause
// : Icons.play_arrow,
// color: _list[index].id == _playId
// ? Colors.white
// : AppColors.styleColor,
// ),
// size: 50,
// isActive: _list[index].id == _playId,
// onTap: () async {
// _songId = index;
// _player(index);
// },
// )
],
),
),
),
);
},
),
)
],
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
height: 50,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppColors.mainColor.withAlpha(0),
AppColors.mainColor,
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
)),
),
)
],
),
// floatingActionButton: FloatingActionButton(
// child: Icon(Icons.music_note),
// onPressed: () async { // String filePath = await FilePicker.getFilePath();
// int status = await _audioPlayer.play("https://traffic.libsyn.com/voicebot/Jan_Konig_on_the_Jovo_Open_Source_Framework_for_Voice_App_Development_-_Voicebot_Podcast_Ep_56.mp3");
// if (status == 1){
// setState(() {
// isPlaying = true;
// });
// }
// },
// )
);
}
Widget slider() {
return Slider(
activeColor: AppColors.styleColor,
inactiveColor: Colors.lightBlue,
value: _duration.inSeconds.toDouble(),
min: 0.0,
max: _position.inSeconds.toDouble(),
divisions: 10,
onChangeStart: (double value) {
print('Start value is ' + value.toString());
},
onChangeEnd: (double value) {
print('Finish value is ' + value.toString());
},
onChanged: (double value) {
setState(() {
seekToSecond(value.toInt());
value = value;
});
});
}
Future<Void> _fileUpdate() async {
String url =
"azonaws.com/input.json";
String arrayObjsText = "";
try {
eos.Response response;
Dio dio = new Dio();
response = await dio.get(url,options: Options(
responseType: ResponseType.plain,
),);
arrayObjsText = response.data;
print(response.data.toString());
} catch (e) {
print(e);
}
var tagObjsJson = jsonDecode(arrayObjsText)['tags'] as List;
this.setState(() {
_list = tagObjsJson.map((tagJson) => MusicData.fromJson(tagJson)).toList();
});
// return _list;
// print(_list);
}
Future<void> _player(int index) async {
if (isPlaying) {
if (_playId == _list[index].id) {
int status = await _audioPlayer.pause();
if (status == 1) {
setState(() {
isPlaying = false;
});
}
} else {
_playId = _list[index].id;
_playURL = _list[index].songURL;
_audioPlayer.stop();
int status = await _audioPlayer.play(_playURL);
if (status == 1) {
setState(() {
isPlaying = true;
});
}
}
} else {
_playId = _list[index].id;
_playURL = _list[index].songURL;
int status = await _audioPlayer.play(_playURL);
if (status == 1) {
setState(() {
isPlaying = true;
});
}
}
}
String _printDuration(Duration duration) {
String twoDigits(int n) {
if (n >= 10) return "$n";
return "0$n";
}
String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60));
String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60));
return "${twoDigits(duration.inHours)}:$twoDigitMinutes:$twoDigitSeconds";
}
void seekToSecond(int second) {
Duration newDuration = Duration(seconds: second);
_audioPlayer.seek(newDuration);
}
}
PageStorage is 3rd option i saw but i found majority of the time this is being used on app where we have a tab.
You still can use this approach with what you want, with or without Tab/ bottom navigation bar.
PageViewClass
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> with SingleTickerProviderStateMixin{
PageController _pageController;
#override
void initState() {
super.initState();
_pageController = PageController();
}
#override
void dispose() {
super.dispose();
_pageController?.dispose();
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: PageView(
controller: _pageController,
physics: NeverScrollableScrollPhysics(), // so the user cannot scroll, only animating when they select an option
children: <Widget>[
Dashboard(playingId: 1, key: PageStorageKey<String>('MyPlayList'), pageController: _pageController), //or the name you want, but you need to give them a key to all the child so it can save the Scroll Position
ListPage(appname: "FirstButton",playingId: 1, key: PageStorageKey<String>('FirstButton'), pageController: _pageController),
ListPage(appname: "SecondButton",playingId: 1, key: PageStorageKey<String>('SecondButton'), pageController: _pageController),
ListPage(appname: "ThirdButton",playingId: 1, key: PageStorageKey<String>('ThirdButton'), pageController: _pageController),
ListPage(appname: "FourthButton",playingId: 1, key: PageStorageKey<String>('FourthButton'), pageController: _pageController)
],
),
)
);
}
}
Now you pass the PageController to all children (add a key and PageController attribute to Screen 1 and 2) and in DashBoard (Screen 1) you can change the onTap of the buttons from the MaterialRoute to this
onTap: () => widget.pageController.jumpToPage(x), //where x is the index of the children of the PageView you want to see (between 0 and 4 in this case)
In Screen 2 you wrap all your Scaffold with a WillPopScope so when the user tap back instead of closing the route it goes back to the Dashboard Widget at index 0
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () {
widget.pageController.jumpToPage(0); // Move back to dashboard (index 0)
return false;
}
child: Scaffold(...)
);
}
You can use other methods of PageController if you want some effects like animations (animateTo, nextPage, previousPage, etc.)