Overview

AnimatedPositioned is a widget that automatically animates when its child widget’s position and size change within a Stack widget. This widget is inspired by Flutter’s AnimatedPositioned widget.

Only works if it’s the child of a Stack.

This widget is a good choice if the size of the child would end up changing as a result of this animation. If the size is intended to remain the same, with only the position changing over time, then consider SlideTransition instead. SlideTransition only triggers a repaint each frame of the animation, whereas AnimatedPositioned will trigger a relayout as well.

Flutter reference: https://api.flutter.dev/flutter/widgets/AnimatedPositioned-class.html

When to use?

  • When dynamically moving widget positions within a Stack
  • When implementing drag and drop or movement animations
  • When animating modal or popup position transitions
  • When moving floating action buttons to different positions
  • When implementing micro-interactions based on moving nameplates or masonry layouts
  • When implementing resize handles or size change effects with animation

Basic usage

import { AnimatedPositioned, Stack, Container, StatefulWidget } from '@flitter/core';

class PositionedExample extends StatefulWidget {
  createState() {
    return new PositionedExampleState();
  }
}

class PositionedExampleState extends State<PositionedExample> {
  isTopLeft = true;

  build() {
    return Column({
      children: [
        ElevatedButton({
          onPressed: () => {
            this.setState(() => {
              this.isTopLeft = !this.isTopLeft;
            });
          },
          child: Text("Move Position"),
        }),
        Container({
          width: 300,
          height: 300,
          child: Stack({
            children: [
              Container({
                width: 300,
                height: 300,
                color: "lightgray",
              }),
              AnimatedPositioned({
                top: this.isTopLeft ? 20 : 220,
                left: this.isTopLeft ? 20 : 220,
                width: 60,
                height: 60,
                duration: 1000, // 1 second
                child: Container({
                  color: "blue",
                  child: Center({
                    child: Text("Move", {
                      style: TextStyle({ color: "white" })
                    }),
                  }),
                }),
              }),
            ],
          }),
        }),
      ],
    });
  }
}

Props

duration (required)

Value: number

Specifies the animation duration in milliseconds.

top (optional)

Value: number | undefined

Specifies the distance from the top of the Stack. Cannot be used simultaneously with bottom.

left (optional)

Value: number | undefined

Specifies the distance from the left of the Stack. Cannot be used simultaneously with right.

right (optional)

Value: number | undefined

Specifies the distance from the right of the Stack. Cannot be used simultaneously with left.

bottom (optional)

Value: number | undefined

Specifies the distance from the bottom of the Stack. Cannot be used simultaneously with top.

width (optional)

Value: number | undefined

Specifies the widget’s width. If not specified, uses the child widget’s intrinsic width.

height (optional)

Value: number | undefined

Specifies the widget’s height. If not specified, uses the child widget’s intrinsic height.

curve (optional)

Value: Curve (default: Curves.linear)

Specifies the animation progression curve. Available curves:

  • Curves.linear: Constant speed
  • Curves.easeIn: Slow start
  • Curves.easeOut: Slow end
  • Curves.easeInOut: Slow start and end
  • Curves.circIn: Circular acceleration start
  • Curves.circOut: Circular deceleration end
  • Curves.circInOut: Circular acceleration/deceleration
  • Curves.backIn: Goes back then starts
  • Curves.backOut: Overshoots target then returns
  • Curves.backInOut: backIn + backOut
  • Curves.anticipate: Anticipatory motion before proceeding
  • Curves.bounceIn: Bounces at start
  • Curves.bounceOut: Bounces at end
  • Curves.bounceInOut: Bounces at start/end

child (optional)

Value: Widget | undefined

The child widget whose position will be animated.

key (optional)

Value: any

A unique identifier for the widget.

Real-world examples

Example 1: Drag and drop simulation

import { AnimatedPositioned, Stack, Container, GestureDetector, Curves } from '@flitter/core';

class DragDropSimulation extends StatefulWidget {
  createState() {
    return new DragDropSimulationState();
  }
}

class DragDropSimulationState extends State<DragDropSimulation> {
  boxPosition = { x: 50, y: 50 };
  targetPosition = { x: 200, y: 200 };
  isAtTarget = false;

  build() {
    return Container({
      width: 400,
      height: 400,
      color: "#f0f0f0",
      child: Stack({
        children: [
          // Target area
          Positioned({
            left: this.targetPosition.x,
            top: this.targetPosition.y,
            child: Container({
              width: 100,
              height: 100,
              decoration: BoxDecoration({
                color: this.isAtTarget ? "green" : "lightgreen",
                borderRadius: BorderRadius.circular(8),
                border: Border.all({ 
                  color: "green", 
                  width: 2, 
                  style: "dashed" 
                }),
              }),
              child: Center({
                child: Text(
                  "Drop Zone",
                  { style: TextStyle({ color: "darkgreen", fontWeight: "bold" }) }
                ),
              }),
            }),
          }),
          
          // Draggable box
          AnimatedPositioned({
            left: this.boxPosition.x,
            top: this.boxPosition.y,
            width: 60,
            height: 60,
            duration: 500,
            curve: Curves.easeInOut,
            child: GestureDetector({
              onTap: () => this.moveToTarget(),
              child: Container({
                decoration: BoxDecoration({
                  color: "blue",
                  borderRadius: BorderRadius.circular(8),
                  boxShadow: [
                    BoxShadow({
                      color: "rgba(0,0,0,0.3)",
                      blurRadius: 4,
                      offset: Offset({ x: 2, y: 2 }),
                    }),
                  ],
                }),
                child: Center({
                  child: Icon({
                    icon: Icons.touch_app,
                    color: "white",
                    size: 24,
                  }),
                }),
              }),
            }),
          }),
          
          // Instruction text
          Positioned({
            bottom: 20,
            left: 20,
            child: Container({
              padding: EdgeInsets.all(12),
              decoration: BoxDecoration({
                color: "rgba(0,0,0,0.7)",
                borderRadius: BorderRadius.circular(8),
              }),
              child: Text(
                "Click the blue square to move it",
                { style: TextStyle({ color: "white", fontSize: 14 }) }
              ),
            }),
          }),
        ],
      }),
    });
  }

  moveToTarget() {
    this.setState(() => {
      if (!this.isAtTarget) {
        this.boxPosition = { x: this.targetPosition.x + 20, y: this.targetPosition.y + 20 };
        this.isAtTarget = true;
      } else {
        this.boxPosition = { x: 50, y: 50 };
        this.isAtTarget = false;
      }
    });
  }
}

Example 2: Multi-widget orchestration

import { AnimatedPositioned, Stack, Container, Curves } from '@flitter/core';

class MultiWidgetOrchestration extends StatefulWidget {
  createState() {
    return new MultiWidgetOrchestrationState();
  }
}

class MultiWidgetOrchestrationState extends State<MultiWidgetOrchestration> {
  pattern = 0; // 0: circle, 1: square, 2: diagonal, 3: wave
  
  getPositions(pattern: number) {
    switch (pattern) {
      case 0: // circle
        return [
          { x: 150, y: 50 },   // top
          { x: 250, y: 150 },  // right
          { x: 150, y: 250 },  // bottom
          { x: 50, y: 150 },   // left
        ];
      case 1: // square
        return [
          { x: 100, y: 100 },  // top-left
          { x: 200, y: 100 },  // top-right
          { x: 200, y: 200 },  // bottom-right
          { x: 100, y: 200 },  // bottom-left
        ];
      case 2: // diagonal
        return [
          { x: 50, y: 50 },
          { x: 120, y: 120 },
          { x: 190, y: 190 },
          { x: 260, y: 260 },
        ];
      case 3: // wave
        return [
          { x: 50, y: 200 },
          { x: 120, y: 120 },
          { x: 190, y: 180 },
          { x: 260, y: 100 },
        ];
      default:
        return [
          { x: 150, y: 150 },
          { x: 150, y: 150 },
          { x: 150, y: 150 },
          { x: 150, y: 150 },
        ];
    }
  }

  build() {
    const positions = this.getPositions(this.pattern);
    const colors = ["#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4"];
    const icons = [Icons.star, Icons.favorite, Icons.diamond, Icons.hexagon];
    
    return Column({
      children: [
        // Pattern selection buttons
        Padding({
          padding: EdgeInsets.all(16),
          child: Row({
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              ElevatedButton({
                onPressed: () => this.changePattern(0),
                child: Text("Circle"),
                style: ButtonStyle({
                  backgroundColor: this.pattern === 0 ? "blue" : "gray",
                }),
              }),
              ElevatedButton({
                onPressed: () => this.changePattern(1),
                child: Text("Square"),
                style: ButtonStyle({
                  backgroundColor: this.pattern === 1 ? "blue" : "gray",
                }),
              }),
              ElevatedButton({
                onPressed: () => this.changePattern(2),
                child: Text("Diagonal"),
                style: ButtonStyle({
                  backgroundColor: this.pattern === 2 ? "blue" : "gray",
                }),
              }),
              ElevatedButton({
                onPressed: () => this.changePattern(3),
                child: Text("Wave"),
                style: ButtonStyle({
                  backgroundColor: this.pattern === 3 ? "blue" : "gray",
                }),
              }),
            ],
          }),
        }),
        
        // Animation area
        Container({
          width: 350,
          height: 350,
          decoration: BoxDecoration({
            color: "#2C3E50",
            borderRadius: BorderRadius.circular(16),
          }),
          child: Stack({
            children: positions.map((pos, index) => {
              return AnimatedPositioned({
                key: ValueKey(index),
                left: pos.x,
                top: pos.y,
                width: 50,
                height: 50,
                duration: 800 + (index * 100), // Staggered animation
                curve: Curves.easeInOut,
                child: Container({
                  decoration: BoxDecoration({
                    color: colors[index],
                    borderRadius: BorderRadius.circular(25),
                    boxShadow: [
                      BoxShadow({
                        color: "rgba(0,0,0,0.3)",
                        blurRadius: 8,
                        offset: Offset({ x: 0, y: 4 }),
                      }),
                    ],
                  }),
                  child: Center({
                    child: Icon({
                      icon: icons[index],
                      color: "white",
                      size: 28,
                    }),
                  }),
                }),
              });
            }),
          }),
        }),
      ],
    });
  }

  changePattern(newPattern: number) {
    this.setState(() => {
      this.pattern = newPattern;
    });
  }
}

Example 3: Modal dialog animation

import { AnimatedPositioned, Stack, Container, Card, Curves } from '@flitter/core';

class ModalAnimation extends StatefulWidget {
  createState() {
    return new ModalAnimationState();
  }
}

class ModalAnimationState extends State<ModalAnimation> {
  showModal = false;

  build() {
    return Stack({
      children: [
        // Main content
        Container({
          width: double.infinity,
          height: double.infinity,
          color: "#f5f5f5",
          child: Center({
            child: ElevatedButton({
              onPressed: () => {
                this.setState(() => {
                  this.showModal = true;
                });
              },
              child: Text("Open Modal"),
            }),
          }),
        }),
        
        // Modal background
        if (this.showModal) AnimatedOpacity({
          opacity: this.showModal ? 0.7 : 0.0,
          duration: 300,
          child: Container({
            width: double.infinity,
            height: double.infinity,
            color: "black",
          }),
        }),
        
        // Modal dialog
        AnimatedPositioned({
          left: 50,
          right: 50,
          top: this.showModal ? 150 : 600, // Slide up from bottom
          duration: 400,
          curve: Curves.easeOut,
          child: this.showModal ? Card({
            child: Container({
              padding: EdgeInsets.all(20),
              child: Column({
                mainAxisSize: MainAxisSize.min,
                children: [
                  Text(
                    "Modal Dialog",
                    { style: TextStyle({ fontSize: 24, fontWeight: "bold" }) }
                  ),
                  SizedBox({ height: 16 }),
                  Text(
                    "This is an animated modal dialog that appears with a slide-up effect from the bottom."
                  ),
                  SizedBox({ height: 20 }),
                  Row({
                    mainAxisAlignment: MainAxisAlignment.end,
                    children: [
                      TextButton({
                        onPressed: () => {
                          this.setState(() => {
                            this.showModal = false;
                          });
                        },
                        child: Text("Cancel"),
                      }),
                      SizedBox({ width: 8 }),
                      ElevatedButton({
                        onPressed: () => {
                          this.setState(() => {
                            this.showModal = false;
                          });
                        },
                        child: Text("Confirm"),
                      }),
                    ],
                  }),
                ],
              }),
            }),
          }) : SizedBox.shrink(),
        }),
      ],
    });
  }
}

Example 4: Size and position simultaneous animation

import { AnimatedPositioned, Stack, Container, Curves } from '@flitter/core';

class SizeAndPositionAnimation extends StatefulWidget {
  createState() {
    return new SizeAndPositionAnimationState();
  }
}

class SizeAndPositionAnimationState extends State<SizeAndPositionAnimation> {
  isExpanded = false;

  build() {
    return GestureDetector({
      onTap: () => {
        this.setState(() => {
          this.isExpanded = !this.isExpanded;
        });
      },
      child: Container({
        width: 400,
        height: 400,
        color: "#34495e",
        child: Stack({
          children: [
            AnimatedPositioned({
              left: this.isExpanded ? 50 : 175,
              top: this.isExpanded ? 50 : 175,
              width: this.isExpanded ? 300 : 50,
              height: this.isExpanded ? 300 : 50,
              duration: 1000,
              curve: Curves.elasticOut,
              child: Container({
                decoration: BoxDecoration({
                  gradient: LinearGradient({
                    colors: this.isExpanded 
                      ? ["#e74c3c", "#c0392b"]
                      : ["#3498db", "#2980b9"],
                    begin: Alignment.topLeft,
                    end: Alignment.bottomRight,
                  }),
                  borderRadius: BorderRadius.circular(this.isExpanded ? 16 : 25),
                  boxShadow: [
                    BoxShadow({
                      color: "rgba(0,0,0,0.3)",
                      blurRadius: this.isExpanded ? 20 : 10,
                      offset: Offset({ x: 0, y: this.isExpanded ? 10 : 5 }),
                    }),
                  ],
                }),
                child: Center({
                  child: Icon({
                    icon: this.isExpanded ? Icons.close : Icons.add,
                    color: "white",
                    size: this.isExpanded ? 48 : 24,
                  }),
                }),
              }),
            }),
            
            // Instruction text
            Positioned({
              bottom: 20,
              left: 20,
              right: 20,
              child: Container({
                padding: EdgeInsets.all(12),
                decoration: BoxDecoration({
                  color: "rgba(255,255,255,0.9)",
                  borderRadius: BorderRadius.circular(8),
                }),
                child: Text(
                  "Tap the screen to see size and position animation",
                  { 
                    style: TextStyle({ 
                      color: "#2c3e50", 
                      fontSize: 14,
                      textAlign: "center"
                    }) 
                  }
                ),
              }),
            }),
          ],
        }),
      }),
    });
  }
}

Notes

  • Must be direct child of Stack: AnimatedPositioned can only be used as a direct child of a Stack widget
  • Conflicting properties: top/bottom and left/right cannot be set simultaneously
  • Performance considerations: Position changes trigger layout recalculation, so applying to many widgets simultaneously may impact performance
  • Animation interruption: If new property values are set during animation, it smoothly transitions from the current value to the new value
  • Negative values allowed: left, top, right, bottom properties can accept negative values
  • Positioned: Basic widget for specifying position within Stack without animation
  • AnimatedAlign: Animates alignment transitions
  • SlideTransition: Use when only animating position while keeping layout unchanged
  • Stack: Parent widget required for using AnimatedPositioned
  • AnimatedContainer: General-purpose widget for animating multiple properties simultaneously