As it can be seen in the GIF below that whenever the child exits the parent Container
the child disappears without animation, which creates a bad impression on the user. How to add a smooth transition exit for the entering and exiting child ?
Chat GPT gave an answer which seems to have the right logic but it gives the same abrupt effect.
Chat GPT Code : -
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Smooth Transition in ListWheelScrollView')),
body: TransitionListWheel(),
),
);
}
}
class TransitionListWheel extends StatefulWidget {
@override
_TransitionListWheelState createState() => _TransitionListWheelState();
}
class _TransitionListWheelState extends State<TransitionListWheel> {
FixedExtentScrollController _scrollController;
double itemHeight = 100.0; // Height of each item in the list
int itemCount = 20;
@override
void initState() {
super.initState();
_scrollController = FixedExtentScrollController();
_scrollController.addListener(_handleScroll);
}
@override
void dispose() {
_scrollController.removeListener(_handleScroll);
_scrollController.dispose();
super.dispose();
}
void _handleScroll() {
setState(() {
// Trigger a rebuild to update the transition animations
});
}
@override
Widget build(BuildContext context) {
return Center(
child: Container(
height: itemHeight * 5, // Visible height of the list
child: ListWheelScrollView.useDelegate(
controller: _scrollController,
itemExtent: itemHeight,
physics: FixedExtentScrollPhysics(),
childDelegate: ListWheelChildBuilderDelegate(
builder: (context, index) {
final double scrollOffset = _scrollController.offset;
final double itemScrollOffset = scrollOffset % itemHeight;
// Calculate transition values for entering and exiting animations
final double enteringScale = 1.0 - (itemScrollOffset / itemHeight);
final double exitingScale = itemScrollOffset / itemHeight;
return AnimatedBuilder(
animation: _scrollController,
builder: (context, child) {
return Transform.scale(
scale: (enteringScale + exitingScale).clamp(0.6, 1.0), // Adjust the range as needed
child: child,
);
},
child: Center(
child: Container(
width: 200,
height: 80,
color: Colors.blue,
child: Center(
child: Text(
'Item $index',
style: TextStyle(color: Colors.white),
),
),
),
),
);
},
childCount: itemCount,
),
),
),
);
}
}
Code which I use : -
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'List',
theme: ThemeData(
primarySwatch: Colors.blue,
),
debugShowCheckedModeBanner: false,
home: const List(),
);
}
}
class List extends StatefulWidget {
const List({Key? key}) : super(key: key);
@override
_ListState createState() => _ListState();
}
class _ListState extends State<List> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: SizedBox(
height: 500,
child: ListWheelScrollView(
itemExtent: 100,
physics: const FixedExtentScrollPhysics(),
onSelectedItemChanged: (value) {
},
children: [
for (int i = 0; i < 5; i++) ...[
Container(
color: Colors.green,
height: 50,
width: 50,
)
]
]),
),
));
}
}
I extended the ChatGPT code, try it:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Smooth Transition in ListWheelScrollView')),
body: TransitionListWheel(),
),
);
}
}
class TransitionListWheel extends StatefulWidget {
@override
_TransitionListWheelState createState() => _TransitionListWheelState();
}
class _TransitionListWheelState extends State<TransitionListWheel> {
late final FixedExtentScrollController _scrollController;
double itemHeight = 100.0; // Height of each item in the list
int itemCount = 20;
int viewCount = 5;
@override
void initState() {
super.initState();
_scrollController = FixedExtentScrollController();
_scrollController.addListener(_handleScroll);
}
@override
void dispose() {
_scrollController.removeListener(_handleScroll);
_scrollController.dispose();
super.dispose();
}
void _handleScroll() {
setState(() {
// Trigger a rebuild to update the transition animations
});
}
@override
Widget build(BuildContext context) {
return Center(
child: Container(
height: itemHeight * viewCount, // Visible height of the list
child: ListWheelScrollView.useDelegate(
controller: _scrollController,
itemExtent: itemHeight,
physics: FixedExtentScrollPhysics(),
childDelegate: ListWheelChildBuilderDelegate(
builder: (context, index) {
final double scrollOffset = _scrollController.offset;
final double itemScrollOffset = scrollOffset % itemHeight;
// Calculate transition values for entering and exiting animations
final double enteringScale =
1.0 - (itemScrollOffset / itemHeight);
final double exitingScale = itemScrollOffset / itemHeight;
final centerOffset = index * itemHeight - scrollOffset;
final itemOpacity = (double x) {
if (x < viewCount ~/ 2 * itemHeight) return 1.0;
if (x > (viewCount ~/ 2 + 1) * itemHeight) return 0.0;
return 1 - (x - viewCount ~/ 2 * itemHeight) / itemHeight;
}(centerOffset.abs() + 25); // adjust as needed
return AnimatedBuilder(
animation: _scrollController,
builder: (context, child) {
// return Text('offset: $centerOffset, opacity: $itemOpacity');
return Transform.scale(
scale: (enteringScale + exitingScale)
.clamp(0.6, 1.0), // Adjust the range as needed
child: Opacity(opacity: itemOpacity, child: child),
);
},
child: Center(
child: Container(
width: 200,
height: 80,
color: Colors.blue,
child: Center(
child: Text(
'Item $index',
style: TextStyle(color: Colors.white),
),
),
),
),
);
},
childCount: itemCount,
),
),
),
);
}
}
Edit: this is how it works
this line calculates the offset from the center for each index. meaning if the index in the center, the offset well be 0, negative if index in the top, positive if index in the bottom. kinda like (-y) axis.
final centerOffset = index * itemHeight - scrollOffset;
we can it's absolute value to tell how far an item from the center
let's say we have 5 items that are visible, meaning Container
height will be 500, hence the two edge items will have centerOffset
of value (-200 and 200)
we can do a switch case logic like this:
if centerOffset
less then 200 (item is close to the center) then opacity will be 1
if centerOffset
more then 200 + itemHeight
(not in the visible area) opacity will be 0
else (item is close to edge, between 200 and 300)
(x - viewCount ~/ 2 * itemHeight)
itemHeight
(sorry I hard coded 100 in previous answer it should be itemHeight
) to make between 0 and 1all the previous numbers are tied to height 500 (viewCount
), that's why I used viewCount ~/ 2
uncomment the Text
in the builder to visualize what I just explained
Note: if you don't like linear opacity you can also transform it
final transformedOpacity = Curves.fastEaseInToSlowEaseOut.transform(itemOpacity);