드롭 다운 버튼

이건선·2025년 1월 19일
0

Flutter

목록 보기
28/30

소개

아래 조건을 만족하는 드롭 다운 버튼

  1. 'DropDownModel' 수 만큼 위젯이 생성 될 것,
  2. 드롭 다운 버튼이 여러개 있을 때, 그 중 하나만 열려있는 상태가 될 것
  3. 열리고 닫히는 애니메이션이 있을 것
  4. 선택 값이 색으로 표현 될 것
  5. 'FormState' 요구하는 다른 위젯에 포함 될 수 있을 것
  6. 필수가 아닌 값은 '빈값 검증' 제외 될 것

목적

  • 쇼핑몰 물건 구매 할 때, "옵션 드롭 다운 위젯" 구현


class DropDownModel {
  final String label;
  final List<String> content;
  String? selectValue;
  final bool? selectOption;

  DropDownModel({required this.label, required this.content, this.selectValue, this.selectOption});

  bool get getOk => selectValue != null;

  Map<String , dynamic> toJson () => {
    "label" : label,
    "content" : content,
    "selectValue" : selectValue,
  };
}

1. Base FormField Widget


class BaseDropdown extends FormField<DropDownModel> {
  BaseDropdown({
    super.key,
    required DropDownModel item,
    super.initialValue,
    String? hint,
    TextStyle? textStyle,
    BoxDecoration? dropdownDecoration,
    BoxDecoration? buttonDecoration,
    BoxDecoration? selectDecoration,
    super.onSaved,
    super.validator,
    ValueChanged<DropDownModel?>? onChanged,
    required void Function(bool) btnOpen,
    required bool currentBtn,
  }) : super(
    autovalidateMode: AutovalidateMode.onUserInteraction,
    builder: (FormFieldState<DropDownModel> state) {
      return _CustomDropdownStateful(
        item: item,
        items: item.content,
        label: item.label,
        value: state.value?.selectValue,
        hint: hint,
        textStyle: textStyle,
        dropdownDecoration: dropdownDecoration,
        buttonDecoration: buttonDecoration,
        onChanged: (DropDownModel? newValue) {
          state.didChange(newValue);
          onChanged?.call(newValue);
        },
        errorText: state.errorText,
        btnOpen: btnOpen,
        currentBtn: currentBtn,
        selectDecoration: selectDecoration,
      );
    },
  );
}

class _CustomDropdownStateful extends StatefulWidget {
  final List<String> items;
  final DropDownModel item;
  final String? value;
  final String? hint;
  final TextStyle? textStyle;
  final BoxDecoration? dropdownDecoration;
  final BoxDecoration? buttonDecoration;
  final BoxDecoration? selectDecoration;
  final ValueChanged<DropDownModel?>? onChanged;
  final String? errorText;
  final void Function(bool) btnOpen;
  final bool currentBtn;
  final String label;

  const _CustomDropdownStateful({
    super.key,
    required this.items,
    required this.item,
    this.value,
    this.hint,
    this.textStyle,
    this.selectDecoration,
    this.dropdownDecoration,
    this.buttonDecoration,
    this.onChanged,
    this.errorText,
    required this.btnOpen,
    required this.currentBtn,
    required this.label,
  });

  
  State<_CustomDropdownStateful> createState() => _CustomDropdownStatefulState();
}

class _CustomDropdownStatefulState extends State<_CustomDropdownStateful>
    with SingleTickerProviderStateMixin {
  bool isDropdownOpen = false;
  late String? _selectValue;

  late Animation<double> animation;
  late AnimationController _controller;
  final Tween<double> _tween = Tween<double>(begin: 0, end: 1);

  
  void initState() {
    super.initState();
    _selectValue = widget.value;
    isDropdownOpen = widget.currentBtn;

    _controller =
        AnimationController(vsync: this, duration: const Duration(milliseconds: 300));
    animation = _tween.animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
    );
  }

  
  void didUpdateWidget(covariant _CustomDropdownStateful oldWidget) {
    // TODO: implement didUpdateWidget
    super.didUpdateWidget(oldWidget);
    if(oldWidget.currentBtn != widget.currentBtn) {
      setState(() {
        isDropdownOpen = widget.currentBtn;
        if (isDropdownOpen) {
          _controller.forward();
        } else {
          _controller.reverse();
        }
      });
    }
  }

  
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _toggleDropdown() {
    setState(() {
      if (isDropdownOpen) {
        _controller.reverse();
      } else {
        _controller.forward();
      }
      isDropdownOpen = !isDropdownOpen;
      widget.btnOpen(isDropdownOpen);
    });
  }

  
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      mainAxisSize: MainAxisSize.min,
      children: [
        Text(
          widget.label,
          style: widget.textStyle ?? const TextStyle(fontSize: 16),
        ),
        GestureDetector(
          onTap: _toggleDropdown,
          child: Container(
            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
            decoration: widget.buttonDecoration ??
                BoxDecoration(
                  border: Border.all(color: Colors.grey),
                  borderRadius: BorderRadius.circular(8),
                ),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text(
                  _selectValue?.toString() ?? widget.hint ?? 'Select an item',
                  style: widget.textStyle ?? const TextStyle(fontSize: 16),
                ),
                Icon(
                  isDropdownOpen
                      ? Icons.arrow_drop_up
                      : Icons.arrow_drop_down,
                  color: Colors.grey,
                ),
              ],
            ),
          ),
        ),
        if (widget.errorText != null)
          Padding(
            padding: const EdgeInsets.only(top: 8),
            child: Text(
              widget.errorText!,
              style: const TextStyle(color: Colors.red, fontSize: 12),
            ),
          ),
        AnimatedBuilder(
          animation: animation,
          builder: (context, child) {
            if (animation.value <= 0) {
              return SizedBox.shrink();
            }
            return ClipRect(
              child: Align(
                heightFactor: animation.value,
                child: Opacity(
                  opacity: animation.value,
                  child: Container(
                    margin: const EdgeInsets.only(top: 8),
                    decoration: widget.dropdownDecoration ??
                        BoxDecoration(
                          border: Border.all(color: Colors.grey),
                          borderRadius: BorderRadius.circular(8),
                          color: Colors.white,
                        ),
                    child: ListView(
                      shrinkWrap: true,
                      padding: EdgeInsets.zero,
                      children: widget.items.map((String item) {
                        final bool isSelected = item == _selectValue;

                        return GestureDetector(
                          behavior: HitTestBehavior.translucent,
                          onTap: () {
                            setState(() {
                              _selectValue = item;
                              widget.item.selectValue = _selectValue;
                            });
                            widget.onChanged?.call(widget.item);
                            _toggleDropdown();
                          },
                          child: Container(
                            decoration: isSelected ? widget.selectDecoration : widget.selectDecoration?.copyWith(color: Colors.transparent),
                            padding: const EdgeInsets.symmetric(
                                horizontal: 16, vertical: 12),
                            child: Text(
                              item.toString(),
                              style: widget.textStyle ?? const TextStyle(fontSize: 16),
                            ),
                          ),
                        );
                      }).toList(),
                    ),
                  ),
                ),
              ),
            );
          },
        ),
      ],
    );
  }
}

2. DropDownCollectionWidget



class DropDownCollectionWidget extends StatefulWidget {
  final List<DropDownModel> children;
  final FormFieldSetter<DropDownModel>? onSaved;
  final GlobalKey<FormState>? singleFormStateKey;
  final FormFieldValidator<DropDownModel>? validator;
  final BoxDecoration? buttonDecoration;
  final BoxDecoration? dropdownDecoration;
  final BoxDecoration? selectDecoration;
  const DropDownCollectionWidget({
    super.key,
    this.selectDecoration,
    this.buttonDecoration,
    this.singleFormStateKey,
    this.dropdownDecoration,
    required this.onSaved,
    required this.validator,
    required this.children,
  });

  
  State<DropDownCollectionWidget> createState() => _DropDownCollectionWidgetState();
}

class _DropDownCollectionWidgetState extends State<DropDownCollectionWidget> {
  late List<bool> _isDropdownOpen;

  
  void initState() {
    super.initState();
    _isDropdownOpen = List<bool>.filled(widget.children.length, false, growable: false);
  }

  void _toggleDropdown(int index) {
    setState(() {
      for (int i = 0; i < _isDropdownOpen.length; i++) {
        _isDropdownOpen[i] = i == index ? !_isDropdownOpen[i] : false;
      }
    });
  }

  
  Widget build(BuildContext context) {
    return  ListView.builder(
      shrinkWrap: true,
      itemCount: widget.children.length,
      itemBuilder: (context, index) {

        DropDownModel _dropModel = widget.children[index];
        bool _isSelectOption = _dropModel.selectOption == true;
        String _hint = _isSelectOption ? "옵션을 선택해 주세요 (선택)" : '옵션을 선택해 주세요.';

        return Padding(
          padding: const EdgeInsets.all(16.0),
          child: BaseDropdown(
            key: widget.key,
            item: _dropModel,
            hint: _hint,
            btnOpen: (bool value) {
              _toggleDropdown(index);
            },
            initialValue: _dropModel,
            currentBtn: _isDropdownOpen[index],
            validator: (DropDownModel? value) {
              if(value?.selectOption == true) {
                return widget.validator?.call(value);

              } else {
                if(value?.selectValue == null) return "값을 입력해 주세요.";
                return widget.validator?.call(value);

              }
            } ,
            onSaved: widget.onSaved,
            buttonDecoration: widget.buttonDecoration ?? BoxDecoration(
              color: Colors.blue.shade100,
              borderRadius: BorderRadius.circular(8),
            ),
            selectDecoration: widget.selectDecoration ?? BoxDecoration(
              color: Colors.blue.shade100,
              borderRadius: BorderRadius.circular(8),
            ),
            dropdownDecoration: widget.dropdownDecoration ?? BoxDecoration(
              color: Colors.blue.shade50,
              borderRadius: BorderRadius.circular(8),
            ),
          ),
        );
      },
    );
  }
}

사용



  final GlobalKey<FormState> _key = GlobalKey<FormState>();
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Focus Node Fix Example')),
      body: Form(
        key: _key,
        child: Column(
          children: [
            DropDownCollectionWidget(
              dropdownDecoration: BoxDecoration(
                color: Colors.redAccent,
                  borderRadius: BorderRadius.circular(8)
              ),
              selectDecoration: BoxDecoration(
                color: Colors.blueAccent,
                borderRadius: BorderRadius.circular(8)
              ),
              children: [
                DropDownModel(label: "third", content: ["없음","third option 1", "third option 2", "third option 3"]),
                DropDownModel(label: "first", content: ["없음","first option 1", "first option 2", "first option 3", "first option 4"]),
                DropDownModel(label: "sec", content: ["없음","sec option 1", "sec option 2"], selectOption: true),
                DropDownModel(label: "fou", content: ["없음","fou option 1"]),
              ],
              validator: (DropDownModel? value) {

                if(value?.selectValue == "없음") return '없음 오류 발생';

                return null;
              },
              onSaved: (value) {
                print("value ---> ${value?.toJson()}");
              },
            ),
            TextFormField(
              validator: (String? value) {

                if(value == null || value.isEmpty) {
                  return 'null 오류 발생';
                }
              },
            ),
            ElevatedButton(onPressed: () {
              print(_key.currentState?.validate());
            }, child: Text("validate")),
            ElevatedButton(onPressed: () {
              _key.currentState?.save();
            }, child: Text("save")),
            ElevatedButton(onPressed: () {
              _key.currentState?.reset();
            }, child: Text("reset")),
          ],
        ),
      )
    );
  }

profile
멋지게 기록하자

0개의 댓글