Flutter Flow Widget

flunge·2021년 12월 14일
0

Flutter

목록 보기
2/13

메인 화면에 FloatingActionButton을 이용해 "알람 추가" 기능을 구현 했는데 앱에 다른 기능이 추가 될 경우를 생각해 확장성이 더 좋다고 생각되는 Flow위젯을 쓰게 됐다.

준비

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

  @override
  _FloatingFlowButtonState createState() => _FloatingFlowButtonState();
}

class _FloatingFlowButtonState extends State<FloatingFlowButton> with TickerProviderStateMixin{

  late AnimationController menuAnimation;
  
  final menuItems = <IconData>[
    Icons.home,
    Icons.new_releases,
    Icons.notifications,
    Icons.settings,
    Icons.menu,
  ];


  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Flow(
      delegate: FloatingFlowButtonDelegate(),
      children: menuItems.map<Widget>((IconData iconData) => buildChild(iconData)).toList(),
    );
  }
}
  1. delegate속성은 required라 필수 이다. Flow의 children으로 만들고 싶은 Icon리스트를 우선 생성하고 Flow의 children속성의 값으로 준다.
  2. menuItems가 List이기 때문에 .map()을 호출한 후 마지막에 toList()를 호출한다.
  3. 또한 애니메이션이 필요하기 때문에
    TickerProviderStateMixin을 명시한다.
class FloatingFlowButtonDelegate extends FlowDelegate{

  final Animation<double> menuAnimation;
  FloatingFlowButtonDelegate({required this.menuAnimation});

  @override
  void paintChildren(FlowPaintingContext context) {
    // TODO: implement paintChildren
  }

  @override
  bool shouldRepaint(covariant FlowDelegate oldDelegate) {
    // TODO: implement shouldRepaint
    throw UnimplementedError();
  }
  
}

별도의 클래스를 만들고 FlowDelegate를 상속받는다.

@override
  void initState() {
    super.initState();

    menuAnimation = AnimationController(
      duration: const Duration(milliseconds: 500),
      vsync: this
    );
  }

menuAnimation변수를 생성하고 initState()에서 초기화 한다.

구현

Widget buildChild(IconData icon){

  return Container(
    child: RawMaterialButton(
      fillColor: Colors.blue,
      shape: const CircleBorder(),
      constraints: BoxConstraints.tight(Size(50,50)),
      onPressed: (){
        print('onpress ${menuAnimation.status}');
        menuAnimation.status == AnimationStatus.dismissed 
          ? menuAnimation.forward()
          : menuAnimation.reverse();
      },
      child: Icon(
        icon,
        size: 20,
      ),
    ),
  );
}

constraints속성에 주는 값이 각각의 버튼의 크기이다.
공식 문서나 다른 글에서 AnimationStatus.completed로 되어 있는 경우가 있는데 초기 상태를 찍어보니 dismissed여서 dismissed로 설정 했다. 작동하지 않는다면 상태를 확인해보고 바꾸면 된다.

@override
void paintChildren(FlowPaintingContext context) {

  double x = context.size.width - context.getChildSize(0)!.width;
  double y = context.size.height - context.getChildSize(0)!.height;

  for(int i = context.childCount-1 ; i>=0; i--){
    Size? buttonSize = context.getChildSize(i);  
    double dx = x;
    double dy = buttonSize!.height * i;
    

    context.paintChild(
      i,
      transform: Matrix4.translationValues(
        dx, 
        y - dy*menuAnimation.value,
        0
      )
    );
  }
}

FloatingFlowButtonDelegate의 paintChildren()메소드
처음 2줄이 중요한데 Flow위젯은

Positioned(
  bottom: 0,
  right: 0,
  child: FloatingFlowButton()
)

이런식으로 사용하면 에러가난다. paintChildren에서 위젯의 위치를 지정해야한다.

그 다음 for문이 ++이 아닌 --인데 이유는 Flow의 children에 들어가는 아이콘들이 그려지는 순서대로 쌓이기 때문이다. 그렇기 때문에 첫 번째 아이콘을 마지막으로 그리도록 해야해서 역순이다.

여기서 한번 정리하고 가자면
1. paintChildren안에서 세부 위치를 지정해야 한다.
2. 그려지는 순서대로 위젯이 쌓이게 된다.

@override
bool shouldRepaint(FloatingFlowButtonDelegate oldDelegate) {
  return menuAnimation != oldDelegate.menuAnimation;
}

마지막으로 shouldRepaint 구현
자동 완성으로 메소드를 만들면 메소드의 인자가 FlowDelegate이라서 oldDelegate에 menuAnimation이 없기 때문에 바꿔줘야한다.

전체 코드

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

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

  @override
  _FloatingFlowButtonState createState() => _FloatingFlowButtonState();
}

class _FloatingFlowButtonState extends State<FloatingFlowButton> with TickerProviderStateMixin{

  late AnimationController menuAnimation;

  final menuItems = <IconData>[
    Icons.home,
    Icons.notifications,
    Icons.settings,
    Icons.menu,
  ];

  @override
  void initState() {
    super.initState();

    menuAnimation = AnimationController(
      duration: const Duration(milliseconds: 1000),

      vsync: this
    );
  }

  Widget buildChild(IconData icon){
    final widgetSizePerWidth = MediaQuery.of(context).size.width/menuItems.length;

    return Container(
      // color: Colors.black,
      child: RawMaterialButton(
        fillColor: Colors.blue,
        shape: const CircleBorder(),
        // constraints: BoxConstraints.tight(Size(widgetSizePerWidth,widgetSizePerWidth)),
        constraints: BoxConstraints.tight(Size(50,50)),
        onPressed: (){
          print('onpress ${menuAnimation.status}');
          menuAnimation.status == AnimationStatus.dismissed 
            ? menuAnimation.forward()
            : menuAnimation.reverse();
        },
        child: Icon(
          icon,
          size: 20,
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    // TODO: implement build

    return Flow(
      delegate: FloatingFlowButtonDelegate(menuAnimation: menuAnimation),
      children: menuItems.map<Widget>((IconData iconData) => buildChild(iconData)).toList(),
    );
  }

  @override
  void dispose() {
    // TODO: implement dispose
    super.dispose();
    menuAnimation.dispose();
  }
}


class FloatingFlowButtonDelegate extends FlowDelegate{

  final Animation<double> menuAnimation;
  FloatingFlowButtonDelegate({required this.menuAnimation}):super(repaint: menuAnimation);

  @override
  void paintChildren(FlowPaintingContext context) {

    double x = context.size.width - context.getChildSize(0)!.width;
    double y = context.size.height - context.getChildSize(0)!.height;

    // for(int i = 0; i<context.childCount; i++){
    for(int i = context.childCount-1 ; i>=0; i--){
      Size? buttonSize = context.getChildSize(i);  
      double dx = x;
      double dy = buttonSize!.height * i;
      

      context.paintChild(
        i,
        transform: Matrix4.translationValues(
          dx, 
          y - dy*menuAnimation.value,
          0
        )
      );
    }
  }

  @override
  bool shouldRepaint(FloatingFlowButtonDelegate oldDelegate) {
    return menuAnimation != oldDelegate.menuAnimation;
  }
}

0개의 댓글