How to make a floating AppBar in Flutter? - flutter

I want to make a floating AppBar in Flutter that overlays on my UI and dismisses when the user scrolls up. I have tried using this dependency - https://pub.dev/packages/material_floating_search_bar but this is only for searching through something.
Update:
This is my code -
DefaultTabController(
length: 2,
child: Scaffold(
body: Stack(
children: [
Positioned(
top: 15,
left: 15,
right: 15,
child: SafeArea(
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: AppBar(
title: Text('Hello', style: kTasksStyle),
centerTitle: true,
backgroundColor: kGreen,
),
),
),
),
],
)),
);
How do I add a TabBar in the bottom parameter of AppBar?

You could use a Stack, with your content and the App bar as children.
For dismissing on scroll you can hide the app bar depending on the offset of your ScrollController or use an Animation.

Screenshot:
For simplicity, I used a ListView.
Code:
class _MainPageState extends State<HomePage> {
final double _appBarHeight = 56;
final double _topPadding = 20;
ScrollController _controller;
double _opacity = 1;
#override
void initState() {
super.initState();
_controller = ScrollController();
_controller.addListener(_listener);
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
void _listener() {
final offset = _controller.offset;
if (offset > _appBarHeight) {
if (_opacity != 0) {
setState(() {
_opacity = 0;
if (_opacity < 0) _opacity = 0;
});
}
} else {
setState(() {
_opacity = 1 - (offset / _appBarHeight);
if (_opacity > 1) _opacity = 1;
});
}
}
Widget get _mainContent {
return ListView.builder(
controller: _controller,
padding: EdgeInsets.only(top: _topPadding + _appBarHeight),
itemCount: 20,
itemBuilder: (_, i) => ListTile(title: Text('Item $i')),
);
}
Widget get _appBar {
return Opacity(
opacity: _opacity,
child: SizedBox.fromSize(
size: Size.fromHeight(_appBarHeight),
child: AppBar(
title: Text('AppBar'),
centerTitle: false,
backgroundColor: Colors.grey,
leading: Icon(Icons.menu),
actions: [
IconButton(
icon: Icon(Icons.place),
onPressed: () {},
)
],
),
),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
_mainContent,
Positioned(
top: _topPadding,
left: 0,
right: 0,
child: _appBar,
),
],
),
);
}
}

Related

Getting my sidebar to transition between 2 states? Flutter

I am trying to create a sidebar that transitions between 2 different states in flutter for a web application. At the moment it immediately moves between the 2 different positions and their is no transition. I suspect I haven't used the AnimatedPositioned Class correctly.
How am I able to get it to animate between the 2 different positions correctly?
class _SideNavigationBarState extends State<SideNavigationBar> with singleTickerProviderStateMixin<SideNavigationBar> {
late AnimationController _animationController;
late StreamController<bool> isSideNavigationBarOpenedStreamController;
late Stream<bool> isSideNavigationBarOpenedStream;
late StreamSink<bool> isSideNavigationBarOpenedSink;
final _animationDuration = const Duration(milliseconds: 500);
#override
void initState() {
super.initState();
_animationController = AnimationController(vsync: this, duration: _animationDuration);
isSideNavigationBarOpenedStreamController = PublishSubject<bool>();
isSideNavigationBarOpenedStream = isSideNavigationBarOpenedStreamController.stream;
isSideNavigationBarOpenedSink = isSideNavigationBarOpenedStreamController.sink;
}
#override
void dispose() {
_animationController.dispose();
isSideNavigationBarOpenedStreamController.close();
isSideNavigationBarOpenedSink.close();
super.dispose();
}
void onIconPressed() {
final animationStatus = _animationController.status;
final isAnimationCompleted = animationStatus == AnimationStatus.completed;
if (isAnimationCompleted) {
isSideNavigationBarOpenedSink.add(false);
_animationController.reverse();
} else {
isSideNavigationBarOpenedSink.add(true);
_animationController.forward();
}
}
#override
Widget build(BuildContext context) {
double size = 52;
final screenWidth = MediaQuery.of(context).size.width;
return StreamBuilder<bool>(
initialData: false,
stream: isSideNavigationBarOpenedStream,
builder: (context, isSideNavigationBarOpenedAsync) {
final icon = isSideNavigationBarOpenedAsync.data! ? Icons.arrow_back_ios
: Icons.arrow_forward_ios;
return AnimatedPositioned(
duration: _animationDuration,
left: isSideNavigationBarOpenedAsync.data! ? 0 : -screenWidth,
right: isSideNavigationBarOpenedAsync.data! ? 0 : screenWidth-45,
child: Row(
children: <Widget>[
isSideNavigationBarOpenedAsync.data!
? Container(
width: 300,
color: Colors.grey,
):
Container(
width: 45,
color: Colors.grey,
),
Align(
alignment: Alignment(0, -0.9),
child: GestureDetector(
onTap: () {
onIconPressed();
},
child: Container(
child: Material(
color: Colors.transparent,
child: InkWell(
child: Container(
width: size,
height: size,
child: Icon(icon,
color: Colors.grey.withOpacity(0.5)
)
),
),
),
)
)
)
],
),
);
},
);
}
I recommend you instead of using some transition, use some basic simple code for it. for example you need to define a drawer in your scaffold, and it will handle everything itself.
Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
drawer: Container(color: Colors.amber),
body: const Center(child: Text('body')),
);

Flutter NestedScrollView header bounce with TabBar and TabbarView

Header pull to refresh can be pulled up when I want to drag it up.
Tabbar sticky.
ListView can swipe to another page.
100% Custom Widget. It supports to change the header value and also support swipe opeartions.
It is not actually a TabBar Widget but it Works exactly like that
import 'package:flutter/material.dart';
import 'dart:math';
class NestedScrolls extends StatefulWidget {
static const listHeader = ['Pakistan', 'China','Iran','Turkey'];
#override
_NestedScrollsState createState() => _NestedScrollsState();
}
class _NestedScrollsState extends State<NestedScrolls> {
var position=0;
var topHeader;
Widget? applyWidget() {
switch(position){
case 0:
setState(() {
topHeader = NestedScrolls.listHeader[0];
});
// return widget if user click over pakistan in tab bar
return grid();
case 1:
setState(() {
topHeader = NestedScrolls.listHeader[1];
});
return list();
case 2:
setState(() {
topHeader = NestedScrolls.listHeader[2];
});
return Container(color: Colors.blue,
child: Center(child: Text(topHeader),),);
case 3:
setState(() {
topHeader = NestedScrolls.listHeader[3];
});
return Container(color: Colors.orange,
child: Center(child: Text(topHeader),),);
}
}
#override
void initState() {
// TODO: implement initState
super.initState();
//initial header name when activity start first time
topHeader = NestedScrolls.listHeader[0];
}
#override
Widget build(BuildContext context) {
topHeader = topHeader;
return Scaffold(
// Persistent AppBar that never scrolls
appBar: AppBar(
title: Text('AppBar'),
elevation: 0.0,
),
body:
Column(
children: <Widget>[
///header
Container(
alignment: Alignment.center,
color: Colors.blueGrey,
height: 90,
child: Text(NestedScrolls.listHeader[position]),
),
/// tabBar
Container(
height: 60,
width: MediaQuery.of(context).size.width,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: NestedScrolls.listHeader.length,
itemBuilder: (con, index) {
return GestureDetector(
onTap: () => setState(() {
position=index;
topHeader = NestedScrolls.listHeader[index];
}),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 2.0, horizontal: 10),
child: Container(alignment: Alignment.center,
width: 100,
color: topHeader==NestedScrolls.listHeader[index]?Colors.black26:Colors.transparent,
child: Text(NestedScrolls.listHeader[index])),
),
);
}),
),
///Widget
Expanded(
child: GestureDetector(
// onHorizontalDragStart: (left){
// print('left : ${left.localPosition.direction}');
// // left.globalPosition.dx
//
// },
onHorizontalDragEnd: (start){
print('start : ${start.velocity.pixelsPerSecond.dx}');
if((start.velocity.pixelsPerSecond.dx)<-700){
if(position<NestedScrolls.listHeader.length-1 && position>=0)
setState(() {
position=position+1;
});
}else{}
if((start.velocity.pixelsPerSecond.dx)>900){
if(position<=NestedScrolls.listHeader.length-1 && position>0)
setState(() {
position=position-1;
});
}
print(position);
},
child: applyWidget()),
),
],
),
);
}
list() {
return SingleChildScrollView(scrollDirection: Axis.vertical,
child: Container(
child: Column(children: [
for(var color in Colors.primaries)
Container(color: color, height: 100.0)
],),
),
);
}
grid() {
return GridView.count(
padding: EdgeInsets.zero,
crossAxisCount: 3,
children: Colors.primaries.map((color) {
return Container(color: color, height: 100.0);
}).toList(),
);
}
}

Size to up animation for widgets

In Flutter you suppose I have a simple Container and I would like to change the size of that to up, for example in this simple screenshot I want to change top container in section 1 to up to have a top container in section 2
and top container in section 1 should behave only 100.0 after size to up
container B in section 1 and section 2 are in the same axis without change position when container A will be resized to up
for example, this is what I want to have
I'm not sure with which one animation I can have this feature
this code work, but this is not what I want to have.
i want to have draggable bottom sheet with changing border radius when bottom sheet arrived to top of screen like with pastes sample video screen and fade0n/out widget inside appbar which that inside top of bottom sheet, which that visible when bottom sheet arrived top or hide when bottom sheet don't have maximum size
import 'package:flutter/material.dart';
void main()=>runApp(SizeUp());
class SizeUp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'test',
home: SizeUpAnim(),
);
}
}
class SizeUpAnim extends StatefulWidget {
#override
State<StatefulWidget> createState() =>_SizeUpAnim();
}
class _SizeUpAnim extends State with SingleTickerProviderStateMixin {
AnimationController _controller;
// ignore: constant_identifier_names
static const _PANEL_HEADER_HEIGHT = 32.0;
bool get _isPanelVisible {
final AnimationStatus status = _controller.status;
return status == AnimationStatus.completed ||
status == AnimationStatus.forward;
}
#override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 100), value: 1.0, vsync: this);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
elevation: 8.0,
title: const Text("Step4"),
leading: IconButton(
onPressed: () {
_controller.fling(velocity: _isPanelVisible ? -1.0 : 1.0);
},
icon: AnimatedIcon(
icon: AnimatedIcons.close_menu,
progress: _controller.view,
),
),
),
body: Column(
children: <Widget>[
Expanded(
child: LayoutBuilder(
builder: _buildStack,
),
),
Text('aaa'),
],
),
);
}
Animation<RelativeRect> _getPanelAnimation(BoxConstraints constraints) {
final double height = constraints.biggest.height;
final double top = height - _PANEL_HEADER_HEIGHT;
const double bottom = -_PANEL_HEADER_HEIGHT;
return RelativeRectTween(
begin: RelativeRect.fromLTRB(0.0, top, 0.0, bottom),
end: const RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0),
).animate( CurvedAnimation(parent: _controller, curve: Curves.linear));
}
Widget _buildStack(BuildContext context, BoxConstraints constraints) {
final Animation<RelativeRect> animation = _getPanelAnimation(constraints);
final ThemeData theme = Theme.of(context);
return Container(
color: theme.primaryColor,
child: Stack(
children: <Widget>[
const Center(
child: Text("base"),
),
PositionedTransition(
rect: animation,
child: Material(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16.0),
topRight: Radius.circular(16.0)),
elevation: 12.0,
child: Container(
height: _PANEL_HEADER_HEIGHT,
child: const Center(child: Text("panel")),
),
),
),
],
),
);
}
#override
void dispose() {
super.dispose();
_controller.dispose();
}
}
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool isLong = false;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('First'),
),
body: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('hey'),
RaisedButton(
onPressed: () {
setImages();
setState(() {
isLong = !isLong;
});
},
child: Text(isLong ? 'long' : 'short'),
),
RaisedButton(
onPressed: _onPressed,
child: Text('open'),
)
],
),
),
);
}
_onPressed() {
Navigator.of(context)
.push(TransparentRoute(builder: (context) => NewWidget(images)));
}
List<String> images = List.generate(
5,
(_) => 'http://placeimg.com/100/100/any',
);
void setImages() {
images = List.generate(
isLong ? 5 : 25,
(_) => 'http://placeimg.com/100/100/any',
);
}
}
class NewWidget extends StatefulWidget {
const NewWidget(this.images, {Key key}) : super(key: key);
final List<String> images;
#override
_NewWidgetState createState() => _NewWidgetState();
}
class _NewWidgetState extends State<NewWidget> {
bool isBig = false;
bool isStack = false;
bool isBounsing = true;
final double topOffset = 200;
final double miniHandleHeigh = 30;
double safeAreaPadding = 0;
double startBigAnimationOffset;
double startStickyOffset;
double backgroundHeight = 0;
double get savedAppBarHeigh => safeAreaPadding + kToolbarHeight;
final ScrollController controller = ScrollController();
#override
void initState() {
WidgetsBinding.instance.addPostFrameCallback(_afterLayout);
super.initState();
}
void _afterLayout(_) {
var media = MediaQuery.of(context);
safeAreaPadding = media.padding.top;
startBigAnimationOffset = topOffset - savedAppBarHeigh;
startStickyOffset = startBigAnimationOffset + 20;
backgroundHeight = media.size.height - miniHandleHeigh - topOffset;
var canScroll = !_isImageSizeBiggerThenBottomSheetSize(
media.size.width,
media.size.height,
);
controller.addListener(
canScroll ? withoutScrolling : withScrolling,
);
}
void withoutScrolling() {
var offset = controller.offset;
if (offset < 0) {
goOut();
} else {
controller.animateTo(
0,
duration: Duration(milliseconds: 100),
curve: Curves.easeIn,
);
}
}
void withScrolling() {
var offset = controller.offset;
if (offset < 0) {
goOut();
} else if (offset < startBigAnimationOffset && isBig) {
setState(() {
isBig = false;
});
} else if (offset > startBigAnimationOffset && !isBig) {
setState(() {
isBig = true;
});
} else if (offset > startStickyOffset && !isStack) {
setState(() {
isStack = true;
});
} else if (offset < startStickyOffset && isStack) {
setState(() {
isStack = false;
});
}
if (offset < topOffset && !isBounsing) {
setState(() => isBounsing = true);
} else if (offset > topOffset && isBounsing) {
setState(() => isBounsing = false);
}
}
void goOut() {
controller.dispose();
Navigator.of(context).pop();
}
_isImageSizeBiggerThenBottomSheetSize(
double screenWidth,
double screenHeight,
) {
// padding: 10, 3 in row;
print(screenHeight);
var itemHeight = (screenWidth - 20) / 3;
print(itemHeight);
var gridMaxHeight = screenHeight - topOffset - miniHandleHeigh;
print(gridMaxHeight);
return (widget.images.length / 3).floor() * itemHeight > gridMaxHeight;
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: isStack ? Colors.white : Colors.transparent,
body: Stack(
children: [
Positioned(
bottom: 0,
right: 0,
left: 0,
child: Container(
constraints: BoxConstraints(minHeight: backgroundHeight),
decoration: BoxDecoration(
color: Colors.white,
),
),
),
ListView(
physics:
isBounsing ? BouncingScrollPhysics() : ClampingScrollPhysics(),
controller: controller,
children: <Widget>[
Container(
alignment: Alignment.bottomCenter,
height: topOffset,
child: TweenAnimationBuilder(
tween: Tween(begin: 0.0, end: isBig ? 1.0 : 0.0),
duration: Duration(milliseconds: 500),
child: Align(
alignment: Alignment.topCenter,
child: Padding(
padding: EdgeInsets.only(top: 15),
child: Container(
decoration: BoxDecoration(
color: Colors.black38,
borderRadius: BorderRadius.circular(2),
),
height: 5,
width: 60,
),
),
),
builder: (_, number, child) {
return Container(
height: savedAppBarHeigh * number + miniHandleHeigh,
decoration: BoxDecoration(
borderRadius: BorderRadius.vertical(
top: Radius.circular((1 - number) * 20)),
color: Colors.white,
),
child: Opacity(opacity: 1 - number, child: child),
);
}),
),
Container(
padding: EdgeInsets.all(10),
constraints: BoxConstraints(
minHeight:
MediaQuery.of(context).size.height - savedAppBarHeigh),
decoration: BoxDecoration(
color: Colors.white,
),
child: getGrid(),
)
],
),
if (isStack)
_AppBar(
title: 'Gallery',
)
],
),
);
}
Widget getGrid() {
return GridView.count(
crossAxisSpacing: 10,
mainAxisSpacing: 10,
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
crossAxisCount: 3,
children: widget.images.map((url) {
return Container(
decoration: BoxDecoration(
border: Border.all(
color: Colors.blueAccent,
),
),
child: Image(
image: NetworkImage(url),
),
);
}).toList(),
);
}
}
class _AppBar extends StatelessWidget {
const _AppBar({Key key, this.title}) : super(key: key);
final Color backgroundColor = Colors.white;
final Color color = Colors.grey;
final String title;
#override
Widget build(BuildContext context) {
return Material(
elevation: 5,
child: Container(
height: kToolbarHeight + MediaQuery.of(context).padding.top,
color: backgroundColor,
child: OverflowBox(
alignment: Alignment.topCenter,
maxHeight: 200,
child: SafeArea(
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: kToolbarHeight),
child: AppBar(
centerTitle: false,
backgroundColor: backgroundColor,
iconTheme: IconThemeData(
color: color, //change your color here
),
primary: false,
title: Text(
title,
style: TextStyle(color: color),
),
elevation: 0,
),
),
),
),
),
);
;
}
}
class TransparentRoute extends PageRoute<void> {
TransparentRoute({
#required this.builder,
RouteSettings settings,
}) : assert(builder != null),
super(settings: settings, fullscreenDialog: false);
final WidgetBuilder builder;
#override
bool get opaque => false;
#override
Color get barrierColor => null;
#override
String get barrierLabel => null;
#override
bool get maintainState => true;
#override
Duration get transitionDuration => Duration(milliseconds: 350);
#override
Widget buildPage(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
final result = builder(context);
return Container(
color: Colors.black.withOpacity(0.5),
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 1),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeIn,
)),
child: result,
),
);
}
}
I think you may try this library, sliding_sheet
when you detect the expand status by controller, then you animate/enlarge the container A.

Hide top header until scroll to certain height

I'm new to flutter and I want to implement something like this: (klook app)
It's basically a button being shown when the user scrolls a bit.
I tried different things with a SliverAppBar and SliverStickyHeader, but I can't make it work like this. I also played with Opacity and Visibility but it moves my hole view and does not 'overlap' my banner/searchby widget.
My code so far:
class _ExplorePageState extends State<ExplorePage> {
ScrollController _scrollController;
bool lastStatus = true;
_scrollListener() {
if (isShrink != lastStatus) {
print("listen");
setState(() {
lastStatus = isShrink;
});
}
}
bool get isShrink {
return _scrollController.hasClients &&
_scrollController.offset > (400 - kToolbarHeight);
}
#override
void initState() {
_scrollController = ScrollController();
_scrollController.addListener(_scrollListener);
super.initState();
}
#override
void dispose() {
_scrollController.removeListener(_scrollListener());
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
controller: _scrollController,
slivers: <Widget>[
SliverStickyHeader(
header: Visibility(
child: Container(
color: Colors.red,
height: isShrink ? 100 : 0,
child: Text('Header 1'),
),
visible: isShrink ? true : false,
maintainState: true,
maintainSize: true,
maintainAnimation: true,
),
sliver: SliverList(
delegate: SliverChildListDelegate(
[
BannerWidget(),
ButtonWidget(),
],
),
),
),
],
),
);
}
}
The BannerWidget and ButtomWidget are two Containers similar to the app shown above.
I hope you can help me or tell me maybe what this behaviour is called.
Thank you!
If you're ok with using CustomScrollView, you could use SliverPersistentHeader with your own delegate. It will allow you to access current header scroll state and make your own layout depending on how much space you have left.
const double _kSearchHeight = 50.0;
const double _kHeaderHeight = 250.0;
class _ExplorePageState extends State<ExplorePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: CustomScrollView(
slivers: <Widget>[
SliverPersistentHeader(
delegate: DelegateWithSearchBar(),
pinned: true,
),
SliverList(
delegate: SliverChildListDelegate(
[
for (int i = 0; i < 4; i++)
Container(
height: 200,
child: Text('test'),
color: Colors.black26
),
],
),
)
],
),
),
);
}
}
class DelegateWithSearchBar extends SliverPersistentHeaderDelegate {
#override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
final showSearchBar = shrinkOffset > _kHeaderHeight - _kSearchHeight;
return Stack(
children: <Widget>[
AnimatedOpacity(
opacity: !showSearchBar ? 1 : 0,
duration: Duration(milliseconds: 100),
child: LayoutBuilder(
builder: (context, constraints) {
return Container(
decoration: BoxDecoration(
image: DecorationImage(
image: NetworkImage('xxx'),
fit: BoxFit.cover
)
),
height: constraints.maxHeight,
child: SafeArea(
child: Container(
padding: EdgeInsets.only(left: 20, bottom: 20),
alignment: Alignment.bottomLeft,
child: Text(
'Sample Text',
style: TextStyle(color: Colors.white, fontSize: 22)
),
),
),
);
}
),
),
AnimatedOpacity(
opacity: showSearchBar ? 1 : 0,
duration: Duration(milliseconds: 100),
child: Container(
height: _kSearchHeight,
color: Colors.white,
alignment: Alignment.center,
child: Text('search bar')
),
),
],
);
}
#override
bool shouldRebuild(SliverPersistentHeaderDelegate _) => true;
#override
double get maxExtent => _kHeaderHeight;
#override
double get minExtent => _kSearchHeight;
}

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