Animation Basics

Overview

Flitter provides the same powerful animation system as Flutter. You can easily implement smooth and natural animations by combining AnimationController, Tween, and Curve.

Why are animations important?

Well-designed animations greatly improve user experience:

  • Visual Feedback: Provide immediate response to user actions
  • State Transitions: Smoothly express changes in screens or elements
  • Attention Guidance: Draw user focus to important information or changes
  • Brand Identity: Express app personality through unique animations
  • Cognitive Continuity: Naturally show element movement or transformation

Core Concepts

1. AnimationController

The core class that controls animation playback, stopping, repeating, etc.:

class MyWidgetState extends State<MyWidget> {
  controller!: AnimationController;
  
  initState(context: BuildContext) {
    super.initState(context);
    // duration is specified in milliseconds
    this.controller = new AnimationController({
      duration: 1000  // 1 second
    });
  }
  
  dispose() {
    this.controller.dispose();
    super.dispose();
  }
}

2. Tween

Defines interpolation between start and end values:

// Number Tween
const sizeTween = new Tween({
  begin: 50,
  end: 200
});

// Color Tween
const colorTween = new ColorTween({
  begin: '#3b82f6',
  end: '#ef4444'
});

// Offset Tween (position movement)
const positionTween = new Tween({
  begin: new Offset(0, 0),
  end: new Offset(100, 100)
});

3. Animation

Connects Tween and AnimationController to generate actual animation values:

const animation = sizeTween.animated(this.controller);

// Apply easing effects with CurvedAnimation
const curvedAnimation = new CurvedAnimation({
  parent: this.controller,
  curve: Curves.easeInOut
});

const animation = sizeTween.animated(curvedAnimation);

4. Animation Listeners

Add listeners to rebuild widgets whenever animation values change:

class MyWidgetState extends State<MyWidget> {
  controller!: AnimationController;
  animation!: Animation<number>;
  
  initState(context: BuildContext) {
    super.initState(context);
    this.controller = new AnimationController({ duration: 1000 });
    this.animation = new Tween({ begin: 0, end: 100 }).animated(this.controller);
    
    // Add listener - call setState whenever animation value changes
    this.controller.addListener(() => {
      this.setState();
    });
  }
  
  build(context: BuildContext): Widget {
    return Container({
      width: this.animation.value,
      height: this.animation.value,
      color: '#3b82f6'
    });
  }
}

Code Examples

Basic Animation Implementation

AnimationController Basic Example

Example animating size, color, and rotation simultaneously

코드 보기
// 커스텀 BoxDecoration Tween
class DecorationTween extends Tween<BoxDecoration> {
  constructor({ begin, end }: { begin: BoxDecoration; end: BoxDecoration }) {
    super({ begin, end });
  }

  protected lerp(t: number): BoxDecoration {
    return BoxDecoration.lerp(this.begin, this.end, t);
  }
}

class AnimationExample extends StatefulWidget {
  createState() {
    return new AnimationExampleState();
  }
}

class AnimationExampleState extends State<AnimationExample> {
  controller!: AnimationController;
  sizeAnimation!: Animation<number>;
  decorationAnimation!: Animation<BoxDecoration>;
  rotationAnimation!: Animation<number>;
  
  initState(context: BuildContext) {
    super.initState(context);
    
    // AnimationController 생성 (duration은 밀리초 단위)
    this.controller = new AnimationController({
      duration: 2000  // 2초
    });
    
    // 크기 애니메이션
    this.sizeAnimation = new Tween({
      begin: 100,
      end: 200
    }).animated(new CurvedAnimation({
      parent: this.controller,
      curve: Curves.easeInOut
    }));
    
    // BoxDecoration 애니메이션 (색상 포함)
    this.decorationAnimation = new DecorationTween({
      begin: new BoxDecoration({
        color: '#3b82f6',
        shape: 'rectangle',
        borderRadius: BorderRadius.circular(16)
      }),
      end: new BoxDecoration({
        color: '#ef4444',
        shape: 'rectangle',
        borderRadius: BorderRadius.circular(16)
      })
    }).animated(new CurvedAnimation({
      parent: this.controller,
      curve: Curves.easeInOut
    }));
    
    // 회전 애니메이션
    this.rotationAnimation = new Tween({
      begin: 0,
      end: Math.PI * 2
    }).animated(this.controller);
    
    // 애니메이션 리스너 추가
    this.controller.addListener(() => {
      this.setState();
    });
  }
  
  dispose() {
    this.controller.dispose();
    super.dispose();
  }
  
  build(context: BuildContext) {
    return GestureDetector({
      onClick: () => {
        if (this.controller.isCompleted) {
          this.controller.reverse();
        } else {
          this.controller.forward();
        }
      },
      child: Transform.rotate({
        angle: this.rotationAnimation.value,
        child: Container({
          width: this.sizeAnimation.value,
          height: this.sizeAnimation.value,
          decoration: this.decorationAnimation.value,
          child: Center({
            child: Text(
              this.controller.isCompleted ? "뒤로" : "앞으로",
              {
                style: new TextStyle({
                  color: '#ffffff',
                  fontSize: 18,
                  fontWeight: 'bold'
                })
              }
            )
          })
        })
      })
    });
  }
}

Animation Control

// Start animation
this.controller.forward();

// Reverse animation
this.controller.reverse();

// Repeat animation
this.controller.repeat();

// Animate to specific value
this.controller.animateTo(0.5);

// Stop animation
this.controller.stop();

// Reset animation
this.controller.reset();

Practical Examples

1. Pulse Animation

class PulseAnimation extends StatefulWidget {
  createState() {
    return new PulseAnimationState();
  }
}

class PulseAnimationState extends State<PulseAnimation> {
  controller!: AnimationController;
  animation!: Animation<number>;
  
  initState(context: BuildContext) {
    super.initState(context);
    
    this.controller = new AnimationController({
      duration: 1000  // 1 second
    });
    
    this.animation = new Tween({
      begin: 1.0,
      end: 1.2
    }).animated(new CurvedAnimation({
      parent: this.controller,
      curve: Curves.easeInOut
    }));
    
    this.controller.repeat({ reverse: true });
  }
  
  dispose() {
    this.controller.dispose();
    super.dispose();
  }
  
  build(context: BuildContext): Widget {
    return Transform.scale({
      scale: this.animation.value,
      child: Container({
        width: 100,
        height: 100,
        decoration: new BoxDecoration({
          color: '#3b82f6',
          shape: 'circle'
        })
      })
    });
  }
}

2. Sequential Animation

class SequentialAnimation extends StatefulWidget {
  createState() {
    return new SequentialAnimationState();
  }
}

class SequentialAnimationState extends State<SequentialAnimation> {
  controller!: AnimationController;
  slideAnimation!: Animation<Offset>;
  fadeAnimation!: Animation<number>;
  
  initState(context: BuildContext) {
    super.initState(context);
    
    this.controller = new AnimationController({
      duration: 2000  // 2 seconds
    });
    
    // Slide animation
    this.slideAnimation = new Tween({
      begin: new Offset(-1, 0),
      end: new Offset(0, 0)
    }).animated(new CurvedAnimation({
      parent: this.controller,
      curve: Curves.easeOut
    }));
    
    // Fade animation
    this.fadeAnimation = new Tween({
      begin: 0.0,
      end: 1.0
    }).animated(new CurvedAnimation({
      parent: this.controller,
      curve: Curves.easeIn
    }));
    
    // Add animation listener
    this.controller.addListener(() => {
      this.setState();
    });
    
    this.controller.forward();
  }
  
  dispose() {
    this.controller.dispose();
    super.dispose();
  }
  
  build(context: BuildContext): Widget {
    return Transform.translate({
      offset: new Offset(
        this.slideAnimation.value.dx * 200,  // x-axis movement
        this.slideAnimation.value.dy * 0     // y-axis movement
      ),
      child: Opacity({
        opacity: this.fadeAnimation.value,
        child: Container({
          width: 200,
          height: 100,
          color: '#10b981',
          child: Center({
            child: Text("Sequential Animation", {
              style: new TextStyle({
                color: '#ffffff',
                fontSize: 18
              })
            })
          })
        })
      })
    });
  }
}

3. Custom Curve Animation

class CustomCurveAnimation extends StatefulWidget {
  createState() {
    return new CustomCurveAnimationState();
  }
}

class CustomCurveAnimationState extends State<CustomCurveAnimation> {
  controller!: AnimationController;
  bounceAnimation!: Animation<number>;
  
  initState(context: BuildContext) {
    super.initState(context);
    
    this.controller = new AnimationController({
      duration: 1500  // 1.5 seconds
    });
    
    // Custom curve for bounce effect
    this.bounceAnimation = new Tween({
      begin: 0,
      end: 300
    }).animated(new CurvedAnimation({
      parent: this.controller,
      curve: Curves.bounceOut
    }));
    
    // Add animation listener
    this.controller.addListener(() => {
      this.setState();
    });
    
    this.controller.forward();
  }
  
  dispose() {
    this.controller.dispose();
    super.dispose();
  }
  
  build(context: BuildContext): Widget {
    return Transform.translate({
      offset: new Offset(this.bounceAnimation.value, 0),
      child: Container({
        width: 50,
        height: 50,
        decoration: new BoxDecoration({
          color: '#10b981',
          shape: 'circle'
        })
      })
    });
  }
}

Important Notes

1. Performance Optimization

Animations can impact performance, so optimization is important:

// ❌ Bad example: Rebuilding entire widget tree
class MyWidgetState extends State<MyWidget> {
  build(context: BuildContext): Widget {
    return Column({
      children: [
        ComplexWidget({}),  // Recreated every time
        Container({
          width: this.animation.value,
          height: 100
        })
      ]
    });
  }
}

// ✅ Good example: Separate only parts affected by animation
class MyWidgetState extends State<MyWidget> {
  complexWidget = ComplexWidget({});  // Created only once
  
  build(context: BuildContext): Widget {
    return Column({
      children: [
        this.complexWidget,  // Reused
        Container({
          width: this.animation.value,
          height: 100
        })
      ]
    });
  }
}

2. Memory Management

AnimationController must be disposed:

dispose() {
  this.controller.dispose();  // Required!
  super.dispose();
}

3. vsync Configuration

You should use SingleTickerProviderStateMixin or TickerProviderStateMixin:

// Flitter does not use mixins
// Create and manage AnimationController directly
class MyState extends State<MyWidget> {
  controller!: AnimationController;
  
  initState(context: BuildContext) {
    super.initState(context);
    this.controller = new AnimationController({
      duration: 1000
    });
  }
  
  dispose() {
    this.controller.dispose();
    super.dispose();
  }
}

Built-in Animation Widgets

Flitter provides built-in widgets for commonly used animations:

  • AnimatedContainer: Automatic animation when properties change
  • AnimatedOpacity: Opacity animation
  • AnimatedPositioned: Position animation within Stack
  • AnimatedScale: Size animation
  • AnimatedRotation: Rotation animation
  • AnimatedSlide: Slide animation

These widgets enable simple animations without AnimationController.

Next Steps

Once you’ve mastered animation basics, learn these topics:

  • Advanced Animations - Complex animation patterns and optimization
  • Custom Painting - Advanced graphics implementation with CustomPaint
  • Widget Reference - Detailed guide for built-in animation widgets