[Flutter] 버튼에 애니메이션 넣기

2023. 3. 28. 01:22Developers 공간 [Shorts]/Frontend

728x90
반응형
<분류>
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 애니메이션 적용하기
    1. timimg은 AnimationController()을 사용했습니다.
    2. 위의 timing에 맞춰 Transform.scale() 함수를 활용해 동작을 반영했습니다.
    3. 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 애니메이션 적용하기
    1. timimg은 AnimationSwitcher()와 transitionBuilder에 넣을 Tween을 사용했습니다.
      ** Tween : begin 부터 end 까지 값을 변화시키거나 Curve등을 활용해 Animation 효과를 변형할 때 사용합니다.
      (Curve와 관련해서는 아래 더보기를 참조하세요)
    2. 위 타이밍에 맞춰 transitionBuilder에 넣을 AnimationBuilder()함수를 활용했으며, Rotate는 Transform() 자체함수를 사용해 Matrix4 구조체를 활용해 효과를 주었습니다. 자세한 내용은 아래 그림과 더보기를 참조하시면 좋습니다.
    3. trigger는 showFront라는 변수를 false ↔ true 변환하는 방법으로 진행했습니다.
더보기

-----------------------------------------------------------------------------------------------------

<Tween과 Curve를 같이 사용한 예시>

animate 
  1. Animation<Offset> cupSlideUpAnimation = Tween(begin: 0.0, end: 0.07).animate(
                CurvedAnimation(
                    parent: animation, 
                    curve: const ShakeCurve(count: 3)));
  2. Animation<double> cupRotateAnimation = Tween(begin: Offset(0, 1), end: Offset(0, 0)).animate(
                CurvedAnimation(
                    parent: animation
                    curve: Curves.elasticOut));
  3. 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,
)
);
}
}

[matrix transform 예시로, 회전 방향은 시계 방향임을 주의합니다]

더보기

-----------------------------------------------------------------------------------------------------

<Matrix4 관련>

[Matrix4 관련 정보 정리]

-----------------------------------------------------------------------------------------------------

 


https://velog.io/@cyb9701/%ED%86%A0%EC%8A%A4-%EC%83%81%ED%92%88%EA%B6%8C-3D-%EC%9D%B8%ED%84%B0%EB%9E%99%EC%85%98

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/

728x90
반응형