Flutter expandable tiles inside grid view - flutter

I'm trying to achieve this functionality on flutter but honestly have no idea how to do it. I've been trying to figure it out for weeks, I tried flutter_staggered_grid_view, which was kind of the closest to this, but that didn't help either. Does anyone have any idea on how to achieve this effect?

You can use the Wrap widget as grid, and use some custom widget with AnimatedContainer to expand and retract the bloc.
//number of childs used in the example
static const itemCount = 8;
//list of each bloc expandable state, that is changed to trigger the animation of the AnimatedContainer
List<bool> expandableState = List.generate(itemCount, (index) => false);
Widget bloc (double width, int index) {
bool isExpanded = expandableState[index];
return GestureDetector(
onTap: () {
setState(() {
//changing the current expandableState
expandableState[index] = !isExpanded;
});
},
child: AnimatedContainer(
duration: Duration(milliseconds: 200),
margin: const EdgeInsets.all(20.0),
width: !isExpanded ? width * 0.4 : width * 0.8,
height: !isExpanded ? width * 0.4 : width * 0.8,
color: Colors.red,
),
);
}
#override
Widget build(BuildContext context) {
double width = MediaQuery.of(context).size.width;
return Scaffold(
body: Align(
child: SingleChildScrollView(
child: Wrap(
children: List.generate(itemCount, (index) {
return bloc(width, index);
}),
),
),
),
);
}

You could have a list of the items that should be expanded and lay them out in the grid view accordingly (using flutter_staggered_grid_view for example).
I edited the example written in the library docs to achieve the following result:
Basically,
create a StatefulWidget and add a list to it (_expandedIndices). The purpose of the list is to keep track of the indices of the items that are expanded.
add a GestureDetector on the grid cells to detect taps and add/remove indices from the list (add the index to the list if it is not already there, otherwise remove it).
Don't forget to put the code that updates the list inside a setState.
in the staggeredTileBuilder configure crossAxisCellCount and mainAxisCellCount based on whether the item in index should be expanded or not.
class StaggeredGridViewWithExpandableCells extends StatefulWidget {
#override
_StaggeredGridViewWithExpandableCellsState createState() =>
_StaggeredGridViewWithExpandableCellsState();
}
class _StaggeredGridViewWithExpandableCellsState
extends State<StaggeredGridViewWithExpandableCells> {
final _expandedIndices = Set<int>();
#override
Widget build(BuildContext context) {
return StaggeredGridView.countBuilder(
crossAxisCount: 4,
itemCount: 16,
itemBuilder: (BuildContext context, int index) => GestureDetector(
onTap: () => setState(() => _expandedIndices.contains(index) ? _expandedIndices.remove(index) : _expandedIndices.add(index)),
child: new Container(
color: Colors.green,
child: new Center(
child: new CircleAvatar(
backgroundColor: Colors.white,
child: new Text('$index'),
),
)),
),
staggeredTileBuilder: (int index) =>
new StaggeredTile.count(_expandedIndices.contains(index) ? 4 : 2, 1),
mainAxisSpacing: 4.0,
crossAxisSpacing: 4.0,
);
}
}

You can use Wrap widget to achieve the result. Please see the code below.
import 'package:flutter/material.dart';
final Color darkBlue = const 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(
appBar: AppBar(title: const Text("Demo")),
body: Center(
child: MyWidget(),
),
),
);
}
}
class MyWidget extends StatefulWidget {
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
final int _cells = 8;
final double _containerSizeSmall = 75;
final double _containerSizeLarge = 170;
final double _padding = 10;
int _clicked = 0;
#override
Widget build(BuildContext context) {
final Size size = MediaQuery.of(context).size;
return SingleChildScrollView(
child: Container(
height: size.height,
width: 240,
child: Wrap(
children: List.generate(
_cells,
(col) => Padding(
padding: EdgeInsets.all(_padding),
child: GestureDetector(
onTap: () {
setState(() {
_clicked != col + 1 ? _clicked = col + 1 : _clicked = 0;
});
},
child: Container(
height: _clicked == col + 1
? _containerSizeLarge
: _containerSizeSmall,
width: _clicked == col + 1
? _containerSizeLarge
: _containerSizeSmall,
decoration: const BoxDecoration(
color: Colors.blue,
borderRadius: const BorderRadius.all(
const Radius.circular(5),
),
),
child: Center(child: Text('${col + 1}')),
),
),
),
),
),
),
);
}
}

Related

Move an item from one list to another with animation in Flutter

I have two vertical lists, one on the left side and the other one on the right, let's call them "Selected List" and "Unselected List".
I want the items in Unselected List to Animate from left side to the right side of the screen and add to Selected List.
the other items should fill the empty space in Unselected List and items in Selected List should free up the space for new item.
Here's the Ui
My Code:
class AddToFave extends StatefulWidget {
const AddToFave({Key? key}) : super(key: key);
#override
_AddToFaveState createState() => _AddToFaveState();
}
class _AddToFaveState extends State<AddToFave> {
List<String> unselected = [ '1','2','3','4','5','6','7','8','9','10'];
List<String> selected = [];
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
width: MediaQuery.of(context).size.width / 5,
height: MediaQuery.of(context).size.height,
child: ListView.builder(
itemCount: selected.length,
itemBuilder: (context, index) {
return InkWell(
onTap: () {
unselected.add(selected[index]);
selected.removeAt(index);
setState(() {});
},
child: Container(
width: MediaQuery.of(context).size.width / 5,
height: MediaQuery.of(context).size.width / 5,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(
MediaQuery.of(context).size.width / 5)),
child: Center(
child: Text(
selected[index],
style: TextStyle(color: Colors.white),
)),
),
);
}),
),
Container(
width: MediaQuery.of(context).size.width / 5,
height: MediaQuery.of(context).size.height,
child: ListView.builder(
itemCount: unselected.length,
itemBuilder: (context, index) {
return InkWell(
onTap: () {
selected.add(unselected[index]);
unselected.removeAt(index);
setState(() {});
},
child: Container(
width: MediaQuery.of(context).size.width / 5,
height: MediaQuery.of(context).size.width / 5,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(
MediaQuery.of(context).size.width / 5)),
child: Center(
child: Text(
unselected[index],
style: TextStyle(color: Colors.white),
)),
),
);
}),
),
],
),
),
);
}
}
Thank you in advance.
This task can be broken into 2 parts.
First, use an AnimatedList instead of a regular ListView, so that when an item is removed, you can control its "exit animation" and shrink its size, thus making other items slowly move upwards to fill in its spot.
Secondly, while the item is being removed from the first list, make an OverlayEntry and animate its position, to create an illusion of the item flying. Once the flying is finished, we can remove the overlay and insert the item in the actual destination list.
Full source code for you to use, as a starting point:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: TwoAnimatedListDemo(),
);
}
}
class TwoAnimatedListDemo extends StatefulWidget {
const TwoAnimatedListDemo({Key? key}) : super(key: key);
#override
_TwoAnimatedListDemoState createState() => _TwoAnimatedListDemoState();
}
class _TwoAnimatedListDemoState extends State<TwoAnimatedListDemo> {
final List<String> _unselected = ['A', 'B', 'C', 'D', 'E', 'F', 'G'];
final List<String> _selected = [];
final _unselectedListKey = GlobalKey<AnimatedListState>();
final _selectedListKey = GlobalKey<AnimatedListState>();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Two Animated List Demo'),
),
body: Row(
children: [
SizedBox(
width: 56,
child: AnimatedList(
key: _unselectedListKey,
initialItemCount: _unselected.length,
itemBuilder: (context, index, animation) {
return InkWell(
onTap: () => _moveItem(
fromIndex: index,
fromList: _unselected,
fromKey: _unselectedListKey,
toList: _selected,
toKey: _selectedListKey,
),
child: Item(text: _unselected[index]),
);
},
),
),
Spacer(),
SizedBox(
width: 56,
child: AnimatedList(
key: _selectedListKey,
initialItemCount: _selected.length,
itemBuilder: (context, index, animation) {
return InkWell(
onTap: () => _moveItem(
fromIndex: index,
fromList: _selected,
fromKey: _selectedListKey,
toList: _unselected,
toKey: _unselectedListKey,
),
child: Item(text: _selected[index]),
);
},
),
),
],
),
);
}
int _flyingCount = 0;
_moveItem({
required int fromIndex,
required List fromList,
required GlobalKey<AnimatedListState> fromKey,
required List toList,
required GlobalKey<AnimatedListState> toKey,
Duration duration = const Duration(milliseconds: 300),
}) {
final globalKey = GlobalKey();
final item = fromList.removeAt(fromIndex);
fromKey.currentState!.removeItem(
fromIndex,
(context, animation) {
return SizeTransition(
sizeFactor: animation,
child: Opacity(
key: globalKey,
opacity: 0.0,
child: Item(text: item),
),
);
},
duration: duration,
);
_flyingCount++;
WidgetsBinding.instance!.addPostFrameCallback((timeStamp) async {
// Find the starting position of the moving item, which is exactly the
// gap its leaving behind, in the original list.
final box1 = globalKey.currentContext!.findRenderObject() as RenderBox;
final pos1 = box1.localToGlobal(Offset.zero);
// Find the destination position of the moving item, which is at the
// end of the destination list.
final box2 = toKey.currentContext!.findRenderObject() as RenderBox;
final box2height = box1.size.height * (toList.length + _flyingCount - 1);
final pos2 = box2.localToGlobal(Offset(0, box2height));
// Insert an overlay to "fly over" the item between two lists.
final entry = OverlayEntry(builder: (BuildContext context) {
return TweenAnimationBuilder(
tween: Tween<Offset>(begin: pos1, end: pos2),
duration: duration,
builder: (_, Offset value, child) {
return Positioned(
left: value.dx,
top: value.dy,
child: Item(text: item),
);
},
);
});
Overlay.of(context)!.insert(entry);
await Future.delayed(duration);
entry.remove();
toList.add(item);
toKey.currentState!.insertItem(toList.length - 1);
_flyingCount--;
});
}
}
class Item extends StatelessWidget {
final String text;
const Item({Key? key, required this.text}) : super(key: key);
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(4.0),
child: CircleAvatar(
child: Text(text),
radius: 24,
),
);
}
}

How can i make pageView.Builder occupy all available space in flutter?

I am trying to swap two widgets on button click OR is there any other way to do this?
When i set
SizedBox(
height: double.infinity,
width: double.infinity, )
it gives error: BoxConstraints forces an infinite height. it works when manually set height and width.
I tried using
Expanded and flexible it gives error like this
RenderBox was not laid out: RenderRepaintBoundary#8b1a5 relayoutBoundary=up14 NEEDS-PAINT
'package:flutter/src/rendering/box.dart':
Failed assertion: line 1785 pos 12: 'hasSize'
Code
class _MyHomePageState extends State<MyHomePage> {
TextEditingController _uri = TextEditingController();
bool flag=false;
bool swap = false;
int _index = 0;
#override
void initState() {
swap = widget.swap;
super.initState();
}
#override
Widget build(BuildContext context) {
Widget swapWidget;
if (!swap) {
swapWidget = Center(
child: Expanded(
// card height
child: PageView.builder(
itemCount: 2,
controller: PageController(viewportFraction: 0.95),
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),
),
),
),
);
},
)
)
);
} else {
swapWidget =new Card(
child:Text('Nothing To show')
);
}
var swapTile = new ListTile(
title: swapWidget,
);
return new Scaffold(
appBar: AppBar( title: Text('My APP')),
body: new ListView(
children: <Widget>[
swapTile,
],
),
);
}
}
What i am doing Wrong?
Thankyou
I am not entirely sure what you are trying to do here, but Few things. PageView widget expands to the maximum allowed space by default, so you don't need the Expanded or Center widget around it.
Second, I don't know why you would want to wrap Card widget with ListTile. You certainly can, but that's not how ListTile and Card are designed.
Third, because PageView widget can expand, it needs some size constraints, so it cannot be put inside a infinitely large widget like ListView, so you can remove that, and here is what I have. I hope it is close to what you were looking for.
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
bool swap = true;
int _index = 0;
#override
Widget build(BuildContext context) {
Widget swapWidget;
if (swap) {
swapWidget = PageView.builder(
itemCount: 2,
controller: PageController(viewportFraction: 0.95),
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),
),
),
),
);
},
);
} else {
swapWidget = Card(child: Text('Nothing To show'));
}
return Scaffold(
body: swapWidget,
);
}
}

Flutter how to remove padding on top of scrollbar listview

First of all, sorry for my bad english. So my question is why there are some padding on top of the scrollbar and how to remove it? I think there's a problem with the overlay widget in my code but I can't find the problem and how to solve it. Please help and thank you. PS: I don't want to use the default dropdown widget because I don't really like the design so I made a custom dropdown button with some help from other code.
So here's my code :
import 'package:flutter/material.dart';
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool dropdownOpen = false;
OverlayEntry dropdown;
dynamic actionKey = LabeledGlobalKey("actionKey");
ScrollController sc = ScrollController();
OverlayEntry createDropdown() {
RenderBox renderBox = actionKey.currentContext.findRenderObject();
Offset offset = renderBox.localToGlobal(Offset.zero);
return OverlayEntry(
builder: (context) {
return Positioned(
left: offset.dx,
width: renderBox.size.width,
top: offset.dy + renderBox.size.height,
height: 230,
child: Material(
elevation: 20,
child: Scrollbar(
isAlwaysShown: true,
controller: sc,
child: ListView.builder(
controller: sc,
padding: EdgeInsets.all(0),
itemCount: 5,
itemBuilder: (BuildContext context, int index){
return Container(
height: 50,
alignment: Alignment.center,
child: Text((index+1).toString())
);
}
)
)
)
);
}
);
}
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(primaryColor: Colors.red),
home: Scaffold(
body: Center(
child: GestureDetector(
key: actionKey,
child: Container(
height: 50,
width: 100,
color: Colors.yellow,
alignment: Alignment.center,
child: Text("Click")
),
onTap: (){
setState((){
if(dropdownOpen){
dropdown.remove();
dropdownOpen = false;
} else {
dropdown = createDropdown();
Overlay.of(context).insert(dropdown);
dropdownOpen = true;
}
});
}
)
)
),
);
}
}
and here's what it looks like :
image

How to implement a horizontal scroll which is controlled by bottom minus bar in flutter

I have to implement a horizontal scroll list in flutter.I could do that and have included the code below(The code is still to be modified but the base of the code is good enough to put in the pictures and other such details)
But the problem is the minus bar below the horizontal scroll.I don't know what feature in flutter allows to do that.I search many things but other than radio boxes,check boxes, switches,etc I am not able to find any details of it.Please have a look at the screenshot of the app ,I have indicated the minus bar control in red.Home screen,the minus bar indicated in red
The code I have written:
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black ,
body: Container(
margin: EdgeInsets.symmetric(vertical: 20.0),
height: 500,
child: ListView(
// This next line does the trick.
scrollDirection: Axis.horizontal,
shrinkWrap: true,
children: <Widget>[
Container(
width:400 ,
color: Colors.red,
),
Container(
width: 400.0,
color: Colors.blue,
),
Container(
width: 400.0,
color: Colors.green,
),
],
),
)
);
}
}
What you want to look for is not ListView but PageView here is a small code sample to try in DartPad and see how you could make your layout.
Basically I am using a PageController to change the current page by taping on certain widgets.
Code
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: MyWidget()),
);
}
}
class MyWidget extends StatefulWidget {
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
final _items = [Colors.red, Colors.blue, Colors.yellow];
final _pageController = PageController();
int _currentPageNotifier = 0;
final double _indicatorWidth = 30;
Widget _buildPageView() {
return PageView.builder(
controller: _pageController,
itemCount: _items.length,
itemBuilder: (context, index) => Center(
child: FlutterLogo(
colors: _items[index],
size: 50,
),
),
onPageChanged: (int index) =>
setState(() => _currentPageNotifier = index),
);
}
Widget _buildIndicator() {
List<Widget> itemWidgets = [];
for (int index = 0; index < _items.length; index++) {
itemWidgets.add(GestureDetector(
onTap: () => _pageController.animateToPage(
index,
duration: Duration(milliseconds: 300),
curve: Curves.ease,
),
child: Container(
decoration: BoxDecoration(
color: _currentPageNotifier == index
? Colors.green
: Colors.grey,
borderRadius: BorderRadius.circular(9),
),
margin: EdgeInsets.only(right: 10),
width: _indicatorWidth,
height: 8,
),
));
}
return Positioned(
bottom: MediaQuery.of(context).size.height / 2 - 50,
left: MediaQuery.of(context).size.width / 2 -
_items.length * _indicatorWidth +
_items.length * 10,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: itemWidgets,
),
);
}
#override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
_buildPageView(),
_buildIndicator(),
],
);
}
}

Flutter: Scrolling to a widget in ListView

How can I scroll to a special widget in a ListView?
For instance I want to scroll automatically to some Container in the ListView if I press a specific button.
ListView(children: <Widget>[
Container(...),
Container(...), #scroll for example to this container
Container(...)
]);
By far, the easiest solution is to use Scrollable.ensureVisible(context). As it does everything for you and work with any widget size. Fetching the context using GlobalKey.
The problem is that ListView won't render non-visible items. Meaning that your target most likely will not be built at all. Which means your target will have no context ; preventing you from using that method without some more work.
In the end, the easiest solution will be to replace your ListView by a SingleChildScrollView and wrap your children into a Column. Example :
class ScrollView extends StatelessWidget {
final dataKey = new GlobalKey();
#override
Widget build(BuildContext context) {
return new Scaffold(
primary: true,
appBar: new AppBar(
title: const Text('Home'),
),
body: new SingleChildScrollView(
child: new Column(
children: <Widget>[
new SizedBox(height: 160.0, width: double.infinity, child: new Card()),
new SizedBox(height: 160.0, width: double.infinity, child: new Card()),
new SizedBox(height: 160.0, width: double.infinity, child: new Card()),
// destination
new Card(
key: dataKey,
child: new Text("data\n\n\n\n\n\ndata"),
)
],
),
),
bottomNavigationBar: new RaisedButton(
onPressed: () => Scrollable.ensureVisible(dataKey.currentContext),
child: new Text("Scroll to data"),
),
);
}
}
NOTE : While this allows to scroll to the desired item easily, consider this method only for small predefined lists. As for bigger lists you'll get performance problems.
But it's possible to make Scrollable.ensureVisible work with ListView ; although it will require more work.
Unfortunately, ListView has no built-in approach to a scrollToIndex() function. You’ll have to develop your own way to measure to that element’s offset for animateTo() or jumpTo(), or you can search through these suggested solutions/plugins or from other posts like flutter ListView scroll to index not available
(the general scrollToIndex issue is discussed at flutter/issues/12319 since 2017, but still with no current plans)
But there is a different kind of ListView that does support scrollToIndex:
ScrollablePositionedList
dependency: scrollable_positioned_list
You set it up exactly like ListView and works the same, except you now have access to a ItemScrollController that does:
jumpTo({index, alignment})
scrollTo({index, alignment, duration, curve})
Simplified example:
ItemScrollController _scrollController = ItemScrollController();
ScrollablePositionedList.builder(
itemScrollController: _scrollController,
itemCount: _myList.length,
itemBuilder: (context, index) {
return _myList[index];
},
)
_scrollController.scrollTo(index: 150, duration: Duration(seconds: 1));
Please not that although the scrollable_positioned_list package is published by google.dev, they explicitly state that their packages are not officially supported Google products. - Source
Screenshot (Fixed height content)
If your items have fixed height, then you can use the following approach.
class HomePage extends StatelessWidget {
final ScrollController _controller = ScrollController();
final double _height = 100.0;
void _animateToIndex(int index) {
_controller.animateTo(
index * _height,
duration: Duration(seconds: 2),
curve: Curves.fastOutSlowIn,
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
child: Icon(Icons.arrow_downward),
onPressed: () => _animateToIndex(10),
),
body: ListView.builder(
controller: _controller,
itemCount: 20,
itemBuilder: (_, i) {
return SizedBox(
height: _height,
child: Card(
color: i == 10 ? Colors.blue : null,
child: Center(child: Text('Item $i')),
),
);
},
),
);
}
}
For people are trying to jump to widget in CustomScrollView.
First, add this plugin to your project.
Then look at my example code below:
class Example extends StatefulWidget {
#override
_ExampleState createState() => _ExampleState();
}
class _ExampleState extends State<Example> {
AutoScrollController _autoScrollController;
final scrollDirection = Axis.vertical;
bool isExpaned = true;
bool get _isAppBarExpanded {
return _autoScrollController.hasClients &&
_autoScrollController.offset > (160 - kToolbarHeight);
}
#override
void initState() {
_autoScrollController = AutoScrollController(
viewportBoundaryGetter: () =>
Rect.fromLTRB(0, 0, 0, MediaQuery.of(context).padding.bottom),
axis: scrollDirection,
)..addListener(
() => _isAppBarExpanded
? isExpaned != false
? setState(
() {
isExpaned = false;
print('setState is called');
},
)
: {}
: isExpaned != true
? setState(() {
print('setState is called');
isExpaned = true;
})
: {},
);
super.initState();
}
Future _scrollToIndex(int index) async {
await _autoScrollController.scrollToIndex(index,
preferPosition: AutoScrollPosition.begin);
_autoScrollController.highlight(index);
}
Widget _wrapScrollTag({int index, Widget child}) {
return AutoScrollTag(
key: ValueKey(index),
controller: _autoScrollController,
index: index,
child: child,
highlightColor: Colors.black.withOpacity(0.1),
);
}
_buildSliverAppbar() {
return SliverAppBar(
brightness: Brightness.light,
pinned: true,
expandedHeight: 200.0,
backgroundColor: Colors.white,
flexibleSpace: FlexibleSpaceBar(
collapseMode: CollapseMode.parallax,
background: BackgroundSliverAppBar(),
),
bottom: PreferredSize(
preferredSize: Size.fromHeight(40),
child: AnimatedOpacity(
duration: Duration(milliseconds: 500),
opacity: isExpaned ? 0.0 : 1,
child: DefaultTabController(
length: 3,
child: TabBar(
onTap: (index) async {
_scrollToIndex(index);
},
tabs: List.generate(
3,
(i) {
return Tab(
text: 'Detail Business',
);
},
),
),
),
),
),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
controller: _autoScrollController,
slivers: <Widget>[
_buildSliverAppbar(),
SliverList(
delegate: SliverChildListDelegate([
_wrapScrollTag(
index: 0,
child: Container(
height: 300,
color: Colors.red,
)),
_wrapScrollTag(
index: 1,
child: Container(
height: 300,
color: Colors.red,
)),
_wrapScrollTag(
index: 2,
child: Container(
height: 300,
color: Colors.red,
)),
])),
],
),
);
}
}
Yeah it's just a example, use your brain to make it this idea become true
This solution improves upon other answers as it does not require hard-coding each elements' heights. Adding ScrollPosition.viewportDimension and ScrollPosition.maxScrollExtent yields the full content height. This can be used to estimate the position of an element at some index. If all elements are the same height, the estimation is perfect.
// Get the full content height.
final contentSize = controller.position.viewportDimension + controller.position.maxScrollExtent;
// Index to scroll to.
final index = 100;
// Estimate the target scroll position.
final target = contentSize * index / itemCount;
// Scroll to that position.
controller.position.animateTo(
target,
duration: const Duration(seconds: 2),
curve: Curves.easeInOut,
);
And a full example:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: "Flutter Test",
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
final controller = ScrollController();
final itemCount = 1000;
return Scaffold(
appBar: AppBar(
title: Text("Flutter Test"),
),
body: Column(
children: [
ElevatedButton(
child: Text("Scroll to 100th element"),
onPressed: () {
final contentSize = controller.position.viewportDimension + controller.position.maxScrollExtent;
final index = 100;
final target = contentSize * index / itemCount;
controller.position.animateTo(
target,
duration: const Duration(seconds: 2),
curve: Curves.easeInOut,
);
},
),
Expanded(
child: ListView.builder(
controller: controller,
itemBuilder: (context, index) {
return ListTile(
title: Text("Item at index $index."),
);
},
itemCount: itemCount,
),
)
],
),
);
}
}
You can use GlobalKey to access buildercontext.
I use GlobalObjectKey with Scrollable.
Define GlobalObjectKey in item of ListView
ListView.builder(
itemCount: category.length,
itemBuilder: (_, int index) {
return Container(
key: GlobalObjectKey(category[index].id),
You can navigate to item from anywhere
InkWell(
onTap: () {
Scrollable.ensureVisible(GlobalObjectKey(category?.id).currentContext);
You add scrollable animation changing property of ensureVisible
Scrollable.ensureVisible(
GlobalObjectKey(category?.id).currentContext,
duration: Duration(seconds: 1),// duration for scrolling time
alignment: .5, // 0 mean, scroll to the top, 0.5 mean, half
curve: Curves.easeInOutCubic);
You can just specify a ScrollController to your listview and call the animateTo method on button click.
A mininmal example to demonstrate animateTo usage :
class Example extends StatefulWidget {
#override
_ExampleState createState() => new _ExampleState();
}
class _ExampleState extends State<Example> {
ScrollController _controller = new ScrollController();
void _goToElement(int index){
_controller.animateTo((100.0 * index), // 100 is the height of container and index of 6th element is 5
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut);
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(),
body: new Column(
children: <Widget>[
new Expanded(
child: new ListView(
controller: _controller,
children: Colors.primaries.map((Color c) {
return new Container(
alignment: Alignment.center,
height: 100.0,
color: c,
child: new Text((Colors.primaries.indexOf(c)+1).toString()),
);
}).toList(),
),
),
new FlatButton(
// on press animate to 6 th element
onPressed: () => _goToElement(6),
child: new Text("Scroll to 6th element"),
),
],
),
);
}
}
Here is the solution for StatefulWidget if you want to made widget visible right after building the view tree.
By extending Remi's answer, you can achieve it with this code:
class ScrollView extends StatefulWidget {
// widget init
}
class _ScrollViewState extends State<ScrollView> {
final dataKey = new GlobalKey();
// + init state called
#override
Widget build(BuildContext context) {
return Scaffold(
primary: true,
appBar: AppBar(
title: const Text('Home'),
),
body: _renderBody(),
);
}
Widget _renderBody() {
var widget = SingleChildScrollView(
child: Column(
children: <Widget>[
SizedBox(height: 1160.0, width: double.infinity, child: new Card()),
SizedBox(height: 420.0, width: double.infinity, child: new Card()),
SizedBox(height: 760.0, width: double.infinity, child: new Card()),
// destination
Card(
key: dataKey,
child: Text("data\n\n\n\n\n\ndata"),
)
],
),
);
setState(() {
WidgetsBinding.instance!.addPostFrameCallback(
(_) => Scrollable.ensureVisible(dataKey.currentContext!));
});
return widget;
}
}
Output:
Use Dependency:
dependencies:
scroll_to_index: ^1.0.6
Code: (Scroll will always perform 6th index widget as its added below as hardcoded, try with scroll index which you required for scrolling to specific widget)
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final scrollDirection = Axis.vertical;
AutoScrollController controller;
List<List<int>> randomList;
#override
void initState() {
super.initState();
controller = AutoScrollController(
viewportBoundaryGetter: () =>
Rect.fromLTRB(0, 0, 0, MediaQuery.of(context).padding.bottom),
axis: scrollDirection);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: ListView(
scrollDirection: scrollDirection,
controller: controller,
children: <Widget>[
...List.generate(20, (index) {
return AutoScrollTag(
key: ValueKey(index),
controller: controller,
index: index,
child: Container(
height: 100,
color: Colors.red,
margin: EdgeInsets.all(10),
child: Center(child: Text('index: $index')),
),
highlightColor: Colors.black.withOpacity(0.1),
);
}),
],
),
floatingActionButton: FloatingActionButton(
onPressed: _scrollToIndex,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
// Scroll listview to the sixth item of list, scrollling is dependent on this number
Future _scrollToIndex() async {
await controller.scrollToIndex(6, preferPosition: AutoScrollPosition.begin);
}
}
I found a perfect solution to it using ListView.
I forgot where the solution comes from, so I posted my code. This credit belongs to other one.
21/09/22:edit. I posted a complete example here, hope it is clearer.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class CScrollToPositionPage extends StatefulWidget {
CScrollToPositionPage();
#override
State<StatefulWidget> createState() => CScrollToPositionPageState();
}
class CScrollToPositionPageState extends State<CScrollToPositionPage> {
static double TEXT_ITEM_HEIGHT = 80;
final _formKey = GlobalKey<FormState>();
late List _controls;
List<FocusNode> _lstFocusNodes = [];
final __item_count = 30;
#override
void initState() {
super.initState();
_controls = [];
for (int i = 0; i < __item_count; ++i) {
_controls.add(TextEditingController(text: 'hello $i'));
FocusNode fn = FocusNode();
_lstFocusNodes.add(fn);
fn.addListener(() {
if (fn.hasFocus) {
_ensureVisible(i, fn);
}
});
}
}
#override
void dispose() {
super.dispose();
for (int i = 0; i < __item_count; ++i) {
(_controls[i] as TextEditingController).dispose();
}
}
#override
Widget build(BuildContext context) {
List<Widget> widgets = [];
for (int i = 0; i < __item_count; ++i) {
widgets.add(TextFormField(focusNode: _lstFocusNodes[i],controller: _controls[i],));
}
return Scaffold( body: Container( margin: const EdgeInsets.all(8),
height: TEXT_ITEM_HEIGHT * __item_count,
child: Form(key: _formKey, child: ListView( children: widgets)))
);
}
Future<void> _keyboardToggled() async {
if (mounted){
EdgeInsets edgeInsets = MediaQuery.of(context).viewInsets;
while (mounted && MediaQuery.of(context).viewInsets == edgeInsets) {
await Future.delayed(const Duration(milliseconds: 10));
}
}
return;
}
Future<void> _ensureVisible(int index,FocusNode focusNode) async {
if (!focusNode.hasFocus){
debugPrint("ensureVisible. has not the focus. return");
return;
}
debugPrint("ensureVisible. $index");
// Wait for the keyboard to come into view
await Future.any([Future.delayed(const Duration(milliseconds: 300)), _keyboardToggled()]);
var renderObj = focusNode.context!.findRenderObject();
if( renderObj == null ) {
return;
}
var vp = RenderAbstractViewport.of(renderObj);
if (vp == null) {
debugPrint("ensureVisible. skip. not working in Scrollable");
return;
}
// Get the Scrollable state (in order to retrieve its offset)
ScrollableState scrollableState = Scrollable.of(focusNode.context!)!;
// Get its offset
ScrollPosition position = scrollableState.position;
double alignment;
if (position.pixels > vp.getOffsetToReveal(renderObj, 0.0).offset) {
// Move down to the top of the viewport
alignment = 0.0;
} else if (position.pixels < vp.getOffsetToReveal(renderObj, 1.0).offset){
// Move up to the bottom of the viewport
alignment = 1.0;
} else {
// No scrolling is necessary to reveal the child
debugPrint("ensureVisible. no scrolling is necessary");
return;
}
position.ensureVisible(
renderObj,
alignment: alignment,
duration: const Duration(milliseconds: 300),
);
}
}
To achieve initial scrolling at a particular index in a list of items
on tap of the floating action button you will be scrolled to an index of 10 in a list of items
class HomePage extends StatelessWidget {
final _controller = ScrollController();
final _height = 100.0;
#override
Widget build(BuildContext context) {
// to achieve initial scrolling at particular index
SchedulerBinding.instance.addPostFrameCallback((_) {
_scrollToindex(20);
});
return Scaffold(
appBar: AppBar(),
floatingActionButton: FloatingActionButton(
onPressed: () => _scrollToindex(10),
child: Icon(Icons.arrow_downward),
),
body: ListView.builder(
controller: _controller,
itemCount: 100,
itemBuilder: (_, i) => Container(
height: _height,
child: Card(child: Center(child: Text("Item $i"))),
),
),
);
}
// on tap, scroll to particular index
_scrollToindex(i) => _controller.animateTo(_height * i,
duration: Duration(seconds: 2), curve: Curves.fastOutSlowIn);
}
I am posting a solution here in which List View will scroll 100 pixel right and left . you can change the value according to your requirements. It might be helpful for someone who want to scroll list in both direction
import 'package:flutter/material.dart';
class HorizontalSlider extends StatelessWidget {
HorizontalSlider({Key? key}) : super(key: key);
// Dummy Month name
List<String> monthName = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"July",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec"
];
ScrollController slideController = new ScrollController();
#override
Widget build(BuildContext context) {
return Container(
child: Flex(
direction: Axis.horizontal,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
InkWell(
onTap: () {
// Here monthScroller.position.pixels represent current postion
// of scroller
slideController.animateTo(
slideController.position.pixels - 100, // move slider to left
duration: Duration(
seconds: 1,
),
curve: Curves.ease,
);
},
child: Icon(Icons.arrow_left),
),
Container(
height: 50,
width: MediaQuery.of(context).size.width * 0.7,
child: ListView(
scrollDirection: Axis.horizontal,
controller: slideController,
physics: ScrollPhysics(),
children: monthName
.map((e) => Padding(
padding: const EdgeInsets.all(12.0),
child: Text("$e"),
))
.toList(),
),
),
GestureDetector(
onTap: () {
slideController.animateTo(
slideController.position.pixels +
100, // move slider 100px to right
duration: Duration(
seconds: 1,
),
curve: Curves.ease,
);
},
child: Icon(Icons.arrow_right),
),
],
),
);
}
}
The simplest way is to call this method inside your InitState method. (not the build to evict unwanted errors)
WidgetsBinding.instance.addPostFrameCallback((_) => Scrollable.ensureVisible(targetKey.currentContext!))
WidgetsBinding.instance.addPostFrameCallback will guarantee that the list is builded and the this automatic search for your target and move the scroll to it. You can then customize the animation of the scroll effect on the Scrollable.ensureVisible method
Note: Remember to add the targetKey (a GlobalKey) to the widget you want to scroll to.
Adding with Rémi Rousselet's answer,
If there is a case you need to scroll past to end scroll position with addition of keyboard pop up, this might be hided by the keyboard. Also you might notice the scroll animation is a bit inconsistent when keyboard pops up(there is addition animation when keyboard pops up), and sometimes acts weird. In that case wait till the keyboard finishes animation(500ms for ios).
BuildContext context = key.currentContext;
Future.delayed(const Duration(milliseconds: 650), () {
Scrollable.of(context).position.ensureVisible(
context.findRenderObject(),
duration: const Duration(milliseconds: 600));
});
You can also simply use the FixedExtentScrollController for same size items with the index of your initialItem :
controller: FixedExtentScrollController(initialItem: itemIndex);
The documentation : Creates a scroll controller for scrollables whose items have the same size.
Simply use page view controller.
Example:
var controller = PageController();
ListView.builder(
controller: controller,
itemCount: 15,
itemBuilder: (BuildContext context, int index) {
return children[index);
},
),
ElevatedButton(
onPressed: () {
controller.animateToPage(5, //any index that you want to go
duration: Duration(milliseconds: 700), curve: Curves.linear);
},
child: Text(
"Contact me",),
You can use the controller.jumpTo(100) after the loading finish