Related
I'm trying to build a screen where two vertically stacked ListViews cause themselves to grow and shrink as a result of being scrolled. Here is an illustration:
The initial state is that both lists take up 50% of the top and bottom of the screen respectively. When the user starts dragging the top list downward (to scroll up) it will initially cause the list to expand to take up 75% of the screen before the normal scrolling behavior starts; when the user changes direction, dragging upwards (to scroll down), then as they get to the bottom of the list it will cause the list to shrink back up to only taking up 50% of the screen (the initial state).
The bottom list would work similarly, dragging up would cause the list to expand upwards to take up 75% of the screen before the normal scrolling behavior starts; when the user changes direction, dragging downwards (to scroll up), then as they get to the top of the list it will shrink back to 50% of the screen.
Here is an animation of what it should look like:
https://share.cleanshot.com/mnZhJF8x
My question is, what is the best widget combination to implement this and how do I tie the scrolling events with resizing the ListViews?
So far, this is as far as I've gotten:
Column(
children: [
SizedBox(
height: availableHeight / 2,
child: ListView(...)
),
Expanded(child: ListView(...)),
],
),
In terms of similar behavior, it appears that the CustomScrollView and SliverAppBar have some of the elements in scrolling behaving I'm going after but it's not obvious to me how to convert that into the the two adjacent lists view I described above.
Any advice would be greatly appreciated, thank you!
hi Check this,
Column(
children: [
Expanded (
flex:7,
child: Container(
child: ListView.builder(
itemCount:50,
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: const Icon(Icons.list),
trailing: const Text(
"GFG",
style: TextStyle(color: Colors.green, fontSize: 15),
),
title: Text("List item $index"));
}),
),
),
Expanded (
flex:3,
child: Container(
child: ListView.builder(
itemCount:50,
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: const Icon(Icons.list),
trailing: const Text(
"GFG",
style: TextStyle(color: Colors.green, fontSize: 15),
),
title: Text("aaaaaaaaa $index"));
}),
),
),
],
),
edit: refactored and maybe better version:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'ExtentableTwoRowScrollable Demo',
home: Scaffold(
body: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return ExtentableTwoRowScrollable(
height: constraints.maxHeight,
);
}),
),
);
}
}
// sorry for the name :)
class ExtentableTwoRowScrollable extends StatefulWidget {
const ExtentableTwoRowScrollable({
super.key,
required this.height,
this.minHeight = 150.0,
});
final double height;
final double minHeight;
#override
State<ExtentableTwoRowScrollable> createState() =>
_ExtentableTwoRowScrollableState();
}
class _ExtentableTwoRowScrollableState extends State<ExtentableTwoRowScrollable>
with SingleTickerProviderStateMixin {
final upperSizeNotifier = ValueNotifier(0.0);
final lowerSizeNotifier = ValueNotifier(0.0);
var upperHeight = 0.0;
var dragOnUpper = true;
void incrementNotifier(ValueNotifier notifier, double increment) {
if (notifier.value + increment >= widget.height - widget.minHeight) return;
if (notifier.value + increment < widget.minHeight) return;
notifier.value += increment;
}
bool handleVerticalDrag(ScrollNotification notification) {
if (notification is ScrollStartNotification &&
notification.dragDetails != null) {
if (notification.dragDetails!.globalPosition.dy <
upperSizeNotifier.value) {
dragOnUpper = true;
} else {
dragOnUpper = false;
}
}
if (notification is ScrollUpdateNotification) {
final delta = notification.scrollDelta ?? 0.0;
if (dragOnUpper) {
if (notification.metrics.extentAfter != 0) {
incrementNotifier(upperSizeNotifier, delta.abs());
incrementNotifier(lowerSizeNotifier, -1 * delta.abs());
} else {
incrementNotifier(upperSizeNotifier, -1 * delta.abs());
incrementNotifier(lowerSizeNotifier, delta.abs());
}
}
if (!dragOnUpper) {
if (notification.metrics.extentBefore != 0) {
incrementNotifier(upperSizeNotifier, -1 * delta.abs());
incrementNotifier(lowerSizeNotifier, delta.abs());
} else {
incrementNotifier(upperSizeNotifier, delta.abs());
incrementNotifier(lowerSizeNotifier, -1 * delta.abs());
}
}
}
return true;
}
#override
Widget build(BuildContext context) {
// initialize ratio of lower and upper, f.e. here 50:50
upperSizeNotifier.value = widget.height / 2;
lowerSizeNotifier.value = widget.height / 2;
return NotificationListener(
onNotification: handleVerticalDrag,
child: Column(
children: [
ValueListenableBuilder<double>(
valueListenable: upperSizeNotifier,
builder: (context, value, child) {
return Container(
color: Colors.greenAccent,
height: value,
child: ListView.builder(
shrinkWrap: true,
itemCount: 40,
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: const Icon(Icons.list),
title: Text("upper ListView $index"));
},
),
);
},
),
ValueListenableBuilder<double>(
valueListenable: lowerSizeNotifier,
builder: (context, value, child) {
return Container(
color: Colors.blueGrey,
height: value,
child: ListView.builder(
shrinkWrap: true,
itemCount: 40,
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: const Icon(Icons.list),
title: Text("lower ListView $index"));
},
),
);
},
),
],
),
);
}
}
here is the older post:
so, here's my shot on this. There might be a less complicated solution of course but I think it's somewhat understandable. At least I've tried to comment good enough.
Let me know if it works for you.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'ExtentableTwoRowScrollable Demo',
home: Scaffold(
body: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return ExtentableTwoRowScrollable(
height: constraints.maxHeight,
);
}),
),
);
}
}
// sorry for the name :)
class ExtentableTwoRowScrollable extends StatefulWidget {
const ExtentableTwoRowScrollable({
super.key,
required this.height,
this.minHeightUpper = 300.0,
this.minHeightLower = 300.0,
});
final double height;
final double minHeightUpper;
final double minHeightLower;
#override
State<ExtentableTwoRowScrollable> createState() =>
_ExtentableTwoRowScrollableState();
}
class _ExtentableTwoRowScrollableState extends State<ExtentableTwoRowScrollable>
with SingleTickerProviderStateMixin {
final upperSizeNotifier = ValueNotifier(0.0);
final lowerSizeNotifier = ValueNotifier(0.0);
var upperHeight = 0.0;
var dragOnUpper = true;
bool handleVerticalDrag(ScrollNotification notification) {
if (notification is ScrollStartNotification &&
notification.dragDetails != null)
// only act on ScrollStartNotification events with dragDetails
{
if (notification.dragDetails!.globalPosition.dy <
upperSizeNotifier.value) {
dragOnUpper = true;
} else {
dragOnUpper = false;
}
}
if (notification is ScrollUpdateNotification &&
notification.dragDetails != null)
// only act on ScrollUpdateNotification events with dragDetails
{
if (dragOnUpper) {
// dragging is going on, was started on upper ListView
if (notification.dragDetails!.delta.direction > 0)
// dragging backward/downwards
{
if (lowerSizeNotifier.value >= widget.minHeightLower)
// expand upper until minHeightLower gets hit
{
lowerSizeNotifier.value -= notification.dragDetails!.delta.distance;
upperSizeNotifier.value += notification.dragDetails!.delta.distance;
}
} else
// dragging forward/upwards
{
if (notification.metrics.extentAfter == 0.0 &&
upperSizeNotifier.value > widget.minHeightUpper)
// when at the end of upper shrink it until minHeightUpper gets hit
{
lowerSizeNotifier.value += notification.dragDetails!.delta.distance;
upperSizeNotifier.value -= notification.dragDetails!.delta.distance;
}
}
}
if (!dragOnUpper) {
// dragging is going on, was started on lower ListView
if (notification.dragDetails!.delta.direction > 0)
// dragging backward/downwards
{
if (notification.metrics.extentBefore == 0.0 &&
lowerSizeNotifier.value > widget.minHeightLower)
// when at the top of lower shrink it until minHeightLower gets hit
{
lowerSizeNotifier.value -= notification.dragDetails!.delta.distance;
upperSizeNotifier.value += notification.dragDetails!.delta.distance;
}
} else
// dragging forward/upwards
{
if (upperSizeNotifier.value >= widget.minHeightUpper)
// expand lower until minHeightUpper gets hit
{
lowerSizeNotifier.value += notification.dragDetails!.delta.distance;
upperSizeNotifier.value -= notification.dragDetails!.delta.distance;
}
}
}
}
return true;
}
#override
Widget build(BuildContext context) {
// initialize ratio of lower and upper, f.e. here 50:50
upperSizeNotifier.value = widget.height / 2;
lowerSizeNotifier.value = widget.height / 2;
return NotificationListener(
onNotification: handleVerticalDrag,
child: Column(
children: [
ValueListenableBuilder<double>(
valueListenable: upperSizeNotifier,
builder: (context, value, child) {
return Container(
color: Colors.greenAccent,
height: value,
child: ListView.builder(
shrinkWrap: true,
itemCount: 40,
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: const Icon(Icons.list),
title: Text("upper ListView $index"));
},
),
);
},
),
ValueListenableBuilder<double>(
valueListenable: lowerSizeNotifier,
builder: (context, value, child) {
return Container(
color: Colors.blueGrey,
height: value,
child: ListView.builder(
shrinkWrap: true,
itemCount: 40,
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: const Icon(Icons.list),
title: Text("lower ListView $index"));
},
),
);
},
),
],
),
);
}
}
I think it's working okayish so far but supporting the "fling" effect - I mean the acceleration when users shoot the scrollable until simulated physics slows it down again - would be really nice, too.
First, initialise two scroll controllers for two of your listviews. Then register a post-frame callback by using WidgetsBinding.instance.addPostFrameCallback to make sure that the scroll controller has been linked to a scroll view. Next, setup scroll listeners in that callback.
To listen to scrolling update you can use scrollController.addListener. Then use if-else cases to catch the position of the scroll, if scroll position equals to maxScrollExtent then the user scrolled bottom and its the other way round for minScrollExtent. Check my edited implementation below:
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
#override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final ScrollController _scrollCtrl1 = ScrollController();
final ScrollController _scrollCtrl2 = ScrollController();
double height1 = 300;
double height2 = 300;
bool isLoading = true;
#override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
setState(() {
isLoading = false;
height1 = SizeConfig.blockSizeVertical! * 50;
height2 = SizeConfig.blockSizeVertical! * 50;
});
_scrollCtrl1.addListener(() {
if (_scrollCtrl1.position.pixels == _scrollCtrl1.position.maxScrollExtent) {
setState(() {
height1 = SizeConfig.blockSizeVertical! * 25;
height2 = SizeConfig.blockSizeVertical! * 75;
});
}
if (_scrollCtrl1.position.pixels == _scrollCtrl1.position.minScrollExtent) {
setState(() {
height1 = SizeConfig.blockSizeVertical! * 75;
height2 = SizeConfig.blockSizeVertical! * 25;
});
}
});
_scrollCtrl2.addListener(() {
if (_scrollCtrl2.position.pixels == _scrollCtrl2.position.maxScrollExtent) {
setState(() {
height1 = SizeConfig.blockSizeVertical! * 25;
height2 = SizeConfig.blockSizeVertical! * 75;
});
}
if (_scrollCtrl2.position.pixels == _scrollCtrl2.position.minScrollExtent) {
setState(() {
height1 = SizeConfig.blockSizeVertical! * 75;
height2 = SizeConfig.blockSizeVertical! * 25;
});
}
});
});
super.initState();
}
#override
Widget build(BuildContext context) {
SizeConfig().init(context);
return Scaffold(
body: !isLoading ? Column(
children: [
AnimatedContainer(
color: Colors.blueGrey,
height: height1,
duration: const Duration(seconds: 1),
curve: Curves.fastOutSlowIn,
child: ListView.builder(
itemCount: 50,
padding: EdgeInsets.zero,
controller: _scrollCtrl1,
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: const Icon(Icons.list),
dense: true,
trailing: const Text(
"GFG",
style: TextStyle(color: Colors.green, fontSize: 15),
),
title: Text("List item $index"));
}),
),
AnimatedContainer(
height: height2,
color: Colors.deepPurpleAccent,
duration: const Duration(seconds: 1),
curve: Curves.fastOutSlowIn,
child: ListView.builder(
itemCount: 50,
padding: EdgeInsets.zero,
controller: _scrollCtrl2,
itemBuilder: (BuildContext context, int index) {
return ListTile(
dense: true,
leading: const Icon(Icons.list),
trailing: const Text(
"GFG",
style: TextStyle(color: Colors.green, fontSize: 15),
),
title: Text("aaaaaaaaa $index"));
}),
),
],
) : const Center(child: CircularProgressIndicator(),),
);
}
}
class SizeConfig {
static MediaQueryData? _mediaQueryData;
static double? screenWidth;
static double? screenHeight;
static double? blockSizeHorizontal;
static double? blockSizeVertical;
/// This class measures the screen height & width.
/// Remember: Always call the init method at the start of your application or in main
void init(BuildContext? context) {
_mediaQueryData = MediaQuery.of(context!);
screenWidth = _mediaQueryData?.size.width;
screenHeight = _mediaQueryData?.size.height;
blockSizeHorizontal = (screenWidth! / 100);
blockSizeVertical = (screenHeight! / 100);
}
}
My main goal is, to create a flexible widget with features.
manage widget position.
rotate widget.
Resize the widget from all sides.
1 and 2, I've managed to make. but the 3rd point, when the widget finishes rotating and resizing the widget, the position will change and be erratic.
i really need you guys help.
thanks
this is my code.
import 'package:flutter/material.dart';
void main() {
return runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
home: const TestingDesign2(),
);
}
}
class Item{
Size size;
Offset offset;
double rotation;
var parentKey;
Item({required this.parentKey, required this.size, required this.offset, required this.rotation });
}
class TestingDesign2 extends StatefulWidget {
const TestingDesign2({super.key});
#override
State<TestingDesign2> createState() => _TestingDesign2State();
}
class _TestingDesign2State extends State<TestingDesign2> {
Item item = new Item(
parentKey: new GlobalKey(),
offset: Offset(10,10),
size: Size(150,150),
rotation: 0
);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Testing design 2")),
body: Container(
color: Colors.blueAccent.withOpacity(0.5),
child: Stack(
alignment: Alignment.center,
children: [ItemTesting(key: item.parentKey, item:item)],
),
));
}
}
class ItemTesting extends StatefulWidget {
Item item;
ItemTesting({super.key, required this.item});
#override
State<ItemTesting> createState() => _ItemTestingState();
}
class _ItemTestingState extends State<ItemTesting> {
double offsetAngle = 0;
bool isRotate = false;
#override
Widget build(BuildContext context) {
return _Item();
}
Widget _Item() {
return Positioned(
left: widget.item.offset.dx,
top: widget.item.offset.dy,
child: Transform.rotate(
angle: widget.item.rotation,
child: SizedBox(
height: widget.item.size.height,
width: widget.item.size.width,
child: Stack(
children: [
GestureDetector(
onPanStart: onPanStart,
onPanUpdate: onPanUpdate,
behavior: HitTestBehavior.translucent,
onPanEnd: (details) {
isRotate = false;
},
onPanCancel: () {
isRotate = false;
},
child: Container(
color: Colors.red,
height: widget.item.size.height,
width: widget.item.size.width,
),
),
IgnorePointer(
child: Align(
alignment: Alignment.topRight,
child: ClipOval(
child: Container(
height: 25,
width: 25,
color: Colors.blue,
child: Icon(Icons.rotate_right_outlined))),
),
),
Align(
alignment: Alignment.centerRight,
child: GestureDetector(
onPanUpdate: resizeRight,
child: Container(
height: 25,
width: 25,
color: Colors.yellow,
child: Icon(Icons.arrow_forward)))),
Align(
alignment: Alignment.bottomCenter,
child: GestureDetector(
onPanUpdate: resizeBottom,
child: Container(
height: 25,
width: 25,
color: Colors.yellow,
child: Icon(Icons.arrow_downward))))
],
),
),
),
);
}
var touchPosition = Offset.zero;
onPanStart(DragStartDetails details) {
Offset centerOfGestureDetector = Offset(widget.item.size.width / 2, widget.item.size.height / 2);
final touchPositionFromCenter =
details.localPosition - centerOfGestureDetector;
offsetAngle = touchPositionFromCenter.direction - widget.item.rotation;
final RenderBox referenceBox =
widget.item.parentKey.currentContext.findRenderObject();
var x = referenceBox.globalToLocal(details.globalPosition);
touchPosition = Offset(x.dx, x.dy + 55);
// top right
if (details.localPosition.dx > (widget.item.size.width - 25) &&
details.localPosition.dy <= 25) {
isRotate = true;
}
}
onPanUpdate(DragUpdateDetails details) {
if (isRotate) {
Offset centerOfGestureDetector = Offset(widget.item.size.width / 2, widget.item.size.height / 2);
final touchPositionFromCenter =
details.localPosition - centerOfGestureDetector;
widget.item.rotation = touchPositionFromCenter.direction - offsetAngle;
} else {
var positionG = widget.item.offset + details.globalPosition;
var positiong2 = positionG - touchPosition;
widget.item.offset = (positiong2 - widget.item.offset);
}
setState(() {});
}
//------------------------------------------------ resize widget
void resizeRight(DragUpdateDetails details) {
setState(() {
widget.item.size = Size(widget.item.size.width + details.delta.dx, widget.item.size.height);
});
}
void resizeBottom(DragUpdateDetails details) {
setState(() {
widget.item.size = Size(widget.item.size.width, widget.item.size.height + details.delta.dy);
});
}
}
https://dartpad.dev/?id=b1cb4617d4dd1a25e815a01135f95222
Rotational Offsets
You can compensate for the rotation by offsetting the position proportional to the cos and sin of the angle.
First, to use cos and sin functions, import the 'dart:math' library at the beginning:
import 'dart:math';
Then in your resizeRight function:
void resizeRight(DragUpdateDetails details) {
setState(() {
widget.item.size = Size(widget.item.size.width + details.delta.dx, widget.item.size.height);
var angle = widget.item.rotation;
var rotationalOffset = Offset(cos(angle) - 1, sin(angle)) * details.delta.dx / 2;
widget.item.offset += rotationalOffset;
});
}
Likewise, in your resizeBottom function:
void resizeBottom(DragUpdateDetails details) {
setState(() {
widget.item.size = Size(widget.item.size.width, widget.item.size.height + details.delta.dy);
var angle = widget.item.rotation;
var rotationalOffset = Offset(-sin(angle), cos(angle) - 1) * details.delta.dy / 2;
widget.item.offset += rotationalOffset;
});
}
When I build the application, I encounter such an error.
lib/input_page/pacman_slider.dart:41:7: Error: A value of type 'Animation<BorderRadius?>' can't be assigned to a variable of type 'Animation' because 'BorderRadius?' is nullable and 'BorderRadius' isn't.
'Animation' is from 'package:flutter/src/animation/animation.dart' ('../../Dev/flutter/packages/flutter/lib/src/animation/animation.dart').
'BorderRadius' is from 'package:flutter/src/painting/border_radius.dart' ('../../Dev/flutter/packages/flutter/lib/src/painting/border_radius.dart').
).animate(CurvedAnimation(
^
Error Code:
void initState() {
super.initState();
pacmanMovementController =
AnimationController(vsync: this, duration: Duration(milliseconds: 400));
_bordersAnimation = BorderRadiusTween(
begin: BorderRadius.circular(8.0),
end: BorderRadius.circular(50.0),
).animate(CurvedAnimation(
parent: widget.submitAnimationController,
curve: Interval(0.0, 0.07),
));
}
Full Code:
const double _pacmanWidth = 21.0;
const double _sliderHorizontalMargin = 24.0;
const double _dotsLeftMargin = 8.0;
class PacmanSlider extends StatefulWidget {
final VoidCallback onSubmit;
final AnimationController submitAnimationController;
const PacmanSlider(
{Key? key,
required this.onSubmit,
required this.submitAnimationController})
: super(key: key);
#override
_PacmanSliderState createState() => _PacmanSliderState();
}
class _PacmanSliderState extends State<PacmanSlider>
with TickerProviderStateMixin {
double _pacmanPosition = 24.0;
late Animation<BorderRadius> _bordersAnimation;
late Animation<double> _submitWidthAnimation;
late AnimationController pacmanMovementController;
late Animation<double> pacmanAnimation;
#override
void initState() {
super.initState();
pacmanMovementController =
AnimationController(vsync: this, duration: Duration(milliseconds: 400));
_bordersAnimation = BorderRadiusTween(
begin: BorderRadius.circular(8.0),
end: BorderRadius.circular(50.0),
).animate(CurvedAnimation(
parent: widget.submitAnimationController,
curve: Interval(0.0, 0.07),
));
}
#override
void dispose() {
pacmanMovementController.dispose();
super.dispose();
}
// double get width => _submitWidthAnimation?.value ?? 0.0;
double get width => _submitWidthAnimation.value;
#override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
_submitWidthAnimation = Tween<double>(
begin: constraints.maxWidth,
end: screenAwareSize(52.0, context),
).animate(CurvedAnimation(
parent: widget.submitAnimationController,
curve: Interval(0.05, 0.15),
));
return AnimatedBuilder(
animation: widget.submitAnimationController,
builder: (context, child) {
Decoration decoration = BoxDecoration(
borderRadius: _bordersAnimation.value,
color: Theme.of(context).primaryColor,
);
return Center(
child: Container(
height: screenAwareSize(52.0, context),
width: width,
decoration: decoration,
child: _submitWidthAnimation.isDismissed
? GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => _animatePacmanToEnd(),
child: Stack(
alignment: Alignment.centerRight,
children: <Widget>[
AnimatedDots(),
_drawDotCurtain(decoration),
_drawPacman(),
],
),
)
: Container(),
),
);
},
);
},
);
}
Widget _drawDotCurtain(Decoration decoration) {
if (width == 0.0) {
return Container();
}
double marginRight =
width - _pacmanPosition - screenAwareSize(_pacmanWidth / 2, context);
return Positioned.fill(
right: marginRight,
child: Container(decoration: decoration),
);
}
Widget _drawPacman() {
pacmanAnimation = _initPacmanAnimation();
return Positioned(
left: _pacmanPosition,
child: GestureDetector(
onHorizontalDragUpdate: (details) => _onPacmanDrag(width, details),
onHorizontalDragEnd: (details) => _onPacmanEnd(width, details),
child: PacmanIcon(),
),
);
}
Animation<double> _initPacmanAnimation() {
Animation<double> animation = Tween(
begin: _pacmanMinPosition(),
end: _pacmanMaxPosition(width),
).animate(pacmanMovementController);
animation.addListener(() {
setState(() {
_pacmanPosition = animation.value;
});
if (animation.status == AnimationStatus.completed) {
_onPacmanSubmit();
}
});
return animation;
}
_onPacmanSubmit() {
widget.onSubmit();
Future.delayed(Duration(seconds: 1), () => _resetPacman());
}
_onPacmanDrag(double width, DragUpdateDetails details) {
setState(() {
_pacmanPosition += details.delta.dx;
_pacmanPosition = math.max(_pacmanMinPosition(),
math.min(_pacmanMaxPosition(width), _pacmanPosition));
});
}
_onPacmanEnd(double width, DragEndDetails details) {
bool isOverHalf =
_pacmanPosition + screenAwareSize(_pacmanWidth / 2, context) >
0.5 * width;
if (isOverHalf) {
_animatePacmanToEnd();
} else {
_resetPacman();
}
}
_animatePacmanToEnd() {
pacmanMovementController.forward(
from: _pacmanPosition / _pacmanMaxPosition(width));
}
_resetPacman() {
if (this.mounted) {
setState(() => _pacmanPosition = _pacmanMinPosition());
}
}
double _pacmanMinPosition() =>
screenAwareSize(_sliderHorizontalMargin, context);
double _pacmanMaxPosition(double sliderWidth) =>
sliderWidth -
screenAwareSize(_sliderHorizontalMargin / 2 + _pacmanWidth, context);
}
class AnimatedDots extends StatefulWidget {
#override
_AnimatedDotsState createState() => _AnimatedDotsState();
}
class _AnimatedDotsState extends State<AnimatedDots>
with TickerProviderStateMixin {
final int numberOfDots = 10;
final double minOpacity = 0.1;
final double maxOpacity = 0.5;
late AnimationController hintAnimationController;
#override
void initState() {
super.initState();
_initHintAnimationController();
hintAnimationController.forward();
}
#override
void dispose() {
hintAnimationController.dispose();
super.dispose();
}
void _initHintAnimationController() {
hintAnimationController = AnimationController(
vsync: this,
duration: Duration(milliseconds: 800),
);
hintAnimationController.addStatusListener((status) {
if (status == AnimationStatus.completed) {
Future.delayed(Duration(milliseconds: 800), () {
if (this.mounted) {
hintAnimationController.forward(from: 0.0);
}
});
}
});
}
#override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(
left: screenAwareSize(
_sliderHorizontalMargin + _pacmanWidth + _dotsLeftMargin,
context),
right: screenAwareSize(_sliderHorizontalMargin, context)),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(numberOfDots, _generateDot)
..add(Opacity(
opacity: maxOpacity,
child: Dot(size: 14.0),
)),
),
);
}
Widget _generateDot(int dotNumber) {
Animation animation = _initDotAnimation(dotNumber);
return AnimatedBuilder(
animation: animation,
builder: (context, child) => Opacity(
opacity: animation.value,
child: child,
),
child: Dot(size: 9.0),
);
}
Animation<double> _initDotAnimation(int dotNumber) {
double lastDotStartTime = 0.4;
double dotAnimationDuration = 0.5;
double begin = lastDotStartTime * dotNumber / numberOfDots;
double end = begin + dotAnimationDuration;
return SinusoidalAnimation(min: minOpacity, max: maxOpacity).animate(
CurvedAnimation(
parent: hintAnimationController,
curve: Interval(begin, end),
),
);
}
}
class SinusoidalAnimation extends Animatable<double> {
SinusoidalAnimation({required this.min, required this.max});
final double min;
final double max;
#override
double transform(double t) {
return min + (max - min) * math.sin(math.pi * t);
}
}
class Dot extends StatelessWidget {
final double size;
const Dot({Key? key, required this.size}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
height: screenAwareSize(size, context),
width: screenAwareSize(size, context),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
),
);
}
}
class PacmanIcon extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(
right: screenAwareSize(16.0, context),
),
child: SvgPicture.asset(
'images/pacman.svg',
height: screenAwareSize(25.0, context),
width: screenAwareSize(21.0, context),
),
);
}
}
As you see in the attached capture, BorderRadiusTween has a nullable value of type BorderRadius?
I'd suggest to either declare _bordersAnimation as Animation<BorderRadius?>or just using an Animation<double> instead
I am trying to recreate RobinHood slide number animation, I am treating every digit as an Item in n animated list, When I am adding an digit to the start of the list the last Item in the list receives a jump and I cant quite figure out how to fix it.
this is the code for every digit
class NumberColView extends StatefulWidget {
final int animateTo;
final bool comma;
final TextStyle textStyle;
final Duration duration;
final Curve curve;
NumberColView(
{#required this.animateTo,
#required this.textStyle,
#required this.duration,
this.comma = false,
#required this.curve})
: assert(animateTo != null && animateTo >= 0 && animateTo < 10);
#override
_NumberColState createState() => _NumberColState();
}
class _NumberColState extends State<NumberColView>
with SingleTickerProviderStateMixin {
ScrollController _scrollController;
double _elementSize = 0.0;
#override
void initState() {
super.initState();
print(_elementSize);
_scrollController = new ScrollController();
WidgetsBinding.instance.addPostFrameCallback((_) {
_elementSize = _scrollController.position.maxScrollExtent / 10;
setState(() {});
});
}
#override
void didUpdateWidget(NumberColView oldWidget) {
if (oldWidget.animateTo != widget.animateTo) {
_scrollController.animateTo(_elementSize * widget.animateTo,
duration: widget.duration, curve: widget.curve);
}
super.didUpdateWidget(oldWidget);
}
#override
Widget build(BuildContext context) {
// print(widget.animateTo);
return Row(
mainAxisSize: MainAxisSize.min,
children: [
IgnorePointer(
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: _elementSize),
child: SingleChildScrollView(
controller: _scrollController,
child: Column(
children: List.generate(10, (position) {
return Text(position.toString(), style: widget.textStyle);
}),
),
),
),
),
widget.comma
? Container(
child: Text(', ',
style:
TextStyle(fontSize: 16, fontWeight: FontWeight.bold)))
: Container(),
],
);
}
}
This the code to build the whole number
class _NumberSlideAnimationState extends State<NumberSlideAnimation> {
#override
void didUpdateWidget(oldWidget) {
if (oldWidget.number.length != widget.number.length) {
int newLen = widget.number.length - oldWidget.number.length;
if(newLen > 0) {
widget.listKey.currentState.insertItem(0,
duration: const Duration(milliseconds: 200));
}
// setState(() {
// animateTo = widget.animateTo;
// });
}
super.didUpdateWidget(oldWidget);
}
Widget digitSlide(BuildContext context, int position, animation) {
int item = int.parse(widget.number[position]);
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 1),
end: Offset(0, 0),
).animate(animation),
child:
// TestCol(animateTo: item, style: widget.textStyle)
NumberColView(
textStyle: widget.textStyle,
duration: widget.duration,
curve: widget.curve,
comma: (widget.number.length - 1) != position &&
(widget.number.length - position) % 3 == 1 ??
true,
animateTo: item,
),
);
}
#override
Widget build(BuildContext context) {
return Container(
height: 40,
child: AnimatedList(
shrinkWrap: true,
scrollDirection: Axis.horizontal,
key: widget.listKey,
initialItemCount: widget.number.length,
itemBuilder: (context, position, animation) {
return digitSlide(context, position, animation);
},
),
);
}
}
I don't know the right solution for your problem, however I know another way and I am just sharing it.
Here the full code If you want to use this method:
update: now 0 will come form bottom as what it should be and the uncharged values will be in there palaces.
class Count extends StatefulWidget {
const Count({Key key}) : super(key: key);
#override
_CountState createState() => _CountState();
}
class _CountState extends State<Count> {
int count = 10;
int count2 = 10;
int count3 = 10;
Alignment alignment = Alignment.topCenter;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ClipRRect(
child: Align(
alignment: Alignment.center,
heightFactor: 0.2,
child: SizedBox(
height: 100,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
...List.generate(count.toString().length, (index) {
final num2IndexInRange =
count2.toString().length - 1 >= index;
final num3IndexInRange =
count3.toString().length - 1 >= index;
final num1 = int.parse(count.toString()[index]),
num2 = num2IndexInRange
? int.parse(count2.toString()[index])
: 9,
num3 = num3IndexInRange
? int.parse(count3.toString()[index])
: 1;
return AnimatedAlign(
key: Key("${num2} ${index}"),
duration: Duration(milliseconds: 500),
alignment: (num1 != num2 || num1 != num3)
? alignment
: Alignment.center,
child: Text(
num3.toString(),
style: TextStyle(
fontSize: 20,
),
),
);
}),
],
),
),
),
),
SizedBox(height: 200),
RaisedButton(
onPressed: () async {
setState(() {
alignment = Alignment.topCenter;
count += 10;
});
await Future.delayed(Duration(milliseconds: 250));
setState(() {
alignment = Alignment.bottomCenter;
count2 = count;
});
await Future.delayed(Duration(milliseconds: 250));
setState(() {
count3 = count;
});
},
),
],
),
);
}
}
if you don't like how the spacing between the numbers changed you can warp the text widget with a sized box and give it a fixed width.
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.