I'm trying to use flutter popup menu button, but I can't seem to make it smaller with a scroll.
Is it doable? Or am I using the wrong widget to do it?
Image below as reference, would like to show only the first 4 / 5 items, and scroll to show the rest!
Thanks in advance!
You can create your own PopUp Widget instead.
A Card wrapped into a AnimatedContainer with specific dimensions and a ListView inside.
Place this widget on your screen using Stack and Positioned widgets so it will be above other elements on the top | right.
class CustomPopup extends StatefulWidget {
CustomPopup({
#required this.show,
#required this.items,
#required this.builderFunction,
});
final bool show;
final List<dynamic> items;
final Function(BuildContext context, dynamic item) builderFunction;
#override
_CustomPopupState createState() => _CustomPopupState();
}
class _CustomPopupState extends State<CustomPopup> {
#override
Widget build(BuildContext context) {
return Offstage(
offstage: !widget.show,
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
height: widget.show ? MediaQuery.of(context).size.height / 3 : 0,
width: MediaQuery.of(context).size.width / 3,
child: Card(
elevation: 3,
child: MediaQuery.removePadding(
context: context,
removeTop: true,
child: ListView.builder(
scrollDirection: Axis.vertical,
itemCount: widget.items.length,
itemBuilder: (context, index) {
Widget item = widget.builderFunction(
context,
widget.items[index],
);
return item;
},
),
),
),
),
);
}
}
return Stack(
children: <Widget>[
Container(
color: Colors.blueAccent,
),
Positioned(
right: 0,
top: 60,
child: CustomPopup(
show: shouldShow,
items: [1, 2, 3, 4, 5, 6, 7, 8],
builderFunction: (context, item) {
return ListTile(
title: Text(item.toString()),
onTap: () {}
);
},
),
),
],
);
You can create this in two ways: the first one is PopupMenuButton widget and the second one is PopupRoute.
class HomePage extends StatefulWidget {
#override
_HomepageState createState() => _HomepageState();
}
class _HomepageState extends State {
Listitems = [1,2,3,4,5,6,7,8,9,10,11,12,13];
#override
Widget build(BuildContext context) {
return Scaffold(body: Center(
child: PopupMenuButton(
child: Icon(Icons.add_shopping_cart),
offset: Offset(-1.0, -220.0),
elevation: 0,
color: Colors.transparent,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))),
itemBuilder: (context) {
return <PopupMenuEntry<Widget>>[
PopupMenuItem<Widget>(
child: Container(
decoration: ShapeDecoration(
color: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10))),
child: Scrollbar(
child: ListView.builder(
padding: EdgeInsets.only(top: 20),
itemCount: items.length,
itemBuilder: (context, index) {
final trans = items[index];
return ListTile(
title: Text(
trans.toString(),
style: TextStyle(
fontSize: 16,
),
),
onTap: () {
//what would you like to do?
},
);
},
),
),
height: 250,
width: 500,
),
)
];
}),
)
You can also adjust the number of items you want to show by reducing or increasing height of the container. I also added a scrollbar just in case.
You can use maxHeight for constrains property.
...
PopupMenuButton(
constraints:
BoxConstraints(minWidth: context.maxWidth, maxHeight: 300),
...
Related
i want to code a POS for german 'Fischbrötchen'. My problem is that the "View" of the Ordertabel dosn't update. I tried man things but nothing worked... can someone help me to point out my Problem ?
So when i click a button a Order should add to the Orders List and then update the View to display the order.
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return CupertinoApp(
home: MyHomePage(),
debugShowCheckedModeBanner: false,
theme: CupertinoThemeData(
brightness: Brightness.light, primaryColor: Colors.black54),
);
}
}
ValueNotifier<int> KundenId = ValueNotifier<int>(0);
List<Map<String, dynamic>> orders = [];
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
final List Getraenke = ["Fritz", "Wasser", "Bier"];
List<Map<String, dynamic>> items = [
{'name': 'Möltenorter', 'price': '4 Euro'},
{'name': 'Matjes', 'price': '4 Euro'},
{'name': 'Bismarkt', 'price': '4 Euro'},
{'name': 'Krabben', 'price': '5,50 Euro'},
{'name': 'Lachs', 'price': '5.50 Euro'},
{'name': 'Lachs Kalt', 'price': '5.50 Euro'},
];
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
child: RightSideContainer(),
);
}
}
class RightSideContainer extends StatefulWidget {
#override
State<StatefulWidget> createState() => RightSideContainerState();
}
class RightSideContainerState extends State<RightSideContainer> {
#override
Widget build(BuildContext context) {
return Row(
children: [
//left side, eingabe
Column(
children: [
Text("Kasse"),
Container(
height: 600,
width: MediaQuery.of(context).size.width / 2,
child: Padding(
padding: EdgeInsets.all(5.0),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Colors.black54,
),
alignment: AlignmentDirectional.topStart,
child: OrderTable(),
))),
],
),
//right side, Ausgabe
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(0),
color: Colors.black.withOpacity(0.5),
),
width: MediaQuery.of(context).size.width / 2,
alignment: Alignment.centerRight,
child: Column(
children: [
Container(
height: 500,
color: Colors.red,
child: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4),
itemCount: items.length,
itemBuilder: (BuildContext context, int index) {
return ButtonPrefab(items_: items[index]);
}),
),
],
))
],
);
}
}
class ButtonPrefab extends StatelessWidget {
final Map<String, dynamic> items_;
const ButtonPrefab({required this.items_});
void addOrder(name, price) {
orders.add({
'kundenId': 0,
'bestellung': name,
'price': price,
});
}
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: CupertinoButton(
child: Text(items_['name']),
color: Colors.black54,
padding: EdgeInsets.all(3),
onPressed: () {
print(orders);
addOrder("name", 2.4);
KundenId.value++;
print(KundenId.value);
},
),
);
}
}
class OrderTable extends StatefulWidget {
#override
State<OrderTable> createState() => _OrderTableState();
}
class _OrderTableState extends State<OrderTable> {
#override
void initState() {
super.initState();
setState(() {});
}
void update() {
setState(() {});
}
#override
Widget build(BuildContext context) {
return Container(
child: Column(
children: [
StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return DataTable(
columnSpacing: 20,
columns: [
DataColumn(
label: Text(
'Kunden ID',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
DataColumn(
label: Text(
'Bestellung',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
DataColumn(
label: Text(
'Preis',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
],
rows: orders
.map(
(order) => DataRow(
cells: [
DataCell(
Text(
order['kundenId'].toString(),
style: TextStyle(fontSize: 16),
),
),
DataCell(
Text(
order['bestellung'],
style: TextStyle(fontSize: 16),
),
),
DataCell(
Text(
order['price'].toString(),
style: TextStyle(fontSize: 16),
),
),
],
),
)
.toList(),
);
})
],
),
);
}
}
I tried to use 'set State' in my Statefull Widget but is dosn't change anything..
Deleted my previous answer and tested your code... and got it working now.
I see you have a Function named update() and you're even using it there, but should use it somewhere else as a callback Function. A callback Function helps you to edit values in your "previous" Widget that called this Widget. Read more here:
How to pass callback in Flutter
Also you have setState() in initState. Don't see the reason to have this there either. You should use setState in initState only for some kind of asyncronus reason, as explained here: Importance of Calling SetState inside initState
Call setState in "previous" Widget on button press after adding your item by using a callback Function (for short keeping, here is only the modified code):
class RightSideContainerState extends State<RightSideContainer> {
void update() { //this is a new Function
setState(() {});
}
#override
Widget build(BuildContext context) {
return Row(
children: [
//left side, eingabe
Column(
children: [
Text("Kasse"),
Container(
height: 600,
width: MediaQuery.of(context).size.width / 2,
child: Padding(
padding: EdgeInsets.all(5.0),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Colors.black54,
),
alignment: AlignmentDirectional.topStart,
child: OrderTable(),
))),
],
),
//right side, Ausgabe
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(0),
color: Colors.black.withOpacity(0.5),
),
width: MediaQuery.of(context).size.width / 2,
alignment: Alignment.centerRight,
child: Column(
children: [
Container(
height: 500,
color: Colors.red,
child: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4),
itemCount: items.length,
itemBuilder: (BuildContext context, int index) {
return ButtonPrefab(items_: items[index], callbackFunction: update); //give the "update (setState)" Function to the "next" Widget for calling it later
}),
),
],
))
],
);
}
}
class ButtonPrefab extends StatelessWidget {
final Map<String, dynamic> items_;
final Function callbackFunction; //get the callback Function of the calling Widget
const ButtonPrefab({required this.items_, required this.callbackFunction}); //get the callback Function of the calling Widget
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: CupertinoButton(
child: Text(items_['name']),
color: Colors.black54,
padding: EdgeInsets.all(3),
onPressed: () {
print(orders);
// addOrder("name", 2.4); // you are always giving "name" and 2.4, but probably need to give the item that's being pushed
addOrder(items_['name'], items_['price']); //like this
KundenId.value++;
print(KundenId.value);
callbackFunction(); //this is the "update" Function I created in the calling Widget, but in this Widget it has a name "callbackFunction"
},
),
);
}
}
class _OrderTableState extends State<OrderTable> {
#override
void initState() {
super.initState();
// setState(() {}); // not necessary
}
// void update() { // not necessary
// setState(() {});
// }
}
There is so many problems here, that I cannot list them one by one.
The basic underlying problem here is that you think having a global variable is a good method to keep your state. It is not. Never has been. In no programming language in the last quarter of a century.
To hold your state (in your case I guess it's orders) use one of the state management patterns.
I suggest taking a look at Provider first. Not because it's the best, but because it is the easiest and explains your problem clearly:
Simple app state management
Once your applications get larger, my personal preference is BLoC, but that is a little to complex for this problem.
I am looking to create a grid with 4 custom widgets that can either add or subtract from a given starting number. See image for reference.
For example, if you press player one, the number would increase or decrease to 100 or 99. But the other 3 players would remain the same.
I had originally used one stateful widget with a separate function for each player, but I am sure there's a way to do it in a more modular way.
class CommanderDamage extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return CommanderDamageState();
}
}
class CommanderDamageState extends State<CommanderDamage> {
int damage = 0;
void update() {
setState(() {
damage++;
});
}
#override
Widget build(context) {
return MaterialApp(
home: Scaffold(
body: GridView.builder(
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
itemCount: 4,
itemBuilder: (BuildContext context, index) {
return Container(
child: Column(
children: <Widget>[
Text("Player " + index.toString()),
InkWell(
onTap: update,
child: Container(
width: 100.0,
height: 100.0,
child: Text(damage),
)
],
),
);
},
),
),
);
}
}
EDIT: I have edited my code to reflect my current. Currently, when the damage area is pressed, the damage increases for all 4 players instead of the one I am pressing.
Wrap your text widget inside InkWell(). Basically what InkWell does is creates a rectangular touch responsive area.
InkWell(
child: Text(
'Player One',
style: TextStyle(
fontSize: 20, color: Colors.white),
onTap: () {
// Your function
}
)
But this make the interactive tap area according to size of the text which is very small, so it's better to wrap it inside a container and provide height-width or some space with padding
InkWell(
child: Container(
width: 100,
height: 100,
child: Text(
'Player One',
style: TextStyle(
fontSize: 20, color: Colors.white), ),
onTap: () {
// Your function
}
)
An inside onTap you can your function and perform changes.
Read more about InkWell:
https://docs.flutter.io/flutter/material/InkWell-class.html
After lots of trial and error I managed to find an answer.
I had to set the state within the onTap instead of making a separate function and calling it in the onTap.
class CommanderDamage extends StatefulWidget {
int damage = 0;
CommanderDamage({this.damage, Key key});
#override
State<StatefulWidget> createState() {
return CommanderDamageState();
}
}
class CommanderDamageState extends State<CommanderDamage> {
var damage = [0, 0, 0, 0, 0, 0];
#override
Widget build(context) {
return MaterialApp(
home: Scaffold(
body: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
height: MediaQuery.of(context).size.height,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft, end: Alignment.bottomRight,
colors: [Color(0xfff6921e), Color(0xffee4036)],
),
),
child: GridView.builder(
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
itemCount: damage.length,
itemBuilder: (BuildContext context, index) {
return Container(
child: Column(
children: <Widget>[
InkWell(
onTap: () {
setState(() {
damage[index]++;
});
},
onLongPress: () {
setState(() {
damage[index] = 0;
});
},
child: Container(
width: 100.0,
height: 100.0,
child: Text(damage[index].toString()),
),
),
],
),
);
},
),
),
],
),
),
);
}
}
I am currently using the carousel-slider library to get a carousel in Flutter.
This library is based on a PageView, and in a PageView the elements are centered.
That's the carousel I get:
And this is what I'd like to have:
Here is the code where is use the CarouselSlider:
CarouselSlider(
height: 150,
viewportFraction: 0.5,
initialPage: 0,
enableInfiniteScroll: false,
items: widget.user.lastGamesPlayed.map((game) {
return Builder(
builder: (BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 10),
child: GestureDetector(
onTap: () {
game.presentGame(context, widget.user);
},
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(25)),
child: Container(
color: Theme.MyColors.lightBlue,
child: Center(
child: Padding(
padding: EdgeInsets.all(20),
child: AutoSizeText(game.name,
style: TextStyle(fontSize: 70),
maxLines: 1)),
),
))));
},
);
}).toList(),
)
And here is the code inside the CarouselSlider library:
#override
Widget build(BuildContext context) {
return getWrapper(PageView.builder(
physics: widget.isScrollEnabled
? AlwaysScrollableScrollPhysics()
: NeverScrollableScrollPhysics(),
scrollDirection: widget.scrollDirection,
controller: widget.pageController,
reverse: widget.reverse,
itemCount: widget.enableInfiniteScroll ? null : widget.items.length,
onPageChanged: (int index) {
int currentPage =
_getRealIndex(index, widget.realPage, widget.items.length);
if (widget.onPageChanged != null) {
widget.onPageChanged(currentPage);
}
},
itemBuilder: (BuildContext context, int i) {
final int index = _getRealIndex(
i + widget.initialPage, widget.realPage, widget.items.length);
return AnimatedBuilder(
animation: widget.pageController,
child: widget.items[index],
builder: (BuildContext context, child) {
// on the first render, the pageController.page is null,
// this is a dirty hack
if (widget.pageController.position.minScrollExtent == null ||
widget.pageController.position.maxScrollExtent == null) {
Future.delayed(Duration(microseconds: 1), () {
setState(() {});
});
return Container();
}
double value = widget.pageController.page - i;
value = (1 - (value.abs() * 0.3)).clamp(0.0, 1.0);
final double height = widget.height ??
MediaQuery.of(context).size.width * (1 / widget.aspectRatio);
final double distortionValue = widget.enlargeCenterPage
? Curves.easeOut.transform(value)
: 1.0;
if (widget.scrollDirection == Axis.horizontal) {
return Center(
child:
SizedBox(height: distortionValue * height, child: child));
} else {
return Center(
child: SizedBox(
width:
distortionValue * MediaQuery.of(context).size.width,
child: child));
}
},
);
},
));
}
How can I prevent elements from being centered?
Thank you in advance
If you don't want to animate page size over scroll there is no need to use this carousel-slider library.
Also, PageView is not the best Widget to achieve the layout you want, you should use a horizontal ListView with PageScrollPhysics.
import 'package:flutter/material.dart';
class Carousel extends StatelessWidget {
Carousel({
Key key,
#required this.items,
#required this.builderFunction,
#required this.height,
this.dividerIndent = 10,
}) : super(key: key);
final List<dynamic> items;
final double dividerIndent;
final Function(BuildContext context, dynamic item) builderFunction;
final double height;
#override
Widget build(BuildContext context) {
return Container(
height: height,
child: ListView.separated(
physics: PageScrollPhysics(),
separatorBuilder: (context, index) => Divider(
indent: dividerIndent,
),
scrollDirection: Axis.horizontal,
itemCount: items.length,
itemBuilder: (context, index) {
Widget item = builderFunction(context, items[index]);
if (index == 0) {
return Padding(
child: item,
padding: EdgeInsets.only(left: dividerIndent),
);
} else if (index == items.length - 1) {
return Padding(
child: item,
padding: EdgeInsets.only(right: dividerIndent),
);
}
return item;
}),
);
}
}
Usage
Carousel(
height: 150,
items: widget.user.lastGamesPlayed,
builderFunction: (context, item) {
return ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(25)),
child: Container(
width: 200,
color: Theme.MyColors.lightBlue,
child: Center(
child: Padding(
padding: EdgeInsets.all(20),
child: AutoSizeText(
item.name,
style: TextStyle(fontSize: 70),
maxLines: 1,
),
),
),
),
);
},
)
UPDATE
As observed by #AdamK, my solution doesn't have the same scroll physics behavior as a PageView, it acts more like a horizontal ListView.
If you are looking for this pagination behavior you should consider to write a custom ScrollPhysics and use it on your scrollable widget.
This is a very well explained article that helps us to achieve the desired effect.
I'm building a Flutter app, mainly for iOS.
One of my views has a text field, and iOS keyboard appears when you tap on it. The problem is - layout does not change smoothly like it does in native iOS apps. Instead, it instantly jumps to the final available screen height even before keyboard opening animation finishes.
I tried wrapping my SafeArea element in AnimatedSize and AnimatedContainer - it didn't help.
My layout code:
SafeArea(child:
Column(children:[
TextField(...)
])
)
How can I make the layout resize smoothly when the keyboard appears?
Expected:
Actual
I use something like that:
AnimatedPadding(
padding: MediaQuery.of(context).viewInsets,
duration: const Duration(milliseconds: 100),
curve: Curves.decelerate,
child: ....
)
This animates the padding based on the viewInsets(software keyboard height).
The desired output can be achieved using AnimatedPadding Widget, though this is not perfect, but better than nothing :d
Open issue as of 15/03/21, for reference
import 'dart:math';
import 'package:flutter/material.dart';
final Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: MyWidget(),
),
),
);
}
}
class MyWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return SafeArea(
bottom: false,
child: Scaffold(
// !!! Important part > to disable default scaffold insets
resizeToAvoidBottomInset: false,
appBar: AppBar(
title: Text("Appbar Title"),
),
body: Stack(
children: [
Scrollbar(
child: ListView.builder(
padding: EdgeInsets.all(0.0),
itemCount: 30,
itemBuilder: (context, i) {
return Container(
height: 100,
width: double.infinity,
color: Colors
.primaries[Random().nextInt(Colors.primaries.length)],
);
},
),
),
Align(
alignment: Alignment.bottomLeft,
child: AnimatedPadding(
padding: MediaQuery.of(context).viewInsets,
// You can change the duration and curve as per your requirement:
duration: const Duration(milliseconds: 200),
curve: Curves.decelerate,
child: InputField()),
)
],
)),
);
}
}
class InputField extends StatefulWidget {
InputField({Key key}) : super(key: key);
#override
_InputFieldState createState() => _InputFieldState();
}
class _InputFieldState extends State<InputField> {
#override
Widget build(BuildContext context) {
return Container(
color: Colors.grey[100],
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
children: [
SizedBox(
width: 60,
child: Icon(Icons.add_a_photo),
),
Flexible(
child: TextField(
style: Theme.of(context).textTheme.bodyText1,
decoration: InputDecoration(
border: InputBorder.none,
hintText: 'Enter text...',
),
),
),
SizedBox(
width: 60,
child: Icon(Icons.send),
),
],
),
);
}
}
Output ->
You can use this package keyboard_visibility and listen to keyboard visibility. Then you can give your logic to implement your feature like you can shorten the home container height. That's not perfect . But I think it's the only way right now.
You need to use keyboard_visibility package and use it to trigger your AnimatedContainer or AnimatedPadding
bool _isKeyboardActive = false;
#override
void initState() {
super.initState();
//add keyboard visibility Listener
KeyboardVisibility.onChange.listen((event) {
setState(() {
_isKeyboardActive = event;
});
});
}
Widget build(BuildContext context){
return AnimatedContainer(
width: _isKeyboardActive ? 200 : MediaQuery.of(context).size.width,
height: 60,
color: Colors.red,
duration: Duration(milliseconds: 600)
)
}
use this as a basis.
You should try setting resizeToAvoidBottomPadding: false like so:
return Scaffold(
key: _scaffoldKey,
resizeToAvoidBottomPadding: false,
I want to create a list of cards scrolling horizontally with snap to fit effect when swiped either from left or right.
Each card has some spacing between them and fit to screen similar to below image
Apart from that these horizontally scrollable list elements should be contained inside a vertically scrollable list.
I all I am able to achieve is only displaying a list of horizontal scrolling cards after following example in flutter docs.
class SnapCarousel extends StatelessWidget {
#override
Widget build(BuildContext context) {
final title = 'Horizontal List';
return MaterialApp(
title: title,
home: Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Container(
margin: EdgeInsets.symmetric(vertical: 20.0),
height: 200.0,
child: ListView(
scrollDirection: Axis.horizontal,
children: <Widget>[
Container(
width: 160.0,
color: Colors.red,
),
Container(
width: 160.0,
color: Colors.blue,
),
Container(
width: 160.0,
color: Colors.green,
),
Container(
width: 160.0,
color: Colors.yellow,
),
Container(
width: 160.0,
color: Colors.orange,
),
],
),
),
),
);
}
}
Use PageView and ListView:
import 'package:flutter/material.dart';
main() => runApp(MaterialApp(home: MyHomePage()));
class MyHomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Carousel in vertical scrollable'),
),
body: ListView.builder(
padding: EdgeInsets.symmetric(vertical: 16.0),
itemBuilder: (BuildContext context, int index) {
if(index % 2 == 0) {
return _buildCarousel(context, index ~/ 2);
}
else {
return Divider();
}
},
),
);
}
Widget _buildCarousel(BuildContext context, int carouselIndex) {
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text('Carousel $carouselIndex'),
SizedBox(
// you may want to use an aspect ratio here for tablet support
height: 200.0,
child: PageView.builder(
// store this controller in a State to save the carousel scroll position
controller: PageController(viewportFraction: 0.8),
itemBuilder: (BuildContext context, int itemIndex) {
return _buildCarouselItem(context, carouselIndex, itemIndex);
},
),
)
],
);
}
Widget _buildCarouselItem(BuildContext context, int carouselIndex, int itemIndex) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 4.0),
child: Container(
decoration: BoxDecoration(
color: Colors.grey,
borderRadius: BorderRadius.all(Radius.circular(4.0)),
),
),
);
}
}
Screenshot:
If you don't want to use any 3rd party packages, you can simply try this:
class _HomePageState extends State<HomePage> {
int _index = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: SizedBox(
height: 200, // card height
child: PageView.builder(
itemCount: 10,
controller: PageController(viewportFraction: 0.7),
onPageChanged: (int index) => setState(() => _index = index),
itemBuilder: (_, i) {
return Transform.scale(
scale: i == _index ? 1 : 0.9,
child: Card(
elevation: 6,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
child: Center(
child: Text(
"Card ${i + 1}",
style: TextStyle(fontSize: 32),
),
),
),
);
},
),
),
),
);
}
}
this is an old question, and I arrived here looking for something else ;-), but what WitVault was lookig is done easy with this package: https://pub.dev/packages/flutter_swiper
The implementation:
Put the dependencies in pubsec.yaml:
dependencies:
flutter_swiper: ^1.1.6
Import it in the page where you need it:
import 'package:flutter_swiper/flutter_swiper.dart';
In the layout:
new Swiper(
itemBuilder: (BuildContext context, int index) {
return new Image.network(
"http://via.placeholder.com/288x188",
fit: BoxFit.fill,
);
},
itemCount: 10,
viewportFraction: 0.8,
scale: 0.9,
)
To achieve the snap effect via ListView, just set the physics to PageScrollPhysics
const List<Widget> children = [
ContainerCard(),
ContainerCard(),
ContainerCard(),
];
ListView.builder(
scrollDirection: Axis.horizontal,
physics: const PageScrollPhysics(), // this for snapping
itemCount: children.length,
itemBuilder: (_, index) => children[index],
)
Advanced Snap List
If you are looking for advanced usages, such as dynamic item sizes, configurable snap points, visualization of items, and essential control (such as scrollToIndex, animate) you should use the native-based SnappyListView with way more features.
SnappyListView(
itemCount: Colors.accents.length,
itemBuilder: (context, index) {
return Container(
height: 100,
color: Colors.accents.elementAt(index),
child: Text("Index: $index"),
),
);
I believe the answer solution from CopsOnRoad is better and simple for someone who don't want to use a 3rd party library. However, since there is no animation, I add the scale animation when the card is viewed (expand) and the previous card is swiped (shrink) using index. So what happened is whenever the first time the page load, 1st and 2nd card won't have any animation, and when the card is swiped, only the previous and current card have the scale animation. So this is my implementation:
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int currentIndex = -1, previousIndex = 0;
double getAnimationValue(int currentIndex, int widgetIndex, int previousIndex,
{bool begin = true}) {
if (widgetIndex == currentIndex) {
return begin ? 0.9 : 1;
} else {
return begin ? 1 : 0.9;
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
SizedBox(
height: 200, // card height
child: PageView.builder(
itemCount: 10,
controller: PageController(viewportFraction: 0.7),
onPageChanged: (int index) {
setState(() {
if (currentIndex != -1) {
previousIndex = currentIndex;
}
currentIndex = index;
});
},
itemBuilder: (_, widgetIndex) {
return (currentIndex != -1 &&
(previousIndex == widgetIndex ||
widgetIndex == currentIndex))
? TweenAnimationBuilder(
duration: const Duration(milliseconds: 400),
tween: Tween<double>(
begin: getAnimationValue(
currentIndex,
widgetIndex,
previousIndex,
),
end: getAnimationValue(
currentIndex,
widgetIndex,
previousIndex,
begin: false,
),
),
builder: (context, value, child) {
return Transform.scale(
scale: value,
child: Card(
elevation: 6,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20)),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Card${widgetIndex + 1}",
style: const TextStyle(fontSize: 30),
),
Text(
"$widgetIndex >> Widget Index << $widgetIndex",
style: const TextStyle(fontSize: 22),
),
Text(
"$currentIndex >> Current Index << $currentIndex",
style: const TextStyle(fontSize: 22),
),
Text(
"$previousIndex >> Previous Index << $previousIndex",
style: const TextStyle(fontSize: 22),
),
],
),
),
);
},
)
: Transform.scale(
// this is used when you want to disable animation when initialized the page
scale:
(widgetIndex == 0 && currentIndex == -1) ? 1 : 0.9,
child: Card(
elevation: 6,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20)),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Card${widgetIndex + 1}",
style: const TextStyle(fontSize: 30),
),
Text(
"$widgetIndex >> Widget Index << $widgetIndex",
style: const TextStyle(fontSize: 22),
),
Text(
"$currentIndex >> Init Index << $currentIndex",
style: const TextStyle(fontSize: 22),
),
Text(
"$previousIndex >> Previous Index << $previousIndex",
style: const TextStyle(fontSize: 22),
),
],
),
),
);
},
),
),
],
),
);
}
}
I used TweenAnimationBuilder for this animation and hardcoded the widget. You can use method for your widget or use package flutter_animate for easy animation whenever necessary.