본문 바로가기
개발 언어/Flutter

Flutter GUI 개발: 애니메이션과 고급 UI 기법

by 주호파파 2025. 6. 15.
728x90
반응형

Flutter에서 사용할 수 있는 다양한 애니메이션 기법과 고급 UI 기술에 대해 알아보겠습니다.

사용자의 경험을 향상시키는 세련된 전환 효과부터 사용자 정의 그래픽까지, Flutter의 강력한 시각적 기능을 활용하는 방법을 배워보세요.

Flutter 애니메이션의 이해

Flutter에서는 크게 두 가지 유형의 애니메이션을 제공합니다: 암시적(implicit) 애니메이션과 명시적(explicit) 애니메이션입니다. 각각의 특징과 사용 사례를 살펴보겠습니다.

암시적 애니메이션

암시적 애니메이션은 위젯의 속성이 변경될 때 자동으로 애니메이션을 적용합니다. 이러한 위젯들은 이름이 'Animated'로 시작합니다(예: AnimatedContainer, AnimatedOpacity 등).

import 'package:flutter/material.dart';

class AnimatedContainerExample extends StatefulWidget {
  @override
  _AnimatedContainerExampleState createState() => _AnimatedContainerExampleState();
}

class _AnimatedContainerExampleState extends State {
  bool _isExpanded = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        setState(() {
          _isExpanded = !_isExpanded;
        });
      },
      child: Center(
        child: AnimatedContainer(
          duration: Duration(milliseconds: 300),
          curve: Curves.easeInOut,
          width: _isExpanded ? 200.0 : 100.0,
          height: _isExpanded ? 200.0 : 100.0,
          color: _isExpanded ? Colors.blue : Colors.red,
          child: Center(
            child: Text(
              "탭하여 ${_isExpanded ? '축소' : '확장'}",
              style: TextStyle(color: Colors.white),
            ),
          ),
        ),
      ),
    );
  }
}

결과: 위 코드는 탭할 때마다 크기와 색상이 부드럽게 변하는 컨테이너를 생성합니다. 초기에는 빨간색 작은 상자이지만, 탭하면 파란색 큰 상자로 애니메이션이 적용됩니다.

명시적 애니메이션

명시적 애니메이션은 개발자가 애니메이션의 모든 측면을 완전히 제어해야 할 때 사용합니다. AnimationController와 함께 사용되며, 더 복잡한 애니메이션을 구현할 수 있습니다.

import 'package:flutter/material.dart';

class RotatingIcon extends StatefulWidget {
  @override
  _RotatingIconState createState() => _RotatingIconState();
}

class _RotatingIconState extends State with SingleTickerProviderStateMixin {
  late AnimationController _controller;

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

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

  @override
  Widget build(BuildContext context) {
    return Center(
      child: RotationTransition(
        turns: _controller,
        child: Icon(
          Icons.refresh,
          size: 50,
          color: Colors.blue,
        ),
      ),
    );
  }
}

결과: 새로고침 아이콘이 2초 동안 계속해서 회전하는 애니메이션이 만들어집니다. 이것은 로딩 표시기나 프로세스 진행 중임을 나타내는 데 유용합니다.

Hero 애니메이션

Hero 애니메이션은 두 화면 간에 위젯이 부드럽게 전환되는 효과를 제공합니다. 사용자가 항목을 탭하여 상세 페이지로 이동할 때 자연스러운 전환 경험을 제공합니다.

// 첫 번째 화면
class PhotoList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('사진 갤러리')),
      body: GridView.builder(
        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 3,
          crossAxisSpacing: 8.0,
          mainAxisSpacing: 8.0,
        ),
        itemCount: 12,
        padding: EdgeInsets.all(8.0),
        itemBuilder: (context, index) {
          return GestureDetector(
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(builder: (context) => PhotoDetail(index: index)),
              );
            },
            child: Hero(
              tag: 'photo-$index',
              child: Container(
                color: Colors.blue[(index * 100) % 900 + 100],
                child: Center(child: Text('사진 $index', style: TextStyle(color: Colors.white))),
              ),
            ),
          );
        },
      ),
    );
  }
}

// 두 번째 화면
class PhotoDetail extends StatelessWidget {
  final int index;
  
  PhotoDetail({required this.index});
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('사진 상세 보기')),
      body: Center(
        child: Hero(
          tag: 'photo-$index',
          child: Container(
            width: 300,
            height: 300,
            color: Colors.blue[(index * 100) % 900 + 100],
            child: Center(
              child: Text(
                '사진 $index 상세 정보',
                style: TextStyle(color: Colors.white, fontSize: 24)
              ),
            ),
          ),
        ),
      ),
    );
  }
}

결과: 갤러리 그리드에서 이미지를 탭하면 해당 이미지가 부드럽게 확장되어 전체 화면 상세 보기로 전환됩니다. 사용자가 뒤로 가기를 탭하면 이미지가 부드럽게 원래 위치로 돌아갑니다.

고급 UI 기법

Custom Painter

CustomPaint 위젯과 CustomPainter 클래스를 사용하면 캔버스에 직접 그리는 방식으로 복잡한 사용자 정의 UI를 구현할 수 있습니다.

import 'package:flutter/material.dart';
import 'dart:math' as math;

class ClockFace extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      width: 200,
      height: 200,
      child: CustomPaint(
        painter: ClockPainter(),
      ),
    );
  }
}

class ClockPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final center = Offset(size.width / 2, size.height / 2);
    final radius = size.width / 2;
    
    // 시계 외곽 그리기
    final paint = Paint()
      ..color = Colors.black
      ..style = PaintingStyle.stroke
      ..strokeWidth = 4;
      
    canvas.drawCircle(center, radius, paint);
    
    // 시간 마커 그리기
    for (int i = 0; i < 12; i++) {
      final double angle = i * math.pi / 6;
      final double markerLength = i % 3 == 0 ? 10 : 5; // 3의 배수 위치에 더 긴 마커
      
      final inner = Offset(
        center.dx + (radius - markerLength) * math.cos(angle),
        center.dy + (radius - markerLength) * math.sin(angle),
      );
      
      final outer = Offset(
        center.dx + radius * math.cos(angle),
        center.dy + radius * math.sin(angle),
      );
      
      canvas.drawLine(inner, outer, paint);
    }
    
    // 현재 시간으로 시침, 분침, 초침 그리기
    final now = DateTime.now();
    
    // 초침
    final secondsPaint = Paint()
      ..color = Colors.red
      ..style = PaintingStyle.stroke
      ..strokeWidth = 1;
      
    final secondHandLength = radius * 0.8;
    final secondsAngle = now.second * math.pi / 30;
    final secondHand = Offset(
      center.dx + secondHandLength * math.cos(secondsAngle - math.pi / 2),
      center.dy + secondHandLength * math.sin(secondsAngle - math.pi / 2),
    );
    
    canvas.drawLine(center, secondHand, secondsPaint);
    
    // 분침
    final minutesPaint = Paint()
      ..color = Colors.black
      ..style = PaintingStyle.stroke
      ..strokeWidth = 2;
      
    final minuteHandLength = radius * 0.7;
    final minutesAngle = now.minute * math.pi / 30 + now.second * math.pi / 1800;
    final minuteHand = Offset(
      center.dx + minuteHandLength * math.cos(minutesAngle - math.pi / 2),
      center.dy + minuteHandLength * math.sin(minutesAngle - math.pi / 2),
    );
    
    canvas.drawLine(center, minuteHand, minutesPaint);
    
    // 시침
    final hoursPaint = Paint()
      ..color = Colors.black
      ..style = PaintingStyle.stroke
      ..strokeWidth = 3;
      
    final hourHandLength = radius * 0.5;
    final hoursAngle = (now.hour % 12) * math.pi / 6 + now.minute * math.pi / 360;
    final hourHand = Offset(
      center.dx + hourHandLength * math.cos(hoursAngle - math.pi / 2),
      center.dy + hourHandLength * math.sin(hoursAngle - math.pi / 2),
    );
    
    canvas.drawLine(center, hourHand, hoursPaint);
    
    // 중앙 점
    final centerPaint = Paint()
      ..color = Colors.black
      ..style = PaintingStyle.fill;
      
    canvas.drawCircle(center, 4, centerPaint);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true; // 매 프레임마다 다시 그립니다.
  }
}

결과: 이 코드는 시침, 분침, 초침이 있는 아날로그 시계 얼굴을 그립니다. 실제 애플리케이션에서는 애니메이션 위젯과 결합하여 매초 업데이트되는 시계를 만들 수 있습니다.

Shader와 그래디언트

Flutter에서 제공하는 다양한 그래디언트를 사용하여 세련된 UI 효과를 만들 수 있습니다.

import 'package:flutter/material.dart';

class GradientButtonExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        width: 200,
        height: 50,
        decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(25),
          gradient: LinearGradient(
            colors: [Colors.purple, Colors.blue],
            begin: Alignment.centerLeft,
            end: Alignment.centerRight,
          ),
          boxShadow: [
            BoxShadow(
              color: Colors.purple.withOpacity(0.3),
              blurRadius: 10,
              offset: Offset(0, 5),
            ),
          ],
        ),
        child: Material(
          color: Colors.transparent,
          child: InkWell(
            borderRadius: BorderRadius.circular(25),
            onTap: () {
              // 버튼 액션
            },
            child: Center(
              child: Text(
                '그래디언트 버튼',
                style: TextStyle(
                  color: Colors.white,
                  fontSize: 16,
                  fontWeight: FontWeight.bold,
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

결과: 보라색에서 파란색으로 그라데이션이 적용된 둥근 모서리의 버튼이 만들어집니다. 버튼은 또한 부드러운 그림자 효과로 페이지에서 살짝 떠 있는 것처럼 보입니다.

Sliver 위젯

Sliver 위젯은 스크롤 가능한 영역에서 고급 효과와 동작을 구현하는 데 사용됩니다. 가장 일반적인 사용 사례 중 하나는 스크롤하면서 축소되는 앱 바를 만드는 것입니다.

import 'package:flutter/material.dart';

class SliverAppBarExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          SliverAppBar(
            expandedHeight: 200.0,
            floating: false,
            pinned: true,
            flexibleSpace: FlexibleSpaceBar(
              title: Text('Sliver 앱바 예제'),
              background: Image.network(
                'https://images.unsplash.com/photo-1519501025264-65ba15a82390',
                fit: BoxFit.cover,
              ),
            ),
          ),
          SliverList(
            delegate: SliverChildBuilderDelegate(
              (BuildContext context, int index) {
                return ListTile(
                  title: Text('항목 $index'),
                  subtitle: Text('여기에 항목 $index에 대한 자세한 설명이 들어갑니다'),
                  leading: CircleAvatar(
                    child: Text('$index'),
                    backgroundColor: Colors.primaries[index % Colors.primaries.length],
                  ),
                );
              },
              childCount: 30,
            ),
          ),
        ],
      ),
    );
  }
}

결과: 스크롤 시 크기가 조정되는 이미지 헤더가 있는 앱을 생성합니다. 위로 스크롤하면 이미지는 점차 사라지고 앱 바는 핀으로 고정된 표준 앱 바로 축소됩니다. 아래로 스크롤하면 이미지가 다시 표시됩니다.

혼합된 애니메이션과 UI

마지막으로, 여러 기술을 결합한 더 복잡한 예제를 살펴보겠습니다. 이 예제는 애니메이션, 커스텀 페인팅 및 고급 레이아웃 기술을 결합합니다.

import 'package:flutter/material.dart';
import 'dart:math' as math;

class AnimatedWavesExample extends StatefulWidget {
  @override
  _AnimatedWavesExampleState createState() => _AnimatedWavesExampleState();
}

class _AnimatedWavesExampleState extends State
    with TickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: Duration(seconds: 5),
    )..repeat();
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: Column(
        children: [
          Expanded(
            child: AnimatedBuilder(
              animation: _controller,
              builder: (context, child) {
                return CustomPaint(
                  painter: WavesPainter(
                    animation: _controller.value,
                  ),
                  child: Container(),
                );
              },
            ),
          ),
          Padding(
            padding: EdgeInsets.all(16),
            child: Text(
              '애니메이션 파도 효과',
              style: TextStyle(
                fontSize: 20,
                fontWeight: FontWeight.bold,
              ),
              textAlign: TextAlign.center,
            ),
          ),
        ],
      ),
    );
  }
}

class WavesPainter extends CustomPainter {
  final double animation;

  WavesPainter({required this.animation});

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.blue.withOpacity(0.5)
      ..style = PaintingStyle.fill;

    final width = size.width;
    final height = size.height;
    
    // 파도 경로 그리기
    final path = Path();
    path.moveTo(0, height * 0.8);
    
    // 첫 번째 파도
    for (double i = 0; i <= width; i++) {
      path.lineTo(
        i,
        height * 0.8 + math.sin((i / width * 2 * math.pi) + animation * 2 * math.pi) * 10,
      );
    }
    
    // 경로 완성
    path.lineTo(width, height);
    path.lineTo(0, height);
    path.close();
    
    canvas.drawPath(path, paint);
    
    // 두 번째 파도 (다른 색상과 위상)
    final paint2 = Paint()
      ..color = Colors.lightBlue.withOpacity(0.3)
      ..style = PaintingStyle.fill;
      
    final path2 = Path();
    path2.moveTo(0, height * 0.85);
    
    for (double i = 0; i <= width; i++) {
      path2.lineTo(
        i,
        height * 0.85 + math.sin((i / width * 3 * math.pi) + animation * 2 * math.pi) * 15,
      );
    }
    
    path2.lineTo(width, height);
    path2.lineTo(0, height);
    path2.close();
    
    canvas.drawPath(path2, paint2);
    
    // 세 번째 파도
    final paint3 = Paint()
      ..color = Colors.blue.withOpacity(0.2)
      ..style = PaintingStyle.fill;
      
    final path3 = Path();
    path3.moveTo(0, height * 0.9);
    
    for (double i = 0; i <= width; i++) {
      path3.lineTo(
        i,
        height * 0.9 + math.sin((i / width * 4 * math.pi) + animation * 2 * math.pi) * 5,
      );
    }
    
    path3.lineTo(width, height);
    path3.lineTo(0, height);
    path3.close();
    
    canvas.drawPath(path3, paint3);
  }

  @override
  bool shouldRepaint(WavesPainter oldDelegate) => true;
}

결과: 화면 하단에 서로 다른 속도로 움직이는 여러 레이어의 파도 애니메이션이 있는 아름다운 효과가 생성됩니다. 이 효과는 물 관련 앱이나 휴식 앱, 또는 로딩 화면의 배경으로 사용하기에 좋습니다.

결론

이번 포스팅에서는 Flutter의 다양한 애니메이션 기법과 고급 UI 기술에 대해 살펴보았습니다. 암시적 애니메이션은 간단하게 구현할 수 있으면서도 훌륭한 시각적 효과를 제공하고, 명시적 애니메이션은 더 복잡하지만 완전한 제어가 가능합니다.

 

Hero 애니메이션은 화면 전환 시 연속성을 제공하며, CustomPainter와 Shader를 통해 사용자 정의 그래픽을 구현할 수 있습니다.

마지막으로 Sliver 위젯을 사용하면 고급 스크롤 효과를 만들 수 있습니다.

이러한 기술들을 마스터하면 매력적이고 전문적인 Flutter 애플리케이션을 만들 수 있습니다. 

728x90
반응형