Flutter Animation Widgets and Localization: Transitions, Hero, AnimatedSwitcher and RTL
Animations do not have strings, and yet they are the widgets most often broken by localization: a slide that enters from the wrong side in Arabic, a Hero that flies across the screen because the title changed length, an AnimatedSwitcher that cross-fades every label when the language switches. This guide covers every animation and transition widget once, with the rule each one needs. It replaces the per-widget posts we used to publish.
The one rule: animate in logical, not physical, coordinates
Offset(1, 0) means "one width to the right". In a right-to-left app, "forward" is to the left. Every slide, every positioned transition and every custom Transform that encodes a direction has to be mirrored when Directionality.of(context) is RTL. Flutter gives you TextDirection-aware versions for the common cases; use them and you rarely have to multiply by minus one yourself.
final rtl = Directionality.of(context) == TextDirection.rtl;
final begin = Offset(rtl ? -1 : 1, 0); // enters from the trailing edge
SlideTransition, PositionedTransition, RelativePositionedTransition, AlignTransition
SlideTransition takes an Offset tween and a textDirection. Pass the direction and the x offset is flipped for you:
SlideTransition(
position: Tween(begin: const Offset(1, 0), end: Offset.zero).animate(controller),
textDirection: Directionality.of(context),
child: panel,
)
PositionedTransition animates a RelativeRect inside a Stack. RelativeRect.fromLTRB is physical; build it with RelativeRectTween from RelativeRect.fromDirectional(textDirection: ..., start: ..., end: ...) and it mirrors. RelativePositionedTransition is the same with a Rect. AlignTransition animates an AlignmentGeometry, and AlignmentDirectional.centerStart to centerEnd is already logical.
FadeTransition, ScaleTransition, RotationTransition, SizeTransition, DecoratedBoxTransition, DefaultTextStyleTransition
FadeTransition, ScaleTransition and DecoratedBoxTransition have no direction. RotationTransition does when the rotation means something ("expand" chevron turning down): a chevron that points to the trailing edge in RTL is already mirrored by Icons.chevron_right's matchTextDirection, so rotating it by a quarter turn still lands pointing down. Do not add a sign flip.
SizeTransition(axis: Axis.horizontal) grows from axisAlignment: -1 which is the start edge in LTR and... still the left in RTL, because axisAlignment is physical. Use 1 in RTL to grow from the trailing edge, or stick to the vertical axis, which is the same everywhere.
DefaultTextStyleTransition animates a TextStyle across a subtree. Its one localization point is height: if you bump line height for Thai or Devanagari, animate to the locale-appropriate value, not to a constant measured in English.
AnimatedContainer, AnimatedPadding, AnimatedAlign, AnimatedPositioned, AnimatedSize, AnimatedOpacity, AnimatedScale, AnimatedRotation, AnimatedSlide, AnimatedDefaultTextStyle, AnimatedCrossFade
The implicit widgets have the same direction story as the explicit ones:
AnimatedPaddingtakesEdgeInsetsGeometry: useEdgeInsetsDirectional.AnimatedAligntakesAlignmentGeometry: useAlignmentDirectional.AnimatedPositionedhasleft/rightonly; there isAnimatedPositionedDirectionalwithstart/end.AnimatedSlidetakes anOffsetand notextDirection, so flipxyourself.AnimatedContainercombines padding, alignment and decoration; use the directional variants of each.AnimatedSizeis the one you want around translated text: it animates the box when a label changes length, so a language switch does not jump.
AnimatedSize(
duration: const Duration(milliseconds: 200),
alignment: AlignmentDirectional.centerStart,
child: Text(context.l10n.statusLabel(status)),
)
AnimatedCrossFade shows one of two children. If the two are translated labels of different length, set alignment to a directional value and let AnimatedSize behaviour (built in) handle the height change.
AnimatedSwitcher, AnimatedList, TweenAnimationBuilder
AnimatedSwitcher animates when its child's key changes. The trap: when the locale changes, every Text under a switcher gets a new string, and if the key is the string itself (ValueKey(label)) the whole screen cross-fades at once. Key by the state (ValueKey(status)), not by the text, so a language switch repaints without animating.
AnimatedList animates row inserts and removals. A removed row that slides out horizontally should use SlideTransition with textDirection as above. Vertical collapse (SizeTransition on the vertical axis) is simpler and works in every language.
TweenAnimationBuilder is generic; it is often used to count a number up. Format the value with NumberFormat inside the builder, every frame, so Arabic users see Eastern Arabic digits counting up:
TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: total),
duration: const Duration(milliseconds: 600),
builder: (context, value, _) => Text(
NumberFormat.simpleCurrency(locale: Localizations.localeOf(context).toString()).format(value),
),
)
Hero
Hero flies a widget between two routes. The flight uses the widget's size on each page, so when the destination title is longer (German) than the source, the text scales during flight and looks wrong. Give the hero child a fixed text style and let the containing box, not the text, be the hero: wrap an image or an avatar, not a label. When the hero must contain text, set flightShuttleBuilder to show the destination text at its final size.
In RTL the flight path mirrors automatically because both positions are measured after layout.
Page transitions
MaterialPageRoute slides up on Android and from the trailing edge on iOS, and the iOS one mirrors in RTL. A custom PageRouteBuilder with a SlideTransition must pass textDirection as in the first section, or every pushed page in an Arabic app enters from the wrong side. The PageTransitionsTheme builders that ship with Flutter are all direction-aware.
Reduced motion
Not localization, but the same audience: users who turn off animations in the OS. Read MediaQuery.disableAnimationsOf(context) and set durations to zero. A language switch that triggers a dozen AnimatedSwitchers is unpleasant with motion on and unreadable with it off.
Checklist
- Every
SlideTransitionand custom page transition passestextDirection. AnimatedPadding,AnimatedAlign,AnimatedPositioneduse the directional variants.AnimatedSwitcherkeys by state, not by translated text.- Translated labels that change length sit in
AnimatedSize. - Numbers in animated counters go through
NumberFormat. - Test every transition once with
Locale('ar').
Related
- Layout widgets and RTL
- Lists, grids and scrolling widgets
- Flutter RTL support: Arabic, Hebrew and Persian
- How to translate a Flutter app
Animations are the last thing to break and the first thing users notice. The strings inside them are the easy part: FlutterLocalisation keeps every language complete so a label never animates into an empty box.