Scrolling a PageView - flutter

I want to use a PageView with vertical axis and move between the pages using the mouse-scroll, but when I use the mouse-scroll the page don't scroll... The page only scroll when I click and swipe to up/down.
There is any way to do that?
I want to keep the property pageSnapping: true
The problem:
When I try to mouse-scroll the page, it don't change, it just back to initial offset.
But when I click and swipe works...
class Body extends StatefulWidget {
#override
_BodyState createState() => _BodyState();
}
class _BodyState extends State<Body> {
PageController _controller = PageController(keepPage: true);
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
height: Sizing.size.height,
width: Sizing.size.width,
child: Stack(
children: <Widget>[
PageView(
scrollDirection: Axis.vertical,
controller: _controller,
children: <Widget>[
Container(color: Colors.red),
Container(color: Colors.blue),
Container(color: Colors.orange),
],
),
],
),
),
);
}
}

To use the mouse scroll you must disable the movement of the PageView by setting
physics: NeverScrollableScrollPhysics().
Then you have to manually intercept the mouse scroll through a Listener. If you also want to recover the PageView classic movement through swipe you must use a GestureDetector.
Here is an example code:
class _HomepageState extends State<Homepage> {
final PageController pageController = PageController();
// this is like a lock that prevent update the PageView multiple times while is
// scrolling
bool pageIsScrolling = false;
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: GestureDetector(
// to detect swipe
onPanUpdate: (details) {
_onScroll(details.delta.dy * -1);
},
child: Listener(
// to detect scroll
onPointerSignal: (pointerSignal) {
if (pointerSignal is PointerScrollEvent) {
_onScroll(pointerSignal.scrollDelta.dy);
}
},
child: PageView(
scrollDirection: Axis.vertical,
controller: pageController,
physics: NeverScrollableScrollPhysics(),
pageSnapping: true,
children: [
Container(color: Colors.red),
Container(color: Colors.blue),
Container(color: Colors.orange),
],
),
),
)),
);
}
void _onScroll(double offset) {
if (pageIsScrolling == false) {
pageIsScrolling = true;
if (offset > 0) {
pageController
.nextPage(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut)
.then((value) => pageIsScrolling = false);
print('scroll down');
} else {
pageController
.previousPage(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut)
.then((value) => pageIsScrolling = false);
print('scroll up');
}
}
}
}

To make stuff scrollable in general you can wrap the widget (>>right click on the widget you want to make scrollable>>refactor>>wrap with widget) in a SingleChildScrollView().

Thanks to Luca!
I just modified the entire thing to do the same, what was the question asked for.
Listener(
onPointerMove: (pointerMove) {
if (pointerMove is PointerMoveEvent) {
_onScroll(pointerMove.delta.dy * -1);
print(pointerMove.delta.dy);
}
},
onPointerSignal: (pointerSignal) {
if (pointerSignal is PointerScrollEvent) {
_onScroll(pointerSignal.scrollDelta.dy);
print(pointerSignal.scrollDelta.dy);
}
},
child: SingleChildScrollView(
child: Container(
constraints: BoxConstraints(
minHeight: SizeConfig.screenHeight * .9),
width: SizeConfig.screenWidth,
height: SizeConfig.screenWidth / 2,
decoration: BoxDecoration(
image: DecorationImage(
alignment: Alignment.centerRight,
image: AssetImage(
'assets/images/background_circles.png'))),
child: PageView(
controller: _pageController,
physics: NeverScrollableScrollPhysics(),
pageSnapping: true,
scrollDirection: Axis.vertical,
children: [

Related

swip animatedSwitcher without a button in flutter

i have an animatedSwitcher but with effect of swip left and right, i reach that by using Two widget:
a PageView builder with a transparent background and an animatedSwitcher. inside a stack i can swip smoothly between widgets.
the problem is the widgets of animatedSwitcher are scrollable but the PageView builder is above the animatedSwitcher so i cant scroll up and down widget of animatedSwitcher. (are not clickable in general).
the aim is to reach the same result as the gif down below. but without a pageview builder.
any suggestion will be helpful and thanks.
the code:
int selectedPage = 0;
PageController _controller = PageController();
#override
Widget build(BuildContext context) {
List<Widget> _pages = [
Container(color: Colors.blue),
Container(color: Colors.red),
];
return Stack(
alignment: Alignment.center,
children: <Widget>[
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: Container(
key: ValueKey(_pages[selectedPage]),
child: _pages[selectedPage],
),
),
PageView.builder(
onPageChanged: (value) {
setState(() {
selectedPage = value;
});
},
controller: _controller,
physics: const ClampingScrollPhysics(),
itemCount: _pages.length,
itemBuilder: (context, index) {
return Container();
},
),
],
);
}

Flutter GestureDetector Not Working during AutoScroll

I was trying to detect when a user tapped a container during autoscroll. I am trying to build a scrolling game where a user taps containers inside a listview. However, my gesture detector onTap method does not work during scrolling. It only works after I am done scroling. How do I detect taps while autoscroll goes through my listview. This is my code:
class ColumnScreen extends StatefulWidget {
const ColumnScreen({Key? key}) : super(key: key);
#override
_ColumnScreenState createState() => _ColumnScreenState();
}
class _ColumnScreenState extends State<ColumnScreen> {
final ScrollController _controller = ScrollController();
void _scrollDown() {
_controller.animateTo(
_controller.position.maxScrollExtent,
duration: Duration(seconds: 2),
curve: Curves.linear,);
}
#override
void initState() {
WidgetsBinding.instance!.addPostFrameCallback((_){
//write or call your logic
//code will run when widget rendering complete
_scrollDown();
});
super.initState();
}
#override
Widget build(BuildContext context) {
final screenHeight = MediaQuery.of(context).size.height;
final screenWidth = MediaQuery.of(context).size.width;
return RotatedBox(
quarterTurns: 2,
child: Container(
height: screenHeight,
width: screenWidth/4,
color: Colors.yellow,
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
child: ListView.builder(
physics: NeverScrollableScrollPhysics(),
itemCount: 5,
controller: _controller,
itemBuilder: (context, index)
{
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTapDown: (_){
print("Tap at ${index}");
},
child: Column(
children: [
Container(
height: screenHeight/4.5,
width: screenWidth/4,
color: Colors.grey,
child: Text("Index: ${index}"),
),
Divider(
thickness: 1,
color: Colors.black,
),
],
),
);
}
),
)
),
);
}
}

How to create a constant looping auto-scroll in Flutter?

I am seeking to create a constant scroll of a dynamic number of images across my screen (similar to a news ticker) in Flutter. I want this to be automatic and a constant speed, that also loops.
The simplest solution I have found is to use the Carousel Package which ticks almost all the boxes, except one. I am unable to get a constant scroll speed
A possible solution was to adjust autoPlayInterval to zero, but unfortunately, this paramater appears to need a value of around 50 or greater to run - therefore creating an even scroll.
Any idea on how to tweak it this with this package? Or another suitable solution?
Simplified code:
#override
Widget build(BuildContext context) {
return Container(
child: CarouselSlider(
items: DynamicImages.list
.map(
(e) => Padding(
padding: const EdgeInsets.all(8.0),
child: Image.asset('assets/images/$e.png'),
),
)
.toList(),
options: CarouselOptions(
autoPlay: true,
autoPlayCurve: Curves.linear,
autoPlayInterval: Duration(milliseconds: 0), /// carousel will not run if set to zero
autoPlayAnimationDuration: Duration(milliseconds: 1000)
),
),
);
}
}
I found a workaround that still takes advantage of the carousel_slider package. Instead of using autoplay, you just give it a CarouselController.
Start the animation by calling nextPage() in initState. Then in the carousel, set onPageChanged to call nextPage() again, which will continuously scroll from page to page with constant speed. Set the duration for the two nextPage() calls to whatever you want, as long as it's the same duration.
The only pause I experience is when it reaches the end of the list, where it needs to loop back to the first image. But it's negligible enough for my use case.
class _WelcomeScreenState extends State<WelcomeScreen> {
List<String> _images = [
"assets/image.jpg",
// ...
];
final CarouselController _carouselController = CarouselController();
#override
void initState() {
super.initState();
// Add this to start the animation
WidgetsBinding.instance.addPostFrameCallback((_) {
_carouselController.nextPage(duration: Duration(milliseconds: 2000));
});
}
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async => false,
child: ScreenBackgroundContainer(
child: Scaffold(
body: SafeArea(
child: Container(
height: double.maxFinite,
width: double.maxFinite,
child: SingleChildScrollView(
physics: BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()),
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Column(
children: [
// Carousel
CarouselSlider(
carouselController: _performerCarouselController,
options: CarouselOptions(
pageSnapping: false,
height: 146,
viewportFraction: 114 / MediaQuery.of(context).size.width,
// Add this
onPageChanged: (_, __) {
_performerCarouselController.nextPage(duration: Duration(milliseconds: 2000));
}),
items: _performerImages
.map((image) => ClipRRect(
borderRadius: BorderRadius.circular(16.0),
child: Image.asset(image, width: 90, fit: BoxFit.cover)))
.toList(),
),
],
),
),
),
),
),
),
);
}
}
Screenshot
So, I've been working on this since posting and come up with a solution. I am posting my answer in case it helps any others in the future.
Basically, instead of using Carousel package, I used ListView.builder which then continuously grows as needed.
Of note, I needed to use WidgetsBinding.instance.addPostFrameCallback((timeStamp) {}); to get this to work.
It would still be great to see any other solutions, (as I am sure the below workaround could be improved).
import 'package:flutter/material.dart';
class ScrollLoop extends StatefulWidget {
const ScrollLoop({Key? key}) : super(key: key);
#override
_ScrollLoopState createState() => _ScrollLoopState();
}
class _ScrollLoopState extends State<ScrollLoop> {
ScrollController _controller = ScrollController();
/// [_list] is growable and holds the assets which will scroll.
List<String> _list = [
"assets/images/image1.png",
/// etc...
];
/// [_list2] holds duplicate data and is used to append to [_list].
List<String> _list2 = [];
/// [_listAppended] ensures [_list] is only appended once per cycle.
bool _listAppended = false;
#override
void initState() {
_list2.addAll(_list);
/// To auto-start the animation when the screen loads.
WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
_startScroll();
});
/// The [_controller] will notify [_list] to be appended when the animation is near completion.
_controller.addListener(
() {
if (_controller.position.pixels >
_controller.position.maxScrollExtent * 0.90) {
if (_listAppended == false) {
_list.addAll(_list2);
_listAppended = true;
}
}
/// The [_controller] will listen for when the animation cycle completes,
/// so this can immediately re-start from the completed position.
if (_controller.position.pixels ==
_controller.position.maxScrollExtent) {
_listAppended = false;
setState(() {});
WidgetsBinding.instance!.addPostFrameCallback(
(timeStamp) {
_startScroll();
},
);
}
},
);
super.initState();
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
}
void _startScroll() {
_controller.animateTo(_controller.position.maxScrollExtent,
duration: Duration(milliseconds: 8000), curve: Curves.linear);
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
final _size = MediaQuery.of(context).size;
return AbsorbPointer(
child: Material(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: ListView.builder(
shrinkWrap: true,
controller: _controller,
scrollDirection: Axis.horizontal,
itemCount: _list.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
width: _size.width / 4,
height: _size.height / 10,
child: Image.asset(_list[index]),
),
);
},
),
),
],
),
),
),
);
}
}

Flutter CustomScrollView slivers stacking

I am trying to create a scrollView using CustomScrollView.
The effect that I need, is very similar to this one.
I need the SliverList to be stacked above the SliverAppbar, without the list taking the whole screen and hiding the SliverAppbar.
The reason I want to do this, is that i need to attach a persistent Positioned widget on top of that list, and it won't appear unless the list is stacked above the SliverAppbar.
Here's my code.
Step one:
Use ListView inside SliverAppBar widget. To make css overflow:hidden effect.
Step two:
Add controller to NestedScrollView and move the button on scrolling in a stack. Plus calculate where you want to stop button moving.
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
ScrollController scrollController;
final double expandedHight = 150.0;
#override
void initState() {
super.initState();
scrollController = new ScrollController();
scrollController.addListener(() => setState(() {}));
}
#override
void dispose() {
scrollController.dispose();
super.dispose();
}
double get top {
double res = expandedHight;
if (scrollController.hasClients) {
double offset = scrollController.offset;
if (offset < (res - kToolbarHeight)) {
res -= offset;
} else {
res = kToolbarHeight;
}
}
return res;
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Stack(
children: [
NestedScrollView(
controller: scrollController,
headerSliverBuilder: (context, value) {
return [
SliverAppBar(
pinned: true,
expandedHeight: expandedHight,
flexibleSpace: ListView(
physics: const NeverScrollableScrollPhysics(),
children: [
AppBar(
title: Text('AfroJack'),
elevation: 0.0,
),
Container(
color: Colors.blue,
height: 100,
alignment: Alignment.center,
child: RaisedButton(
child: Text('folow'),
onPressed: () => print('folow pressed'),
),
),
],
),
),
];
},
body: ListView.builder(
physics: const NeverScrollableScrollPhysics(),
itemCount: 80,
itemBuilder: (BuildContext context, int index) {
return Text(
'text_string'.toUpperCase(),
style: TextStyle(
color: Colors.white,
),
);
},
),
),
Positioned(
top: top,
width: MediaQuery.of(context).size.width,
child: Align(
child: RaisedButton(
onPressed: () => print('shuffle pressed'),
child: Text('Suffle'),
),
),
),
],
),
);
}
}

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