Fluter 애니메이션을 만졌어!
개시하다
나는 애니메이션에 관심이 생겨서
Flutter
에서 애니메이션 입문에 들어갔다.잘못된 인식이 있으면, 평론란에서 교수님께 가르침을 청하십시오.🙇
하고 싶은 일
쓰고 싶은 게 많지만 여기서 기본적인 애니메이션을 실례로 소개하고 싶어요!
실제로 보면 빠를 것 같아요. 아래gif를 보세요.
4장의 카드를 열면 대화상자에 4장의 카드에 적힌 포인트의 합계가 표시됩니다.
카드 초과에 신경 써...
↓ 카드가 화면에 수납되는지 여기서 확인할 수 있을 것 같아요!
컴퓨터가 아니면 안 보일 수도 있어요.🙇
기본 코드
애니메이션을 가져오기 전에 기본 코드를 확인하십시오!
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
SliverGridDelegateWithFixedCrossAxisCount
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
Tween<double>
을 사용했고, 다른 것은 ColorTween
와RectTween
https://api.flutter.dev/flutter/widgets/TweenAnimationBuilder-class.html
Transform
transform
을 부여Matrix4
함으로써 4D 동작을 표현할 수 있다.(이 근처에서 더 알고 싶어요...)alignment: Alignment.center,
transform: Matrix4.identity()..rotateY(math.pi),
이 부근의 속성을 수여하여 표현할 수 있다カードを縦中央を軸として回転
.다음은 공식 홈페이지입니다.
https://api.flutter.dev/flutter/widgets/Transform-class.html
AnimatedOpacity
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),
)
],
),
),
),
),
),
),
)),
),
);
},
);
},
),
),
);
}
}
주안점
ConsumerWidget
→HookConsumerWidget
변경isOpenCard
useEffect의 두 번째 매개 변수를 주어 첫 번째 매개 변수를 실행하는 함수의 정시Future.delayed(const Duration(milliseconds: 800))
에서 비동기식〆
나는 또 다른 hooks 중 하나
useAnimationController
로 각양각색의 애니메이션을 넣고 싶지만 좋은 예가 떠오르지 않는다🤷♂️나는 좀 더 시간을 써서 고려하고 싶다.
이상입니다.
여기까지 읽어주셔서 감사합니다!!
Reference
이 문제에 관하여(Fluter 애니메이션을 만졌어!), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://zenn.dev/tsuboi/articles/cb3bf9d93589f2텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)