Skip to content

Instantly share code, notes, and snippets.

@hawkkiller
Last active November 19, 2025 10:55
Show Gist options
  • Select an option

  • Save hawkkiller/e53639eb09a7fbe347bd4c335b0ad73f to your computer and use it in GitHub Desktop.

Select an option

Save hawkkiller/e53639eb09a7fbe347bd4c335b0ad73f to your computer and use it in GitHub Desktop.
This is a cool sliding transition for widgets. It's inspired by the wolt_modal_sheet package, which has the same animation but is coupled with the Bottom Sheet/Pages API. This implementation should be also more performant and less fragile.
import 'package:flutter/material.dart';
/// A widget that transitions between two children using a fade and slide animation.
class PageTransitionSwitcher extends StatelessWidget {
const PageTransitionSwitcher({
required this.child,
this.isForwardMove = true,
super.key,
});
/// The child widget to transition between.
final Widget child;
final bool isForwardMove;
static const _duration = Duration(milliseconds: 350);
static const _sizeCurve = Interval(0 / 350, 300 / 350, curve: Curves.fastOutSlowIn);
static const _opacityIncomingCurve = Interval(150 / 350, 1);
static const _opacityOutgoingCurve = Interval(50 / 350, 150 / 350);
static const _slideCurve = Interval(50 / 350, 1, curve: Curves.fastOutSlowIn);
Key? get _currentChildKey => child.key;
@override
Widget build(BuildContext context) {
return AnimatedSize(
alignment: Alignment.topCenter,
duration: _duration,
curve: _sizeCurve,
child: AnimatedSwitcher(
duration: _duration,
layoutBuilder: (currentChild, previousChildren) => Stack(
clipBehavior: Clip.none,
children: [
...previousChildren.map((child) => Positioned(top: 0, left: 0, right: 0, child: child)),
if (currentChild != null) currentChild,
],
),
transitionBuilder: (child, animation) => _transitionBuilder(
child,
animation,
currentChildKey: _currentChildKey,
isForwardMove: isForwardMove,
textDirection: Directionality.of(context),
),
child: child,
),
);
}
}
enum _TransitionState { incoming, outgoing }
Widget _transitionBuilder(
Widget child,
Animation<double> animation, {
required Key? currentChildKey,
required bool isForwardMove,
required TextDirection textDirection,
}) {
final isIncoming = child.key == currentChildKey;
final opacityCurve = isIncoming
? PageTransitionSwitcher._opacityIncomingCurve
: PageTransitionSwitcher._opacityOutgoingCurve;
final reverseOpacityCurve = isIncoming
? PageTransitionSwitcher._opacityOutgoingCurve
: PageTransitionSwitcher._opacityIncomingCurve;
final state = isIncoming ? _TransitionState.incoming : _TransitionState.outgoing;
return FadeTransition(
opacity: CurvedAnimation(
parent: animation,
curve: opacityCurve,
reverseCurve: reverseOpacityCurve,
),
child: SlideTransition(
position: _slidePosition(
animation,
state: state,
textDirection: textDirection,
isForwardMove: isForwardMove,
),
child: child,
),
);
}
Animation<Offset> _slidePosition(
Animation<double> animation, {
required _TransitionState state,
required TextDirection textDirection,
required bool isForwardMove,
}) {
final directionMultiplier =
(textDirection == TextDirection.ltr ? 1 : -1) * (isForwardMove ? 1 : -1);
switch (state) {
case _TransitionState.incoming:
final incomingBeginOffset = Offset(.3 * directionMultiplier, 0);
return Tween<Offset>(begin: incomingBeginOffset, end: Offset.zero).animate(
CurvedAnimation(
parent: animation,
curve: PageTransitionSwitcher._slideCurve,
),
);
case _TransitionState.outgoing:
final outgoingEndOffset = Offset(.3 * -directionMultiplier, 0);
return Tween<Offset>(begin: outgoingEndOffset, end: Offset.zero).animate(
CurvedAnimation(
parent: animation,
curve: PageTransitionSwitcher._slideCurve,
),
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment