← Back to Blog

Flutter AlignTransition Localization: Dynamic Alignment Animations for Multilingual Apps

flutteraligntransitionanimationalignmentlocalizationrtl

Flutter AlignTransition Localization: Dynamic Alignment Animations for Multilingual Apps

AlignTransition provides explicit control over alignment-based animations using an Animation controller. Unlike AnimatedAlign which responds automatically to alignment changes, AlignTransition gives you precise control over animation timing and curves. This guide covers comprehensive strategies for localizing AlignTransition widgets in Flutter multilingual applications.

Understanding AlignTransition Localization

AlignTransition widgets require localization for:

  • Chat interfaces: Message bubble positioning (sent vs received)
  • RTL support: Directional alignment for bidirectional languages
  • Focus indicators: Moving focus states across UI elements
  • Loading states: Animated position indicators
  • Notification badges: Badge position animations
  • Toggle switches: Custom animated toggle controls

Basic AlignTransition with Localized Content

Start with a simple alignment animation:

import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

class LocalizedAlignTransitionDemo extends StatefulWidget {
  const LocalizedAlignTransitionDemo({super.key});

  @override
  State<LocalizedAlignTransitionDemo> createState() => _LocalizedAlignTransitionDemoState();
}

class _LocalizedAlignTransitionDemoState extends State<LocalizedAlignTransitionDemo>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<AlignmentGeometry> _alignmentAnimation;
  bool _isAlignedStart = true;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 500),
    );
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _updateAnimation();
  }

  void _updateAnimation() {
    final isRtl = Directionality.of(context) == TextDirection.rtl;

    _alignmentAnimation = AlignmentGeometryTween(
      begin: isRtl ? AlignmentDirectional.centerEnd : AlignmentDirectional.centerStart,
      end: isRtl ? AlignmentDirectional.centerStart : AlignmentDirectional.centerEnd,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.easeInOutCubic,
    ));
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _toggleAlignment() {
    setState(() {
      _isAlignedStart = !_isAlignedStart;
      if (_isAlignedStart) {
        _controller.reverse();
      } else {
        _controller.forward();
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Scaffold(
      appBar: AppBar(title: Text(l10n.alignTransitionTitle)),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            Text(
              l10n.alignTransitionDescription,
              style: Theme.of(context).textTheme.bodyLarge,
              textAlign: TextAlign.center,
            ),
            const SizedBox(height: 32),
            Container(
              height: 100,
              decoration: BoxDecoration(
                border: Border.all(
                  color: Theme.of(context).colorScheme.outline,
                ),
                borderRadius: BorderRadius.circular(12),
              ),
              child: AlignTransition(
                alignment: _alignmentAnimation,
                child: Semantics(
                  label: l10n.movingIndicatorAccessibility(
                    _isAlignedStart ? l10n.start : l10n.end,
                  ),
                  child: Container(
                    width: 80,
                    height: 80,
                    decoration: BoxDecoration(
                      color: Theme.of(context).colorScheme.primary,
                      borderRadius: BorderRadius.circular(8),
                    ),
                    child: Center(
                      child: Icon(
                        Icons.arrow_forward,
                        color: Theme.of(context).colorScheme.onPrimary,
                      ),
                    ),
                  ),
                ),
              ),
            ),
            const SizedBox(height: 24),
            ElevatedButton(
              onPressed: _toggleAlignment,
              child: Text(
                _isAlignedStart ? l10n.moveToEnd : l10n.moveToStart,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

ARB File Structure for AlignTransition

{
  "alignTransitionTitle": "Alignment Animation",
  "@alignTransitionTitle": {
    "description": "Title for align transition demo"
  },
  "alignTransitionDescription": "Tap the button to animate the box position",
  "movingIndicatorAccessibility": "Moving indicator, currently at {position}",
  "@movingIndicatorAccessibility": {
    "placeholders": {
      "position": {"type": "String"}
    }
  },
  "start": "start",
  "end": "end",
  "moveToEnd": "Move to End",
  "moveToStart": "Move to Start"
}

RTL-Aware Chat Message Interface

Create a chat interface with animated message positioning:

import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

class LocalizedChatWithAlignTransition extends StatefulWidget {
  const LocalizedChatWithAlignTransition({super.key});

  @override
  State<LocalizedChatWithAlignTransition> createState() => _LocalizedChatWithAlignTransitionState();
}

class _LocalizedChatWithAlignTransitionState extends State<LocalizedChatWithAlignTransition>
    with TickerProviderStateMixin {
  final List<_ChatMessage> _messages = [];
  final TextEditingController _textController = TextEditingController();
  final ScrollController _scrollController = ScrollController();

  void _sendMessage(AppLocalizations l10n) {
    if (_textController.text.trim().isEmpty) return;

    setState(() {
      _messages.add(_ChatMessage(
        text: _textController.text,
        isSentByMe: true,
        timestamp: DateTime.now(),
        controller: AnimationController(
          vsync: this,
          duration: const Duration(milliseconds: 400),
        )..forward(),
      ));
      _textController.clear();
    });

    // Simulate reply
    Future.delayed(const Duration(seconds: 1), () {
      if (mounted) {
        setState(() {
          _messages.add(_ChatMessage(
            text: l10n.autoReplyMessage,
            isSentByMe: false,
            timestamp: DateTime.now(),
            controller: AnimationController(
              vsync: this,
              duration: const Duration(milliseconds: 400),
            )..forward(),
          ));
        });
        _scrollToBottom();
      }
    });

    _scrollToBottom();
  }

  void _scrollToBottom() {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (_scrollController.hasClients) {
        _scrollController.animateTo(
          _scrollController.position.maxScrollExtent,
          duration: const Duration(milliseconds: 300),
          curve: Curves.easeOut,
        );
      }
    });
  }

  @override
  void dispose() {
    for (final message in _messages) {
      message.controller.dispose();
    }
    _textController.dispose();
    _scrollController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;
    final isRtl = Directionality.of(context) == TextDirection.rtl;

    return Scaffold(
      appBar: AppBar(
        title: Text(l10n.chatTitle),
        actions: [
          IconButton(
            icon: const Icon(Icons.delete_outline),
            onPressed: () {
              setState(() {
                for (final message in _messages) {
                  message.controller.dispose();
                }
                _messages.clear();
              });
            },
            tooltip: l10n.clearChat,
          ),
        ],
      ),
      body: Column(
        children: [
          Expanded(
            child: _messages.isEmpty
                ? Center(
                    child: Text(
                      l10n.noChatMessages,
                      style: Theme.of(context).textTheme.bodyLarge?.copyWith(
                        color: Theme.of(context).colorScheme.onSurfaceVariant,
                      ),
                    ),
                  )
                : ListView.builder(
                    controller: _scrollController,
                    padding: const EdgeInsets.all(16),
                    itemCount: _messages.length,
                    itemBuilder: (context, index) {
                      final message = _messages[index];

                      final alignAnimation = AlignmentGeometryTween(
                        begin: message.isSentByMe
                            ? (isRtl ? AlignmentDirectional.centerStart : AlignmentDirectional.centerEnd)
                            : (isRtl ? AlignmentDirectional.centerEnd : AlignmentDirectional.centerStart),
                        end: message.isSentByMe
                            ? (isRtl ? AlignmentDirectional.centerStart : AlignmentDirectional.centerEnd)
                            : (isRtl ? AlignmentDirectional.centerEnd : AlignmentDirectional.centerStart),
                      ).animate(message.controller);

                      final slideAnimation = Tween<Offset>(
                        begin: Offset(message.isSentByMe ? 1.0 : -1.0, 0),
                        end: Offset.zero,
                      ).animate(CurvedAnimation(
                        parent: message.controller,
                        curve: Curves.easeOutCubic,
                      ));

                      return Padding(
                        padding: const EdgeInsets.only(bottom: 12),
                        child: SlideTransition(
                          position: slideAnimation,
                          child: AlignTransition(
                            alignment: alignAnimation,
                            child: Semantics(
                              label: l10n.chatMessageAccessibility(
                                message.isSentByMe ? l10n.you : l10n.them,
                                message.text,
                                _formatTime(message.timestamp, l10n),
                              ),
                              child: Container(
                                constraints: BoxConstraints(
                                  maxWidth: MediaQuery.of(context).size.width * 0.75,
                                ),
                                padding: const EdgeInsets.symmetric(
                                  horizontal: 16,
                                  vertical: 10,
                                ),
                                decoration: BoxDecoration(
                                  color: message.isSentByMe
                                      ? Theme.of(context).colorScheme.primary
                                      : Theme.of(context).colorScheme.surfaceContainerHighest,
                                  borderRadius: BorderRadius.only(
                                    topLeft: const Radius.circular(16),
                                    topRight: const Radius.circular(16),
                                    bottomLeft: Radius.circular(message.isSentByMe ? 16 : 4),
                                    bottomRight: Radius.circular(message.isSentByMe ? 4 : 16),
                                  ),
                                ),
                                child: Column(
                                  crossAxisAlignment: CrossAxisAlignment.end,
                                  children: [
                                    Text(
                                      message.text,
                                      style: TextStyle(
                                        color: message.isSentByMe
                                            ? Theme.of(context).colorScheme.onPrimary
                                            : Theme.of(context).colorScheme.onSurface,
                                      ),
                                    ),
                                    const SizedBox(height: 4),
                                    Text(
                                      _formatTime(message.timestamp, l10n),
                                      style: TextStyle(
                                        fontSize: 11,
                                        color: message.isSentByMe
                                            ? Theme.of(context).colorScheme.onPrimary.withOpacity(0.7)
                                            : Theme.of(context).colorScheme.onSurfaceVariant,
                                      ),
                                    ),
                                  ],
                                ),
                              ),
                            ),
                          ),
                        ),
                      );
                    },
                  ),
          ),
          Container(
            padding: const EdgeInsets.all(8),
            decoration: BoxDecoration(
              color: Theme.of(context).colorScheme.surface,
              boxShadow: [
                BoxShadow(
                  color: Colors.black.withOpacity(0.1),
                  blurRadius: 4,
                  offset: const Offset(0, -2),
                ),
              ],
            ),
            child: SafeArea(
              child: Row(
                children: [
                  Expanded(
                    child: TextField(
                      controller: _textController,
                      decoration: InputDecoration(
                        hintText: l10n.typeMessageHint,
                        border: OutlineInputBorder(
                          borderRadius: BorderRadius.circular(24),
                        ),
                        contentPadding: const EdgeInsets.symmetric(
                          horizontal: 16,
                          vertical: 8,
                        ),
                      ),
                      onSubmitted: (_) => _sendMessage(l10n),
                    ),
                  ),
                  const SizedBox(width: 8),
                  IconButton.filled(
                    onPressed: () => _sendMessage(l10n),
                    icon: const Icon(Icons.send),
                    tooltip: l10n.sendMessage,
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }

  String _formatTime(DateTime time, AppLocalizations l10n) {
    final hour = time.hour.toString().padLeft(2, '0');
    final minute = time.minute.toString().padLeft(2, '0');
    return '$hour:$minute';
  }
}

class _ChatMessage {
  final String text;
  final bool isSentByMe;
  final DateTime timestamp;
  final AnimationController controller;

  _ChatMessage({
    required this.text,
    required this.isSentByMe,
    required this.timestamp,
    required this.controller,
  });
}

Animated Focus Indicator

Create a focus indicator that moves between items:

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>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  int _selectedIndex = 0;

  final List<_MenuItem> _menuItems = [];

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _selectItem(int index) {
    if (index == _selectedIndex) return;

    setState(() {
      _selectedIndex = index;
    });
    _controller.forward(from: 0);
  }

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    final menuItems = [
      _MenuItem(Icons.home, l10n.menuHome),
      _MenuItem(Icons.search, l10n.menuSearch),
      _MenuItem(Icons.favorite, l10n.menuFavorites),
      _MenuItem(Icons.person, l10n.menuProfile),
    ];

    return Scaffold(
      appBar: AppBar(title: Text(l10n.animatedMenuTitle)),
      body: Column(
        children: [
          const SizedBox(height: 32),
          Container(
            margin: const EdgeInsets.symmetric(horizontal: 16),
            padding: const EdgeInsets.all(4),
            decoration: BoxDecoration(
              color: Theme.of(context).colorScheme.surfaceContainerHighest,
              borderRadius: BorderRadius.circular(12),
            ),
            child: LayoutBuilder(
              builder: (context, constraints) {
                final itemWidth = (constraints.maxWidth - 8) / menuItems.length;

                return Stack(
                  children: [
                    // Animated background indicator
                    AnimatedBuilder(
                      animation: _controller,
                      builder: (context, child) {
                        return AlignTransition(
                          alignment: AlignmentGeometryTween(
                            begin: _getAlignment(_selectedIndex, menuItems.length),
                            end: _getAlignment(_selectedIndex, menuItems.length),
                          ).animate(_controller),
                          child: Container(
                            width: itemWidth,
                            height: 48,
                            decoration: BoxDecoration(
                              color: Theme.of(context).colorScheme.primary,
                              borderRadius: BorderRadius.circular(8),
                            ),
                          ),
                        );
                      },
                    ),
                    // Menu items
                    Row(
                      children: menuItems.asMap().entries.map((entry) {
                        final index = entry.key;
                        final item = entry.value;
                        final isSelected = index == _selectedIndex;

                        return Expanded(
                          child: Semantics(
                            button: true,
                            selected: isSelected,
                            label: l10n.menuItemAccessibility(
                              item.label,
                              isSelected ? l10n.selected : l10n.notSelected,
                            ),
                            child: InkWell(
                              onTap: () => _selectItem(index),
                              borderRadius: BorderRadius.circular(8),
                              child: SizedBox(
                                height: 48,
                                child: Row(
                                  mainAxisAlignment: MainAxisAlignment.center,
                                  children: [
                                    Icon(
                                      item.icon,
                                      size: 20,
                                      color: isSelected
                                          ? Theme.of(context).colorScheme.onPrimary
                                          : Theme.of(context).colorScheme.onSurfaceVariant,
                                    ),
                                    const SizedBox(width: 8),
                                    Text(
                                      item.label,
                                      style: TextStyle(
                                        color: isSelected
                                            ? Theme.of(context).colorScheme.onPrimary
                                            : Theme.of(context).colorScheme.onSurfaceVariant,
                                        fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
                                      ),
                                    ),
                                  ],
                                ),
                              ),
                            ),
                          ),
                        );
                      }).toList(),
                    ),
                  ],
                );
              },
            ),
          ),
          const SizedBox(height: 32),
          Expanded(
            child: Center(
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  Icon(
                    menuItems[_selectedIndex].icon,
                    size: 64,
                    color: Theme.of(context).colorScheme.primary,
                  ),
                  const SizedBox(height: 16),
                  Text(
                    l10n.selectedPage(menuItems[_selectedIndex].label),
                    style: Theme.of(context).textTheme.headlineSmall,
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }

  AlignmentGeometry _getAlignment(int index, int total) {
    if (total == 1) return Alignment.center;
    final fraction = index / (total - 1);
    return Alignment.lerp(
      Alignment.centerLeft,
      Alignment.centerRight,
      fraction,
    )!;
  }
}

class _MenuItem {
  final IconData icon;
  final String label;

  _MenuItem(this.icon, this.label);
}

Animated Step Progress Indicator

Create a progress indicator with moving alignment:

import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

class LocalizedStepProgressIndicator extends StatefulWidget {
  const LocalizedStepProgressIndicator({super.key});

  @override
  State<LocalizedStepProgressIndicator> createState() => _LocalizedStepProgressIndicatorState();
}

class _LocalizedStepProgressIndicatorState extends State<LocalizedStepProgressIndicator>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<AlignmentGeometry> _alignmentAnimation;
  int _currentStep = 0;
  int _previousStep = 0;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 400),
    );
    _updateAnimation();
  }

  void _updateAnimation() {
    _alignmentAnimation = AlignmentGeometryTween(
      begin: _getStepAlignment(_previousStep),
      end: _getStepAlignment(_currentStep),
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.easeInOutCubic,
    ));
  }

  AlignmentGeometry _getStepAlignment(int step) {
    const totalSteps = 4;
    final fraction = step / (totalSteps - 1);
    return AlignmentDirectional.lerp(
      AlignmentDirectional.centerStart,
      AlignmentDirectional.centerEnd,
      fraction,
    )!;
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _goToStep(int step) {
    if (step < 0 || step > 3 || step == _currentStep) return;

    setState(() {
      _previousStep = _currentStep;
      _currentStep = step;
      _updateAnimation();
      _controller.forward(from: 0);
    });
  }

  void _nextStep() => _goToStep(_currentStep + 1);
  void _previousStepAction() => _goToStep(_currentStep - 1);

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    final steps = [
      _Step(l10n.stepAccount, Icons.person),
      _Step(l10n.stepDetails, Icons.edit),
      _Step(l10n.stepPayment, Icons.payment),
      _Step(l10n.stepConfirm, Icons.check),
    ];

    return Scaffold(
      appBar: AppBar(title: Text(l10n.stepProgressTitle)),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            // Progress indicator
            Container(
              height: 80,
              padding: const EdgeInsets.symmetric(horizontal: 24),
              child: Stack(
                children: [
                  // Background line
                  Positioned(
                    top: 20,
                    left: 24,
                    right: 24,
                    child: Container(
                      height: 4,
                      decoration: BoxDecoration(
                        color: Theme.of(context).colorScheme.surfaceContainerHighest,
                        borderRadius: BorderRadius.circular(2),
                      ),
                    ),
                  ),
                  // Progress line
                  Positioned(
                    top: 20,
                    left: 24,
                    right: 24,
                    child: LayoutBuilder(
                      builder: (context, constraints) {
                        final progress = _currentStep / (steps.length - 1);
                        return Align(
                          alignment: AlignmentDirectional.centerStart,
                          child: AnimatedContainer(
                            duration: const Duration(milliseconds: 400),
                            curve: Curves.easeInOutCubic,
                            height: 4,
                            width: constraints.maxWidth * progress,
                            decoration: BoxDecoration(
                              color: Theme.of(context).colorScheme.primary,
                              borderRadius: BorderRadius.circular(2),
                            ),
                          ),
                        );
                      },
                    ),
                  ),
                  // Step indicators
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: steps.asMap().entries.map((entry) {
                      final index = entry.key;
                      final step = entry.value;
                      final isCompleted = index < _currentStep;
                      final isCurrent = index == _currentStep;

                      return Semantics(
                        button: true,
                        label: l10n.stepAccessibility(
                          index + 1,
                          step.label,
                          isCompleted
                              ? l10n.completed
                              : isCurrent
                                  ? l10n.current
                                  : l10n.upcoming,
                        ),
                        child: GestureDetector(
                          onTap: () => _goToStep(index),
                          child: Column(
                            children: [
                              AnimatedContainer(
                                duration: const Duration(milliseconds: 300),
                                width: 44,
                                height: 44,
                                decoration: BoxDecoration(
                                  color: isCompleted || isCurrent
                                      ? Theme.of(context).colorScheme.primary
                                      : Theme.of(context).colorScheme.surface,
                                  border: Border.all(
                                    color: isCompleted || isCurrent
                                        ? Theme.of(context).colorScheme.primary
                                        : Theme.of(context).colorScheme.outline,
                                    width: 2,
                                  ),
                                  shape: BoxShape.circle,
                                ),
                                child: Center(
                                  child: isCompleted
                                      ? Icon(
                                          Icons.check,
                                          color: Theme.of(context).colorScheme.onPrimary,
                                          size: 20,
                                        )
                                      : Text(
                                          '${index + 1}',
                                          style: TextStyle(
                                            color: isCurrent
                                                ? Theme.of(context).colorScheme.onPrimary
                                                : Theme.of(context).colorScheme.onSurface,
                                            fontWeight: FontWeight.bold,
                                          ),
                                        ),
                                ),
                              ),
                              const SizedBox(height: 8),
                              Text(
                                step.label,
                                style: TextStyle(
                                  fontSize: 12,
                                  fontWeight: isCurrent ? FontWeight.w600 : FontWeight.normal,
                                  color: isCurrent
                                      ? Theme.of(context).colorScheme.primary
                                      : Theme.of(context).colorScheme.onSurfaceVariant,
                                ),
                              ),
                            ],
                          ),
                        ),
                      );
                    }).toList(),
                  ),
                ],
              ),
            ),
            const SizedBox(height: 32),
            // Current step indicator with align transition
            Container(
              height: 200,
              decoration: BoxDecoration(
                color: Theme.of(context).colorScheme.surfaceContainerHighest,
                borderRadius: BorderRadius.circular(16),
              ),
              child: AlignTransition(
                alignment: _alignmentAnimation,
                child: Padding(
                  padding: const EdgeInsets.all(24),
                  child: Column(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      Icon(
                        steps[_currentStep].icon,
                        size: 48,
                        color: Theme.of(context).colorScheme.primary,
                      ),
                      const SizedBox(height: 16),
                      Text(
                        l10n.stepContent(_currentStep + 1, steps[_currentStep].label),
                        style: Theme.of(context).textTheme.titleLarge,
                        textAlign: TextAlign.center,
                      ),
                    ],
                  ),
                ),
              ),
            ),
            const Spacer(),
            // Navigation buttons
            Row(
              children: [
                if (_currentStep > 0)
                  Expanded(
                    child: OutlinedButton(
                      onPressed: _previousStepAction,
                      child: Text(l10n.previousStep),
                    ),
                  ),
                if (_currentStep > 0) const SizedBox(width: 16),
                Expanded(
                  child: ElevatedButton(
                    onPressed: _currentStep < 3 ? _nextStep : null,
                    child: Text(
                      _currentStep < 3 ? l10n.nextStep : l10n.complete,
                    ),
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

class _Step {
  final String label;
  final IconData icon;

  _Step(this.label, this.icon);
}

Animated Toggle Switch

Create a custom toggle with alignment animation:

import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

class LocalizedAnimatedToggle extends StatefulWidget {
  const LocalizedAnimatedToggle({super.key});

  @override
  State<LocalizedAnimatedToggle> createState() => _LocalizedAnimatedToggleState();
}

class _LocalizedAnimatedToggleState extends State<LocalizedAnimatedToggle>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<AlignmentGeometry> _alignmentAnimation;
  late Animation<Color?> _colorAnimation;
  bool _isEnabled = false;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 250),
    );

    _alignmentAnimation = AlignmentGeometryTween(
      begin: AlignmentDirectional.centerStart,
      end: AlignmentDirectional.centerEnd,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.easeInOutCubic,
    ));
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _colorAnimation = ColorTween(
      begin: Theme.of(context).colorScheme.surfaceContainerHighest,
      end: Theme.of(context).colorScheme.primary,
    ).animate(_controller);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _toggle() {
    setState(() {
      _isEnabled = !_isEnabled;
      if (_isEnabled) {
        _controller.forward();
      } else {
        _controller.reverse();
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Scaffold(
      appBar: AppBar(title: Text(l10n.customToggleTitle)),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            _SettingRow(
              icon: Icons.notifications,
              title: l10n.notificationsToggle,
              subtitle: l10n.notificationsDescription,
              isEnabled: _isEnabled,
              controller: _controller,
              alignmentAnimation: _alignmentAnimation,
              colorAnimation: _colorAnimation,
              onToggle: _toggle,
              l10n: l10n,
            ),
            const Divider(),
            _buildToggleSetting(
              context,
              Icons.dark_mode,
              l10n.darkModeToggle,
              l10n.darkModeDescription,
              l10n,
            ),
            const Divider(),
            _buildToggleSetting(
              context,
              Icons.location_on,
              l10n.locationToggle,
              l10n.locationDescription,
              l10n,
            ),
            const Divider(),
            _buildToggleSetting(
              context,
              Icons.sync,
              l10n.autoSyncToggle,
              l10n.autoSyncDescription,
              l10n,
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildToggleSetting(
    BuildContext context,
    IconData icon,
    String title,
    String subtitle,
    AppLocalizations l10n,
  ) {
    return _IndependentToggleRow(
      icon: icon,
      title: title,
      subtitle: subtitle,
      l10n: l10n,
    );
  }
}

class _SettingRow extends StatelessWidget {
  final IconData icon;
  final String title;
  final String subtitle;
  final bool isEnabled;
  final AnimationController controller;
  final Animation<AlignmentGeometry> alignmentAnimation;
  final Animation<Color?> colorAnimation;
  final VoidCallback onToggle;
  final AppLocalizations l10n;

  const _SettingRow({
    required this.icon,
    required this.title,
    required this.subtitle,
    required this.isEnabled,
    required this.controller,
    required this.alignmentAnimation,
    required this.colorAnimation,
    required this.onToggle,
    required this.l10n,
  });

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 12),
      child: Row(
        children: [
          Icon(icon, color: Theme.of(context).colorScheme.primary),
          const SizedBox(width: 16),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  title,
                  style: Theme.of(context).textTheme.titleMedium,
                ),
                Text(
                  subtitle,
                  style: Theme.of(context).textTheme.bodySmall?.copyWith(
                    color: Theme.of(context).colorScheme.onSurfaceVariant,
                  ),
                ),
              ],
            ),
          ),
          Semantics(
            toggled: isEnabled,
            label: l10n.toggleAccessibility(title, isEnabled ? l10n.on : l10n.off),
            child: GestureDetector(
              onTap: onToggle,
              child: AnimatedBuilder(
                animation: controller,
                builder: (context, child) {
                  return Container(
                    width: 56,
                    height: 32,
                    padding: const EdgeInsets.all(4),
                    decoration: BoxDecoration(
                      color: colorAnimation.value,
                      borderRadius: BorderRadius.circular(16),
                    ),
                    child: AlignTransition(
                      alignment: alignmentAnimation,
                      child: Container(
                        width: 24,
                        height: 24,
                        decoration: BoxDecoration(
                          color: Theme.of(context).colorScheme.surface,
                          shape: BoxShape.circle,
                          boxShadow: [
                            BoxShadow(
                              color: Colors.black.withOpacity(0.2),
                              blurRadius: 4,
                              offset: const Offset(0, 2),
                            ),
                          ],
                        ),
                      ),
                    ),
                  );
                },
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class _IndependentToggleRow extends StatefulWidget {
  final IconData icon;
  final String title;
  final String subtitle;
  final AppLocalizations l10n;

  const _IndependentToggleRow({
    required this.icon,
    required this.title,
    required this.subtitle,
    required this.l10n,
  });

  @override
  State<_IndependentToggleRow> createState() => _IndependentToggleRowState();
}

class _IndependentToggleRowState extends State<_IndependentToggleRow>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<AlignmentGeometry> _alignmentAnimation;
  late Animation<Color?> _colorAnimation;
  bool _isEnabled = false;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 250),
    );

    _alignmentAnimation = AlignmentGeometryTween(
      begin: AlignmentDirectional.centerStart,
      end: AlignmentDirectional.centerEnd,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.easeInOutCubic,
    ));
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _colorAnimation = ColorTween(
      begin: Theme.of(context).colorScheme.surfaceContainerHighest,
      end: Theme.of(context).colorScheme.primary,
    ).animate(_controller);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _toggle() {
    setState(() {
      _isEnabled = !_isEnabled;
      if (_isEnabled) {
        _controller.forward();
      } else {
        _controller.reverse();
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return _SettingRow(
      icon: widget.icon,
      title: widget.title,
      subtitle: widget.subtitle,
      isEnabled: _isEnabled,
      controller: _controller,
      alignmentAnimation: _alignmentAnimation,
      colorAnimation: _colorAnimation,
      onToggle: _toggle,
      l10n: widget.l10n,
    );
  }
}

Complete ARB File for AlignTransition

{
  "@@locale": "en",

  "alignTransitionTitle": "Alignment Animation",
  "alignTransitionDescription": "Tap the button to animate the box position",
  "movingIndicatorAccessibility": "Moving indicator, currently at {position}",
  "@movingIndicatorAccessibility": {
    "placeholders": {
      "position": {"type": "String"}
    }
  },
  "start": "start",
  "end": "end",
  "moveToEnd": "Move to End",
  "moveToStart": "Move to Start",

  "chatTitle": "Chat",
  "clearChat": "Clear chat",
  "noChatMessages": "No messages yet. Start a conversation!",
  "typeMessageHint": "Type a message...",
  "sendMessage": "Send",
  "autoReplyMessage": "Thanks for your message! This is an automated reply.",
  "you": "You",
  "them": "Them",
  "chatMessageAccessibility": "Message from {sender}: {message}, sent at {time}",
  "@chatMessageAccessibility": {
    "placeholders": {
      "sender": {"type": "String"},
      "message": {"type": "String"},
      "time": {"type": "String"}
    }
  },

  "animatedMenuTitle": "Animated Menu",
  "menuHome": "Home",
  "menuSearch": "Search",
  "menuFavorites": "Favorites",
  "menuProfile": "Profile",
  "menuItemAccessibility": "{item}, {state}",
  "@menuItemAccessibility": {
    "placeholders": {
      "item": {"type": "String"},
      "state": {"type": "String"}
    }
  },
  "selected": "selected",
  "notSelected": "not selected",
  "selectedPage": "Currently viewing: {page}",
  "@selectedPage": {
    "placeholders": {
      "page": {"type": "String"}
    }
  },

  "stepProgressTitle": "Step Progress",
  "stepAccount": "Account",
  "stepDetails": "Details",
  "stepPayment": "Payment",
  "stepConfirm": "Confirm",
  "stepAccessibility": "Step {number}: {label}, {state}",
  "@stepAccessibility": {
    "placeholders": {
      "number": {"type": "int"},
      "label": {"type": "String"},
      "state": {"type": "String"}
    }
  },
  "completed": "completed",
  "current": "current",
  "upcoming": "upcoming",
  "stepContent": "Step {number}: {title}",
  "@stepContent": {
    "placeholders": {
      "number": {"type": "int"},
      "title": {"type": "String"}
    }
  },
  "previousStep": "Previous",
  "nextStep": "Next",
  "complete": "Complete",

  "customToggleTitle": "Settings",
  "notificationsToggle": "Notifications",
  "notificationsDescription": "Receive push notifications",
  "darkModeToggle": "Dark Mode",
  "darkModeDescription": "Enable dark theme",
  "locationToggle": "Location Services",
  "locationDescription": "Allow location access",
  "autoSyncToggle": "Auto Sync",
  "autoSyncDescription": "Automatically sync data",
  "toggleAccessibility": "{setting}, {state}",
  "@toggleAccessibility": {
    "placeholders": {
      "setting": {"type": "String"},
      "state": {"type": "String"}
    }
  },
  "on": "on",
  "off": "off"
}

Best Practices Summary

  1. Use AlignmentGeometryTween: Supports both Alignment and AlignmentDirectional for RTL
  2. Handle RTL properly: Always use AlignmentDirectional for bidirectional support
  3. Combine with other transitions: AlignTransition works well with SlideTransition and FadeTransition
  4. Dispose controllers: Always dispose AnimationControllers in the dispose method
  5. Add accessibility labels: Use Semantics with position state information
  6. Use appropriate curves: easeInOutCubic provides smooth movement feel
  7. Consider layout constraints: Ensure parent containers have proper dimensions
  8. Test bidirectional layouts: Verify animations work correctly in RTL languages
  9. Animate related properties: Combine alignment with color or size for richer effects
  10. Keep animations performant: Avoid animating very heavy widgets

Conclusion

AlignTransition provides precise control over position-based animations using explicit AnimationControllers. By combining proper RTL support through AlignmentDirectional with smooth animations, you can create polished chat interfaces, focus indicators, step progress components, and custom toggles that work seamlessly across all languages. The key is using AlignmentGeometryTween for directional awareness and always testing with both LTR and RTL text directions.

Remember to always dispose of your AnimationControllers and leverage the animation composition capabilities to create rich, accessible UI experiences for your multilingual users.