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 speedCurves.easeIn
: Slow startCurves.easeOut
: Slow endCurves.easeInOut
: Slow start and endCurves.circIn
: Circular acceleration startCurves.circOut
: Circular deceleration endCurves.circInOut
: Circular acceleration/decelerationCurves.backIn
: Goes back then startsCurves.backOut
: Overshoots target then returnsCurves.backInOut
: backIn + backOutCurves.anticipate
: Anticipatory motion before proceedingCurves.bounceIn
: Bounces at startCurves.bounceOut
: Bounces at endCurves.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
Related widgets
- 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