Overview

AnimatedRotation is a widget that automatically animates when its rotation angle (turns) changes, providing smooth rotation transitions. This widget is inspired by Flutter’s AnimatedRotation widget.

Animated version of Transform.rotate which automatically transitions the child’s rotation over a given duration whenever the given rotation changes.

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

When to use?

  • When creating loading indicators or spinners
  • When visually representing state changes of icons or buttons
  • When rotating elements based on user interaction
  • When creating transition animations for checkboxes, switches, etc.
  • When simulating movement of mechanical components
  • When implementing card flip or rotation effects

Basic usage

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

class RotationExample extends StatefulWidget {
  createState() {
    return new RotationExampleState();
  }
}

class RotationExampleState extends State<RotationExample> {
  turns = 0;

  build() {
    return Column({
      children: [
        ElevatedButton({
          onPressed: () => {
            this.setState(() => {
              this.turns += 0.25; // 90-degree rotation
            });
          },
          child: Text("Rotate"),
        }),
        AnimatedRotation({
          turns: this.turns,
          duration: 500, // 0.5 seconds
          child: Container({
            width: 100,
            height: 100,
            color: "blue",
            child: Center({
              child: Icon({
                icon: Icons.star,
                color: "white",
                size: 40,
              }),
            }),
          }),
        }),
      ],
    });
  }
}

Props

turns (required)

Value: number

Specifies the amount of rotation. 1.0 represents one complete full rotation (360 degrees).

  • 0.0 = No rotation
  • 0.25 = 90-degree rotation
  • 0.5 = 180-degree rotation
  • 1.0 = 360-degree rotation (one full turn)
  • 2.0 = 720-degree rotation (two full turns)
  • Negative values rotate counterclockwise

duration (required)

Value: number

Specifies the animation duration in milliseconds.

alignment (optional)

Value: Alignment (default: Alignment.center)

Specifies the center point of rotation. Available values:

  • Alignment.center: Rotate around center
  • Alignment.topLeft: Rotate around top-left corner
  • Alignment.topRight: Rotate around top-right corner
  • Alignment.bottomLeft: Rotate around bottom-left corner
  • Alignment.bottomRight: Rotate around bottom-right corner
  • Custom alignment: Alignment.of({ x: -0.5, y: 0.5 })

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 to be rotated.

key (optional)

Value: any

A unique identifier for the widget.

Real-world examples

Example 1: Loading spinner

import { AnimatedRotation, Container, Icon, Curves } from '@flitter/core';

class LoadingSpinner extends StatefulWidget {
  createState() {
    return new LoadingSpinnerState();
  }
}

class LoadingSpinnerState extends State<LoadingSpinner> {
  turns = 0;
  isLoading = false;

  async startLoading() {
    this.setState(() => {
      this.isLoading = true;
    });

    // Continuous rotation animation
    const interval = setInterval(() => {
      if (this.isLoading) {
        this.setState(() => {
          this.turns += 1; // One full rotation each time
        });
      } else {
        clearInterval(interval);
      }
    }, 1000);

    // Complete loading after 5 seconds
    setTimeout(() => {
      this.setState(() => {
        this.isLoading = false;
      });
      clearInterval(interval);
    }, 5000);
  }

  build() {
    return Column({
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        AnimatedRotation({
          turns: this.turns,
          duration: 1000,
          curve: Curves.linear,
          child: Container({
            width: 60,
            height: 60,
            decoration: BoxDecoration({
              border: Border.all({ color: "blue", width: 4 }),
              borderRadius: BorderRadius.circular(30),
            }),
            child: CustomPaint({
              painter: SpinnerPainter(),
            }),
          }),
        }),
        SizedBox({ height: 20 }),
        ElevatedButton({
          onPressed: this.isLoading ? null : () => this.startLoading(),
          child: Text(this.isLoading ? "Loading..." : "Start Loading"),
        }),
      ],
    });
  }
}

Example 2: Interactive icon button

import { AnimatedRotation, Container, GestureDetector, Icon, Curves } from '@flitter/core';

class InteractiveIconButton extends StatefulWidget {
  createState() {
    return new InteractiveIconButtonState();
  }
}

class InteractiveIconButtonState extends State<InteractiveIconButton> {
  isExpanded = false;
  rotation = 0;

  toggle() {
    this.setState(() => {
      this.isExpanded = !this.isExpanded;
      this.rotation += 0.5; // 180-degree rotation
    });
  }

  build() {
    return Column({
      children: [
        GestureDetector({
          onTap: () => this.toggle(),
          child: Container({
            width: 80,
            height: 80,
            decoration: BoxDecoration({
              color: this.isExpanded ? "red" : "blue",
              borderRadius: BorderRadius.circular(40),
              boxShadow: [
                BoxShadow({
                  color: "rgba(0,0,0,0.3)",
                  blurRadius: 8,
                  offset: Offset({ x: 0, y: 4 }),
                }),
              ],
            }),
            child: AnimatedRotation({
              turns: this.rotation,
              duration: 300,
              curve: Curves.easeInOut,
              child: Icon({
                icon: this.isExpanded ? Icons.close : Icons.add,
                color: "white",
                size: 32,
              }),
            }),
          }),
        }),
        SizedBox({ height: 20 }),
        AnimatedOpacity({
          opacity: this.isExpanded ? 1.0 : 0.0,
          duration: 200,
          child: Column({
            children: [
              ListTile({
                leading: Icon(Icons.photo),
                title: Text("Photo"),
                onTap: () => {},
              }),
              ListTile({
                leading: Icon(Icons.video),
                title: Text("Video"),
                onTap: () => {},
              }),
              ListTile({
                leading: Icon(Icons.file),
                title: Text("File"),
                onTap: () => {},
              }),
            ],
          }),
        }),
      ],
    });
  }
}

Example 3: Mechanical clock

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

class MechanicalClock extends StatefulWidget {
  createState() {
    return new MechanicalClockState();
  }
}

class MechanicalClockState extends State<MechanicalClock> {
  hourRotation = 0;
  minuteRotation = 0;
  secondRotation = 0;

  initState() {
    super.initState();
    this.updateTime();
    
    // Update time every second
    setInterval(() => {
      this.updateTime();
    }, 1000);
  }

  updateTime() {
    const now = new Date();
    const hours = now.getHours() % 12;
    const minutes = now.getMinutes();
    const seconds = now.getSeconds();

    this.setState(() => {
      this.hourRotation = (hours + minutes / 60) / 12; // One turn in 12 hours
      this.minuteRotation = minutes / 60; // One turn in 60 minutes
      this.secondRotation = seconds / 60; // One turn in 60 seconds
    });
  }

  build() {
    return Container({
      width: 200,
      height: 200,
      decoration: BoxDecoration({
        border: Border.all({ color: "black", width: 4 }),
        borderRadius: BorderRadius.circular(100),
        color: "white",
      }),
      child: Stack({
        children: [
          // Clock face
          ...Array.from({ length: 12 }, (_, i) => {
            const angle = (i * 30) * Math.PI / 180;
            const x = 80 + 70 * Math.sin(angle);
            const y = 80 - 70 * Math.cos(angle);
            
            return Positioned({
              left: x,
              top: y,
              child: Container({
                width: 20,
                height: 20,
                child: Center({
                  child: Text(
                    (i === 0 ? 12 : i).toString(),
                    { style: TextStyle({ fontWeight: "bold", fontSize: 14 }) }
                  ),
                }),
              }),
            });
          }),
          
          // Hour hand
          Center({
            child: AnimatedRotation({
              turns: this.hourRotation,
              duration: 1000,
              curve: Curves.easeInOut,
              alignment: Alignment.bottomCenter,
              child: Container({
                width: 4,
                height: 50,
                color: "black",
                child: Container({
                  width: 4,
                  height: 40,
                  color: "black",
                }),
              }),
            }),
          }),
          
          // Minute hand
          Center({
            child: AnimatedRotation({
              turns: this.minuteRotation,
              duration: 1000,
              curve: Curves.easeInOut,
              alignment: Alignment.bottomCenter,
              child: Container({
                width: 3,
                height: 70,
                color: "darkblue",
              }),
            }),
          }),
          
          // Second hand
          Center({
            child: AnimatedRotation({
              turns: this.secondRotation,
              duration: 100,
              curve: Curves.linear,
              alignment: Alignment.bottomCenter,
              child: Container({
                width: 1,
                height: 80,
                color: "red",
              }),
            }),
          }),
          
          // Center dot
          Center({
            child: Container({
              width: 8,
              height: 8,
              decoration: BoxDecoration({
                color: "black",
                borderRadius: BorderRadius.circular(4),
              }),
            }),
          }),
        ],
      }),
    });
  }
}

Example 4: Card flip animation

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

class CardFlip extends StatefulWidget {
  createState() {
    return new CardFlipState();
  }
}

class CardFlipState extends State<CardFlip> {
  isFlipped = false;
  rotation = 0;

  flipCard() {
    this.setState(() => {
      this.isFlipped = !this.isFlipped;
      this.rotation += 0.5; // 180-degree rotation
    });
  }

  build() {
    const showBack = (this.rotation % 1) > 0.25 && (this.rotation % 1) < 0.75;
    
    return GestureDetector({
      onTap: () => this.flipCard(),
      child: Container({
        width: 200,
        height: 300,
        child: AnimatedRotation({
          turns: this.rotation,
          duration: 600,
          curve: Curves.easeInOut,
          child: Container({
            decoration: BoxDecoration({
              borderRadius: BorderRadius.circular(16),
              boxShadow: [
                BoxShadow({
                  color: "rgba(0,0,0,0.3)",
                  blurRadius: 10,
                  offset: Offset({ x: 0, y: 5 }),
                }),
              ],
            }),
            child: showBack ? 
              // Card back
              Container({
                decoration: BoxDecoration({
                  gradient: LinearGradient({
                    colors: ["#667eea", "#764ba2"],
                    begin: Alignment.topLeft,
                    end: Alignment.bottomRight,
                  }),
                  borderRadius: BorderRadius.circular(16),
                }),
                child: Center({
                  child: Column({
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Icon({
                        icon: Icons.star,
                        color: "white",
                        size: 64,
                      }),
                      SizedBox({ height: 16 }),
                      Text(
                        "Card Back",
                        { style: TextStyle({ color: "white", fontSize: 24, fontWeight: "bold" }) }
                      ),
                    ],
                  }),
                }),
              }) :
              // Card front
              Container({
                decoration: BoxDecoration({
                  gradient: LinearGradient({
                    colors: ["#f093fb", "#f5576c"],
                    begin: Alignment.topLeft,
                    end: Alignment.bottomRight,
                  }),
                  borderRadius: BorderRadius.circular(16),
                }),
                child: Center({
                  child: Column({
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Icon({
                        icon: Icons.favorite,
                        color: "white",
                        size: 64,
                      }),
                      SizedBox({ height: 16 }),
                      Text(
                        "Card Front",
                        { style: TextStyle({ color: "white", fontSize: 24, fontWeight: "bold" }) }
                      ),
                      SizedBox({ height: 8 }),
                      Text(
                        "Tap to flip",
                        { style: TextStyle({ color: "white", fontSize: 14, opacity: 0.8 }) }
                      ),
                    ],
                  }),
                }),
              })
          ),
        }),
      }),
    });
  }
}

Notes

  • turns value meaning: 1.0 represents one complete full rotation (360 degrees)
  • Continuous rotation: Continuously increasing the turns value creates ongoing rotation animation
  • Direction control: Negative values rotate counterclockwise
  • Performance considerations: Applying rotation animation to many widgets simultaneously may impact performance
  • Alignment importance: The rotation center point significantly affects the visual effect
  • Transform.rotate: Basic widget for applying rotation without animation
  • AnimatedContainer: General-purpose widget for animating multiple transformations simultaneously
  • RotationTransition: Rotation animation using explicit Animation controllers
  • AnimatedScale: Widget for animating size changes
  • Transform: Basic widget for applying various transformations (rotation, scale, position)