I'm able to achieve the curve effect, thanks to @pskink suggestion. The only thing I did differently was using quadratic Bezier curve formula instead of using a circle formula.
enum ShiftMode {
forward,
backward,
}
class BezierCurveItems extends StatefulWidget {
const BezierCurveItems({super.key});
@override
State<BezierCurveItems> createState() => _BezierCurveItemsState();
}
class _BezierCurveItemsState extends State<BezierCurveItems>
with SingleTickerProviderStateMixin {
late double _width;
late double _height;
late int _visibleItemCount;
ShiftMode? _shiftMode;
late double _itemWidth;
late AnimationController _controller;
late Animation<double> _animation;
late Tween<double> _tween;
List<Widget> _items = [
Icon(Icons.ac_unit),
Icon(Icons.access_time),
Icon(Icons.account_box_sharp),
Icon(Icons.account_balance_wallet),
Icon(Icons.account_tree),
];
@override
void initState() {
_width = 360.0;
_height = 80.0;
_visibleItemCount = 5;
_itemWidth = _width / _visibleItemCount;
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
);
_tween = Tween<double>(begin: 0.0, end: 1.0);
_animation = _tween.animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
super.initState();
}
_shiftRightForward() {
_shiftMode = ShiftMode.forward;
_controller.reset();
_controller.forward();
}
_shiftLeftForward() {
_shiftMode = ShiftMode.backward;
_controller.reset();
_controller.forward();
}
double _getX(int i, double av) {
// Calculate x position for the item
double xPos = (i * (_width / (_visibleItemCount)));
if (_shiftMode == ShiftMode.forward) {
double xPosNext = ((i + 1) * (_width / (_visibleItemCount)));
double interpolateX = lerpDouble(xPos, xPosNext, av)!;
return interpolateX;
} else if (_shiftMode == ShiftMode.backward) {
double xPosPrev = ((i - 1) * (_width / (_visibleItemCount)));
double interpolateX = lerpDouble(xPos, xPosPrev, av)!;
return interpolateX;
} else {
return xPos;
}
}
double _getY(int i, double av) {
double yPos = _calculateBezierY(i);
if (_shiftMode == ShiftMode.forward) {
double yPosNext = _calculateBezierY(i + 1);
return lerpDouble(yPos, yPosNext, av)!;
} else if (_shiftMode == ShiftMode.backward) {
double yPosPrev = _calculateBezierY(i - 1);
return lerpDouble(yPos, yPosPrev, av)!;
} else {
return yPos;
}
}
double _calculateBezierY(int i) {
// Bezier curve control points
Offset p0 = Offset(0, _height / 2);
Offset p1 = Offset(_width / 2, 0); // Control point
Offset p2 = Offset(_width, _height / 2);
double t = i / (_visibleItemCount - 1);
return (1 - t) * (1 - t) * p0.dy + 2 * (1 - t) * t * p1.dy + t * t * p2.dy;
}
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FloatingActionButton(
onPressed: () => _shiftLeftForward(),
child: Icon(Icons.navigate_before),
),
FloatingActionButton(
onPressed: () async {
_shiftRightForward();
await Future.delayed(const Duration(seconds: 1));
_shiftRightForward();
},
child: Icon(Icons.navigate_next),
)
],
),
appBar: AppBar(title: Text("Bezier Curve Items")),
body: Container(
width: 360.0,
height: 80.0,
color: Colors.grey.shade400,
child: AnimatedBuilder(
animation: _animation,
builder: (BuildContext context, Widget? child) {
return Stack(
children: [
for (int i = 0; i < _visibleItemCount; i++)
Transform.translate(
offset: Offset(
_getX(i, _animation.value),
_getY(i, _animation.value),
),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.black54),
),
alignment: Alignment.center,
width: _itemWidth,
child: _items[i],
),
),
if (_shiftMode == ShiftMode.forward)
Transform.translate(
offset: Offset(
_getX(-1, _animation.value),
_getY(-1, _animation.value),
),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.black54),
),
alignment: Alignment.center,
width: _itemWidth,
child: _items[0],
),
),
if (_shiftMode == ShiftMode.backward)
Transform.translate(
offset: Offset(
_getX(_visibleItemCount, _animation.value),
_getY(_visibleItemCount, _animation.value),
),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.black54),
),
alignment: Alignment.center,
width: _itemWidth,
child: _items[0],
),
),
],
);
},
),
),
);
}
}