Flutter AnimatedAlign Localization: Dynamic Positioning, RTL Alignment, and Accessible Transitions
AnimatedAlign smoothly animates changes to widget alignment within its parent. Proper localization ensures that alignment transitions, RTL-aware positioning, and dynamic layouts work seamlessly across languages. This guide covers comprehensive strategies for localizing AnimatedAlign widgets in Flutter.
Understanding AnimatedAlign Localization
AnimatedAlign widgets require localization for:
- RTL alignment: Mirroring alignments for right-to-left languages
- Dynamic positioning: Moving content based on localized states
- Focus indicators: Positioning highlights based on text direction
- Interactive elements: Aligning feedback elements correctly
- Accessibility: Announcing position changes for screen readers
- Responsive layouts: Adapting alignment to content direction
Basic AnimatedAlign with RTL Support
Start with a simple AnimatedAlign that respects text direction:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedAlignedContent extends StatefulWidget {
const LocalizedAlignedContent({super.key});
@override
State<LocalizedAlignedContent> createState() => _LocalizedAlignedContentState();
}
class _LocalizedAlignedContentState extends State<LocalizedAlignedContent> {
bool _isExpanded = false;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final isRtl = Directionality.of(context) == TextDirection.rtl;
// Determine alignment based on state and text direction
final alignment = _isExpanded
? Alignment.center
: (isRtl ? Alignment.centerRight : Alignment.centerLeft);
return Scaffold(
appBar: AppBar(title: Text(l10n.alignmentDemoTitle)),
body: Column(
children: [
Expanded(
child: Container(
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.outline,
),
borderRadius: BorderRadius.circular(12),
),
child: AnimatedAlign(
duration: const Duration(milliseconds: 400),
curve: Curves.easeOutCubic,
alignment: alignment,
child: Semantics(
label: _isExpanded
? l10n.contentCenteredAccessibility
: l10n.contentStartAccessibility,
child: Card(
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_isExpanded ? Icons.center_focus_strong : Icons.format_align_left,
size: 48,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 16),
Text(
_isExpanded ? l10n.centeredContent : l10n.alignedContent,
style: Theme.of(context).textTheme.titleMedium,
),
],
),
),
),
),
),
),
),
Padding(
padding: const EdgeInsets.all(16),
child: ElevatedButton.icon(
onPressed: () => setState(() => _isExpanded = !_isExpanded),
icon: Icon(_isExpanded ? Icons.compress : Icons.expand),
label: Text(_isExpanded ? l10n.alignToStart : l10n.centerContent),
),
),
],
),
);
}
}
ARB File Structure for AnimatedAlign
{
"alignmentDemoTitle": "Alignment Demo",
"@alignmentDemoTitle": {
"description": "Title for alignment demo screen"
},
"contentCenteredAccessibility": "Content is centered",
"@contentCenteredAccessibility": {
"description": "Accessibility label when content is centered"
},
"contentStartAccessibility": "Content is aligned to start",
"@contentStartAccessibility": {
"description": "Accessibility label when content is at start"
},
"centeredContent": "Centered",
"@centeredContent": {
"description": "Label for centered state"
},
"alignedContent": "Start Aligned",
"@alignedContent": {
"description": "Label for start-aligned state"
},
"centerContent": "Center Content",
"@centerContent": {
"description": "Button label to center content"
},
"alignToStart": "Align to Start",
"@alignToStart": {
"description": "Button label to align content to start"
}
}
Multi-Position Alignment Selector
Create a selector that moves content to different positions:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
enum ContentPosition { topStart, topCenter, topEnd, centerStart, center, centerEnd, bottomStart, bottomCenter, bottomEnd }
class LocalizedPositionSelector extends StatefulWidget {
const LocalizedPositionSelector({super.key});
@override
State<LocalizedPositionSelector> createState() => _LocalizedPositionSelectorState();
}
class _LocalizedPositionSelectorState extends State<LocalizedPositionSelector> {
ContentPosition _position = ContentPosition.center;
Alignment _getAlignment(ContentPosition position, bool isRtl) {
// Map positions to alignments, respecting RTL
final alignments = {
ContentPosition.topStart: isRtl ? Alignment.topRight : Alignment.topLeft,
ContentPosition.topCenter: Alignment.topCenter,
ContentPosition.topEnd: isRtl ? Alignment.topLeft : Alignment.topRight,
ContentPosition.centerStart: isRtl ? Alignment.centerRight : Alignment.centerLeft,
ContentPosition.center: Alignment.center,
ContentPosition.centerEnd: isRtl ? Alignment.centerLeft : Alignment.centerRight,
ContentPosition.bottomStart: isRtl ? Alignment.bottomRight : Alignment.bottomLeft,
ContentPosition.bottomCenter: Alignment.bottomCenter,
ContentPosition.bottomEnd: isRtl ? Alignment.bottomLeft : Alignment.bottomRight,
};
return alignments[position]!;
}
String _getPositionLabel(AppLocalizations l10n, ContentPosition position) {
return switch (position) {
ContentPosition.topStart => l10n.positionTopStart,
ContentPosition.topCenter => l10n.positionTopCenter,
ContentPosition.topEnd => l10n.positionTopEnd,
ContentPosition.centerStart => l10n.positionCenterStart,
ContentPosition.center => l10n.positionCenter,
ContentPosition.centerEnd => l10n.positionCenterEnd,
ContentPosition.bottomStart => l10n.positionBottomStart,
ContentPosition.bottomCenter => l10n.positionBottomCenter,
ContentPosition.bottomEnd => l10n.positionBottomEnd,
};
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final isRtl = Directionality.of(context) == TextDirection.rtl;
return Scaffold(
appBar: AppBar(title: Text(l10n.positionSelectorTitle)),
body: Column(
children: [
// Preview area
Expanded(
child: Container(
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: BorderRadius.circular(16),
),
child: AnimatedAlign(
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOutCubic,
alignment: _getAlignment(_position, isRtl),
child: Semantics(
liveRegion: true,
label: l10n.contentAtPosition(_getPositionLabel(l10n, _position)),
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Theme.of(context).colorScheme.primary.withOpacity(0.3),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Icon(
Icons.widgets,
color: Theme.of(context).colorScheme.onPrimary,
size: 32,
),
),
),
),
),
),
// Position grid
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.selectPosition,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 12),
_buildPositionGrid(l10n, isRtl),
],
),
),
],
),
);
}
Widget _buildPositionGrid(AppLocalizations l10n, bool isRtl) {
final positions = [
[ContentPosition.topStart, ContentPosition.topCenter, ContentPosition.topEnd],
[ContentPosition.centerStart, ContentPosition.center, ContentPosition.centerEnd],
[ContentPosition.bottomStart, ContentPosition.bottomCenter, ContentPosition.bottomEnd],
];
return Column(
children: positions.map((row) {
return Row(
children: row.map((position) {
final isSelected = _position == position;
return Expanded(
child: Padding(
padding: const EdgeInsets.all(4),
child: Semantics(
selected: isSelected,
button: true,
label: l10n.selectPositionAccessibility(_getPositionLabel(l10n, position)),
child: InkWell(
onTap: () => setState(() => _position = position),
borderRadius: BorderRadius.circular(8),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: isSelected
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isSelected
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline,
),
),
child: Icon(
_getPositionIcon(position),
color: isSelected
? Theme.of(context).colorScheme.onPrimary
: Theme.of(context).colorScheme.onSurface,
size: 20,
),
),
),
),
),
);
}).toList(),
);
}).toList(),
);
}
IconData _getPositionIcon(ContentPosition position) {
return switch (position) {
ContentPosition.topStart => Icons.north_west,
ContentPosition.topCenter => Icons.north,
ContentPosition.topEnd => Icons.north_east,
ContentPosition.centerStart => Icons.west,
ContentPosition.center => Icons.center_focus_strong,
ContentPosition.centerEnd => Icons.east,
ContentPosition.bottomStart => Icons.south_west,
ContentPosition.bottomCenter => Icons.south,
ContentPosition.bottomEnd => Icons.south_east,
};
}
}
Chat Message Alignment
Create chat bubbles with proper alignment based on sender:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedChatMessage extends StatelessWidget {
final String message;
final String sender;
final bool isCurrentUser;
final DateTime timestamp;
const LocalizedChatMessage({
super.key,
required this.message,
required this.sender,
required this.isCurrentUser,
required this.timestamp,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final isRtl = Directionality.of(context) == TextDirection.rtl;
// Determine alignment: current user messages go to the end
// In LTR: current user = right, other = left
// In RTL: current user = left, other = right
final alignment = isCurrentUser
? (isRtl ? Alignment.centerLeft : Alignment.centerRight)
: (isRtl ? Alignment.centerRight : Alignment.centerLeft);
final bubbleColor = isCurrentUser
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.surfaceVariant;
final textColor = isCurrentUser
? Theme.of(context).colorScheme.onPrimary
: Theme.of(context).colorScheme.onSurfaceVariant;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: AnimatedAlign(
duration: const Duration(milliseconds: 300),
curve: Curves.easeOutCubic,
alignment: alignment,
child: Semantics(
label: l10n.chatMessageAccessibility(
sender,
message,
_formatTime(context, timestamp),
),
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.75,
),
child: Column(
crossAxisAlignment: isCurrentUser
? (isRtl ? CrossAxisAlignment.start : CrossAxisAlignment.end)
: (isRtl ? CrossAxisAlignment.end : CrossAxisAlignment.start),
children: [
if (!isCurrentUser)
Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Text(
sender,
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: Theme.of(context).colorScheme.outline,
),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: bubbleColor,
borderRadius: BorderRadius.only(
topLeft: const Radius.circular(16),
topRight: const Radius.circular(16),
bottomLeft: Radius.circular(isCurrentUser ? 16 : 4),
bottomRight: Radius.circular(isCurrentUser ? 4 : 16),
),
),
child: Text(
message,
style: TextStyle(color: textColor),
),
),
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
_formatTime(context, timestamp),
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: Theme.of(context).colorScheme.outline,
),
),
),
],
),
),
),
),
);
}
String _formatTime(BuildContext context, DateTime time) {
final locale = Localizations.localeOf(context);
return '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}';
}
}
// Chat screen example
class LocalizedChatScreen extends StatefulWidget {
const LocalizedChatScreen({super.key});
@override
State<LocalizedChatScreen> createState() => _LocalizedChatScreenState();
}
class _LocalizedChatScreenState extends State<LocalizedChatScreen> {
final _messageController = TextEditingController();
final List<_ChatMessage> _messages = [];
@override
void dispose() {
_messageController.dispose();
super.dispose();
}
void _sendMessage(AppLocalizations l10n) {
if (_messageController.text.trim().isEmpty) return;
setState(() {
_messages.add(_ChatMessage(
message: _messageController.text,
sender: l10n.you,
isCurrentUser: true,
timestamp: DateTime.now(),
));
});
_messageController.clear();
// Simulate reply
Future.delayed(const Duration(seconds: 1), () {
if (mounted) {
setState(() {
_messages.add(_ChatMessage(
message: l10n.autoReplyMessage,
sender: l10n.supportAgent,
isCurrentUser: false,
timestamp: DateTime.now(),
));
});
}
});
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: Text(l10n.chatTitle),
),
body: Column(
children: [
Expanded(
child: _messages.isEmpty
? Center(
child: Text(
l10n.startConversation,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.outline,
),
),
)
: ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 16),
itemCount: _messages.length,
itemBuilder: (context, index) {
final msg = _messages[index];
return LocalizedChatMessage(
message: msg.message,
sender: msg.sender,
isCurrentUser: msg.isCurrentUser,
timestamp: msg.timestamp,
);
},
),
),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, -2),
),
],
),
child: Row(
children: [
Expanded(
child: TextField(
controller: _messageController,
decoration: InputDecoration(
hintText: l10n.typeMessage,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(24),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
onSubmitted: (_) => _sendMessage(l10n),
),
),
const SizedBox(width: 8),
FloatingActionButton(
mini: true,
onPressed: () => _sendMessage(l10n),
tooltip: l10n.sendMessage,
child: const Icon(Icons.send),
),
],
),
),
],
),
);
}
}
class _ChatMessage {
final String message;
final String sender;
final bool isCurrentUser;
final DateTime timestamp;
_ChatMessage({
required this.message,
required this.sender,
required this.isCurrentUser,
required this.timestamp,
});
}
Loading Indicator with Alignment
Create a loading indicator that aligns based on content:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedAlignedLoader extends StatefulWidget {
const LocalizedAlignedLoader({super.key});
@override
State<LocalizedAlignedLoader> createState() => _LocalizedAlignedLoaderState();
}
class _LocalizedAlignedLoaderState extends State<LocalizedAlignedLoader> {
bool _isLoading = false;
bool _hasContent = false;
Future<void> _loadContent() async {
setState(() => _isLoading = true);
await Future.delayed(const Duration(seconds: 2));
setState(() {
_isLoading = false;
_hasContent = true;
});
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final isRtl = Directionality.of(context) == TextDirection.rtl;
// Loader starts at center, then moves to top-start when content loads
final loaderAlignment = _hasContent
? (isRtl ? Alignment.topRight : Alignment.topLeft)
: Alignment.center;
return Scaffold(
appBar: AppBar(title: Text(l10n.contentLoaderTitle)),
body: Stack(
children: [
// Main content
if (_hasContent)
Padding(
padding: const EdgeInsets.only(top: 80),
child: ListView.builder(
itemCount: 20,
itemBuilder: (context, index) {
return ListTile(
leading: CircleAvatar(child: Text('${index + 1}')),
title: Text(l10n.itemTitle(index + 1)),
subtitle: Text(l10n.itemSubtitle),
);
},
),
)
else if (!_isLoading)
Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.cloud_download,
size: 64,
color: Theme.of(context).colorScheme.outline,
),
const SizedBox(height: 16),
Text(l10n.noContentYet),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _loadContent,
child: Text(l10n.loadContent),
),
],
),
),
// Animated loader
AnimatedAlign(
duration: const Duration(milliseconds: 500),
curve: Curves.easeOutCubic,
alignment: loaderAlignment,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 300),
opacity: _isLoading ? 1.0 : (_hasContent ? 0.0 : 0.0),
child: Semantics(
label: l10n.loadingAccessibility,
child: Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Theme.of(context).colorScheme.primary,
),
),
const SizedBox(width: 12),
Text(
l10n.loadingContent,
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
],
),
),
),
),
),
],
),
);
}
}
Focus Highlight Indicator
Create a focus indicator that moves between elements:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedFocusIndicator extends StatefulWidget {
const LocalizedFocusIndicator({super.key});
@override
State<LocalizedFocusIndicator> createState() => _LocalizedFocusIndicatorState();
}
class _LocalizedFocusIndicatorState extends State<LocalizedFocusIndicator> {
int _focusedIndex = 0;
final int _itemCount = 4;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final isRtl = Directionality.of(context) == TextDirection.rtl;
final menuItems = [
_MenuItem(l10n.menuDashboard, Icons.dashboard),
_MenuItem(l10n.menuAnalytics, Icons.analytics),
_MenuItem(l10n.menuReports, Icons.description),
_MenuItem(l10n.menuSettings, Icons.settings),
];
return Scaffold(
appBar: AppBar(title: Text(l10n.navigationTitle)),
body: Row(
children: [
// Sidebar navigation
Container(
width: 200,
color: Theme.of(context).colorScheme.surfaceVariant,
child: Stack(
children: [
// Focus indicator
AnimatedAlign(
duration: const Duration(milliseconds: 250),
curve: Curves.easeOutCubic,
alignment: Alignment(
isRtl ? 1.0 : -1.0,
-1.0 + (_focusedIndex * 2.0 / (_itemCount - 1)),
),
child: Container(
width: 4,
height: 56,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(2),
),
),
),
// Menu items
Column(
children: menuItems.asMap().entries.map((entry) {
final index = entry.key;
final item = entry.value;
final isFocused = index == _focusedIndex;
return Semantics(
selected: isFocused,
button: true,
label: l10n.menuItemAccessibility(item.label, index + 1, menuItems.length),
child: InkWell(
onTap: () => setState(() => _focusedIndex = index),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
height: 56,
padding: EdgeInsets.only(
left: isRtl ? 16 : (isFocused ? 20 : 16),
right: isRtl ? (isFocused ? 20 : 16) : 16,
),
color: isFocused
? Theme.of(context).colorScheme.primary.withOpacity(0.1)
: Colors.transparent,
child: Row(
children: [
Icon(
item.icon,
color: isFocused
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(width: 12),
Text(
item.label,
style: TextStyle(
color: isFocused
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurfaceVariant,
fontWeight: isFocused ? FontWeight.bold : FontWeight.normal,
),
),
],
),
),
),
);
}).toList(),
),
],
),
),
// Content area
Expanded(
child: Center(
child: Text(
l10n.selectedMenuItem(menuItems[_focusedIndex].label),
style: Theme.of(context).textTheme.headlineSmall,
),
),
),
],
),
);
}
}
class _MenuItem {
final String label;
final IconData icon;
_MenuItem(this.label, this.icon);
}
Notification Badge Alignment
Create notification badges that align correctly:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedNotificationBadge extends StatelessWidget {
final int count;
final Widget child;
final bool showBadge;
const LocalizedNotificationBadge({
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;
// Badge goes to top-end (top-right in LTR, top-left in RTL)
final badgeAlignment = isRtl ? Alignment.topLeft : Alignment.topRight;
return Stack(
clipBehavior: Clip.none,
children: [
child,
AnimatedAlign(
duration: const Duration(milliseconds: 200),
curve: Curves.easeOutBack,
alignment: badgeAlignment,
child: AnimatedScale(
duration: const Duration(milliseconds: 200),
scale: showBadge && count > 0 ? 1.0 : 0.0,
child: Transform.translate(
offset: Offset(isRtl ? -8 : 8, -8),
child: Semantics(
label: l10n.notificationBadgeAccessibility(count),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
constraints: const BoxConstraints(minWidth: 20),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.error,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: Theme.of(context).colorScheme.surface,
width: 2,
),
),
child: Text(
count > 99 ? '99+' : count.toString(),
style: TextStyle(
color: Theme.of(context).colorScheme.onError,
fontSize: 10,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
),
),
),
),
],
);
}
}
// Usage example
class NotificationBadgeDemo extends StatefulWidget {
const NotificationBadgeDemo({super.key});
@override
State<NotificationBadgeDemo> createState() => _NotificationBadgeDemoState();
}
class _NotificationBadgeDemoState extends State<NotificationBadgeDemo> {
int _notificationCount = 5;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: Text(l10n.notificationsTitle),
actions: [
LocalizedNotificationBadge(
count: _notificationCount,
child: IconButton(
icon: const Icon(Icons.notifications),
onPressed: () {},
tooltip: l10n.viewNotifications,
),
),
const SizedBox(width: 8),
],
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
LocalizedNotificationBadge(
count: _notificationCount,
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(16),
),
child: Icon(
Icons.mail,
size: 40,
color: Theme.of(context).colorScheme.primary,
),
),
),
const SizedBox(height: 32),
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_all),
tooltip: l10n.clearAllNotifications,
),
],
),
],
),
),
);
}
}
Complete ARB File for AnimatedAlign
{
"@@locale": "en",
"alignmentDemoTitle": "Alignment Demo",
"contentCenteredAccessibility": "Content is centered",
"contentStartAccessibility": "Content is aligned to start",
"centeredContent": "Centered",
"alignedContent": "Start Aligned",
"centerContent": "Center Content",
"alignToStart": "Align to Start",
"positionSelectorTitle": "Position Selector",
"positionTopStart": "Top Start",
"positionTopCenter": "Top Center",
"positionTopEnd": "Top End",
"positionCenterStart": "Center Start",
"positionCenter": "Center",
"positionCenterEnd": "Center End",
"positionBottomStart": "Bottom Start",
"positionBottomCenter": "Bottom Center",
"positionBottomEnd": "Bottom End",
"selectPosition": "Select position:",
"contentAtPosition": "Content is at {position}",
"@contentAtPosition": {
"placeholders": {"position": {"type": "String"}}
},
"selectPositionAccessibility": "Move content to {position}",
"@selectPositionAccessibility": {
"placeholders": {"position": {"type": "String"}}
},
"chatTitle": "Chat",
"startConversation": "Start a conversation",
"typeMessage": "Type a message...",
"sendMessage": "Send message",
"you": "You",
"supportAgent": "Support Agent",
"autoReplyMessage": "Thanks for your message! We'll get back to you soon.",
"chatMessageAccessibility": "{sender} said: {message} at {time}",
"@chatMessageAccessibility": {
"placeholders": {
"sender": {"type": "String"},
"message": {"type": "String"},
"time": {"type": "String"}
}
},
"contentLoaderTitle": "Content Loader",
"noContentYet": "No content loaded yet",
"loadContent": "Load Content",
"loadingContent": "Loading...",
"loadingAccessibility": "Loading content, please wait",
"itemTitle": "Item {number}",
"@itemTitle": {
"placeholders": {"number": {"type": "int"}}
},
"itemSubtitle": "Tap for details",
"navigationTitle": "Navigation",
"menuDashboard": "Dashboard",
"menuAnalytics": "Analytics",
"menuReports": "Reports",
"menuSettings": "Settings",
"menuItemAccessibility": "{label}, menu item {current} of {total}",
"@menuItemAccessibility": {
"placeholders": {
"label": {"type": "String"},
"current": {"type": "int"},
"total": {"type": "int"}
}
},
"selectedMenuItem": "Selected: {item}",
"@selectedMenuItem": {
"placeholders": {"item": {"type": "String"}}
},
"notificationsTitle": "Notifications",
"viewNotifications": "View notifications",
"addNotification": "Add notification",
"removeNotification": "Remove notification",
"clearAllNotifications": "Clear all notifications",
"notificationBadgeAccessibility": "{count} {count, plural, =0{no notifications} =1{notification} other{notifications}}",
"@notificationBadgeAccessibility": {
"placeholders": {"count": {"type": "int"}}
}
}
Best Practices Summary
- Use directional alignment: Swap start/end alignments based on RTL state
- Avoid hardcoded left/right: Use AlignmentDirectional or calculate based on text direction
- Provide accessibility labels: Announce position changes for screen readers
- Choose appropriate curves: Use
easeOutCubicfor natural-feeling movement - Keep durations reasonable: 250-500ms works well for alignment animations
- Consider content flow: Ensure aligned content doesn't overlap incorrectly
- Test with RTL layouts: Verify alignments flip correctly for Arabic/Hebrew
- Handle edge cases: Ensure content stays within bounds in all locales
- Combine with other animations: Use with opacity or scale for polished effects
- Support keyboard navigation: Ensure focus indicators align correctly
Conclusion
AnimatedAlign is essential for creating smooth, directionally-aware positioning animations in multilingual Flutter apps. By properly handling RTL layouts, providing accessibility feedback, and using directional terminology, you create intuitive experiences for users worldwide. The patterns shown here—position selectors, chat messages, loaders, and navigation indicators—can be adapted for any application requiring animated alignment transitions.
Remember to test your alignments with various locales to ensure content positions correctly regardless of the user's language and text direction preferences.