2023. 3. 28. 01:22ㆍDevelopers 공간 [Shorts]/Frontend
<분류>
A. 수단
- OS/Platform/Tool : Linux, Kubernetes(k8s), Docker, AWS
- Package Manager : node.js, yarn, brew,
- Compiler/Transpillar : React, Nvcc, gcc/g++, Babel, Flutter
- Module Bundler : React, Webpack, Parcel
B. 언어
- C/C++, python, Javacsript, Typescript, Go-Lang, CUDA, Dart, HTML/CSS
C. 라이브러리 및 프레임워크 및 SDK
- OpenCV, OpenCL, FastAPI, PyTorch, Tensorflow, Nsight
1. What? (현상)
Flutter는 2D로 보이지만, 사실 사용하지 않을 뿐 3D로 Rendering되는 중이고, 3D Rendering에 더 최적화되어있다고 할 수 있습니다.
이번엔 버튼에 Transform과 Animation을 활용해 애니메이션 효과를 주는 방법을 정리하고자 합니다.
구현은 Stateful 위젯으로 진행했으며, 아래와 같은 에러메시지를 피하려면 State에 with TickerProviderStateMixin을 해주어야 합니다.
The argument type '_ButtonViewState' can't be assigned to the parameter type 'TickerProvider'.
** Flutter engine은 60frames/s 마다 Rendering하는 scheduler를 가지고 있는데, state가 바뀌어 setState()가 불리거나 UI를 update해야 하는 순간이 오면, 그다음 scheduled frame에 Rendering을 진행합니다.
** Ticker : Flutter engine이 새로운 frame을 Rendering하기 위해 사용하는 timer로, Timer보다 정교하고, Rendering 타이밍(Refresh Rate)에 맞춰 사용할 수 있기 때문에 사용됩니다. 또한 Ticker는 해당 위젯이 Visible할 때만 불립니다.
** SingleTickerProviderStateMixin : 하나의 AnimationController를 사용시
** TickerProviderStateMixin : 하나 이상의 AnimationController를 사용시
2. Why? (원인)
- X
3. How? (해결책)
- 버튼에 Scale 애니메이션 적용하기 :
- timimg은 AnimationController()을 사용했습니다.
- 위의 timing에 맞춰 Transform.scale() 함수를 활용해 동작을 반영했습니다.
- Trigger 방법은 위 선언한 AnimationController에 직접 forward(), reverse() 함수를 사용했습니다.
// Caller
ButtonView(
onTap: viewModel.onLightPressed,
heroTag:'light',
child: ImageIcon(
viewModel.light_on
? AssetImage("images/on.png")
: AssetImage("images/off.png"),
size: 50,
color: viewModel.light_on ? kSelectedColor : kSubColor,
),
)
// Callee
import 'package:flutter/material.dart';
class ButtonView extends StatefulWidget {
final VoidCallback onTap; // 혹은 final Function(bool)? onTap;
final Widget child;
final String heroTag;
ButtonView({
required this.onTap,
required this.child,
required this.heroTag,
Key? key
}) : super(key: key);
@override
_ButtonViewState createState() => _ButtonViewState();
}
// Callee State
class _ButtonViewState extends State<ButtonView> with SingleTickerProviderStateMixin{
final Duration time = const Duration(milliseconds: 100);
late AnimationController _buttonController = AnimationController(
vsync: this,
duration: time,
lowerBound: 0.0,
upperBound: 0.4,
)..addListener(()=>setState((){}));
@override
void initState(){
super.initState();
}
@override
Widget build(BuildContext context) {
return FloatingActionButton(
backgroundColor: kMainColor.withOpacity(0.0),
heroTag: widget.heroTag,
onPressed: (){
_buttonController.forward();
print('forward!!');
Future.delayed(
time,
(){_buttonController.reverse();}
);
print('backward!!');
widget.onTap();
},
child:Transform.scale(
scale:1+_buttonController.value,
child:widget.child
)
);
}
}
- 버튼에 Rotate 애니메이션 적용하기 :
- timimg은 AnimationSwitcher()와 transitionBuilder에 넣을 Tween을 사용했습니다.
** Tween : begin 부터 end 까지 값을 변화시키거나 Curve등을 활용해 Animation 효과를 변형할 때 사용합니다.
(Curve와 관련해서는 아래 더보기를 참조하세요) - 위 타이밍에 맞춰 transitionBuilder에 넣을 AnimationBuilder()함수를 활용했으며, Rotate는 Transform() 자체함수를 사용해 Matrix4 구조체를 활용해 효과를 주었습니다. 자세한 내용은 아래 그림과 더보기를 참조하시면 좋습니다.
- trigger는 showFront라는 변수를 false ↔ true 변환하는 방법으로 진행했습니다.
- timimg은 AnimationSwitcher()와 transitionBuilder에 넣을 Tween을 사용했습니다.
-----------------------------------------------------------------------------------------------------
<Tween과 Curve를 같이 사용한 예시>
animate- Animation<Offset> cupSlideUpAnimation = Tween(begin: 0.0, end: 0.07).animate(
CurvedAnimation(
parent: animation,
curve: const ShakeCurve(count: 3))); - Animation<double> cupRotateAnimation = Tween(begin: Offset(0, 1), end: Offset(0, 0)).animate(
CurvedAnimation(
parent: animation,
curve: Curves.elasticOut)); - Animation<double> cupFadeInAnimation = Tween(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: animation,
curve: Curves.easeOutCubic));
-----------------------------------------------------------------------------------------------------
// Caller
ButtonView2(
onTap: viewModel.onReversePressed,
child: ImageIcon(
AssetImage("images/btn.png"),
size: 50,
color: kSubColor,
),
heroTag: 'reverse'
)
// Callee
import 'package:flutter/material.dart';
import 'dart:math' as math;
class ButtonView2 extends StatefulWidget {
final VoidCallback onTap;
final Widget child;
Widget? child_back;
final String heroTag;
ButtonView2({
required this.onTap,
required this.child,
required this.heroTag,
Key? key
}) : super(key: key){
// initialize back image with original image
child_back = Transform(
key: ValueKey('back'),
transform: Matrix4.rotationY(math.pi),
child:child,
alignment: Alignment.center,
);
}
@override
_ButtonView2State createState() => _ButtonView2State();
}
// Callee State
class _ButtonView2State extends State<ButtonView2>{
bool showFront = true;
final Duration time = const Duration(milliseconds: 500);
@override
void initState(){
super.initState();
}
Widget wrapAnimatedBuilder(Widget widget, Animation<double> animation) {
// 0~1 to pi~0
final rotate = Tween(begin: math.pi, end: 0.0).animate(animation);
return AnimatedBuilder(
animation: rotate,
child: widget,
builder: (_, widget) {
//check if current widget is back
final isBack = showFront
? widget?.key == ValueKey('back')
: widget?.key != ValueKey('back');
// when back, pi/2~pi/2~0
// when front, pi~pi/2~0
final value = isBack ? math.min(rotate.value, math.pi / 2) : rotate.value;
var tilt = ((animation.value - 0.5).abs() - 0.5) * 0.0025;
return Transform(
transform: Matrix4.rotationY(value)..setEntry(3, 0, tilt),
child: widget,
alignment: Alignment.center,
);
},
);
}
@override
Widget build(BuildContext context) {
return FloatingActionButton(
backgroundColor: kMainColor.withOpacity(0.0),
heroTag: widget.heroTag,
onPressed: (){
showFront=!showFront;
widget.onTap();
},
child:AnimatedSwitcher(
transitionBuilder: wrapAnimatedBuilder,
layoutBuilder: (widget, list){
return Stack(children:[widget!, ...list]);
},
duration : time,
child : showFront? widget.child : widget.child_back,
)
);
}
}
-----------------------------------------------------------------------------------------------------
<Matrix4 관련>
-----------------------------------------------------------------------------------------------------
https://velog.io/@larsien/UI-challenge-dribble
https://lucky516.tistory.com/123
https://blog.codefactory.ai/flutter/card-flip/
https://codewithandrea.com/articles/flutter-timer-vs-ticker/
'Developers 공간 [Shorts] > Frontend' 카테고리의 다른 글
[Flutter] Platform 정보를 확인하기 위한 방법 정리 (0) | 2023.04.02 |
---|---|
[Flutter] Stacked에서 view와 viewModel의 생성 순서 (0) | 2023.04.02 |
[Flutter] Portrait, Landscape mode 막기 (0) | 2023.03.28 |
[Flutter] ios 셋팅 수정 및 다양한 상황에서 문제 해결하기 (0) | 2023.03.27 |
[Flutter] 카메라 촬영 후 보이는 그대로 저장하기 (1) | 2023.03.26 |