ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Flutter 버튼 컴포넌트 만들기
    카테고리 없음 2024. 6. 23. 18:56

    Flutter 버튼 컴포넌트 만들기

    들어가기 전

    안녕하세요. 요새 플러터 디자인을 입히느라 정신없는 지스입니다. (주말에도 일을 ㅜㅜㅜㅠㅠ)

    요새 한참 디자인 입히면서 컴포넌트를 만드는 작업을 같이 하고 있는데요. 컴포넌트를 만들면서 제가 어떻게 대응을 했는지 말해보고자 합니다.

    먼저 디자인팀과 저희는 플러터로 만드는걸 미리 얘기 했고, Material Design 3 가이드라인도 같이 한번 보고 얘기를 나눠봤습니다.

    다만 디자인쪽에서는 Material Design 3를 사용한 기본 컴포넌트 보다는 커스텀을 하고 싶어하는 쪽으로 얘기가 기울였고 저희도 최대한 공부를 하고 맞춰드릴 수 있도록 다같이 공부를 했습니다.

    그래서 컴포넌트의 Button, Input TextField, Bottom Sheet, Chip, Date Picker 등 Material Design Component를 쓰는게 아닌 새롭게 구현을 하여 사용하고 있습니다.

    그 중 가장 많이 쓰이는 버튼쪽 부분을 제가 어떻게 구현을 하였는지 한번 보겠습니다.

    Design Guide

     

    먼저 버튼쪽 디자인 가이드를 최대한 비슷하게 만들어 봤습니다.
    보시게 되면 Primary, Secondary 이렇게 나누어져 모양이 다르게 나옵니다.
    그리고 State에 따른 디자인도 다르고, 버튼의 사이즈도 총 4가지로 다르게 나옵니다.

    이 디자인 가이드를 보고 한번 버튼을 만들어 보겠습니다.

    Button

    먼저 State와, Size, Primary Type등 을 정리해 봤습니다. 제일 만만한게 enum 이였던 것 같습니다.

    enum ButtonType {  
      primary,  
      secondary;  
    }  
    
    enum ButtonSizeType {  
      small,  
      medium,  
      large,  
      xLarge;  
    }  

    이런식으로 버튼의 타입과, 사이즈를 나누어 정의를 해두었습니다.

    버튼을 눌렀을 때, 뗏을때 감지를 하기 위해 GestureDetector Widget을 사용하고,

    Tap Down , Tap Up시 부드럽게 색상을 바꾸기 위하여 AnimatedContainer를 사용하였습니다.

    그리고 onTapUp, Down시 각각 색상을 바꿔주어야 하기 때문에

    Stateless 보다 StatefulWidget을 사용하여 setState() 메서드를 사용하게 구현 했습니다.

    class Button extends StatefulWidget {  
      const Button({  
        super.key,  
        required this.onPressed,  
      });  
    
      final VoidCallback onPressed;  
    
      @override  
      State<Button> createState() => _ButtonState();  
    }  
    
    class _ButtonState extends State<Button> {  
      bool _isPressed = false;  
    
      void _onPressed({required bool isPressed}) {  
        if (_isPressed == isPressed) return;  
    
        setState(() => _isPressed = isPressed);  
      }  
    
      @override  
      Widget build(BuildContext context) {  
        return GestureDetector(  
          onTapUp: (_) {  
            _onPressed(isPressed: false);  
            widget.onPressed();  
          },  
          onTapDown: (_) => _onPressed(isPressed: true),  
          onTapCancel: () => _onPressed(isPressed: false),  
          child: AnimatedContainer(  
            duration: const Duration(milliseconds: 200),  
            child: const Text(''),  
          ),  
        );  
      }  
    }

    onTapUp은 손가락을 떼었을때, 호출되는 콜백이니 해당 부분에 콜백 메서드를 넣어두어 외부에서 처리 할 수 있도록 구현했습니다.

    그리고 State 내부에 isPressed bool 값을 넣어 눌렸을때 상태와 안눌렸을때의 상태를 처리할 수 있도록 했습니다.

    그리고 이제 해당 버튼이 어떤 사이즈인지, 어떤 타입인지 알기 위하여 위에서 정의해 두었던 사이즈,타입 열거형을 주입받도록 합니다.

      const Button({  
        super.key,  
        required this.buttonType,  
        required this.sizeType,  
        this.isDisabled = false,  
        this.icon,  
        this.text,  
        this.onPressed,  
      });  
    
    
      final ButtonType buttonType;  
      final ButtonSizeType sizeType;  
      final VoidCallback? onPressed;  
      final String? icon;  
      final String? text;  
      final bool isDisabled;  

    자 이렇게 되면 기본적인 작업은 끝났고, 이제 Button Type에 맞는 색상을 분기처리하고,Size에 맞는 Padding, Font, Hieght를 구현하면 됩니다.
    이때는 가장 편한게 메서드를 만들어 사용하는거나 위에서 정의했던 열거형에 프로퍼티를 두고 쓰는게 가장 편했던 것 같아요..!

    저는 size, type enum은 Containd 버튼에만 쓰이는게 아닌 Flat이나 다른 종류의 버튼에도 쓰일 수 있기 때문에 메서드를 만들어 사용하였습니다.

    버튼안에 들어가 있는 text와 icon은 nullable로 받아서 처리를 해주습니다. 텍스트와 아이콘은 없을 수 있는 경우가 있기 때문이죠.

    @override  
      Widget build(BuildContext context) {  
        return GestureDetector(  
          onTapUp: (_) {  
            _onPressed(isPressed: false);  
            widget.onPressed?.call();  
          },  
          onTapDown: (_) => _onPressed(isPressed: true),  
          onTapCancel: () => _onPressed(isPressed: false),  
          child: AnimatedContainer(  
            duration: const Duration(milliseconds: 200),  
            color: _getBackgroundColor(),  
            height: _getHeight(),  
            decoration: BoxDecoration(borderRadius: _getBorderRadius()),  
            child: Row(  
              mainAxisSize: MainAxisSize.min,  
              mainAxisAlignment: MainAxisAlignment.center,  
              children: [  
                if (widget.icon != null) Image.asset(widget.icon!),  
                if (widget.text != null)  
                  Padding(  
                    padding: _getPadding(),  
                    child: Text(  
                      widget.text ?? '',  
                      style: _getTypograph(  
                        type: widget.buttonType,  
                      ),  
                    ),  
                  ),  
              ],  
            ),  
          ),  
        );

    if문을 사용하여 icon이 null이 아닐때, text가 null이 아닐때 처리를 해주었습니다.

    그리고 typo, padding, height, background color는 메서드로 따로 만들어두었습니다.

    그중 backgroundColor 부분만 한번 봐볼까요?

    Color _getBackgroundColor() {  
         if (widget.isDisabled) {  
          switch (widget.buttonType) {  
            case ButtonType.primary:  
              return Colors.grey.shade400;  
            case ButtonType.secondary:  
              return Colors.white24;  
          }  
        } else {  
          switch (widget.buttonType) {  
            case ButtonType.primary:  
              return _isPressed  
                  ? Colors.black  
                  : Colors.red;  
            case ButtonType.secondary:  
              return _isPressed  
                  ? Colors.black12  
                  : Colors.redAccent;  
          }  
        }  
      }

    disabled 일 때와 아닐때를 분기를 하여 처리하였습니다. 그리고 button Type에 맞는 컬러를 넣어 두고 현재 isPressed 상태인지 아닌지 로 눌렸을때와 아닐때의 컬러를 다르게 주었습니다. (색상은 임의로 넣었습니다)

    자 이제 완성입니다. 이렇게 해두면 button type에 맞는 컴포넌트를 만들 수 있게 되었습니다.

    마지막으로 만약 다른 사람들이 좀 더 편하게 컴포넌트를 사용할 수 있도록 해보겠습니다.

    바로 factory 메서드를 이용하는 것이죠.

    factory Button.primary({  
        required final ButtonSizeType sizeType,  
        VoidCallback? onPressed,  
        String? icon,  
        String? text,  
        bool isDisabled = false,  
      }) =>  
          Button(  
            buttonType: ButtonType.primary,  
            sizeType: sizeType,  
            onPressed: onPressed,  
            icon: icon,  
            text: text,  
            isDisabled: isDisabled,  
          );  
    
      factory Button.secondary({  
        required final ButtonSizeType sizeType,  
        VoidCallback? onPressed,  
        String? icon,  
        String? text,  
        bool isDisabled = false,  
      }) =>  
          Button(  
            buttonType: ButtonType.secondary,  
            sizeType: sizeType,  
            onPressed: onPressed,  
            icon: icon,  
            text: text,  
            isDisabled: isDisabled,  
          );  
    
    
          /// 사용  
          Button.primary(sizeType: ButtonSizeType.small),  
          Button.secondary(sizeType: ButtonSizeType.xLarge),  

    fatory 메서드를 만들어 두면 누군가 사용할때 더 편하게 사용할 수 있던 것 같아요..!
    이렇게 간단하게 커스텀 버튼을 만들어 보았습니다.

    마치며

    처음에는 디자인가이드를 보고 어떻게 만들어야 하지? 했지만 차근차근 하나씩 떼내서 만들어보니 생각보다 그렇게 어려운 작업은 아니였던 것 같아요.
    제일 어려웠던게 저는 제가 만들었으니 어떻게 쓰는법을 알지만, 다른사람들이 사용했을때 편하게 쓸 수 있는 방법을 생각하다 보니 이 부분이 좀 걸렸던 것 같습니다.

    다음에는 다크모드 기능을 추가하면서 어떤 이슈가 있었고, 어떻게 처리했는지 하는 부분으로 돌아오겠습니다 ..! 그전까지 기다려주세요...!

Designed by Tistory.