Flutter AnimatedPositioned Localization: Sliding Panels, Dynamic Positioning, and RTL Animations
AnimatedPositioned automatically animates changes to a widget's position within a Stack. Proper localization ensures that sliding panels, floating elements, and dynamic positioning work seamlessly across languages and text directions. This guide covers comprehensive strategies for localizing AnimatedPositioned widgets in Flutter.
Understanding AnimatedPositioned Localization
AnimatedPositioned widgets require localization for:
- RTL positioning: Mirroring left/right positions for right-to-left languages
- Dynamic offsets: Adjusting positions based on localized content width
- Sliding panels: Localizing slide-in menus and drawers
- Floating elements: Positioning tooltips and badges near localized text
- Accessibility labels: Screen reader announcements for position changes
- Directional animations: Ensuring animations flow correctly in RTL layouts
Basic AnimatedPositioned with RTL Support
Start with a simple AnimatedPositioned that adapts to text direction:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedSlidingPanel extends StatefulWidget {
const LocalizedSlidingPanel({super.key});
@override
State<LocalizedSlidingPanel> createState() => _LocalizedSlidingPanelState();
}
class _LocalizedSlidingPanelState extends State<LocalizedSlidingPanel> {
bool _isPanelOpen = false;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final isRtl = Directionality.of(context) == TextDirection.rtl;
final screenWidth = MediaQuery.of(context).size.width;
final panelWidth = screenWidth * 0.75;
return Scaffold(
appBar: AppBar(
title: Text(l10n.slidingPanelTitle),
leading: IconButton(
icon: const Icon(Icons.menu),
onPressed: () => setState(() => _isPanelOpen = !_isPanelOpen),
tooltip: _isPanelOpen ? l10n.closePanel : l10n.openPanel,
),
),
body: Stack(
children: [
// Main content
GestureDetector(
onTap: () {
if (_isPanelOpen) setState(() => _isPanelOpen = false);
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
color: _isPanelOpen
? Colors.black.withOpacity(0.3)
: Colors.transparent,
child: Center(
child: Text(l10n.mainContent),
),
),
),
// Sliding panel
AnimatedPositioned(
duration: const Duration(milliseconds: 300),
curve: Curves.easeOutCubic,
top: 0,
bottom: 0,
left: isRtl
? null
: (_isPanelOpen ? 0 : -panelWidth),
right: isRtl
? (_isPanelOpen ? 0 : -panelWidth)
: null,
width: panelWidth,
child: Semantics(
label: _isPanelOpen
? l10n.panelOpenAccessibility
: l10n.panelClosedAccessibility,
child: Material(
elevation: _isPanelOpen ? 8 : 0,
child: Container(
color: Theme.of(context).colorScheme.surface,
child: SafeArea(
child: Column(
crossAxisAlignment: isRtl
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Text(
l10n.menuTitle,
style: Theme.of(context).textTheme.headlineSmall,
),
),
const Divider(),
_buildMenuItem(l10n.menuHome, Icons.home),
_buildMenuItem(l10n.menuProfile, Icons.person),
_buildMenuItem(l10n.menuSettings, Icons.settings),
_buildMenuItem(l10n.menuHelp, Icons.help),
],
),
),
),
),
),
),
],
),
);
}
Widget _buildMenuItem(String label, IconData icon) {
final isRtl = Directionality.of(context) == TextDirection.rtl;
return ListTile(
leading: isRtl ? null : Icon(icon),
trailing: isRtl ? Icon(icon) : null,
title: Text(label),
onTap: () => setState(() => _isPanelOpen = false),
);
}
}
ARB File Structure for AnimatedPositioned
{
"slidingPanelTitle": "Sliding Panel",
"@slidingPanelTitle": {
"description": "Title for the sliding panel screen"
},
"openPanel": "Open menu panel",
"@openPanel": {
"description": "Tooltip for opening the panel"
},
"closePanel": "Close menu panel",
"@closePanel": {
"description": "Tooltip for closing the panel"
},
"mainContent": "Main content area",
"@mainContent": {
"description": "Placeholder text for main content"
},
"panelOpenAccessibility": "Navigation panel is open",
"@panelOpenAccessibility": {
"description": "Accessibility label when panel is open"
},
"panelClosedAccessibility": "Navigation panel is closed",
"@panelClosedAccessibility": {
"description": "Accessibility label when panel is closed"
},
"menuTitle": "Navigation",
"@menuTitle": {
"description": "Title of the navigation menu"
},
"menuHome": "Home",
"@menuHome": {
"description": "Home menu item"
},
"menuProfile": "Profile",
"@menuProfile": {
"description": "Profile menu item"
},
"menuSettings": "Settings",
"@menuSettings": {
"description": "Settings menu item"
},
"menuHelp": "Help",
"@menuHelp": {
"description": "Help menu item"
}
}
Floating Action Button with Dynamic Position
Create a FAB that animates to different positions:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
enum FabPosition { bottomRight, bottomLeft, topRight, topLeft, center }
class LocalizedAnimatedFab extends StatefulWidget {
const LocalizedAnimatedFab({super.key});
@override
State<LocalizedAnimatedFab> createState() => _LocalizedAnimatedFabState();
}
class _LocalizedAnimatedFabState extends State<LocalizedAnimatedFab> {
FabPosition _position = FabPosition.bottomRight;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final isRtl = Directionality.of(context) == TextDirection.rtl;
final size = MediaQuery.of(context).size;
final positions = _calculatePositions(size, isRtl);
return Scaffold(
appBar: AppBar(title: Text(l10n.fabPositionTitle)),
body: Stack(
children: [
// Position selector
Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
l10n.selectPosition,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 16),
Wrap(
spacing: 8,
runSpacing: 8,
children: FabPosition.values.map((pos) {
return ChoiceChip(
label: Text(_getPositionLabel(l10n, pos)),
selected: _position == pos,
onSelected: (_) => setState(() => _position = pos),
);
}).toList(),
),
],
),
),
// Animated FAB
AnimatedPositioned(
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOutCubic,
top: positions[_position]!.top,
bottom: positions[_position]!.bottom,
left: positions[_position]!.left,
right: positions[_position]!.right,
child: Semantics(
button: true,
label: l10n.fabAccessibility(_getPositionLabel(l10n, _position)),
child: FloatingActionButton(
onPressed: () => _showSnackBar(l10n),
tooltip: l10n.fabTooltip,
child: const Icon(Icons.add),
),
),
),
],
),
);
}
Map<FabPosition, _PositionOffsets> _calculatePositions(Size size, bool isRtl) {
const padding = 16.0;
const fabSize = 56.0;
final centerX = (size.width - fabSize) / 2;
final centerY = (size.height - fabSize - kToolbarHeight) / 2;
// Swap left/right for RTL
final startPadding = isRtl ? null : padding;
final endPadding = isRtl ? padding : null;
final startNull = isRtl ? padding : null;
final endNull = isRtl ? null : padding;
return {
FabPosition.bottomRight: _PositionOffsets(
bottom: padding,
right: endNull,
left: startNull,
),
FabPosition.bottomLeft: _PositionOffsets(
bottom: padding,
left: startPadding,
right: endPadding,
),
FabPosition.topRight: _PositionOffsets(
top: padding,
right: endNull,
left: startNull,
),
FabPosition.topLeft: _PositionOffsets(
top: padding,
left: startPadding,
right: endPadding,
),
FabPosition.center: _PositionOffsets(
top: centerY,
left: centerX,
),
};
}
String _getPositionLabel(AppLocalizations l10n, FabPosition position) {
return switch (position) {
FabPosition.bottomRight => l10n.positionBottomEnd,
FabPosition.bottomLeft => l10n.positionBottomStart,
FabPosition.topRight => l10n.positionTopEnd,
FabPosition.topLeft => l10n.positionTopStart,
FabPosition.center => l10n.positionCenter,
};
}
void _showSnackBar(AppLocalizations l10n) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.fabPressed)),
);
}
}
class _PositionOffsets {
final double? top;
final double? bottom;
final double? left;
final double? right;
_PositionOffsets({this.top, this.bottom, this.left, this.right});
}
Tooltip Positioning Based on Content
Create tooltips that position correctly for localized text:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedAnimatedTooltip extends StatefulWidget {
final String targetText;
final String tooltipText;
final Widget child;
const LocalizedAnimatedTooltip({
super.key,
required this.targetText,
required this.tooltipText,
required this.child,
});
@override
State<LocalizedAnimatedTooltip> createState() => _LocalizedAnimatedTooltipState();
}
class _LocalizedAnimatedTooltipState extends State<LocalizedAnimatedTooltip> {
bool _showTooltip = false;
final GlobalKey _targetKey = GlobalKey();
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final isRtl = Directionality.of(context) == TextDirection.rtl;
return Stack(
clipBehavior: Clip.none,
children: [
// Target widget
GestureDetector(
key: _targetKey,
onTap: () => setState(() => _showTooltip = !_showTooltip),
onLongPress: () => setState(() => _showTooltip = true),
child: widget.child,
),
// Animated tooltip
if (_showTooltip)
Positioned(
top: -48,
left: isRtl ? null : 0,
right: isRtl ? 0 : null,
child: TweenAnimationBuilder<double>(
duration: const Duration(milliseconds: 200),
tween: Tween(begin: 0, end: 1),
builder: (context, value, child) {
return Opacity(
opacity: value,
child: Transform.translate(
offset: Offset(0, (1 - value) * 8),
child: child,
),
);
},
child: GestureDetector(
onTap: () => setState(() => _showTooltip = false),
child: Semantics(
liveRegion: true,
label: l10n.tooltipShowing(widget.tooltipText),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.inverseSurface,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Text(
widget.tooltipText,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onInverseSurface,
),
),
),
),
),
),
),
],
);
}
}
Notification Badge with Animated Position
Create a badge that animates its position:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedAnimatedBadge extends StatelessWidget {
final int count;
final Widget child;
final bool showBadge;
const LocalizedAnimatedBadge({
super.key,
required this.count,
required this.child,
this.showBadge = true,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final isRtl = Directionality.of(context) == TextDirection.rtl;
return Stack(
clipBehavior: Clip.none,
children: [
child,
AnimatedPositioned(
duration: const Duration(milliseconds: 200),
curve: Curves.easeOutBack,
top: showBadge && count > 0 ? -4 : 0,
right: isRtl ? null : (showBadge && count > 0 ? -4 : 8),
left: isRtl ? (showBadge && count > 0 ? -4 : 8) : null,
child: AnimatedScale(
duration: const Duration(milliseconds: 200),
scale: showBadge && count > 0 ? 1 : 0,
child: Semantics(
label: l10n.notificationCount(count),
child: Container(
padding: const EdgeInsets.all(4),
constraints: const BoxConstraints(
minWidth: 20,
minHeight: 20,
),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.error,
shape: count > 9 ? BoxShape.rectangle : BoxShape.circle,
borderRadius: count > 9 ? BorderRadius.circular(10) : null,
border: Border.all(
color: Theme.of(context).colorScheme.surface,
width: 2,
),
),
child: Center(
child: Text(
count > 99 ? l10n.badgeOverflow : count.toString(),
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: Theme.of(context).colorScheme.onError,
fontWeight: FontWeight.bold,
fontSize: 10,
),
),
),
),
),
),
),
],
);
}
}
// Usage example
class NotificationIconExample extends StatefulWidget {
const NotificationIconExample({super.key});
@override
State<NotificationIconExample> createState() => _NotificationIconExampleState();
}
class _NotificationIconExampleState extends State<NotificationIconExample> {
int _notificationCount = 3;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
LocalizedAnimatedBadge(
count: _notificationCount,
child: IconButton(
icon: const Icon(Icons.notifications),
onPressed: () {},
tooltip: l10n.notificationsTooltip,
),
),
const SizedBox(height: 16),
Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: () => setState(() => _notificationCount++),
icon: const Icon(Icons.add),
tooltip: l10n.addNotification,
),
IconButton(
onPressed: () => setState(() {
if (_notificationCount > 0) _notificationCount--;
}),
icon: const Icon(Icons.remove),
tooltip: l10n.removeNotification,
),
IconButton(
onPressed: () => setState(() => _notificationCount = 0),
icon: const Icon(Icons.clear),
tooltip: l10n.clearNotifications,
),
],
),
],
);
}
}
Draggable Positioned Element
Create a draggable element with snap-to-position:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedDraggablePositioned extends StatefulWidget {
const LocalizedDraggablePositioned({super.key});
@override
State<LocalizedDraggablePositioned> createState() =>
_LocalizedDraggablePositionedState();
}
class _LocalizedDraggablePositionedState
extends State<LocalizedDraggablePositioned> {
Offset _position = const Offset(100, 100);
bool _isDragging = false;
final List<Offset> _snapPoints = [];
@override
void didChangeDependencies() {
super.didChangeDependencies();
_updateSnapPoints();
}
void _updateSnapPoints() {
final size = MediaQuery.of(context).size;
final isRtl = Directionality.of(context) == TextDirection.rtl;
// Define snap points that respect RTL
final startX = isRtl ? size.width - 80 : 20.0;
final endX = isRtl ? 20.0 : size.width - 80;
_snapPoints.clear();
_snapPoints.addAll([
Offset(startX, 100), // Top start
Offset(endX, 100), // Top end
Offset(size.width / 2 - 30, size.height / 2 - 30), // Center
Offset(startX, size.height - 180), // Bottom start
Offset(endX, size.height - 180), // Bottom end
]);
}
Offset _findNearestSnapPoint(Offset position) {
Offset nearest = _snapPoints.first;
double minDistance = double.infinity;
for (final point in _snapPoints) {
final distance = (position - point).distance;
if (distance < minDistance) {
minDistance = distance;
nearest = point;
}
}
return nearest;
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final isRtl = Directionality.of(context) == TextDirection.rtl;
return Scaffold(
appBar: AppBar(title: Text(l10n.draggableTitle)),
body: Stack(
children: [
// Snap point indicators
..._snapPoints.map((point) => Positioned(
left: point.dx,
top: point.dy,
child: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.outline.withOpacity(0.3),
width: 2,
style: BorderStyle.solid,
),
borderRadius: BorderRadius.circular(12),
),
),
)),
// Instructions
Positioned(
top: 16,
left: isRtl ? null : 16,
right: isRtl ? 16 : null,
child: Text(
l10n.dragInstructions,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.outline,
),
),
),
// Draggable element
AnimatedPositioned(
duration: _isDragging
? Duration.zero
: const Duration(milliseconds: 300),
curve: Curves.easeOutCubic,
left: _position.dx,
top: _position.dy,
child: GestureDetector(
onPanStart: (_) => setState(() => _isDragging = true),
onPanUpdate: (details) {
setState(() {
_position += details.delta;
});
},
onPanEnd: (_) {
setState(() {
_isDragging = false;
_position = _findNearestSnapPoint(_position);
});
},
child: Semantics(
label: l10n.draggableElementAccessibility,
hint: l10n.draggableHint,
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
width: 60,
height: 60,
decoration: BoxDecoration(
color: _isDragging
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(_isDragging ? 0.3 : 0.2),
blurRadius: _isDragging ? 16 : 8,
offset: Offset(0, _isDragging ? 8 : 4),
),
],
),
child: Icon(
Icons.drag_indicator,
color: _isDragging
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onPrimary,
),
),
),
),
),
],
),
);
}
}
Animated Tab Indicator
Create a tab indicator that slides between positions:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedAnimatedTabIndicator extends StatefulWidget {
const LocalizedAnimatedTabIndicator({super.key});
@override
State<LocalizedAnimatedTabIndicator> createState() =>
_LocalizedAnimatedTabIndicatorState();
}
class _LocalizedAnimatedTabIndicatorState
extends State<LocalizedAnimatedTabIndicator> {
int _selectedIndex = 0;
final List<GlobalKey> _tabKeys = List.generate(4, (_) => GlobalKey());
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final isRtl = Directionality.of(context) == TextDirection.rtl;
final tabs = [
_TabItem(l10n.tabHome, Icons.home),
_TabItem(l10n.tabSearch, Icons.search),
_TabItem(l10n.tabFavorites, Icons.favorite),
_TabItem(l10n.tabProfile, Icons.person),
];
return Column(
children: [
Container(
height: 64,
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: BorderRadius.circular(16),
),
child: LayoutBuilder(
builder: (context, constraints) {
final tabWidth = constraints.maxWidth / tabs.length;
return Stack(
children: [
// Animated indicator
AnimatedPositioned(
duration: const Duration(milliseconds: 250),
curve: Curves.easeOutCubic,
top: 4,
bottom: 4,
left: isRtl
? null
: _selectedIndex * tabWidth + 4,
right: isRtl
? _selectedIndex * tabWidth + 4
: null,
width: tabWidth - 8,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Theme.of(context)
.colorScheme
.primary
.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
),
),
// Tab buttons
Row(
textDirection: isRtl ? TextDirection.rtl : TextDirection.ltr,
children: tabs.asMap().entries.map((entry) {
final index = entry.key;
final tab = entry.value;
final isSelected = index == _selectedIndex;
return Expanded(
child: Semantics(
selected: isSelected,
label: l10n.tabAccessibility(
tab.label,
index + 1,
tabs.length,
),
child: GestureDetector(
key: _tabKeys[index],
onTap: () => setState(() => _selectedIndex = index),
behavior: HitTestBehavior.opaque,
child: Center(
child: AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 200),
style: Theme.of(context)
.textTheme
.labelLarge!
.copyWith(
color: isSelected
? Theme.of(context).colorScheme.onPrimary
: Theme.of(context)
.colorScheme
.onSurfaceVariant,
fontWeight: isSelected
? FontWeight.bold
: FontWeight.normal,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
tab.icon,
size: 20,
color: isSelected
? Theme.of(context)
.colorScheme
.onPrimary
: Theme.of(context)
.colorScheme
.onSurfaceVariant,
),
const SizedBox(width: 4),
Text(tab.label),
],
),
),
),
),
),
);
}).toList(),
),
],
);
},
),
),
// Content area
Expanded(
child: Center(
child: Text(
l10n.tabContent(tabs[_selectedIndex].label),
style: Theme.of(context).textTheme.headlineSmall,
),
),
),
],
);
}
}
class _TabItem {
final String label;
final IconData icon;
_TabItem(this.label, this.icon);
}
Complete ARB File for AnimatedPositioned
{
"@@locale": "en",
"slidingPanelTitle": "Sliding Panel",
"@slidingPanelTitle": {
"description": "Title for the sliding panel demo"
},
"openPanel": "Open menu panel",
"@openPanel": {
"description": "Tooltip for opening the panel"
},
"closePanel": "Close menu panel",
"@closePanel": {
"description": "Tooltip for closing the panel"
},
"mainContent": "Main content area",
"@mainContent": {
"description": "Main content placeholder"
},
"panelOpenAccessibility": "Navigation panel is open",
"@panelOpenAccessibility": {
"description": "Accessibility label for open panel"
},
"panelClosedAccessibility": "Navigation panel is closed",
"@panelClosedAccessibility": {
"description": "Accessibility label for closed panel"
},
"menuTitle": "Navigation",
"@menuTitle": {
"description": "Navigation menu title"
},
"menuHome": "Home",
"menuProfile": "Profile",
"menuSettings": "Settings",
"menuHelp": "Help",
"fabPositionTitle": "FAB Position",
"@fabPositionTitle": {
"description": "Title for FAB position demo"
},
"selectPosition": "Select FAB position:",
"@selectPosition": {
"description": "Instruction to select position"
},
"positionBottomEnd": "Bottom End",
"positionBottomStart": "Bottom Start",
"positionTopEnd": "Top End",
"positionTopStart": "Top Start",
"positionCenter": "Center",
"fabAccessibility": "Add button at {position}",
"@fabAccessibility": {
"description": "FAB accessibility label",
"placeholders": {
"position": {"type": "String"}
}
},
"fabTooltip": "Add item",
"fabPressed": "Button pressed",
"tooltipShowing": "Tooltip: {text}",
"@tooltipShowing": {
"description": "Accessibility for showing tooltip",
"placeholders": {
"text": {"type": "String"}
}
},
"notificationCount": "{count} {count, plural, =0{notifications} =1{notification} other{notifications}}",
"@notificationCount": {
"description": "Notification count label",
"placeholders": {
"count": {"type": "int"}
}
},
"badgeOverflow": "99+",
"@badgeOverflow": {
"description": "Badge text when count exceeds 99"
},
"notificationsTooltip": "Notifications",
"addNotification": "Add notification",
"removeNotification": "Remove notification",
"clearNotifications": "Clear all notifications",
"draggableTitle": "Draggable Element",
"dragInstructions": "Drag the element to snap points",
"draggableElementAccessibility": "Draggable element",
"draggableHint": "Double-tap and hold to drag",
"tabHome": "Home",
"tabSearch": "Search",
"tabFavorites": "Favorites",
"tabProfile": "Profile",
"tabAccessibility": "{label}, tab {current} of {total}",
"@tabAccessibility": {
"description": "Tab accessibility label",
"placeholders": {
"label": {"type": "String"},
"current": {"type": "int"},
"total": {"type": "int"}
}
},
"tabContent": "{tab} content",
"@tabContent": {
"description": "Tab content placeholder",
"placeholders": {
"tab": {"type": "String"}
}
}
}
Best Practices Summary
- Mirror positions for RTL: Swap left/right properties based on text direction
- Use directional terminology: Say "start/end" instead of "left/right" in user-facing text
- Calculate positions dynamically: Account for different text lengths in positioning
- Provide accessibility labels: Announce position changes for screen readers
- Use semantic positioning: Reference logical positions rather than absolute coordinates
- Handle edge cases: Ensure elements don't overflow screen boundaries in any locale
- Test with RTL languages: Verify animations flow correctly in right-to-left layouts
- Use appropriate curves: Choose animation curves that feel natural for the direction of movement
- Consider text expansion: Languages like German expand 30%+ compared to English
- Snap to logical positions: Use locale-aware snap points for draggable elements
Conclusion
Proper AnimatedPositioned localization ensures smooth, directionally-correct animations across all languages. By handling RTL layouts, calculating dynamic positions, and providing comprehensive accessibility labels, you create polished sliding and positioning animations that feel native to users worldwide. The patterns shown here—sliding panels, positioned FABs, badges, and tab indicators—can be adapted to any Flutter application requiring animated positioning.
Remember to test your positioned animations with different locales to verify that sliding directions, snap points, and accessibility work correctly for all your supported languages.