Related
I'm writing an audio player. Like most media players (Youtube, Spotify, etc), I want a "remote" overlay on the screen while media is playing. No matter what the user is doing, they should be able to control the media.
I accomplished that with a Stack under MaterialApp
MaterialApp(
title: 'MyApp',
navigatorObservers: [gRouteObserver],
routes: appRoutes,
builder: (context, child) {
return Stack(children: [
child!,
Positioned(
bottom: 55,
width: MediaQuery.of(context).size.width,
child: StatefulBuilder(builder: (context, _setState) {
gPlayer.widgetSBRefresher = _setState;
return gPlayer.started ? gPlayer.widget : const SizedBox(height: 0);
}))
]);
});
gPlayer.widget references this
class MiniPlayer extends StatefulWidget {
const MiniPlayer({Key? key}) : super(key: key);
#override
State<MiniPlayer> createState() => MiniPlayerState();
}
class MiniPlayerState extends State<MiniPlayer> with AutomaticKeepAliveClientMixin {
#override
bool get wantKeepAlive => true;
#override
Widget build(context) {
super.build(context);
return Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
color: Colors.black,
child: Padding(
padding: const EdgeInsets.all(2.0),
child: Column(
children: [
Material(
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
AvatarAlone(id: gPlayer.current!.owner),
Expanded(
child: Padding(
padding: const EdgeInsets.all(5.0),
child: Text(gPlayer.playing
? "Now Playing"
: "Paused"),
),
),
// here is the code I'll
// be talking about -->
IconButton(
color: Colors.white,
iconSize: 20,
icon: const Icon(MyIcons.bookmark),
onPressed: gPlayer.bookmarkBuilder,
),
InkWell(child: Icon(gPlayer.playing ? MyIcons.pauseCircle : MyIcons.playCircle, size: 50), onTap: gPlayer.playPause)
],
),
),
],
),
),
),
));
}
refresh() {
setState(() {});
}
}
I used a code comment to point out this icon button.
IconButton(
color: Colors.white,
iconSize: 20,
icon: const Icon(MyIcons.bookmark),
onPressed: gPlayer.bookmarkBuilder,
),
So, when this widget is open and the app is on the home route ("/"), I can do
bookmarkBuilder() {
Scaffold.of(gScaffApp.currentContext!).openDrawer();
}
and it will open the drawer.
I've attached the same drawers to all my routes' scaffolds.
When other routes are up, with their own scaffolds, I want bookmarkBuilder to open the drawer on the topmost route. But I can't quite figure out how.
So I have a working solution to this, but I don't love it.
I created a global variable, gScaffs, with gScaffApp as the first element.
List<GlobalKey<ScaffoldState>> gScaffs = [gScaffApp];
My secondary routes all use the same base scaffold widget
class _CardScaffoldState extends State<CardScaffold> {
#override
initState() {
super.initState();
gScaffs.add(GlobalKey());
}
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async => false,
child: Scaffold(key: gScaffs.last,
drawer: DrawerBookmarks()
...
And the dispose method looks like this.
#override
dispose() {
super.dispose();
gScaffs.removeLast();
}
And then, in my bookmarkBuilder function, I have this.
It's not clear to me why, but gScaffApp needs the drawer triggered one way, while the CardScaffolds need the drawer triggered the other way.
bookmarkBuilder() {
if (gScaffApp == gScaffs.last) {
Scaffold.of(gScaffApp.currentContext!).openDrawer();
} else {
gScaffs.last.currentState!.openDrawer();
}
}
I am creating a simple app in Flutter. There are 7 images on 1 screen. I need a function that you can change an image when you tap one of the images. However, now when I tap an image, the other 6 images are also changed. I made a variable "isReal" to put into buildButton() and "isReal" would be switched true and false in the For statement which switch "isReal" in buildButton(). But, that did not work. Could you give me some advice on this problem? Thank you.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:audioplayers/audioplayers.dart';
class Screen extends StatefulWidget {
#override
_ScreenState createState() => _ScreenState();
}
class _ScreenState extends State<Screen> {
bool isReal = true;
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
backgroundColor: Colors.teal[100],
// appBar: AppBar(
// title: Text('AnimalSounds'), backgroundColor: Colors.teal),
body: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
buildButton('cat.mp3', Colors.red, 'images/cat.png',
'images/cat_real.jpg'),
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
buildButton('dog.mp3', Colors.yellow, 'images/dog.png',
'images/cow.png'),
buildButton('cow.mp3', Colors.orange, 'images/cow.png',
'images/dog.png'),
])),
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
buildButton('pig.mp3', Colors.green, 'images/pig.png',
'images/elephant.png'),
buildButton('elephant.mp3', Colors.teal,
'images/elephant.png', 'images/rooster.png'),
buildButton('rooster.mp3', Colors.blue,
'images/rooster.png', 'images/pig.png'),
])),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
buildButton('goat.mp3', Colors.purple, 'images/goat.jpg',
'images/pig.png'),
],
)),
],
),
)));
}
Expanded buildButton(sound, color, simpleImage, realImage) {
return Expanded(
child: FlatButton(
onPressed: () {
setState(() {
isReal = !isReal;
});
final player = AudioCache();
player.play(sound);
},
color: color,
child: isReal ? Image.asset(simpleImage) : Image.asset(realImage),
));
}
}
Ok, you have variable isReal that is the same for entire class (i.e. each button use the same variable). So when you change it's value by tapping on one button it affects all other buttons as well.
To solve this issue I would recommend to move button implementation into a separate Statefull widget. This way you can keep your Screen class as Stateless.
UPD:
Obviously you should watch some tutorials on how to make this on your own. But just for this time this is how it should look like after you separate widgets.
What I did here is:
Create new widget class FlipButton
Move code from your method into build function of new widget
Add parameters to constructor
This way when each FlipButton will have it's own isReal variable.
NOTE: I didn't try to compile it so there might be some errors.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:audioplayers/audioplayers.dart';
class Screen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
backgroundColor: Colors.teal[100],
body: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
//replace all occurances on `buildButton` method with new widget
FlipButton(sound: 'cat.mp3', color: Colors.red, simpleImage: 'images/cat.png', realImage: 'images/cat_real.jpg'),
Expanded(
child: Row(crossAxisAlignment: CrossAxisAlignment.stretch, children: <Widget>[
FlipButton(sound: 'dog.mp3', color: Colors.yellow, simpleImage: 'images/dog.png', realImage: 'images/cow.png'),
FlipButton(sound: 'cow.mp3', color: Colors.orange, simpleImage: 'images/cow.png', realImage: 'images/dog.png'),
])),
Expanded(
child: Row(crossAxisAlignment: CrossAxisAlignment.stretch, children: <Widget>[
FlipButton(sound: 'pig.mp3', color: Colors.green, simpleImage: 'images/pig.png', realImage: 'images/elephant.png'),
FlipButton(sound: 'elephant.mp3', color: Colors.teal, simpleImage: 'images/elephant.png', realImage: 'images/rooster.png'),
FlipButton(sound: 'rooster.mp3', color: Colors.blue, simpleImage: 'images/rooster.png', realImage: 'images/pig.png'),
])),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
FlipButton(sound: 'goat.mp3', color: Colors.purple, simpleImage: 'images/goat.jpg', realImage: 'images/pig.png'),
],
)),
],
),
),
),
);
}
}
/// You can copy this widget into separate file for better formatting
///
class FlipButton extends StatefulWidget {
//declare final variables
final String sound;
final Color color;
final String simpleImage;
final String realImage;
//constructor for this class
const FlipButton({
Key? key,
required this.sound,
required this.color,
required this.simpleImage,
required this.realImage,
}) : super(key: key);
#override
_FlipButtonState createState() => _FlipButtonState();
}
class _FlipButtonState extends State<FlipButton> {
//inside the state declare variable that is about to change
bool isReal = false;
#override
Widget build(BuildContext context) {
return Expanded(
child: FlatButton(
onPressed: () {
setState(() {
isReal = !isReal;
});
final player = AudioCache();
player.play(sound);
},
color: widget.color,
child: isReal ? Image.asset(widget.simpleImage) : Image.asset(widget.realImage),
));
}
}
You can use Random class from dart:math to generate the next random image.
Exemple :
int imageNumber = 1;
void updateImage() {
setState(() {
//Random.nextInt(n) returns random integer from 0 to n-1
imageNumber = Random().nextInt(7) + 1;
});
}
#override
Widget build(BuildContext context) {
return Center(
child: Expanded(
child: Padding(
padding: const EdgeInsets.all(50.0),
child: FlatButton(
child: Image.asset('images/dice$imageNumber.png'),
onPressed: () {
updateImage();
},
),
),
),
);
}
The idea behind this is i would like users to select their sex, sexual orientation and relationship status. So there would be a button in the first row saying "Male" another one saying "female" and another one saying "other". Below that there would be 3 other buttons saying "straight" "gay/lesbian" and "other" and below that another row with 3 buttons (options). Once the user clicks a button the button toggles on essentially and it goes from e.g black to green. I created the boolean toggleOn and will be setting to true or false on button press. The issue is right now i am getting an error on the screen and i think it is because i am going over the available pixels and singleChildScrollView does not work. Any ideas?
Also i am sorry if you are cringing right now i am just making a personal project to learn to code.
This is the screen where i would like to have 3 rows with 3 buttons each
import 'package:./components/rounded_button.dart';
import 'package:./screens/register_screen.dart';
import 'package:flutter/material.dart';
class UserRegisterPreferences extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Scaffold(
backgroundColor: Color(0xff1e1e1e),
body: Body(),
),
);
}
}
class Body extends StatefulWidget {
#override
_BodyState createState() => _BodyState();
}
class _BodyState extends State<Body> {
#override
Widget build(BuildContext context) {
bool toggleOn = false;
Size size = MediaQuery.of(context).size;
return Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
verticalDirection: VerticalDirection.down,
children: <Widget>[
Container(
child: SingleChildScrollView(
child: Column(
children: <Widget>[
roundedButton(
press: () => setState(() => toggleOn = !toggleOn),
),
roundedButton(
press: () => setState(() => toggleOn = !toggleOn),
),
roundedButton(),
],
),
),
),
Column(
children: <Widget>[
roundedButton(),
roundedButton(),
roundedButton(),
],
),
Column(children: <Widget>[
roundedButton(),
roundedButton(),
roundedButton(),
]),
],
);
}
}
This is the file that contains the button so i can keep reusing on the app
import 'package:./constants.dart';
import 'package:flutter/material.dart';
// ignore: camel_case_types
class roundedButton extends StatelessWidget {
final String text;
final Function press;
final Color color, textColor;
const roundedButton({
Key key,
this.text,
this.press,
this.color = kPrimaryColor,
this.textColor = Colors.white,
}) : super(key: key);
#override
Widget build(BuildContext context) {
Size size = MediaQuery.of(context).size;
return Container(
margin: EdgeInsets.symmetric(vertical: 10),
width: size.width * 0.7,
child: ClipRRect(
borderRadius: BorderRadius.circular(29),
child: TextButton(
style: TextButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 20, horizontal: 40),
primary: Colors.white,
backgroundColor: kPrimaryColor,
),
onPressed: press,
child: Text(text),
),
),
);
}
}
I have a fairly simple flutter app. It has a chat feature.
However, I have a problem with the chat feature.
It's made up of a widget does Scaffold and in it SingleChildScrollView - which has a message-list (container) and input-area (container). Code is attached.
Problem is: if I click on the input box, the keyboard opens and it pushes the message-list.
Pushing the message-list is an acceptable thing if you are already at the bottom of the message-list.
However, if the user scrolled up and saw some old messages, I don't want the message-list widget to be pushed up.
Also, I don't want the message-list to be pushed up if I have only a handful of messages (because that just makes the messages disappear when keyboard opens, and then I need to go and scroll to the messages that have been pushed [user is left with 0 visible messages until they scroll]).
I tried different approaches - like
resizeToAvoidBottomInset: false
But nothing seems to work for me, and this seems like it should be a straightforward behavior (for example, whatsapp act like the desired behavior).
My only option I fear is to listen to keyboard opening event, but I was hoping for a more elegant solution.
Here's my code:
#override
Widget build(BuildContext context) {
height = MediaQuery.of(context).size.height;
width = MediaQuery.of(context).size.width;
return Scaffold(
body: SingleChildScrollView(
child: Column(
children: <Widget>[
SizedBox(height: height * 0.1),
buildMessageList(), // container
buildInputArea(context), // container
],
),
),
);
Widget buildInputArea(BuildContext context) {
return Container(
height: height * 0.1,
width: width,
child: Row(
children: <Widget>[
buildChatInput(),
buildSendButton(context),
],
),
);
}
Widget buildMessageList() {
return Container(
height: height * 0.8,
width: width,
child: ListView.builder(
controller: scrollController,
itemCount: messages.length,
itemBuilder: (BuildContext context, int index) {
return buildSingleMessage(index);
},
),
);
}
This seems to work for me:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
ScrollController _controller = ScrollController();
#override
Widget build(BuildContext context) {
SystemChrome.setEnabledSystemUIOverlays([]);
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
body: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
buildMessageList(),
buildInputArea(context),
],
),
),
);
}
Widget buildInputArea(BuildContext context) {
return Row(
children: <Widget>[
Expanded(
child: TextField(),
),
RaisedButton(
onPressed: null,
child: Icon(Icons.send),
),
],
);
}
Widget buildMessageList() {
return Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: 50,
controller: _controller,
itemBuilder: (BuildContext context, int index) {
return Padding(
padding: EdgeInsets.all(10),
child: Container(
color: Colors.red,
height: 20,
child: Text(index.toString()),
),
);
},
),
);
}
}
I think the problem is that you are using fixed sizes for all widgets. In this case it is better to use Expanded for the ListView and removing the SingleChildScrollView. That way the whole Column won't scroll, but only the ListView.
Try to use Stack:
#override
Widget build(BuildContext context) {
height = MediaQuery.of(context).size.height;
width = MediaQuery.of(context).size.width;
return Scaffold(
body: Stack(
children: <Widget>[
SingleChildScrollView(
child: Column(
children: <Widget>[
SizedBox(height: height * 0.1),
buildMessageList(),
],
),
),
Positioned(
bottom: 0.0,
child: buildInputArea(context),
),
],
),
);
}
Setting resizeToAvoidBottomInset property to false in your Scaffold should work.
You can use NotificationListener to listen to scroll notifications to detect that user is at the bottom of the message-list. If you are at the bottom you can then set resizeToAvoidBottomInset to true.
Something like this should work
final resizeToAvoidBottomInset = true;
_onScrollNotification (BuildContext context, ScrollNotification scrollNotification) {
if (scrollNotification is ScrollUpdateNotification) {
// detect scroll position here
// and set resizeToAvoidBottomInset to false if needed
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: this.resizeToAvoidBottomInset,
body: NotificationListener<ScrollNotification>(
onNotification: (scrollNotification) {
return _onScrollNotification(context, scrollNotification);
},
child: SingleChildScrollView(
child: Column(
children: <Widget>[
buildMessageList(), // container
buildInputArea(context), // container
],
),
),
),
);
}
this is technically already answered, and the answer is almost correct. However, I have found a better solution to this. Previously the author mentions that he wants to have a similar experience to WhatsApp. By using the previous solution, the listview would not be able to scrolldown to maxExtent when the sent button is pressed. To fix this I implemented Flex instead of Expanded, and use a singlechildscrollview for the input area
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
ScrollController _controller = ScrollController();
TextEditingController _textcontroller=TextEditingController();
List<String> messages=[];
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
body: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Flexible(
child: ListView.builder(
shrinkWrap: true,
itemCount: messages.length,
controller: _controller,
itemBuilder: (BuildContext context, int index) {
print("From listviewbuilder: ${messages[index]}");
return Padding(
padding: EdgeInsets.all(10),
child: Container(
color: Colors.red,
height: 20,
child: Text(messages[index])
),
);
},
),
),
SingleChildScrollView(
child: Row(
children: <Widget>[
Expanded(
child: TextField(controller: _textcontroller),
),
RaisedButton(
onPressed: (){
Timer(Duration(milliseconds: 100), () {
_controller.animateTo(
_controller.position.maxScrollExtent,
//scroll the listview to the very bottom everytime the user inputs a message
curve: Curves.easeOut,
duration: const Duration(milliseconds: 300),
);
});
setState(() {
messages.add(_textcontroller.text);
});
print(messages);
},
child: Icon(Icons.send),
),
],
),
),
],
),
),
);
}
}
It's better to use flex because expanded as the documentation says, expands over available space, whereas flex would resize to the appropriate proportion. This way if you are going for the "WhatsApp experience" in which the listview scrolls down once you sent a message. The listview would resize when the keyboard pops up and you will get to the bottom, instead of it not going fully to the bottom.
I want to navigate from "Now Showing" to "Coming Soon" with a left swipe on the image, Moreover, I want the Appbar to not to move when I swipe, but I think it is only possible with tab bars and I am not sure, please give some advice if you know how to achieve this
enter image description here
As per GaboBrandX, he is correct. But you can also do one thing with the tabs also. The sliding will not work. It is complex, but you can give it a shot.
The picture I will give you, so there would be Tabs and below that there would be a container each container gets replaces by a click.
TabController controller;
int activeIndex = 0;
#override
void initState() {
super.initState();
this.tabController = TabController(length: 3, vsync: this);
}
//This changes the activeIndex based upon the tabController index
onTabChanged(){
this.setState((){
this.activeTabIndex = this.tabController.index;
});
}
//This will return your container, based upon your tabs selected
Widget getActiveTabView(){
case 1: {return YourSecondContainer();}
break;
default: {return YourFirstContainer();}
}
//Here is your full layout
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
TMTabBar(titles: 'XYZ', controller: this.tabController, onChange: this.onTabChanged),
this.getActiveTabView(),
]
);
}
//Create a TabBarWidget and do this
class TMTabBar extends StatefulWidget {
String/List<String> titles;
TabController controller;
VoidCallback onChange;
TMTabBar({#required this.titles, #required this.controller, this.onChange});
#override
_TMTabBarState createState() => _TMTabBarState();
}
class _TMTabBarState extends State<TMTabBar> {
#override
void initState() {
//this is for changing the content as per the tabbar
this.widget.controller.addListener((){
if(this.widget.controller.indexIsChanging){
if(this.widget.onChange != null) this.widget.onChange();
}
});
super.initState();
}
#override
Widget build(BuildContext context) {
return TabBar(tab: YourTabs);
}
This basically gives you, what you're hoping for. Hope that helps. Thanks :)
Here I've made an example of what your looking for using a PageView. I've put only text on PageView's children, but you can put there your ListViews or anything you need. When tapping on a button the PageView navigates to the corresponding "page". This can be a starting point for you:
import 'package:flutter/material.dart';
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
PageController _pageController = PageController(
initialPage: 0,
);
goToPage(num page) {
_pageController.animateToPage(
page,
duration: Duration(milliseconds: 350),
curve: Curves.easeIn,
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Stack(
children: <Widget>[
Align(
alignment: Alignment.topCenter,
child: Container(
width: double.infinity,
height: 60.0,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Expanded(
child: RaisedButton(
onPressed: () => goToPage(0),
child: Text('Now Showing'),
),
),
SizedBox(
width: 4.0,
),
Expanded(
child: RaisedButton(
onPressed: () => goToPage(1),
child: Text('Coming Soon'),
),
),
],
),
),
),
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
width: double.infinity,
height: MediaQuery.of(context).size.height - 60.0,
child: PageView(
controller: _pageController,
children: <Widget>[
Center(
child: Text('Tab 1'),
),
Center(
child: Text('Tab 2'),
),
],
),
),
),
],
),
),
);
}
}