I have a Row that consists x number of Button widgets. So when user clicks on one of the Button I need to fade out the other buttons and the slide the clicked button to the starting position(0,0) of the Row layout. I managed to get the fade out animation to work.
final _opacityTween = Tween<double>(begin: 1, end: 0);
_opacityAnimation = _opacityTween.animate(CurvedAnimation(
parent: _controller,
curve: new Interval(0.00, 0.50, curve: Curves.linear)
// curve: Curves.ease
));
// And using the opacityAnimation value in Opacity widget.
return Opacity(
opacity: _opacityAnimation.value,
child: child,
);
However I have been unsuccessful to using Animation classes and widgets to slide the child widget to the start position of the Row. Basically I am not sure what kind of classes could be useful to achieve such animation. I can use the Tween and set the end position to (0, 0) and use Translation to achieve it, however I am not sure how to set the begin position of the Animation when I initialise it as the position of a child is not known.
Try this solution. I thing it is almost what you need. You just have to add SizeTransition widget.
Ask if you have any question.
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
int _pressedButton;
Animation<double> _animation;
AnimationController _controller;
#override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
_animation = Tween<double>(begin: 1.0, end: 0.0)
.animate(_controller)
..addListener(() {
setState(() { });
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Test"),
),
body: Column(
children: <Widget>[
Row(
children: <Widget>[
_animatedButton(1),
_animatedButton(2),
_animatedButton(3),
_animatedButton(4)
]
),
Text(_pressedButton != null ? "$_pressedButton button pressed" : "No button pressed"),
RaisedButton(
child: Text("Reset"),
onPressed: () {
_controller.reset();
}
)
]
)
);
}
Widget _animatedButton(int button) {
if (button != _pressedButton) {
return SizeTransition(
sizeFactor: _animation,
axis: Axis.horizontal,
child: Opacity(
opacity: _animation.value,
child: _button(button)
)
);
} else {
return _button(button);
}
}
Widget _button(int button) {
return RaisedButton(
onPressed: () => onButtonClick(button),
child: Text("Button $button")
);
}
void onButtonClick(int button) {
setState(() {
_pressedButton = button;
});
_controller.forward();
}
}
UPDATE 1
Check one more way to do what you need. There are few different things in code below.
Change SingleTickerProviderStateMixin to TickerProviderStateMixin.
Use two animation controllers and animations (for fading out of unselected buttons and sliding left selected button).
Run animations sequentially (see animation tweens listeners).
Add bool _buttonSelected to track completion of animations.
Use _buttonSelected to build correct widget (Widget _buttonsWidget())
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
int _pressedButton = -1;
bool _buttonSelected = false;
Animation<double> _animationFadeOut;
AnimationController _controllerFadeOut;
Animation<double> _animationSlideLeft;
AnimationController _controllerSlideLeft;
#override
void initState() {
super.initState();
_initFadeOutAnimation();
_initSlideLeftAnimation();
}
void _initFadeOutAnimation() {
_controllerFadeOut = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
_animationFadeOut = Tween<double>(begin: 1.0, end: 0.0)
.animate(_controllerFadeOut)
..addListener(() {
setState(() {
if (_controllerFadeOut.isCompleted && !_controllerSlideLeft.isAnimating) {
_controllerSlideLeft.forward();
}
});
});
}
void _initSlideLeftAnimation() {
_controllerSlideLeft = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
_animationSlideLeft = Tween<double>(begin: 1.0, end: 0.0)
.animate(_controllerSlideLeft)
..addListener(() {
setState(() {
if (_controllerSlideLeft.isCompleted) {
_buttonSelected = true;
}
});
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Test"),
),
body: Column(
children: <Widget>[
_buttonsWidget(),
Text(_pressedButton != null ? "$_pressedButton button pressed" : "No button pressed"),
RaisedButton(
child: Text("Reset"),
onPressed: () {
_controllerFadeOut.reset();
_controllerSlideLeft.reset();
_pressedButton = -1;
_buttonSelected = false;
}
)
]
)
);
}
Widget _buttonsWidget() {
if (_buttonSelected) {
return Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[_buttonWidget(_pressedButton)]
);
} else {
return Row(
children: <Widget>[
_animatedButtonWidget(1),
_animatedButtonWidget(2),
_animatedButtonWidget(3),
_animatedButtonWidget(4)
]
);
}
}
Widget _animatedButtonWidget(int button) {
if (button == _pressedButton) {
return _buttonWidget(button);
} else {
return SizeTransition(
sizeFactor: _animationSlideLeft,
axis: Axis.horizontal,
child: Opacity(
opacity: _animationFadeOut.value,
child: _buttonWidget(button)
)
);
}
}
Widget _buttonWidget(int button) {
return RaisedButton(
onPressed: () => _onButtonClick(button),
child: Text("Button $button")
);
}
void _onButtonClick(int button) {
setState(() {
_pressedButton = button;
});
_controllerFadeOut.forward();
}
}
Related
I have a page in my app which if opened, displays 3 text flying in from the left of the screen, one after the other at a duration of 1 sec, to sit in the center of the page. How can I achieve this animation and what flutter package must I use?
I made the most basic Animation components, I hope useful to you
import 'dart:math';
import 'package:flutter/material.dart';
enum AnimationType {
ROTATION,
OFFSET,
}
enum OffsetType {
UP,
DOWN,
LEFT,
RIGHT,
}
typedef AnimationSwich = bool Function();
class BaseAnimationWidget extends StatefulWidget {
const BaseAnimationWidget(
{Key? key,
required this.type,
required this.body,
this.animationSwich,
this.rotationValue,
this.offset,
this.duration,
this.offsetType})
: assert(type == AnimationType.ROTATION
? rotationValue != null
: type == AnimationType.OFFSET
? offset != null && offsetType != null
: true),
super(key: key);
final AnimationSwich? animationSwich;
final Widget body;
final Offset? offset;
final double? rotationValue;
final AnimationType type;
final Duration? duration;
final OffsetType? offsetType;
#override
State<BaseAnimationWidget> createState() => _BaseAnimationWidgetState();
}
class _BaseAnimationWidgetState extends State<BaseAnimationWidget>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
#override
void initState() {
super.initState();
_animationController = AnimationController(
duration: widget.duration ?? Duration(milliseconds: 300), vsync: this);
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
Offset get _offset => widget.offsetType == OffsetType.UP ||
widget.offsetType == OffsetType.DOWN
? Offset(
widget.offset!.dx, widget.offset!.dy * _animationController.value)
: Offset(
widget.offset!.dx * _animationController.value, widget.offset!.dy);
#override
Widget build(BuildContext context) {
if (widget.animationSwich == null) {
DoNothingAction();
} else if (mounted && widget.animationSwich!()) {
_animationController.forward();
} else if (mounted && !widget.animationSwich!()) {
_animationController.reverse();
}
return AnimatedBuilder(
animation: _animationController,
builder: (context, _) {
return widget.type == AnimationType.ROTATION
? Transform.rotate(
angle: widget.rotationValue! * _animationController.value,
child: widget.body)
: Transform.translate(
offset: _offset,
child: widget.body,
);
});
}
}
class MyWidget extends StatefulWidget {
const MyWidget({super.key});
#override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
var _animationSwich = true;
#override
Widget build(BuildContext context) {
var width = MediaQuery.of(context).size.width;
var height = MediaQuery.of(context).size.height;
return Scaffold(
body: SafeArea(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Left
BaseAnimationWidget(
type: AnimationType.OFFSET,
duration: Duration(seconds: 1),
body: Text('Test'),
offset: Offset(-width, 0),
offsetType: OffsetType.LEFT,
animationSwich: () => _animationSwich,
),
// Right
BaseAnimationWidget(
type: AnimationType.OFFSET,
duration: Duration(seconds: 1),
body: Text('Test'),
offset: Offset(width, 0),
offsetType: OffsetType.RIGHT,
animationSwich: () => _animationSwich,
),
// Up
BaseAnimationWidget(
type: AnimationType.OFFSET,
duration: Duration(seconds: 1),
body: Text('Test'),
offset: Offset(0, -height),
offsetType: OffsetType.UP,
animationSwich: () => _animationSwich,
),
// Down
BaseAnimationWidget(
type: AnimationType.OFFSET,
duration: Duration(seconds: 1),
body: Text('Test'),
offset: Offset(0, height),
offsetType: OffsetType.DOWN,
animationSwich: () => _animationSwich,
),
// Rotation
BaseAnimationWidget(
type: AnimationType.ROTATION,
duration: Duration(seconds: 1),
body: Text('Test'),
offset: Offset(-width, 0),
offsetType: OffsetType.LEFT,
rotationValue: pi,
animationSwich: () => _animationSwich,
),
TextButton(
onPressed: () {
_animationSwich = !_animationSwich;
setState(() {});
},
child: Text('Action!'))
],
))),
);
}
}
I am trying to change the widget size (scale) when I drag the widget up. I am using the GestureDetector and onPanUpdate method and Now I want to by Matrix4.identity to change the size of the widget:
return GestureDetector(
onPanUpdate: (details) {
_position += details.delta;
setState(() {});
},
child: Stack(
children: [
SizedBox(
child: LayoutBuilder(builder: (context, constraints) {
final milliSeconds = _isDragging ? 0 : 400;
final center = constraints.smallest.center(Offset.zero);
final scaledMatrix = Matrix4.identity();
// ..translate(center.dx, center.dy)
// ..scale(1.0, 0.5)
// ..translate(-center.dx, -center.dy);
return AnimatedContainer(
curve: Curves.easeInOut,
duration: Duration(milliseconds: milliSeconds),
transform: scaledMatrix
..translate(_position.dx, _position.dy),
child: widget.widgets[0]);
}),
),
],
));
Now when the widget is dragged I change the position but how can I change the widget scale ?
You can use Matrix4.identity()..scale api to achieve this.
import 'package:flutter/material.dart';
void main() => runApp(MaterialApp(
home: Scaffold(appBar: AppBar(), body: _Drag()),
));
class _Drag extends StatefulWidget {
#override
_DragState createState() => _DragState();
}
class _DragState extends State<_Drag> with SingleTickerProviderStateMixin {
bool shouldScaleDown = true; // change value when needed
double _top = 300; // distance to top
double _left = 150; // distance to left
double scale = 1;
#override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
top: _top,
left: _left,
child: GestureDetector(
onPanDown: (DragDownDetails e) {
print("fingure down: ${e.globalPosition}");
},
onPanUpdate: (DragUpdateDetails e) {
setState(() {
_left += e.delta.dx;
_top += e.delta.dy;
if (e.delta.dy > 0) {
scale = scale - 0.01;
}
if (e.delta.dy < 0) {
scale = scale + 0.01;
}
});
},
onPanEnd: (DragEndDetails e) {
print("fingure away from screen");
print(e.velocity);
},
child: AnimatedContainer(
color: Colors.blueAccent,
width: 100,
height: 100,
duration: Duration(milliseconds: 300),
transform: Matrix4.identity()..scale(scale, scale),
child: Container(),
),
),
)
],
);
}
}
I have an app, it has a page that act as an entry point and showing a TabView containing 3 or more pages on it. It uses NestedScrollView and SliverAppBar to give some animation when user scroll the view.
I want to implement lazy load of a paginated list but since it does not allows me to use a controller inside the CustomScrollView as mentioned in the docs in this line:
builder: (BuildContext context) {
return CustomScrollView(
// The "controller" and "primary" members should be left
// unset, so that the NestedScrollView can control this
// inner scroll view.
// If the "controller" property is set, then this scroll
// view will not be associated with the NestedScrollView.
// The PageStorageKey should be unique to this ScrollView;
// it allows the list to remember its scroll position when
// the tab view is not on the screen.
key: PageStorageKey<String>(name),
slivers: <Widget>[
I cannot make use of ScrollController in the child page to get the scroll value to trigger the loadMore function. Fortunately, there is a similar widget to listen the scroll event called ScrollNotification. But I don't know which property is holding the value of the maximum scroll limit.
Tried to compare the available properties by this:
bool _onScrollNotification(ScrollNotification notification) {
if (notification is! ScrollEndNotification) return false;
print('extentBefore: ${notification.metrics.extentBefore}');
print('extentAfter: ${notification.metrics.extentAfter}');
print('maxScrollExtent: ${notification.metrics.maxScrollExtent}');
return true;
}
But its seems like they doesn't hold any fixed value as I need. It always changed its value independently.
I also cannot use the ScrollController on the parent page (the tabview_holder) since each page in each tabs has independent bloc, events, data & fetching algorithm. With that in mind, how can I achieve this requirement?
Please have a look at my script:
tabview_holder.dart (not a real file name, just to illustrate it)
class EventPage extends StatefulWidget {
EventPage({Key key}) : super(key: key);
#override
_EventPageState createState() => _EventPageState();
}
class _EventPageState extends State<EventPage>
with SingleTickerProviderStateMixin {
final ScrollController _scrollController = ScrollController();
final List<Widget> _tabs = [
Tab(text: 'My Events'),
Tab(text: "Private Events"),
Tab(text: "Division Events"),
Tab(text: "Department Events"),
Tab(text: "Public Events"),
];
double _bottomNavigatorPosition = 0.0;
double _gradientStop = 0.2;
TabController _tabController;
#override
void initState() {
super.initState();
_scrollController.addListener(_scrollListener);
_tabController = TabController(
initialIndex: 0,
length: _tabs.length,
vsync: this,
);
}
#override
void dispose() {
_scrollController.dispose();
_tabController.dispose();
super.dispose();
}
void _scrollListener() {
ScrollDirection direction = _scrollController.position.userScrollDirection;
switch (direction) {
case ScrollDirection.reverse:
setState(() {
_gradientStop = 0.0;
_bottomNavigatorPosition = -100.0;
});
return;
break;
case ScrollDirection.forward:
case ScrollDirection.idle:
setState(() {
_gradientStop = 0.2;
_bottomNavigatorPosition = 0.0;
});
break;
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Stack(
children: [
NestedScrollView(
controller: _scrollController,
headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(
context),
sliver: SliverAppBar(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
automaticallyImplyLeading: false,
floating: true,
expandedHeight: 100,
flexibleSpace: FlexibleSpaceBar(
background: Container(
child: Stack(
children: [
Positioned(
left: 30.0,
bottom: 10,
child: PageHeader(title: 'Events'),
),
],
),
),
),
),
),
SliverPersistentHeader(
pinned: true,
delegate: _SliverAppBarDelegate(
TabBar(
controller: _tabController,
isScrollable: true,
indicator: BubbleTabIndicator(
indicatorHeight: 35.0,
indicatorColor: Theme.of(context).primaryColor,
tabBarIndicatorSize: TabBarIndicatorSize.tab,
),
tabs: _tabs,
),
),
),
];
},
body: TabBarView(
controller: _tabController,
children: [
MyEventsPage(),
PrivateEventsPage(),
MyEventsPage(),
MyEventsPage(),
MyEventsPage(),
],
),
),
_buildBottomGradient(),
_buildBottomNavigator(),
],
),
),
);
}
Widget _buildBottomGradient() {
return IgnorePointer(
child: AnimatedContainer(
duration: Duration(milliseconds: 200),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
stops: [_gradientStop / 2, _gradientStop],
colors: [
Color(0xFF121212),
Colors.transparent,
],
),
),
),
);
}
Widget _buildBottomNavigator() {
return AnimatedPositioned(
duration: Duration(milliseconds: 200),
left: 0.0,
right: 0.0,
bottom: _bottomNavigatorPosition,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: PageNavigator(
primaryButtonText: 'Create new event',
onPressedPrimaryButton: () {
Navigator.of(context).pushNamed(Routes.EVENT_CREATE);
},
),
),
);
}
}
tabview_item.dart
class MyEventsPage extends StatefulWidget {
MyEventsPage({Key key}) : super(key: key);
#override
_MyEventsPageState createState() => _MyEventsPageState();
}
class _MyEventsPageState extends State<MyEventsPage>
with AutomaticKeepAliveClientMixin<MyEventsPage> {
Completer<void> _refreshCompleter;
PaginatedEvent _paginated;
MyEventsBloc _myEventsBloc;
bool _isFetchingMoreInBackground;
#override
void initState() {
super.initState();
_myEventsBloc = BlocProvider.of<MyEventsBloc>(context);
_myEventsBloc.add(MyEventsPageInitialized());
_refreshCompleter = Completer<void>();
_isFetchingMoreInBackground = false;
}
void _set(PaginatedEvent paginated) {
setState(() {
_paginated = paginated;
});
_refreshCompleter?.complete();
_refreshCompleter = Completer();
}
void _add(Event data) {
setState(() {
_paginated.data.add(data);
});
}
void _update(Event data) {
final int index = _paginated.data.indexWhere((leave) {
return leave.id == data.id;
});
setState(() {
_paginated.data[index] = data;
});
}
void _destroy(Event data) {
final int index = _paginated.data.indexWhere((leave) {
return leave.id == data.id;
});
setState(() {
_paginated.data.removeAt(index);
});
}
void _append(PaginatedEvent paginated) {
setState(() {
_paginated.currentPage = paginated.currentPage;
_paginated.data.addAll(paginated.data);
});
}
bool _onScrollNotification(ScrollNotification notification) {
if (notification is! ScrollEndNotification) return false;
print('extentBefore: ${notification.metrics.extentBefore}');
print('extentAfter: ${notification.metrics.extentAfter}');
print('maxScrollExtent: ${notification.metrics.maxScrollExtent}');
return true;
}
#override
Widget build(BuildContext context) {
super.build(context);
return RefreshIndicator(
onRefresh: () {
_myEventsBloc.add(MyEventsRefreshRequested());
return _refreshCompleter.future;
},
child: NotificationListener<ScrollNotification>(
onNotification: _onScrollNotification,
child: CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
SliverToBoxAdapter(
child: BlocConsumer<MyEventsBloc, MyEventsState>(
listener: (context, state) {
if (state is MyEventsLoadSuccess) {
_set(state.data);
}
if (state is MyEventsCreateSuccess) {
_add(state.data);
}
if (state is MyEventsUpdateSuccess) {
_update(state.data);
}
if (state is MyEventsDestroySuccess) {
_destroy(state.data);
}
if (state is MyEventsLoadMoreSuccess) {
_append(state.data);
}
},
builder: (context, state) {
if (state is MyEventsLoadSuccess) {
return EventList(data: _paginated.data);
}
return ListLoading();
},
),
),
],
),
),
);
}
#override
bool get wantKeepAlive => true;
}
Finally found the answer by my own after doing some research. Not a perfect solution but it works.
bool _onScrollNotification(UserScrollNotification notification) {
/// Make sure it listening to the nearest depth of scrollable views
/// and ignore notifications if scroll axis is not vertical.
if (notification.depth == 0 && notification.metrics.axis == Axis.vertical) {
ScrollDirection direction = notification.direction;
if (direction == ScrollDirection.reverse && !_isFetchingMoreData) {
/// Check if the user is scrolling the list downward to prevent
/// function call on upward. Also check if there is any fetch
/// queues, if it still fetching, skip this step and do nothing.
/// It was necessary to prevent the notification to bubble up
/// the widget with `_loadMoreData()` call.
if (_paginated.currentPage < _paginated.lastPage)
/// If the conditions above are passed, we are safe to load more.
return _loadMoreData();
}
}
return true;
}
I just build and deployed a flutter web app. The problem I encountered is that it doesn't scroll when I press arrow keys, also there is no scroll bar. (Only 2 figure gesture scrolling is possible)
I'm using SingleChildScrollView() with the column as its child.
Is there a way to implement them?
Or just one of them?
The code from Karan works, but when the app is in Debug Mode, instead of using the event.logicalKey.debugName == "Arrow Up", we could use event.logicalKey == LogicalKeyboardKey.arrowUp which works in both the debug and release mode.
class _MyKeyboardScrollingPageState extends State<MyKeyboardScrollingPage> {
final ScrollController _controller = ScrollController();
final FocusNode _focusNode = FocusNode();
void _handleKeyEvent(RawKeyEvent event) {
var offset = _controller.offset;
if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
setState(() {
if (kReleaseMode) {
_controller.animateTo(offset - 200, duration: Duration(milliseconds: 30), curve: Curves.ease);
} else {
_controller.animateTo(offset - 200, duration: Duration(milliseconds: 30), curve: Curves.ease);
}
});
}
else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
setState(() {
if (kReleaseMode) {
_controller.animateTo(offset + 200, duration: Duration(milliseconds: 30), curve: Curves.ease);
} else {
_controller.animateTo(offset + 200, duration: Duration(milliseconds: 30), curve: Curves.ease);
}
});
}
}
#override
void dispose() {
_focusNode.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: RawKeyboardListener(
autoFocus = true,
focusNode = _focusNode,
onKey: _handleKeyEvent,
child: SingleChildScrollView(
controller: _controller,
child: SomeAwesomeWidget(),
),
),
);
}
}
I found one solution ...
Hope this helps someone with the same issue...
Using RawKeyboardListener(), we can listen to any keyboard stroke.
class _MyHomePageState extends State<MyHomePage> {
final ScrollController _controller = ScrollController();
final FocusNode _focusNode = FocusNode()
#override
void dispose() {
_focusNode.dispose();
super.dispose();
}
void _handleKeyEvent(RawKeyEvent event) {
var offset = _controller.offset; //Getting current position
if (event.logicalKey.debugName == "Arrow Down") {
setState(() {
if (kReleaseMode) {
//This block only runs when the application was compiled in release mode.
_controller.animateTo(offset + 50,
duration: Duration(milliseconds: 200), curve: Curves.ease);
} else {
// This will only print useful information in debug mode.
// print(_controller.position); to get information..
_controller.animateTo(offset + 50,
duration: Duration(milliseconds: 200), curve: Curves.ease);
}
});
} else if (event.logicalKey.debugName == "Arrow Up"){
setState(() {
if (kReleaseMode) {
_controller.animateTo(offset - 50,
duration: Duration(milliseconds: 200), curve: Curves.ease);
} else {
_controller.animateTo(offset - 50,
duration: Duration(milliseconds: 200), curve: Curves.ease);
}
});
#override
Widget build(BuildContext context) {
return Scaffold(
body: RawKeyboardListener(
autofocus: true,
focusNode: _focusNode,
onKey: _handleKeyEvent,
child: SingleChildScrollView(
controller: _controller,
child:...
}
}
You can wrap ScrollBar to SingleChildScrollView to show scroll bar, like this:
Scrollbar(
child: SingleChildScrollView(
child: Container(),
));
For the answers that are mentioned above to work, you need the following imports:
import 'package:flutter/services.dart';
import 'package:flutter/foundation.dart';
The Most simplest way to Scroll using mouse or Keyboard Arrow keys on Flutter Web is
ListView(
primary: true,
scrollDirection: Axis.vertical,
children:
No need to pass any ScrollController
I'm doing something similar to this video: https://youtu.be/fpqHUp4Sag0
With the following code I generate the listview but when using the controller in this way the element is located at the top of the listview and I need it to be centered
Widget _buildLyric() {
return ListView.builder(
itemBuilder: (BuildContext context, int index) => _buildPhrase(lyric[index]),
itemCount: lyric.length,
itemExtent: 90.0,
controller: _scrollController,
);
}
void goToNext() {
i += 1;
if (i == lyric.length - 1) {
setState(() {
finishedSync = true;
});
}
syncLyric.addPhrase(
lyric[i], playerController.value.position.inMilliseconds);
_scrollController.animateTo(i*90.0,
curve: Curves.ease, duration: new Duration(milliseconds: 300));
}
Using center and shrinkWrap: true
Center(
child: new ListView.builder(
shrinkWrap: true,
itemCount: list.length,
itemBuilder: (BuildContext context, int index) {
return Text("Centered item");
},
),
);
You're going to have to do some math! (Nooooo, not the mathssssss).
It seems as though your goToNext() function is called while the app is running, rather than during build time. This makes it a little easier - you can simply use context.size. Otherwise you'd have to use a LayoutBuilder and maxHeight.
You can then divide this in two to get the half, then add/subtract whatever you need to get your item positioned how you want (since you've specified it's height as 90 in the example, I assume you could use 45 to get what you want).
Here's an example you can paste into a file to run:
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(Wid());
class Wid extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text("Scrolling by time"),
),
body: new Column(
children: <Widget>[
Expanded(child: Container()),
Container(
height: 300.0,
color: Colors.orange,
child: ScrollsByTime(
itemExtent: 90.0,
),
),
Expanded(child: Container()),
],
),
),
);
}
}
class ScrollsByTime extends StatefulWidget {
final double itemExtent;
const ScrollsByTime({Key key, #required this.itemExtent}) : super(key: key);
#override
ScrollsByTimeState createState() {
return new ScrollsByTimeState();
}
}
class ScrollsByTimeState extends State<ScrollsByTime> {
final ScrollController _scrollController = new ScrollController();
#override
void initState() {
super.initState();
Timer.periodic(Duration(seconds: 1), (timer) {
_scrollController.animateTo(
(widget.itemExtent * timer.tick) - context.size.height / 2.0 + widget.itemExtent / 2.0,
duration: Duration(milliseconds: 300),
curve: Curves.ease,
);
});
}
#override
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder: (context, index) {
return Center(child: Text("Item $index"));
},
itemExtent: widget.itemExtent,
controller: _scrollController,
);
}
}
I had a similar problem, but with the horizontal listview. You should use ScrollController and NotificationListener. When you receive endScroll event you should calculate offset and use scroll controller animateTo method to center your items.
class SwipeCalendarState extends State<SwipeCalendar> {
List<DateTime> dates = List();
ScrollController _controller;
final itemWidth = 100.0;
#override
void initState() {
_controller = ScrollController();
_controller.addListener(_scrollListener);
for (var i = 1; i < 365; i++) {
var date = DateTime.now().add(Duration(days: i));
dates.add(date);
}
// TODO: implement initState
super.initState();
}
#override
Widget build(BuildContext context) {
// TODO: implement build
return Container(
height: 200,
child: Stack(
children: <Widget>[buildListView()],
),
);
}
void _onStartScroll(ScrollMetrics metrics) {
}
void _onUpdateScroll(ScrollMetrics metrics){
}
void _onEndScroll(ScrollMetrics metrics){
print("scroll before = ${metrics.extentBefore}");
print("scroll after = ${metrics.extentAfter}");
print("scroll inside = ${metrics.extentInside}");
var halfOfTheWidth = itemWidth/2;
var offsetOfItem = metrics.extentBefore%itemWidth;
if (offsetOfItem < halfOfTheWidth) {
final offset = metrics.extentBefore - offsetOfItem;
print("offsetOfItem = ${offsetOfItem} offset = ${offset}");
Future.delayed(Duration(milliseconds: 50), (){
_controller.animateTo(offset, duration: Duration(milliseconds: 100), curve: Curves.linear);
});
} else if (offsetOfItem > halfOfTheWidth){
final offset = metrics.extentBefore + offsetOfItem;
print("offsetOfItem = ${offsetOfItem} offset = ${offset}");
Future.delayed(Duration(milliseconds: 50), (){
_controller.animateTo(offset, duration: Duration(milliseconds: 100), curve: Curves.linear);
});
}
}
Widget buildListView() {
return NotificationListener<ScrollNotification>(
onNotification: (scrollNotification) {
if (scrollNotification is ScrollStartNotification) {
_onStartScroll(scrollNotification.metrics);
} else if (scrollNotification is ScrollUpdateNotification) {
_onUpdateScroll(scrollNotification.metrics);
} else if (scrollNotification is ScrollEndNotification) {
_onEndScroll(scrollNotification.metrics);
}
},
child: ListView.builder(
itemCount: dates.length,
controller: _controller,
scrollDirection: Axis.horizontal,
itemBuilder: (context, i) {
var item = dates[i];
return Container(
height: 100,
width: itemWidth,
child: Center(
child: Text("${item.day}.${item.month}.${item.year}"),
),
);
}),
);
}
}
IMO the link you have posted had some wheel like animation. Flutter provides this type of animation with ListWheelScrollView and rest can be done with the fade in animation and change in font weight with ScrollController.