Fluter 애니메이션을 만졌어!

136971 단어 FlutterDarttech

개시하다


나는 애니메이션에 관심이 생겨서 Flutter에서 애니메이션 입문에 들어갔다.
잘못된 인식이 있으면, 평론란에서 교수님께 가르침을 청하십시오.🙇

하고 싶은 일


쓰고 싶은 게 많지만 여기서 기본적인 애니메이션을 실례로 소개하고 싶어요!
실제로 보면 빠를 것 같아요. 아래gif를 보세요.
4장의 카드를 열면 대화상자에 4장의 카드에 적힌 포인트의 합계가 표시됩니다.

카드 초과에 신경 써...
↓ 카드가 화면에 수납되는지 여기서 확인할 수 있을 것 같아요!
컴퓨터가 아니면 안 보일 수도 있어요.🙇
https://user-images.githubusercontent.com/63396451/147625511-26ff0678-c7dd-4ee4-b928-123211451996.MP4

기본 코드


애니메이션을 가져오기 전에 기본 코드를 확인하십시오!
import 'package:flutter/material.dart';
import 'package:flutter_animation/constanins.dart';

class CardPage extends StatelessWidget {
  const CardPage({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        elevation: 0,
        backgroundColor: const Color(0x44000000),
      ),
      body: SafeArea(
        child: LayoutBuilder(
          builder: (context, constrains) {
            return GridView.builder(
              itemCount: 4,
              physics: const NeverScrollableScrollPhysics(),
              gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 2,
                mainAxisSpacing: 24,
                crossAxisSpacing: 24,
                childAspectRatio: constrains.maxWidth / constrains.maxHeight,
              ),
              itemBuilder: (context, index) => Container(
                padding: const EdgeInsets.all(defaultPadding),
                decoration: BoxDecoration(
                  color: primaryColor.withOpacity(0.1),
                  border: Border.all(color: primaryColor),
                  borderRadius: const BorderRadius.all(Radius.circular(6)),
                ),
                child: Center(
                  child: Text.rich(
                    TextSpan(
                      text: '0',
                      style: Theme.of(context).textTheme.headline4!.copyWith(
                            fontWeight: FontWeight.w600,
                            color: Colors.white,
                          ),
                      children: const [
                        TextSpan(
                          text: "pt",
                          style: TextStyle(fontSize: 24),
                        )
                      ],
                    ),
                  ),
                ),
              ),
            );
          },
        ),
      ),
    );
  }
}

주요 부품


LayoutBuilder
  • 친Widget이 하위 Widget의 크기를 제한하고 하위 고유 사이즈에 의존하지 않을 때 편리하다
  • 처음 레이아웃이나 화면의 크기가 변할 때만 호출됩니다
  • 다음은 공식 유튜브 사이트입니다.
    SliverGridDelegateWithFixedCrossAxisCount
  • 가로 축에 고정된 수의 슬라이스가 있는 메쉬 레이아웃 생성
  • 각 영화의 종횡비(child Aspect Ratio)와 영화 사이의 간격(main Axis Spacing,cross Axis Spacing) 등을 지정할 수 있다
  • 다음은 공식 홈페이지입니다.
    https://api.flutter.dev/flutter/rendering/SliverGridDelegateWithFixedCrossAxisCount-class.html
    또한 각 카드의 점위나 카드가 열려 있는지 여부(애니메이션을 가져올 때 사용)는 상태 관리의 대상이다.
    상태 관리 사용riverpod.
    취지에서 벗어나 관련riverpod은 별로 접촉하지 않았다.이해해 주세요.🙇
    card_notifier.dart
    import 'dart:math';
    import 'package:hooks_riverpod/hooks_riverpod.dart';
    import 'package:freezed_annotation/freezed_annotation.dart';
    
    part 'card_notifier.freezed.dart';
    
    
    class CardState with _$CardState {
      const factory CardState({
        @Default(0) int ptOfCard1,
        @Default(0) int ptOfCard2,
        @Default(0) int ptOfCard3,
        @Default(0) int ptOfCard4,
        @Default(false) bool isOpenCard1,
        @Default(false) bool isOpenCard2,
        @Default(false) bool isOpenCard3,
        @Default(false) bool isOpenCard4,
      }) = _CardState;
    }
    
    final cardNotifierProvider =
        StateNotifierProvider.autoDispose<CardNotifier, CardState>((ref) {
      return CardNotifier(ref.read);
    });
    
    class CardNotifier extends StateNotifier<CardState> {
      CardNotifier(this._read) : super(const CardState());
      final Reader _read;
    
      void Function()? toOpenCard1() {
        state = state.copyWith(
          isOpenCard1: true,
          ptOfCard1: Random().nextInt(26),
          // Random().nextInt(26) は 0 ~ 25 までのいずれかをランダムで表す
        );
      }
    
      void Function()? toOpenCard2() {
        state = state.copyWith(
          isOpenCard2: true,
          ptOfCard2: Random().nextInt(26),
        );
      }
    
      void Function()? toOpenCard3() {
        state = state.copyWith(
          isOpenCard3: true,
          ptOfCard3: Random().nextInt(26),
        );
      }
    
      void Function()? toOpenCard4() {
        state = state.copyWith(
          isOpenCard4: true,
          ptOfCard4: Random().nextInt(26),
        );
      }
    }
    
    
    view 방면은 다음과 같다.
    주요 부분만 튀어나오다.
    card_page.dart
    import 'package:flutter/material.dart';
    import 'package:flutter_animation/UI/card_notifier.dart';
    import 'package:flutter_animation/constanins.dart';
    import 'package:hooks_riverpod/hooks_riverpod.dart';
    
    -class CardPage extends StatelessWidget {
    +class CardPage extends ConsumerWidget {
      const CardPage({Key? key}) : super(key: key);
    
      
    - Widget build(BuildContext context) {
    + Widget build(BuildContext context, WidgetRef ref) {
    +   final state = ref.watch(cardNotifierProvider);
    +   final notifier = ref.watch(cardNotifierProvider.notifier);
    
        List<int> listCard = [
          state.ptOfCard1,
          state.ptOfCard2,
          state.ptOfCard3,
          state.ptOfCard4
        ];
    
        List<void Function()?> onTap = [
          notifier.toOpenCard1,
          notifier.toOpenCard2,
          notifier.toOpenCard3,
          notifier.toOpenCard4,
        ];
    
        return Scaffold(
          appBar: AppBar(
            elevation: 0,
            backgroundColor: const Color(0x44000000),
          ),
          body: SafeArea(
            child: LayoutBuilder(
              builder: (context, constrains) {
                return GridView.builder(
                  itemCount: 4,
                  physics: const NeverScrollableScrollPhysics(),
                  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                    crossAxisCount: 2,
                    mainAxisSpacing: 24,
                    crossAxisSpacing: 24,
                    childAspectRatio: constrains.maxWidth / constrains.maxHeight,
                  ),
                  itemBuilder: (context, index) {
    +                return GestureDetector(
    +                 onTap: onTap[index],
                      child: Container(
                        padding: const EdgeInsets.all(defaultPadding),
                        decoration: BoxDecoration(
                          color: primaryColor.withOpacity(0.1),
                          border: Border.all(color: primaryColor),
                          borderRadius: const BorderRadius.all(Radius.circular(6)),
                        ),
                        child: Center(
                          child: Text.rich(
                            TextSpan(
    +                         text: listCard[index].toString(),
                              style:
                                  Theme.of(context).textTheme.headline3!.copyWith(
                                        fontWeight: FontWeight.w600,
                                        color: Colors.white,
                                      ),
                              children: const [
                                TextSpan(
                                  text: "pt",
                                  style: TextStyle(fontSize: 24),
                                )
                              ],
                            ),
                          ),
                        ),
                      ),
                    );
                  },
                );
              },
            ),
          ),
        );
      }
    }
    
    이렇게 되면 각 카드를 먼저 클릭하면 수치가 바뀐다.

    애니메이션 가져오기


    그럼 다음 준비는 여기까지 하고 테마 애니메이션을 가져옵니다!
    애니메이션을 하고 싶은 곳은 두 가지입니다.

  • 카드 회전 애니메이션

  • 대화상자가 열리면 각 카드의 점 페이드 애니메이션
  • 네.
    다음 각도에서 코드를 확인하세요.
    card_notifier.dart
    +import 'dart:math';
    import 'package:hooks_riverpod/hooks_riverpod.dart';
    import 'package:freezed_annotation/freezed_annotation.dart';
    
    part 'card_notifier.freezed.dart';
    
    @freezed
    class CardState with _$CardState {
      const factory CardState({
        @Default(0) int ptOfCard1,
        @Default(0) int ptOfCard2,
        @Default(0) int ptOfCard3,
        @Default(0) int ptOfCard4,
        @Default(false) bool isOpenCard1,
        @Default(false) bool isOpenCard2,
        @Default(false) bool isOpenCard3,
        @Default(false) bool isOpenCard4,
    +   @Default(0) double angle1,
    +   @Default(0) double angle2,
    +   @Default(0) double angle3,
    +   @Default(0) double angle4,
      }) = _CardState;
    }
    
    final cardNotifierProvider =
        StateNotifierProvider.autoDispose<CardNotifier, CardState>((ref) {
      return CardNotifier(ref.read);
    });
    
    class CardNotifier extends StateNotifier<CardState> {
      CardNotifier(this._read) : super(const CardState());
      final Reader _read;
    
      void Function()? toOpenCard1() {
        state = state.copyWith(
          isOpenCard1: true,
          ptOfCard1: Random().nextInt(26),
    +     angle1: (state.angle1 + pi) % (2 * pi),
        );
      }
    
      void Function()? toOpenCard2() {
        state = state.copyWith(
          isOpenCard2: true,
          ptOfCard2: Random().nextInt(26),
    +     angle2: (state.angle2 + pi) % (2 * pi),
        );
      }
    
      void Function()? toOpenCard3() {
        state = state.copyWith(
          isOpenCard3: true,
          ptOfCard3: Random().nextInt(26),
    +     angle3: (state.angle3 + pi) % (2 * pi),
        );
      }
    
      void Function()? toOpenCard4() {
        state = state.copyWith(
          isOpenCard4: true,
          ptOfCard4: Random().nextInt(26),
     +    angle4: (state.angle4 + pi) % (2 * pi),
        );
      }
    }
    
    card_page.dart
    import 'dart:math' as math;
    import 'package:flutter/material.dart';
    import 'package:flutter_animation/UI/card_notifier.dart';
    import 'package:flutter_animation/constanins.dart';
    import 'package:hooks_riverpod/hooks_riverpod.dart';
    
    class CardPage extends ConsumerWidget {
      const CardPage({Key? key}) : super(key: key);
    
      
      Widget build(BuildContext context, WidgetRef ref) {
        final state = ref.watch(cardNotifierProvider);
        final notifier = ref.watch(cardNotifierProvider.notifier);
    
        List<int> listCard = [
          state.ptOfCard1,
          state.ptOfCard2,
          state.ptOfCard3,
          state.ptOfCard4
        ];
    
        List<void Function()?> onTap = [
          notifier.toOpenCard1,
          notifier.toOpenCard2,
          notifier.toOpenCard3,
          notifier.toOpenCard4,
        ];
    
        return Scaffold(
          appBar: AppBar(
            elevation: 0,
            backgroundColor: const Color(0x44000000),
          ),
          body: SafeArea(
            child: LayoutBuilder(
              builder: (context, constrains) {
                return GridView.builder(
                  itemCount: 4,
                  physics: const NeverScrollableScrollPhysics(),
                  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                    crossAxisCount: 2,
                    mainAxisSpacing: 24,
                    crossAxisSpacing: 24,
                    childAspectRatio: constrains.maxWidth / constrains.maxHeight,
                  ),
                  itemBuilder: (context, index) {
                    final isOpen = [
                      state.isOpenCard1,
                      state.isOpenCard2,
                      state.isOpenCard3,
                      state.isOpenCard4,
                    ][index];
    
                    final angle = [
                      state.angle1,
                      state.angle2,
                      state.angle3,
                      state.angle4,
                    ];
    
                    return GestureDetector(
                      onTap: onTap[index],
                      child: TweenAnimationBuilder(
                        tween: Tween<double>(begin: 0, end: angle[index]),
                        duration: defaultDuration,
                        builder: ((BuildContext context, double val, __) {
                          return Transform(
                            alignment: Alignment.center,
                            transform: Matrix4.identity()
                              ..setEntry(3, 2, 0.001)
                              ..rotateY(val),
                            child: Transform(
                              alignment: Alignment.center,
                              transform: Matrix4.identity()..rotateY(math.pi),
                              child: Container(
                                padding: const EdgeInsets.all(defaultPadding),
                                decoration: BoxDecoration(
                                  color: primaryColor.withOpacity(0.1),
                                  border: Border.all(color: primaryColor),
                                  borderRadius:
                                      const BorderRadius.all(Radius.circular(6)),
                                ),
                                child: AnimatedOpacity(
                                  duration: defaultDuration1,
                                  opacity: isOpen ? 1 : 0,
                                  child: Center(
                                    child: Text.rich(
                                      TextSpan(
                                        text: listCard[index].toString(),
                                        style: Theme.of(context)
                                            .textTheme
                                            .headline3!
                                            .copyWith(
                                              fontWeight: FontWeight.w600,
                                              color: Colors.white,
                                            ),
                                        children: const [
                                          TextSpan(
                                            text: "pt",
                                            style: TextStyle(fontSize: 24),
                                          )
                                        ],
                                      ),
                                    ),
                                  ),
                                ),
                              ),
                            ),
                          );
                        }),
                      ),
                    );
                  },
                );
              },
            ),
          ),
        );
      }
    }
    

    주안점

  • 카드의 (반) 회전을 사용하기 위해 필요한 π 가져오기math, 상태 관리에 기술된 angle에서 카드를 가볍게 두드릴 때(state.angle1 + pi) % (2 * pi), 반회전
  • 을 나타낸다.
    TweenAnimationBuilder
  • 목표 값이 변경될 때마다 WidgetBuilder
  • 이번 Tween의 유형은 Tween<double>을 사용했고, 다른 것은 ColorTweenRectTween
  • Tween은 애니메이션의 목표 값도 정의할 수 있습니다.Begin에서 Tween까지.end 애니메이션 실행
  • 다음은 공식 홈페이지입니다.
    https://api.flutter.dev/flutter/widgets/TweenAnimationBuilder-class.html
    Transform
  • Transform은 하위 부품을 그리기 전에 변형을 적용하는 애플릿이며, 이 대상은 그리기 전에 변형
  • 을 적용합니다.
  • 지정된 속성transform을 부여Matrix4함으로써 4D 동작을 표현할 수 있다.(이 근처에서 더 알고 싶어요...)
  • 이번 예에서
    alignment: Alignment.center,
    transform: Matrix4.identity()..rotateY(math.pi),
    
    이 부근의 속성을 수여하여 표현할 수 있다カードを縦中央を軸として回転.
    다음은 공식 홈페이지입니다.
    https://api.flutter.dev/flutter/widgets/Transform-class.html
    AnimatedOpacity
  • Opacity 에디션
  • 주어진 불투명도가 바뀔 때마다 주어진 시간 내에 자동으로 하위 Widget의 불투명도(페이드/페이드)
  • 애니마테~가 설치된 위젯은 AnimatedContainer,AnimatedDefaultTextStyle,AnimatedPositioned,AnimatedSwitcher,AnimatedSwitcher 등도 많이 준비했다
  • 다음은 공식 홈페이지입니다.
    https://api.flutter.dev/flutter/widgets/AnimatedOpacity-class.html

    대화 상자 열기


    마지막 네 장의 카드를 열 때 대화상자를 팝업하는 기능을 실현합니다.
    다음 각도 버튼을 사용하여 코드를 확인하십시오.
    card_notifier.dart
    import 'dart:math';
    import 'package:flutter/material.dart';
    import 'package:hooks_riverpod/hooks_riverpod.dart';
    import 'package:freezed_annotation/freezed_annotation.dart';
    
    part 'card_notifier.freezed.dart';
    
    @freezed
    class CardState with _$CardState {
      const factory CardState({
        @Default(0) int ptOfCard1,
        @Default(0) int ptOfCard2,
        @Default(0) int ptOfCard3,
        @Default(0) int ptOfCard4,
    +   @Default(0) int totalPt,
        @Default(false) bool isOpenCard1,
        @Default(false) bool isOpenCard2,
        @Default(false) bool isOpenCard3,
        @Default(false) bool isOpenCard4,
        @Default(0) double angle1,
        @Default(0) double angle2,
        @Default(0) double angle3,
        @Default(0) double angle4,
      }) = _CardState;
    }
    
    final cardNotifierProvider =
        StateNotifierProvider.autoDispose<CardNotifier, CardState>((ref) {
      return CardNotifier(ref.read);
    });
    
    class CardNotifier extends StateNotifier<CardState> {
      CardNotifier(this._read) : super(const CardState());
      final Reader _read;
    
      void Function()? toOpenCard1() {
        state = state.copyWith(
          isOpenCard1: true,
          ptOfCard1: Random().nextInt(26),
          angle1: (state.angle1 + pi) % (2 * pi),
        );
      }
    
      void Function()? toOpenCard2() {
        state = state.copyWith(
          isOpenCard2: true,
          ptOfCard2: Random().nextInt(26),
          angle2: (state.angle2 + pi) % (2 * pi),
        );
      }
    
      void Function()? toOpenCard3() {
        state = state.copyWith(
          isOpenCard3: true,
          ptOfCard3: Random().nextInt(26),
          angle3: (state.angle3 + pi) % (2 * pi),
        );
      }
    
      void Function()? toOpenCard4() {
        state = state.copyWith(
          isOpenCard4: true,
          ptOfCard4: Random().nextInt(26),
          angle4: (state.angle4 + pi) % (2 * pi),
        );
      }
    
    + Future<void> toShowDialog(BuildContext context) async {
    +   if (state.isOpenCard1 &&
    +       state.isOpenCard2 &&
    +       state.isOpenCard3 &&
    +       state.isOpenCard4) {
          state = state.copyWith(
            totalPt: state.ptOfCard1 +
                state.ptOfCard2 +
                state.ptOfCard3 +
                state.ptOfCard4,
          );
          await Future.delayed(const Duration(milliseconds: 500));
          _showDialog(context);
    +     await Future.delayed(const Duration(milliseconds: 800));
    +     state = state.copyWith(
    +       isOpenCard1: false,
    +       isOpenCard2: false,
    +       isOpenCard3: false,
    +       isOpenCard4: false,
    +     );
        }
      }
      
      Future<Widget?> _showDialog(BuildContext context) {
        return showDialog<Widget>(
          context: context,
          builder: (context) {
            return AlertDialog(
              backgroundColor: Colors.transparent,
              title: Center(
                child: Text.rich(
                  TextSpan(
                    text: '${state.totalPt}',
                    style: Theme.of(context).textTheme.headline3!.copyWith(
                          fontWeight: FontWeight.w600,
                          color: Colors.white,
                        ),
                    children: const [
                      TextSpan(
                        text: "pt",
                        style: TextStyle(fontSize: 24),
                      )
                    ],
                  ),
                ),
              ),
            );
          },
        );
      }
    }
    
    card_page.dart
    import 'dart:math' as math;
    import 'package:flutter/material.dart';
    import 'package:flutter_animation/UI/card_notifier.dart';
    import 'package:flutter_animation/constanins.dart';
    import 'package:flutter_hooks/flutter_hooks.dart';
    import 'package:hooks_riverpod/hooks_riverpod.dart';
    
    -class CardPage extends ConsumerWidget {
    +class CardPage extends HookConsumerWidget {
      const CardPage({Key? key}) : super(key: key);
    
      
      Widget build(BuildContext context, WidgetRef ref) {
        final state = ref.watch(cardNotifierProvider);
        final notifier = ref.watch(cardNotifierProvider.notifier);
    
        List<int> listCard = [
          state.ptOfCard1,
          state.ptOfCard2,
          state.ptOfCard3,
          state.ptOfCard4
        ];
    
        List<void Function()?> onTap = [
          notifier.toOpenCard1,
          notifier.toOpenCard2,
          notifier.toOpenCard3,
          notifier.toOpenCard4,
        ];
    
    +   useEffect(() {
    +     WidgetsBinding.instance?.addPostFrameCallback((_) async {
    +       await notifier.toShowDialog(context);
    +     });
    +   }, [
    +     state.isOpenCard1,
    +     state.isOpenCard2,
    +     state.isOpenCard3,
    +     state.isOpenCard4,
    +   ]);
    
        return Scaffold(
          appBar: AppBar(
            elevation: 0,
            backgroundColor: const Color(0x44000000),
          ),
          body: SafeArea(
            child: LayoutBuilder(
              builder: (context, constrains) {
                return GridView.builder(
                  itemCount: 4,
                  physics: const NeverScrollableScrollPhysics(),
                  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                    crossAxisCount: 2,
                    mainAxisSpacing: 24,
                    crossAxisSpacing: 24,
                    childAspectRatio: constrains.maxWidth / constrains.maxHeight,
                  ),
                  itemBuilder: (context, index) {
                    final isOpen = [
                      state.isOpenCard1,
                      state.isOpenCard2,
                      state.isOpenCard3,
                      state.isOpenCard4,
                    ][index];
    
                    final angle = [
                      state.angle1,
                      state.angle2,
                      state.angle3,
                      state.angle4,
                    ];
                    return GestureDetector(
                      onTap: onTap[index],
                      child: TweenAnimationBuilder(
                        tween: Tween<double>(begin: 0, end: angle[index]),
                        duration: defaultDuration,
                        builder: ((BuildContext context, double val, __) =>
                            Transform(
                              alignment: Alignment.center,
                              transform: Matrix4.identity()
                                ..setEntry(3, 2, 0.001)
                                ..rotateY(val),
                              child: Transform(
                                alignment: Alignment.center,
                                transform: Matrix4.identity()..rotateY(math.pi),
                                child: Container(
                                  padding: const EdgeInsets.all(defaultPadding),
                                  decoration: BoxDecoration(
                                    color: primaryColor.withOpacity(0.1),
                                    border: Border.all(color: primaryColor),
                                    borderRadius:
                                        const BorderRadius.all(Radius.circular(6)),
                                  ),
                                  child: AnimatedOpacity(
                                    duration: defaultDuration1,
                                    opacity: isOpen ? 1 : 0,
                                    child: Center(
                                      child: Text.rich(
                                        TextSpan(
                                          text: listCard[index].toString(),
                                          style: Theme.of(context)
                                              .textTheme
                                              .headline3!
                                              .copyWith(
                                                fontWeight: FontWeight.w600,
                                                color: Colors.white,
                                              ),
                                          children: const [
                                            TextSpan(
                                              text: "pt",
                                              style: TextStyle(fontSize: 24),
                                            )
                                          ],
                                        ),
                                      ),
                                    ),
                                  ),
                                ),
                              ),
                            )),
                      ),
                    );
                  },
                );
              },
            ),
          ),
        );
      }
    }
    
    

    주안점

  • hooks의 하나인useEffect를 사용하기 위해ConsumerWidgetHookConsumerWidget변경
  • useEffect의 첫 번째 파라미터에 대해 모든 카드가 열릴 때 대화상자를 여는 처리
  • 각각 isOpenCarduseEffect의 두 번째 매개 변수를 주어 첫 번째 매개 변수를 실행하는 함수의 정시
  • 를 제어한다.
  • Future.delayed(const Duration(milliseconds: 800))에서 비동기식

  • 나는 또 다른 hooks 중 하나useAnimationController로 각양각색의 애니메이션을 넣고 싶지만 좋은 예가 떠오르지 않는다🤷‍♂️
    나는 좀 더 시간을 써서 고려하고 싶다.
    이상입니다.
    여기까지 읽어주셔서 감사합니다!!

    좋은 웹페이지 즐겨찾기