I want to add onTap to each category in my horizontal list view so how can I do it?
class HorizontalList extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
height: 80.0,
child: ListView(
scrollDirection: Axis.horizontal,
children: <Widget>[
Category(
image_location: "catogories/name.png",
),
],
),
);
}}
You can wrap your Category widget with GestureDetector widget.
class HorizontalList extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
height: 80.0,
child: ListView(
scrollDirection: Axis.horizontal,
children: <Widget>[
GestureDetector(
onTap: () {
//This will be called on tap
},
child:Category(
image_location: "catogories/name.png",
),
),
],
),
);
}}
If you are using small sized images, you can use IconButton().
class HorizontalList extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
height: 80.0,
child: ListView(
scrollDirection: Axis.horizontal,
children: <Widget>[
IconButton(icon: Image.asset(<name_here(or any other optoin such as Image.network()>)), onPressed: <function_here:fox example clickEvent()>)
],
),
);
}
}
You can also use
icon: Icon(icon: Icons.<icon_name>, color: Colors.<colors_name>)
, if you're using Icons.
And if you're trying to do something else, comment below !
Related
I have an app that shows its content in a SingleChildScrollView. There is Container with a transparent color that I'd like to change the color of to red when the SingleChildScrollView is scrolled to any other position than the start position and then change the color back to transparent when the SingleChildScrollView is scrolled back to its starting position. Code:
class App extends StatefulWidget {
const App({Key? key}) : super(key: key);
#override
State<App> createState() => _AppState();
}
class _AppState extends State<App> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
Column(
children: [
Flexible(
child: ScrollConfiguration(
behavior: RemoveScrollGlow(),
child: SingleChildScrollView(
child: Column(
children: [
Stack(...) //This is the top section of the page
],
),
),
),
),
],
),
Container(
color: Colors.transparent, //This is the Color I want to change based on the position of the SingleChildScrollView
height: 120,
child: Column(...)
),
],
),
backgroundColor: Colors.white,
);
}
}
EDIT: I managed to make it work by wrapping the SingleChildScrollView in a NotificationListener and updating the color based on the notification like this:
class _AppState extends State<App> {
Color bannercolor = Colors.transparent;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
Column(
children: [
Flexible(
child: ScrollConfiguration(
behavior: RemoveScrollGlow(),
child: NotificationListener<ScrollUpdateNotification>(
onNotification: (scrollEnd) {
final metrics = scrollEnd.metrics;
if (metrics.pixels != 0) {
setState(() {
bannercolor = Colors.white;
});
} else {
setState(() {
bannercolor = Colors.transparent;
});
}
return true;
},
child: SingleChildScrollView(
child: Column(
children: [
Column(...),
],
),
),
),
),
),
],
),
Container(
color: bannercolor,
height: 120,
child: Column(...),
),
],
),
backgroundColor: Colors.white,
);
}
}
You can try listening to the scroll controller offset like this
class App extends StatefulWidget {
const App({Key? key}) : super(key: key);
#override
State<App> createState() => _AppState();
}
class _AppState extends State<App> {
final ScrollController _scrollController = ScrollController ();
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
Column(
children: [
Flexible(
child: ScrollConfiguration(
behavior: RemoveScrollGlow(),
child: SingleChildScrollView(
controller: _scrollController, //add controller here
child: Column(
children: [
Stack(...) //This is the top section of the page
],
),
),
),
),
],
),
AnimatedBuilder(
animation: _scrollController,
builder: (context, _content) {
return Container (
(_scrollController.offset>20)? Colors.blue: Colors.transparent,
height: 120,
child: Column(...)
);
}
),
],
),
backgroundColor: Colors.white,
);
}
}
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.
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(home: Scaffold(body: Center(child: TestWidget())));
}
}
class TestWidget extends StatefulWidget {
TestWidgetState createState() => new TestWidgetState();
}
class TestWidgetState extends State<TestWidget> {
List<Widget> _bodyItems = [];
List<Widget> _topItems = [];
final Widget _boundary = Column(
children: <Widget>[
SizedBox(
height: 10,
),
SizedBox(
child: Container(
color: Colors.black12,
),
height: 1,
),
SizedBox(
height: 10,
),
],
);
#override
void initState() {
super.initState();
Widget e = GestureDetector(key: Key("0"), onTap: () {
onChangedFunction(Key("0"));
},child: Text("This text is on body range"));
_bodyItems.add(e);
_topItems = [];
}
void onChangedFunction(Key key) async {
setState(() {
_bodyItems.removeAt(0);
_topItems.add(Text("This text is on top range."));
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: <Widget>[
SizedBox(
height: 10,
),
ConstrainedBox(
constraints: BoxConstraints(
maxHeight: 100,
),
child: ListView(
scrollDirection: Axis.horizontal,
children: _topItems,
),
),
_boundary,
ConstrainedBox(
constraints: BoxConstraints(
minHeight: 450,
maxHeight: 450,
),
child: Column(
children: _bodyItems,
),
),
_boundary,
],
)));
}
}
result
top items : [Text("This text is on top range.")]
This code deletes the body item when the widget of the body item is tapped and added item at the top item.
Looking at the result, you can see that the data disappeared from the body item widget and the data was added to the top item widget.
However, the data of the top item is not render.
I want to know what the reason is.
This happens because Children is constant, you can see that if you look into definition.
You will find following line in implementation of ListView.
List<Widget> children = const <Widget>[],
but you can change widget in side children, so you have to give list as a children of list.
As Foolowing.
children: [..._topItems],
Replace _topItems and _bodyItems with below
children: [..._topItems], & children: [..._bodyItems], --inside your body
I'm trying to make a grid of products using GridView.Builder but it gives error :
Vertical viewport was given unbounded height.
I tried to use flexible on GridView it worked but I need to use GridView.Builder Specifically
and if I tried to wrap it with Flexible or specific height container it doesn't scroll ,any tips?
import 'package:flutter/material.dart';
class Products extends StatefulWidget {
#override
_ProductsState createState() => _ProductsState();
}
class _ProductsState extends State<Products> {
var productList=[
{
"name":"Blazer",
"picture":"images/products/blazer1.jpeg",
"oldPrice":120,
"price":100
},
{
"name":"Dress",
"picture":"images/products/dress1.jpeg",
"oldPrice":120,
"price":100
},
{
"name":"hills",
"picture":"images/products/hills1.jpeg",
"oldPrice":11,
"price":10
},
{
"name":"pants",
"picture":"images/products/pants2.jpeg",
"oldPrice":12,
"price":200,
}
];
#override
Widget build(BuildContext context) {
return GridView.builder(
scrollDirection: Axis.vertical,
itemCount: productList.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
itemBuilder: (context,index){
return SingalProduct(
name: productList[index]['name'],
picture: productList[index]['picture'],
oldPrice: productList[index]['oldPrice'],
price: productList[index]['price'],
);
},
);
}
}
class SingalProduct extends StatelessWidget {
final name,picture,oldPrice,price;
SingalProduct({this.name,this.picture,this.oldPrice,this.price});
#override
Widget build(BuildContext context) {
return Card(
child: Hero(
tag: name,
child: InkWell(
onTap: (){},
child: GridTile(
footer: Container(
height: 40,
color: Colors.white,
child: Padding(
padding: EdgeInsets.fromLTRB(8, 12, 0, 0),
child: Text(name,textAlign: TextAlign.start,style: TextStyle(fontWeight: FontWeight.bold,fontSize: 16),),
),
),
child: Image.asset(picture,fit: BoxFit.cover, ),
),
),
),
);
}
}
I have a Scaffold with a ListView as a child to be able to scroll.
There is a header container at top of it and a column with a list of images.
I want to load this images only when they appears in viewport but all of them are starting to load at the same time.
This is not happening when I'm adding this images to the children of ListView but I'd like to put them to a separate widget to keep my code clean. Is it possible?
Here is a simplified example:
const List<String> images = [
'https://cataas.com/cat?hash=1',
'https://cataas.com/cat?hash=2',
'https://cataas.com/cat?hash=3',
'https://cataas.com/cat?hash=4',
'https://cataas.com/cat?hash=5',
'https://cataas.com/cat?hash=6',
'https://cataas.com/cat?hash=7',
'https://cataas.com/cat?hash=8',
];
// Scaffold child
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ListView(
children: [
// Header
Placeholder(
fallbackHeight: 300,
),
Category(),
SizedBox(height: 16),
Category(),
],
);
}
}
class Category extends StatelessWidget {
#override
Widget build(BuildContext context) {
print('Build category');
return Column(
children: [
Padding(
padding: EdgeInsets.all(32),
child: Text('CATEGORY TITLE'),
),
...images.map((image) => CategoryItem(image)),
],
);
}
}
class CategoryItem extends StatelessWidget {
final String imageUrl;
CategoryItem(this.imageUrl);
#override
Widget build(BuildContext context) {
print('Build image: $imageUrl');
return FadeInImage.memoryNetwork(
fit: BoxFit.cover,
placeholder: kTransparentImage,
image: imageUrl,
);
}
}
You need to use ListView.builder, because it renders its children lazily. Also you'll need to use one single list.
The problem is that you don't know the image's size before it's loaded, so all images with 0 height will be built instantly. But if you set fixed height to them, it will work as intended.
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
final children = [
// Header
Placeholder(
fallbackHeight: 300,
),
Padding(
padding: EdgeInsets.all(32),
child: Text('CATEGORY TITLE'),
),
...images.map((image) => CategoryItem(image)),
SizedBox(height: 16),
Padding(
padding: EdgeInsets.all(32),
child: Text('CATEGORY TITLE'),
),
...images.map((image) => CategoryItem(image)),
];
return Scaffold(
body: ListView.builder(
itemCount: children.length,
itemBuilder: (context, i) => children[i],
)
);
}
}