코딩강의/meals_app(플러터-유데미)

~196. Explicit Animations: Playing the Animation with AnimatedBuilder

김마드 2023. 11. 6. 16:15

1. 애니메이션을 구현해보자.

 

애니메이션에는 explicit(명시적), implicit(암묵적) 2가지의 종류가 있다. 아래 내용이 잘 정리되어있다.

https://velog.io/@broccolism/Flutter-%ED%94%8C%EB%9F%AC%ED%84%B0-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EC%A0%84%EC%B2%B4%EB%B3%B4%EA%B8%B0

 

[Flutter] 플러터 애니메이션 전체보기

플러터에서는 2가지 방법으로 애니메이션을 구현할 수 있다.

velog.io

 

명시적은 매뉴얼로 사용자가 직접 만드는 것이고, 암묵적인 방법은 플러터에서 제공하는 애니메이션을 가져다 쓰는 것이다.

 

먼저 명시적 방법을 알아보자.

 

- categories.dart

import 'package:flutter/material.dart';

import 'package:meals/data/dummy_data.dart';
import 'package:meals/models/meal.dart';
import 'package:meals/widgets/category_grid_item.dart';
import 'package:meals/screens/meals.dart';
import 'package:meals/models/category.dart';

class CategoriesScreen extends StatefulWidget {
  const CategoriesScreen({
    super.key,
    required this.availableMeals,
  });

  final List<Meal> availableMeals;

  @override
  State<CategoriesScreen> createState() => _CategoriesScreenState();
}

class _CategoriesScreenState extends State<CategoriesScreen>
    with SingleTickerProviderStateMixin {
  late AnimationController _animationController;

  @override
  void initState() {
    super.initState();

    _animationController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
      lowerBound: 0,
      upperBound: 1,
    );

    _animationController.forward();
  }

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

  void _selectCategory(BuildContext context, Category category) {
    final filteredMeals = widget.availableMeals
        .where((meal) => meal.categories.contains(category.id))
        .toList();

    Navigator.of(context).push(
      MaterialPageRoute(
        builder: (ctx) => MealsScreen(
          title: category.title,
          meals: filteredMeals,
        ),
      ),
    ); // Navigator.push(context, route)
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animationController,
      child: GridView(
        padding: const EdgeInsets.all(24),
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2,
          childAspectRatio: 3 / 2,
          crossAxisSpacing: 20,
          mainAxisSpacing: 20,
        ),
        children: [
          // availableCategories.map((category) => CategoryGridItem(category: category)).toList()
          for (final category in availableCategories)
            CategoryGridItem(
              category: category,
              onSelectCategory: () {
                _selectCategory(context, category);
              },
            )
        ],
      ),
      builder: (context, child) => Padding(
          padding: EdgeInsets.only(
            top: 100 - _animationController.value * 100,
          ),
          child: child),
    );
  }
}

1) 먼저 stateful 상태로 바꿔준다. (애니메이션이 계속 움직이면서 리빌딩 되어야하기 때문)

2) late AnimationController _animationController; --> late를 사용함으로써, init후에 할당하겠다는 것

  @override
  void initState() { -->init시 (build위젯 실행 전) 아래를 먼저 실행
    super.initState();

    _animationController = AnimationController(
      vsync: this, 
      duration: const Duration(milliseconds: 300),
      lowerBound: 0,
      upperBound: 1,
    );

    _animationController.forward(); -->1번만 실행하겠다는 뜻, 반복 실행도 가능하고 여러가지 기능이 있어보인다.
  }

  @override -->build가 끝나면 해당 컨트롤러를 dispose를 메모리에서 삭제해주자 (최적화) 
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

3) build 부분을 보면, AnimatedBuilder로 감싸준다. 그리고, animation은 컨트롤러를, child에는 바뀌지 않아야할 부분을 넣고 builder에서 어떻게 애니메이션화 할 지 설정한다. 여기서는 padding을 통해 카테고리 화면이 아래에서 위로 올라오게끔 설정했다. top 패딩값이 초기에는 100이었다가 점차 0이 되면서 애니메이션한다.  (_animationController.value 초기값이 0이고 최고 1이다 0->1로 변경되는것, 따라서 값을 대입하면 top 패딩값은 100-> 0으로 된다)

그리고 마지막 child에는 위에서 설정한 child값을 넣는다. (최적화를 위해, child값은 리빌딩 되지 않는다.)