[Flutter] Flow & FlowDelegate

CHOI·2021년 12월 12일
1

[Flutter] 위젯

목록 보기
2/6
post-thumbnail

Flow 란?

Flow 위젯은 여러 가지 위젯 목록들이 비슷하게 움직이거나 애니메이션 효과를 조정해야 할 때 사용하기 좋은 위젯입니다.

여러 가지 위젯들을 모두 애니메이션 컨트롤러로 조정을 하게 되면 불필요한 코드 양도 많아지고 많은 양의 애니메이션 위젯을 일일이 컨트롤하기가 귀찮을 겁니다.
또한 렌더링 타임도 과부하가 올 수도 있기 때문에 비슷한 움직임을 조정해야 할 경우에는 Flow 위젯을 추천드립니다.

사용 방법

Flow 위젯을 사용하기 위해서는 FlowFlowDelegate 위젯만 설정을 해주면 간단하게 이용 가능합니다.
(어느 정도의 수학적 계산도 조금 필요로 합니다)

1. Flow 설정

위젯 리스트를 설정을 하고 Flow 위젯에 넣어주면 됩니다.
그리고 마지막으로 클릭한 위젯을 알기 위해서 마지막 클릭 변수를 추가했습니다.

  IconData _lastTapped = Icons.notifications;
  final List<IconData> _menuItems = <IconData>[
    Icons.menu,
    Icons.home,
    Icons.new_releases,
    Icons.notifications,
    Icons.settings,
  ];


  // 마지막 클릭 아이콘 업데이트.
  void _updateMenu({required IconData icon}) {
    if (icon != Icons.menu) {
      setState(() => _lastTapped = icon);
    }
  }
  
  
  Widget build(BuildContext context) {
    return Flow(
      children: _menuItems.map<Widget>((IconData icon) => _flowMenuItem(icon)).toList(),
    );
  }

  // 메뉴 버튼.
  Widget _flowMenuItem(IconData icon) {
    return SizedBox(
      width: _buttonSize,
      height: _buttonSize,
      child: FloatingActionButton(
        elevation: 0,
        backgroundColor: _lastTapped == icon ? Colors.amber[700] : Colors.blue,
        onPressed: () {
         _updateMenu(icon: icon);
        },
        child: Icon(
          icon,
          color: Colors.white,
        ),
      ),
    );
  }

2. Animation Controller 설정

AnimationController는 위젯들의 움직임을 실행시키거나 확인하기 위해서 필요로 합니다.
위젯을 클릭을 할 경우, 위젯들의 애니메이션을 실행시킵니다. 반대로 다시 클릭을 할 경우, 애니메이션 효과를 반대로 실행시킵니다.

AnimationController을 생성해 주고, FloatingActionButton을 클릭할 때마다, AnimationController의 방향을 변경 시켜줍니다.

late AnimationController _menuAnimation;


  void initState() {
  super.initState();
    _menuAnimation = AnimationController(
      duration: const Duration(milliseconds: 250),
      vsync: this,
  );
}
  
onPressed: () {
  _updateMenu(icon: icon);
  _menuAnimation.status == AnimationStatus.completed
    ? _menuAnimation.reverse()
    : _menuAnimation.forward();
},

3. FlowDelegate 설정

FlowDelegate만 설정을 해주면 Flow을 위젯을 사용할 수 있습니다.
FlowDelegate는 위젯 리스트들의 움직이는 좌표값을 지정을 해주는 class로 좌표값을 설정하기 위해서 여러 가지 변수값들을 받아옵니다.

class CustomFlowMenuDelegate extends FlowDelegate {
  final Animation<double> menuAnimation;
  final double buttonSize;
  final Size deviceSize;
  final EdgeInsets devicePadding;

  CustomFlowMenuDelegate({
    required this.menuAnimation,
    required this.buttonSize,
    required this.deviceSize,
    required this.devicePadding,
  }) : super(repaint: menuAnimation);
}

FlowDelegate 안에는 shouldRepaintpaintChildren으로 위젯의 이동 위치와 변경사항을 확인할 수 있습니다.

shouldRepaint

shouldRepaint은 Flutter에게 변경 사항이 없을 때 아무것도 없을을 알려주고 있습니다. 이렇게 되면, 위젯은 화면 전체에 해당 지역과 목표 위치에 도달하게 됩니다.
쉬게 말해 전의 애니메이션과 지금의 애니메이션이 동일하다고 하면 false를 반환을 하고, 동일하지 않으면 true를 반환합니다.


bool shouldRepaint(CustomFlowMenuDelegate oldDelegate) {
  return menuAnimation != oldDelegate.menuAnimation;
}

paintChildren

paintChildren은 위젯 리스트에게 각각 위치할 좌표를 알려주는 역할입니다.
index는 위젯 리스트들 중에서 어떤 위젯의 값을 변경할 것인지 위젯을 지정하는 것이고, transform은 지정한 위젯의 변경할 위치값을 지정하는 것입니다.

context.paintChild(index, transform);

_startX와 _starY는 초기 위치를 알려주는 변수입니다. 저는 오른쪽 하단에 배치하기 위해서 deviceSize와 buttonSize 그리고 devicePadding 값을 가지고 와서 초기 위치값을 지정해 주었습니다.
_dx값은 위젯 리스트들이 초기 위치에서 이동할 위치의 값을 나타냅니다.
_dx값을 토대로 _x값과 _y값을 지정해줍니다.


void paintChildren(FlowPaintingContext context) {
  final _startX = deviceSize.width - buttonSize;
  final _startY = deviceSize.height - buttonSize - devicePadding.bottom;
  for (int i = context.childCount - 1; 0 <= i; --i) {
    final _dx = (buttonSize) * i;
    final _x = _startX - (_dx * menuAnimation.value);
    final _y = _startY - (_dx * menuAnimation.value);
    context.paintChild(
      i,
      transform: Matrix4.translationValues(
        _x,
        _y,
        0,
      ),
    );
  }
}

전체 코드

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

void main() => runApp(const FlowApp());

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

  
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        floatingActionButton: FlowMenu(),
        body: Center(
          child: Text('Flow Example'),
        ),
      ),
    );
  }
}

class FlowMenu extends StatefulWidget {
  const FlowMenu({Key? key}) : super(key: key);

  
  State<FlowMenu> createState() => _FlowMenuState();
}

class _FlowMenuState extends State<FlowMenu> with SingleTickerProviderStateMixin {
  // size.
  late Size _size;
  late EdgeInsets _padding;

  late AnimationController _menuAnimation;
  IconData _lastTapped = Icons.notifications;
  final List<IconData> _menuItems = <IconData>[
    Icons.menu,
    Icons.home,
    Icons.new_releases,
    Icons.notifications,
    Icons.settings,
  ];

  // 버튼 크기.
  static const double _buttonSize = 60;

  // 마지막 클릭 아이콘 업데이트.
  void _updateMenu({required IconData icon}) {
    if (icon != Icons.menu) {
      setState(() => _lastTapped = icon);
    }
  }

  
  void initState() {
    super.initState();
    _menuAnimation = AnimationController(
      duration: const Duration(milliseconds: 250),
      vsync: this,
    );
  }

  
  void didChangeDependencies() {
    super.didChangeDependencies();
    _size = MediaQuery.of(context).size;
    _padding = MediaQuery.of(context).padding;
  }

  
  Widget build(BuildContext context) {
    return Flow(
      delegate: CustomFlowMenuDelegate(
        menuAnimation: _menuAnimation,
        buttonSize: _buttonSize,
        deviceSize: _size,
        devicePadding: _padding,
      ),
      children: _menuItems.map<Widget>((IconData icon) => _flowMenuItem(icon)).toList(),
    );
  }

  // 메뉴 버튼.
  Widget _flowMenuItem(IconData icon) {
    return SizedBox(
      width: _buttonSize,
      height: _buttonSize,
      child: FloatingActionButton(
        elevation: 0,
        backgroundColor: _lastTapped == icon ? Colors.amber[700] : Colors.blue,
        onPressed: () {
          _updateMenu(icon: icon);
          _menuAnimation.status == AnimationStatus.completed
              ? _menuAnimation.reverse()
              : _menuAnimation.forward();
        },
        child: Icon(
          icon,
          color: Colors.white,
        ),
      ),
    );
  }
}

class CustomFlowMenuDelegate extends FlowDelegate {
  final Animation<double> menuAnimation;
  final double buttonSize;
  final Size deviceSize;
  final EdgeInsets devicePadding;

  CustomFlowMenuDelegate({
    required this.menuAnimation,
    required this.buttonSize,
    required this.deviceSize,
    required this.devicePadding,
  }) : super(repaint: menuAnimation);

  
  bool shouldRepaint(CustomFlowMenuDelegate oldDelegate) {
    return menuAnimation != oldDelegate.menuAnimation;
  }

  
  void paintChildren(FlowPaintingContext context) {
    final _startX = deviceSize.width - buttonSize;
    final _startY = deviceSize.height - buttonSize - devicePadding.bottom;

    for (int i = context.childCount - 1; 0 <= i; --i) {
      final _dx = (buttonSize) * i;
      final _x = _startX - (_dx * menuAnimation.value);
      final _y = _startY - (_dx * menuAnimation.value);
      context.paintChild(
        i,
        transform: Matrix4.translationValues(
          _x,
          _y,
          0,
        ),
      );
    }
  }
}

자료 출처

Flutter Api - Flow Class
Flutter Youtube - Widget of th Week
How To Use Flow Widget [2021] Control Multiple Animations

profile
Mobile App Developer

0개의 댓글