← Back to Blog

Flutter PositionedTransition Localization: Animated Positioning in Stack for Multilingual Apps

flutterpositionedtransitionanimationpositioninglocalizationrtl

Flutter PositionedTransition Localization: Animated Positioning in Stack for Multilingual Apps

PositionedTransition provides explicit control over position animations within a Stack using an Animation controller. Unlike AnimatedPositioned which handles animation internally, PositionedTransition gives you precise control over timing, curves, and synchronization with other animations. This guide covers comprehensive strategies for localizing PositionedTransition widgets in Flutter multilingual applications.

Understanding PositionedTransition Localization

PositionedTransition widgets require localization for:

  • Sliding panels: Drawer-like panels with localized content
  • Floating action buttons: Animated FAB positioning
  • Tooltips and popovers: Positioned hints and information
  • Game elements: Moving UI components with labels
  • Notification badges: Animated badge positioning
  • Context menus: Positioned menu items

Basic PositionedTransition with Localized Content

Start with a simple sliding panel:

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>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<RelativeRect> _panelAnimation;
  bool _isPanelOpen = false;

  static const double _panelWidth = 280.0;

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

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

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

    _panelAnimation = RelativeRectTween(
      begin: RelativeRect.fromLTRB(
        isRtl ? -_panelWidth : MediaQuery.of(context).size.width,
        0,
        isRtl ? MediaQuery.of(context).size.width : -_panelWidth,
        0,
      ),
      end: RelativeRect.fromLTRB(
        isRtl ? 0 : MediaQuery.of(context).size.width - _panelWidth,
        0,
        isRtl ? MediaQuery.of(context).size.width - _panelWidth : 0,
        0,
      ),
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.easeOutCubic,
    ));
  }

  void _togglePanel() {
    setState(() {
      _isPanelOpen = !_isPanelOpen;
      if (_isPanelOpen) {
        _controller.forward();
      } else {
        _controller.reverse();
      }
    });
  }

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

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

    return Scaffold(
      appBar: AppBar(
        title: Text(l10n.slidingPanelTitle),
        actions: [
          IconButton(
            onPressed: _togglePanel,
            icon: Icon(_isPanelOpen ? Icons.close : Icons.menu),
            tooltip: _isPanelOpen ? l10n.closePanel : l10n.openPanel,
          ),
        ],
      ),
      body: Stack(
        children: [
          // Main content
          Center(
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                Icon(
                  Icons.touch_app,
                  size: 64,
                  color: Theme.of(context).colorScheme.primary,
                ),
                const SizedBox(height: 16),
                Text(
                  l10n.panelInstructions,
                  style: Theme.of(context).textTheme.bodyLarge,
                  textAlign: TextAlign.center,
                ),
              ],
            ),
          ),
          // Sliding panel
          PositionedTransition(
            rect: _panelAnimation,
            child: Semantics(
              container: true,
              label: l10n.settingsPanelAccessibility,
              child: Material(
                elevation: 8,
                child: Container(
                  width: _panelWidth,
                  color: Theme.of(context).colorScheme.surface,
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Container(
                        padding: const EdgeInsets.all(16),
                        color: Theme.of(context).colorScheme.primaryContainer,
                        child: Row(
                          children: [
                            CircleAvatar(
                              child: Text(l10n.userInitial),
                            ),
                            const SizedBox(width: 12),
                            Expanded(
                              child: Column(
                                crossAxisAlignment: CrossAxisAlignment.start,
                                children: [
                                  Text(
                                    l10n.userName,
                                    style: Theme.of(context).textTheme.titleMedium,
                                  ),
                                  Text(
                                    l10n.userEmail,
                                    style: Theme.of(context).textTheme.bodySmall,
                                  ),
                                ],
                              ),
                            ),
                          ],
                        ),
                      ),
                      Expanded(
                        child: ListView(
                          children: [
                            _PanelMenuItem(
                              icon: Icons.person,
                              label: l10n.menuProfile,
                              onTap: () {},
                            ),
                            _PanelMenuItem(
                              icon: Icons.settings,
                              label: l10n.menuSettings,
                              onTap: () {},
                            ),
                            _PanelMenuItem(
                              icon: Icons.notifications,
                              label: l10n.menuNotifications,
                              onTap: () {},
                            ),
                            _PanelMenuItem(
                              icon: Icons.help,
                              label: l10n.menuHelp,
                              onTap: () {},
                            ),
                            const Divider(),
                            _PanelMenuItem(
                              icon: Icons.logout,
                              label: l10n.menuLogout,
                              onTap: () {},
                            ),
                          ],
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class _PanelMenuItem extends StatelessWidget {
  final IconData icon;
  final String label;
  final VoidCallback onTap;

  const _PanelMenuItem({
    required this.icon,
    required this.label,
    required this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: Icon(icon),
      title: Text(label),
      onTap: onTap,
    );
  }
}

ARB File Structure for PositionedTransition

{
  "slidingPanelTitle": "Sliding Panel Demo",
  "@slidingPanelTitle": {
    "description": "Title for sliding panel demo screen"
  },
  "openPanel": "Open panel",
  "closePanel": "Close panel",
  "panelInstructions": "Tap the menu icon to open the settings panel",
  "settingsPanelAccessibility": "Settings panel",
  "userInitial": "J",
  "userName": "John Doe",
  "userEmail": "john.doe@example.com",
  "menuProfile": "Profile",
  "menuSettings": "Settings",
  "menuNotifications": "Notifications",
  "menuHelp": "Help & Support",
  "menuLogout": "Log Out"
}

Floating Help Button with Position Animation

Create a floating help button that moves to avoid content:

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

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

  @override
  State<LocalizedFloatingHelper> createState() => _LocalizedFloatingHelperState();
}

class _LocalizedFloatingHelperState extends State<LocalizedFloatingHelper>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<RelativeRect> _positionAnimation;

  bool _isKeyboardVisible = false;
  bool _isHelpExpanded = false;

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

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

  void _updateAnimation() {
    final screenHeight = MediaQuery.of(context).size.height;
    final bottomPadding = MediaQuery.of(context).viewInsets.bottom;

    _positionAnimation = RelativeRectTween(
      begin: RelativeRect.fromLTRB(
        0,
        screenHeight - 80,
        16,
        16,
      ),
      end: RelativeRect.fromLTRB(
        0,
        screenHeight - bottomPadding - 200,
        16,
        bottomPadding + 16,
      ),
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.easeOutBack,
    ));

    if (bottomPadding > 0 && !_isKeyboardVisible) {
      _isKeyboardVisible = true;
      _controller.forward();
    } else if (bottomPadding == 0 && _isKeyboardVisible) {
      _isKeyboardVisible = false;
      _controller.reverse();
    }
  }

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

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

    return Scaffold(
      appBar: AppBar(title: Text(l10n.floatingHelperTitle)),
      body: Stack(
        children: [
          // Form content
          SingleChildScrollView(
            padding: const EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  l10n.contactFormHeader,
                  style: Theme.of(context).textTheme.headlineSmall,
                ),
                const SizedBox(height: 24),
                TextField(
                  decoration: InputDecoration(
                    labelText: l10n.nameFieldLabel,
                    border: const OutlineInputBorder(),
                  ),
                ),
                const SizedBox(height: 16),
                TextField(
                  decoration: InputDecoration(
                    labelText: l10n.emailFieldLabel,
                    border: const OutlineInputBorder(),
                  ),
                  keyboardType: TextInputType.emailAddress,
                ),
                const SizedBox(height: 16),
                TextField(
                  decoration: InputDecoration(
                    labelText: l10n.messageFieldLabel,
                    border: const OutlineInputBorder(),
                    alignLabelWithHint: true,
                  ),
                  maxLines: 5,
                ),
                const SizedBox(height: 24),
                SizedBox(
                  width: double.infinity,
                  child: ElevatedButton(
                    onPressed: () {},
                    child: Text(l10n.sendMessageButton),
                  ),
                ),
              ],
            ),
          ),
          // Floating help button
          PositionedTransition(
            rect: _positionAnimation,
            child: Align(
              alignment: AlignmentDirectional.bottomEnd,
              child: Column(
                mainAxisSize: MainAxisSize.min,
                crossAxisAlignment: CrossAxisAlignment.end,
                children: [
                  if (_isHelpExpanded)
                    Semantics(
                      container: true,
                      label: l10n.helpTooltipAccessibility,
                      child: Container(
                        margin: const EdgeInsets.only(bottom: 8),
                        padding: const EdgeInsets.all(12),
                        constraints: const BoxConstraints(maxWidth: 250),
                        decoration: BoxDecoration(
                          color: Theme.of(context).colorScheme.inverseSurface,
                          borderRadius: BorderRadius.circular(12),
                        ),
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Text(
                              l10n.helpTooltipTitle,
                              style: TextStyle(
                                color: Theme.of(context).colorScheme.onInverseSurface,
                                fontWeight: FontWeight.bold,
                              ),
                            ),
                            const SizedBox(height: 4),
                            Text(
                              l10n.helpTooltipContent,
                              style: TextStyle(
                                color: Theme.of(context).colorScheme.onInverseSurface,
                                fontSize: 13,
                              ),
                            ),
                          ],
                        ),
                      ),
                    ),
                  Semantics(
                    button: true,
                    label: l10n.helpButtonAccessibility,
                    child: FloatingActionButton(
                      mini: true,
                      onPressed: () {
                        setState(() {
                          _isHelpExpanded = !_isHelpExpanded;
                        });
                      },
                      child: Icon(_isHelpExpanded ? Icons.close : Icons.help_outline),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

Animated Card Stack

Create a stack of cards with position animations:

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

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

  @override
  State<LocalizedCardStack> createState() => _LocalizedCardStackState();
}

class _LocalizedCardStackState extends State<LocalizedCardStack>
    with TickerProviderStateMixin {
  late List<AnimationController> _controllers;
  late List<Animation<RelativeRect>> _animations;

  int _currentIndex = 0;

  final List<Color> _cardColors = [
    Colors.blue,
    Colors.green,
    Colors.orange,
    Colors.purple,
    Colors.red,
  ];

  @override
  void initState() {
    super.initState();
    _controllers = List.generate(
      _cardColors.length,
      (index) => AnimationController(
        vsync: this,
        duration: const Duration(milliseconds: 400),
      ),
    );
  }

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

  void _updateAnimations() {
    final screenWidth = MediaQuery.of(context).size.width;

    _animations = List.generate(_controllers.length, (index) {
      return RelativeRectTween(
        begin: RelativeRect.fromLTRB(
          20 + (index * 8.0),
          100 + (index * 16.0),
          20 + ((_controllers.length - 1 - index) * 8.0),
          200 - (index * 16.0),
        ),
        end: RelativeRect.fromLTRB(
          screenWidth,
          100 + (index * 16.0),
          -screenWidth + 40,
          200 - (index * 16.0),
        ),
      ).animate(CurvedAnimation(
        parent: _controllers[index],
        curve: Curves.easeInOut,
      ));
    });
  }

  void _swipeCard() {
    if (_currentIndex < _cardColors.length) {
      _controllers[_currentIndex].forward().then((_) {
        setState(() {
          _currentIndex++;
        });
      });
    }
  }

  void _resetCards() {
    for (final controller in _controllers) {
      controller.reset();
    }
    setState(() {
      _currentIndex = 0;
    });
  }

  @override
  void dispose() {
    for (final controller in _controllers) {
      controller.dispose();
    }
    super.dispose();
  }

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

    final cardTitles = [
      l10n.cardTitle1,
      l10n.cardTitle2,
      l10n.cardTitle3,
      l10n.cardTitle4,
      l10n.cardTitle5,
    ];

    final cardDescriptions = [
      l10n.cardDescription1,
      l10n.cardDescription2,
      l10n.cardDescription3,
      l10n.cardDescription4,
      l10n.cardDescription5,
    ];

    return Scaffold(
      appBar: AppBar(
        title: Text(l10n.cardStackTitle),
        actions: [
          IconButton(
            onPressed: _resetCards,
            icon: const Icon(Icons.refresh),
            tooltip: l10n.resetCards,
          ),
        ],
      ),
      body: Stack(
        children: [
          // Progress indicator
          Positioned(
            top: 16,
            left: 16,
            right: 16,
            child: Column(
              children: [
                Text(
                  l10n.cardProgress(_currentIndex, _cardColors.length),
                  style: Theme.of(context).textTheme.bodyMedium,
                ),
                const SizedBox(height: 8),
                LinearProgressIndicator(
                  value: _currentIndex / _cardColors.length,
                ),
              ],
            ),
          ),
          // Cards
          ...List.generate(_cardColors.length, (index) {
            final reversedIndex = _cardColors.length - 1 - index;
            return PositionedTransition(
              rect: _animations[reversedIndex],
              child: GestureDetector(
                onHorizontalDragEnd: (details) {
                  if (reversedIndex == _currentIndex &&
                      details.primaryVelocity != null &&
                      details.primaryVelocity!.abs() > 500) {
                    _swipeCard();
                  }
                },
                child: Semantics(
                  label: l10n.cardAccessibility(
                    reversedIndex + 1,
                    cardTitles[reversedIndex],
                  ),
                  child: Card(
                    elevation: 4 + (reversedIndex * 2.0),
                    color: _cardColors[reversedIndex],
                    child: Padding(
                      padding: const EdgeInsets.all(24),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Row(
                            children: [
                              Container(
                                padding: const EdgeInsets.symmetric(
                                  horizontal: 12,
                                  vertical: 6,
                                ),
                                decoration: BoxDecoration(
                                  color: Colors.white.withOpacity(0.3),
                                  borderRadius: BorderRadius.circular(20),
                                ),
                                child: Text(
                                  l10n.cardNumber(reversedIndex + 1),
                                  style: const TextStyle(
                                    color: Colors.white,
                                    fontWeight: FontWeight.bold,
                                  ),
                                ),
                              ),
                            ],
                          ),
                          const Spacer(),
                          Text(
                            cardTitles[reversedIndex],
                            style: const TextStyle(
                              color: Colors.white,
                              fontSize: 24,
                              fontWeight: FontWeight.bold,
                            ),
                          ),
                          const SizedBox(height: 8),
                          Text(
                            cardDescriptions[reversedIndex],
                            style: TextStyle(
                              color: Colors.white.withOpacity(0.9),
                              fontSize: 14,
                            ),
                          ),
                          const SizedBox(height: 16),
                          Text(
                            l10n.swipeToNext,
                            style: TextStyle(
                              color: Colors.white.withOpacity(0.7),
                              fontSize: 12,
                            ),
                          ),
                        ],
                      ),
                    ),
                  ),
                ),
              ),
            );
          }),
          // Empty state
          if (_currentIndex >= _cardColors.length)
            Center(
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  Icon(
                    Icons.check_circle,
                    size: 64,
                    color: Theme.of(context).colorScheme.primary,
                  ),
                  const SizedBox(height: 16),
                  Text(
                    l10n.allCardsReviewed,
                    style: Theme.of(context).textTheme.headlineSmall,
                  ),
                  const SizedBox(height: 8),
                  ElevatedButton(
                    onPressed: _resetCards,
                    child: Text(l10n.startOver),
                  ),
                ],
              ),
            ),
        ],
      ),
    );
  }
}

Tooltip Positioning System

Create tooltips that position themselves based on available space:

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

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

  @override
  State<LocalizedTooltipSystem> createState() => _LocalizedTooltipSystemState();
}

class _LocalizedTooltipSystemState extends State<LocalizedTooltipSystem>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  int? _activeTooltip;
  Offset _tooltipPosition = Offset.zero;
  late Animation<RelativeRect> _tooltipAnimation;

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

  void _showTooltip(int index, GlobalKey key) {
    final RenderBox? renderBox = key.currentContext?.findRenderObject() as RenderBox?;
    if (renderBox == null) return;

    final position = renderBox.localToGlobal(Offset.zero);
    final size = renderBox.size;
    final screenSize = MediaQuery.of(context).size;

    // Calculate tooltip position
    double top = position.dy - 60;
    double left = position.dx + size.width / 2 - 100;
    double right = screenSize.width - left - 200;
    double bottom = screenSize.height - top - 50;

    // Adjust if tooltip would go off screen
    if (top < 50) {
      top = position.dy + size.height + 10;
      bottom = screenSize.height - top - 50;
    }
    if (left < 10) {
      left = 10;
      right = screenSize.width - 210;
    }

    setState(() {
      _activeTooltip = index;
      _tooltipAnimation = RelativeRectTween(
        begin: RelativeRect.fromLTRB(left, top + 20, right, bottom - 20),
        end: RelativeRect.fromLTRB(left, top, right, bottom),
      ).animate(CurvedAnimation(
        parent: _controller,
        curve: Curves.easeOutBack,
      ));
    });

    _controller.forward(from: 0);
  }

  void _hideTooltip() {
    _controller.reverse().then((_) {
      setState(() {
        _activeTooltip = null;
      });
    });
  }

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

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

    final features = [
      _Feature(
        icon: Icons.speed,
        title: l10n.featurePerformanceTitle,
        tooltip: l10n.featurePerformanceTooltip,
        key: GlobalKey(),
      ),
      _Feature(
        icon: Icons.security,
        title: l10n.featureSecurityTitle,
        tooltip: l10n.featureSecurityTooltip,
        key: GlobalKey(),
      ),
      _Feature(
        icon: Icons.cloud,
        title: l10n.featureCloudTitle,
        tooltip: l10n.featureCloudTooltip,
        key: GlobalKey(),
      ),
      _Feature(
        icon: Icons.support,
        title: l10n.featureSupportTitle,
        tooltip: l10n.featureSupportTooltip,
        key: GlobalKey(),
      ),
    ];

    return Scaffold(
      appBar: AppBar(title: Text(l10n.tooltipDemoTitle)),
      body: Stack(
        children: [
          GridView.builder(
            padding: const EdgeInsets.all(16),
            gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 2,
              mainAxisSpacing: 16,
              crossAxisSpacing: 16,
            ),
            itemCount: features.length,
            itemBuilder: (context, index) {
              final feature = features[index];
              return Semantics(
                button: true,
                label: l10n.featureCardAccessibility(feature.title),
                hint: l10n.tapForMoreInfo,
                child: Card(
                  key: feature.key,
                  child: InkWell(
                    onTap: () {
                      if (_activeTooltip == index) {
                        _hideTooltip();
                      } else {
                        _showTooltip(index, feature.key);
                      }
                    },
                    borderRadius: BorderRadius.circular(12),
                    child: Padding(
                      padding: const EdgeInsets.all(16),
                      child: Column(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          Icon(
                            feature.icon,
                            size: 48,
                            color: Theme.of(context).colorScheme.primary,
                          ),
                          const SizedBox(height: 12),
                          Text(
                            feature.title,
                            style: Theme.of(context).textTheme.titleMedium,
                            textAlign: TextAlign.center,
                          ),
                          const SizedBox(height: 4),
                          Icon(
                            Icons.info_outline,
                            size: 16,
                            color: Theme.of(context).colorScheme.onSurfaceVariant,
                          ),
                        ],
                      ),
                    ),
                  ),
                ),
              );
            },
          ),
          // Tooltip overlay
          if (_activeTooltip != null)
            GestureDetector(
              onTap: _hideTooltip,
              behavior: HitTestBehavior.translucent,
              child: Container(
                color: Colors.transparent,
              ),
            ),
          if (_activeTooltip != null)
            PositionedTransition(
              rect: _tooltipAnimation,
              child: FadeTransition(
                opacity: _controller,
                child: IgnorePointer(
                  child: Container(
                    padding: const EdgeInsets.all(12),
                    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(
                      features[_activeTooltip!].tooltip,
                      style: TextStyle(
                        color: Theme.of(context).colorScheme.onInverseSurface,
                        fontSize: 13,
                      ),
                      textAlign: TextAlign.center,
                    ),
                  ),
                ),
              ),
            ),
        ],
      ),
    );
  }
}

class _Feature {
  final IconData icon;
  final String title;
  final String tooltip;
  final GlobalKey key;

  _Feature({
    required this.icon,
    required this.title,
    required this.tooltip,
    required this.key,
  });
}

Multi-Position Floating Action Button

Create a FAB that can move to different positions:

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

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

  @override
  State<LocalizedMovableFab> createState() => _LocalizedMovableFabState();
}

class _LocalizedMovableFabState extends State<LocalizedMovableFab>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<RelativeRect> _fabAnimation;

  _FabPosition _currentPosition = _FabPosition.bottomRight;

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

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

  RelativeRect _getPositionRect(_FabPosition position) {
    final screenSize = MediaQuery.of(context).size;
    const padding = 16.0;
    const fabSize = 56.0;

    switch (position) {
      case _FabPosition.topLeft:
        return RelativeRect.fromLTRB(
          padding,
          padding + kToolbarHeight + MediaQuery.of(context).padding.top,
          screenSize.width - padding - fabSize,
          screenSize.height - padding - fabSize - kToolbarHeight,
        );
      case _FabPosition.topRight:
        return RelativeRect.fromLTRB(
          screenSize.width - padding - fabSize,
          padding + kToolbarHeight + MediaQuery.of(context).padding.top,
          padding,
          screenSize.height - padding - fabSize - kToolbarHeight,
        );
      case _FabPosition.bottomLeft:
        return RelativeRect.fromLTRB(
          padding,
          screenSize.height - padding - fabSize,
          screenSize.width - padding - fabSize,
          padding,
        );
      case _FabPosition.bottomRight:
        return RelativeRect.fromLTRB(
          screenSize.width - padding - fabSize,
          screenSize.height - padding - fabSize,
          padding,
          padding,
        );
      case _FabPosition.center:
        return RelativeRect.fromLTRB(
          (screenSize.width - fabSize) / 2,
          (screenSize.height - fabSize) / 2,
          (screenSize.width - fabSize) / 2,
          (screenSize.height - fabSize) / 2,
        );
    }
  }

  void _updateAnimation(_FabPosition newPosition) {
    final startRect = _getPositionRect(_currentPosition);
    final endRect = _getPositionRect(newPosition);

    _fabAnimation = RelativeRectTween(
      begin: startRect,
      end: endRect,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.easeInOutCubic,
    ));
  }

  void _moveToPosition(_FabPosition position) {
    if (position == _currentPosition) return;

    _updateAnimation(position);
    _controller.forward(from: 0).then((_) {
      setState(() {
        _currentPosition = position;
      });
    });
  }

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

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

    return Scaffold(
      appBar: AppBar(title: Text(l10n.movableFabTitle)),
      body: Stack(
        children: [
          // Position selection UI
          Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  l10n.selectFabPosition,
                  style: Theme.of(context).textTheme.titleMedium,
                ),
                const SizedBox(height: 16),
                Wrap(
                  spacing: 8,
                  runSpacing: 8,
                  children: [
                    _PositionChip(
                      label: l10n.positionTopLeft,
                      isSelected: _currentPosition == _FabPosition.topLeft,
                      onTap: () => _moveToPosition(_FabPosition.topLeft),
                    ),
                    _PositionChip(
                      label: l10n.positionTopRight,
                      isSelected: _currentPosition == _FabPosition.topRight,
                      onTap: () => _moveToPosition(_FabPosition.topRight),
                    ),
                    _PositionChip(
                      label: l10n.positionCenter,
                      isSelected: _currentPosition == _FabPosition.center,
                      onTap: () => _moveToPosition(_FabPosition.center),
                    ),
                    _PositionChip(
                      label: l10n.positionBottomLeft,
                      isSelected: _currentPosition == _FabPosition.bottomLeft,
                      onTap: () => _moveToPosition(_FabPosition.bottomLeft),
                    ),
                    _PositionChip(
                      label: l10n.positionBottomRight,
                      isSelected: _currentPosition == _FabPosition.bottomRight,
                      onTap: () => _moveToPosition(_FabPosition.bottomRight),
                    ),
                  ],
                ),
                const SizedBox(height: 24),
                Text(
                  l10n.fabPositionInstructions,
                  style: Theme.of(context).textTheme.bodyMedium,
                ),
              ],
            ),
          ),
          // Animated FAB
          PositionedTransition(
            rect: _fabAnimation,
            child: Semantics(
              button: true,
              label: l10n.actionButtonLabel,
              child: FloatingActionButton(
                onPressed: () {
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(content: Text(l10n.fabPressed)),
                  );
                },
                child: const Icon(Icons.add),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

enum _FabPosition {
  topLeft,
  topRight,
  center,
  bottomLeft,
  bottomRight,
}

class _PositionChip extends StatelessWidget {
  final String label;
  final bool isSelected;
  final VoidCallback onTap;

  const _PositionChip({
    required this.label,
    required this.isSelected,
    required this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    return FilterChip(
      label: Text(label),
      selected: isSelected,
      onSelected: (_) => onTap(),
    );
  }
}

Complete ARB File for PositionedTransition

{
  "@@locale": "en",

  "slidingPanelTitle": "Sliding Panel",
  "openPanel": "Open panel",
  "closePanel": "Close panel",
  "panelInstructions": "Tap the menu icon to open the settings panel",
  "settingsPanelAccessibility": "Settings panel",
  "userInitial": "J",
  "userName": "John Doe",
  "userEmail": "john.doe@example.com",
  "menuProfile": "Profile",
  "menuSettings": "Settings",
  "menuNotifications": "Notifications",
  "menuHelp": "Help & Support",
  "menuLogout": "Log Out",

  "floatingHelperTitle": "Floating Help",
  "contactFormHeader": "Contact Us",
  "nameFieldLabel": "Your Name",
  "emailFieldLabel": "Email Address",
  "messageFieldLabel": "Your Message",
  "sendMessageButton": "Send Message",
  "helpButtonAccessibility": "Help button",
  "helpTooltipAccessibility": "Help information",
  "helpTooltipTitle": "Need Help?",
  "helpTooltipContent": "Fill out this form and we'll get back to you within 24 hours.",

  "cardStackTitle": "Card Stack",
  "resetCards": "Reset cards",
  "cardProgress": "{current} of {total} reviewed",
  "@cardProgress": {
    "placeholders": {
      "current": {"type": "int"},
      "total": {"type": "int"}
    }
  },
  "cardNumber": "#{number}",
  "@cardNumber": {
    "placeholders": {
      "number": {"type": "int"}
    }
  },
  "cardTitle1": "Welcome",
  "cardDescription1": "Get started with your journey",
  "cardTitle2": "Discover",
  "cardDescription2": "Explore new possibilities",
  "cardTitle3": "Create",
  "cardDescription3": "Build something amazing",
  "cardTitle4": "Connect",
  "cardDescription4": "Join our community",
  "cardTitle5": "Achieve",
  "cardDescription5": "Reach your goals",
  "swipeToNext": "Swipe to continue →",
  "cardAccessibility": "Card {number}: {title}",
  "@cardAccessibility": {
    "placeholders": {
      "number": {"type": "int"},
      "title": {"type": "String"}
    }
  },
  "allCardsReviewed": "All cards reviewed!",
  "startOver": "Start Over",

  "tooltipDemoTitle": "Feature Tooltips",
  "featurePerformanceTitle": "Performance",
  "featurePerformanceTooltip": "Lightning-fast performance with optimized algorithms",
  "featureSecurityTitle": "Security",
  "featureSecurityTooltip": "Enterprise-grade security with encryption",
  "featureCloudTitle": "Cloud Sync",
  "featureCloudTooltip": "Automatic cloud synchronization across devices",
  "featureSupportTitle": "24/7 Support",
  "featureSupportTooltip": "Round-the-clock customer support available",
  "featureCardAccessibility": "{title} feature card",
  "@featureCardAccessibility": {
    "placeholders": {
      "title": {"type": "String"}
    }
  },
  "tapForMoreInfo": "Tap for more information",

  "movableFabTitle": "Movable FAB",
  "selectFabPosition": "Select FAB Position",
  "positionTopLeft": "Top Left",
  "positionTopRight": "Top Right",
  "positionCenter": "Center",
  "positionBottomLeft": "Bottom Left",
  "positionBottomRight": "Bottom Right",
  "fabPositionInstructions": "The floating action button will animate to the selected position. This demonstrates how PositionedTransition can move widgets smoothly within a Stack.",
  "actionButtonLabel": "Action button",
  "fabPressed": "FAB pressed!"
}

Best Practices Summary

  1. Use RelativeRectTween: PositionedTransition requires RelativeRect animations
  2. Handle RTL layouts: Swap left/right values for RTL language support
  3. Dispose controllers properly: Always dispose AnimationControllers in the dispose method
  4. Update animations on dependency changes: Use didChangeDependencies for screen size changes
  5. Add accessibility labels: Use Semantics to describe positioned element purpose
  6. Consider keyboard visibility: Adjust positions when keyboard appears
  7. Use appropriate curves: easeOutCubic for smooth movements, easeInOutCubic for bidirectional
  8. Clip parent Stack: Use clipBehavior to prevent overflow during animation
  9. Handle gestures properly: Add gesture detectors for interactive positioned elements
  10. Test across screen sizes: Ensure positions work on different device sizes

Conclusion

PositionedTransition provides precise control over position animations within Stack widgets in Flutter apps. By using explicit AnimationControllers with RelativeRectTween, you can create polished sliding panels, floating helpers, card stacks, and tooltip systems that enhance the user experience across all languages. The key is combining proper RTL support with smooth, well-timed animations that feel natural and responsive.

Remember to always dispose of your AnimationControllers and test your position animations with different screen sizes and orientations to ensure they work correctly across all devices and languages.