[Flutter] Date Picker(날짜 선택기) 만들기

Tyger·2023년 3월 25일
1

Flutter

목록 보기
33/64

Date Picker(날짜 선택기) 만들기

이번에는 Date Picker를 직접 만들어 보려고 한다.

날짜 선택기는 Flutter에서도 Material / Cupertino 스타일에 맞게 각각 제공해주고 있다. 그 외에도 많은 라이브러리에서 Localization이 포함된 날짝 선택기를 제공하고 있다.

개인적으로 심플한 날짜 선택기를 사용하고 싶을 때가 있다. 라이브러리를 사용하기는 싫고 그렇다고 기본 달력을 사용하기에는 디자인이 별로고.. 물론 날짜 선택기 테마를 지정하여 사용할 수는 있지만, 그래도 원하는 디자인을 완벽하게 커스텀하기는 쉽지 않았다. 그래서 한 번 만들어 보기로 하였다.

IOS의 달력 스타일로 Cupertino Date Picker와 유사하게 만들어 보았고, 제공되는 코드는 그대로 사용 가능하도록 상태 관리 라이브러리도 사용하지 않았다.

Flutter

먼저 어떤 구조로 개발을 해야 하는지에 대해서 먼저 살펴보자. Cupertino 스타일의 날짜 선택기를 개발하기 위해서는 Vertical ListView가 필요하면서, 해당 아이템의 스크롤이 멈추는 구간이 각 아이템에 맞춰서 정확히 포지션이 멈춰야 한다. 또한 날짜 선택기를 실행 했을 때 포지션이 현재 날짜로 맞춰져야 한다.

UI 부분 외에도 날짜를 계산해야 한다. 윤년도 있고, 요일 수도 월 마다 다르기에 이 조건을 충족하는 로직이 필요할 것이다.

천천히 개발하면서 자세히 살펴보도록 하자.

UI

ListView를 사용하면서 아이템에 맞는 스크롤 포커싱을 해결하기 위해 ListWheelScrollView를 사용하기로 하고, physics를 FixedExtentScrollPhysics로 주어 스크롤 포지션이 멈추는 픽셀이 아이템에 맞출 수 있도록 해주었다.
ScrollView의 controller로 FixedExtentScrollController 사용하여 초기 날짜 세팅 및 일자가 변경될 떄에 스크롤 포지션을 이동시켜 주었다.

먼저 Wrap을 사용해서 년/월/일에 해당하는 ListWheelScrollView를 배치하였다.

Wrap(
	children: [
		_pickerForm(
			controller: _yearController,
            date: _year,
            dateIndex: 0,
            current: value.substring(0, 4)),
		_pickerForm(
			controller: _monthController,
			date: _month,
			dateIndex: 1,
			current: value.substring(4, 6)),
		_pickerForm(
			controller: _dayController,		
            date: _day,
            dateIndex: 2,
            current: value.substring(6, 8)),
		],
	),

pickerForm이다. perspective 값을 최소화로 해서 ListWheel의 모양이 평면이 되도록 해주었다.

SizedBox _pickerForm({
    required List<String> date,
    required int dateIndex,
    required String current,
    required FixedExtentScrollController controller,
  }) {
    return SizedBox(
      height: _itemSize.height,
      width: _itemSize.width,
      child: Padding(
        padding: const EdgeInsets.symmetric(vertical: 12),
        child: ListWheelScrollView(
          controller: controller,
          onSelectedItemChanged: (int i) {
            HapticFeedback.lightImpact();
            _changedDate(dateIndex, i);
            widget.onChanged(currentDate.value);
          },
          squeeze: 0.6,
          perspective: 0.00001,
          physics: const FixedExtentScrollPhysics(),
          itemExtent: 30,
          children: [
            ...List.generate(
                date.length,
                (index) => Text(
                      date[index],
                      style: TextStyle(
                        fontWeight: FontWeight.bold,
                        fontSize: date[index] == current ? 12 : 11,
                        color: date[index] == current
                            ? Colors.white
                            : Colors.white54,
                      ),
                    )),
          ],
        ),
      ),
    );
  }

초기 진입시 현재 날짜에 맞는 포지션으로 스크롤이 이동될 수 있도록 controller를 사용하여 스크롤을 이동시켜 주고 있다.

late FixedExtentScrollController _yearController;
late FixedExtentScrollController _monthController;
late FixedExtentScrollController _dayController;


  void initState() {
    int _initialYear = _year.indexOf(currentDate.value.substring(0, 4));
    int _initialMonth = _month.indexOf(currentDate.value.substring(4, 6));
    int _initialDay = _day.indexOf(currentDate.value.substring(6, 8));
    _yearController = FixedExtentScrollController(initialItem: _initialYear);
    _monthController = FixedExtentScrollController(initialItem: _initialMonth);
    _dayController = FixedExtentScrollController(initialItem: _initialDay);
    super.initState();
  }

DateTime

날짜 선택기의 날짜를 세팅해주기 위해 year, month, day 리스트를 사용하였다.

 List<String> _year = [];
final List<String> _month = List.generate(
      12, (index) => index < 9 ? "0${index + 1}" : "${index + 1}");
List<String> _day = [];

interval은 옵션 값으로 현재 연도를 기준으로 몇 년치를 세팅해줄 것인지를 결정하는 값이다. 해당 interval 값을 기준으로 연도를 세팅주어 year 변수에 넣어준다.

 _year = List.generate(
        widget.interval == null || widget.interval == 0
            ? 100
            : widget.interval!,
        (index) => widget.interval == null || widget.interval! < 1
            ? "${index + (_currentYear - 50)}"
            : "${index + (_currentYear - (widget.interval == 1 ? 0 : widget.interval! ~/ 2))}");

날짜가 변경될 때에 호출되는 기능으로 dateType 값은 년/월/일의 타입이고, index는 선택되어진 리스트의 index 값이다.

날짜가 변경될 때마다 currentDate값을 변경해주고 있는데, 연도를 변경했을 때에 2월을 선택하게 되면 윤년 계산을 필요로하는 로직을 실행시키고, 월을 변경시키면 일자를 담고있는 day 리스트가 변경되어야 하기에 일자도 함께 세팅해 주어야 한다.

void _changedDate(int dateType, int index) {
    String _currentYear = currentDate.value.substring(0, 4);
    String _currentMonth = currentDate.value.substring(4, 6);
    String _currentDay = currentDate.value.substring(6, 8);

    switch (dateType) {
      case 0:
        currentDate.value = _year[index] + _currentMonth + _currentDay;
        if (_currentMonth == "02") {
          _leapYearChecked();
        }
        break;
      case 1:
        currentDate.value = _currentYear + _month[index] + _currentDay;
        _daySetting(false);
        break;
      case 2:
        currentDate.value = _currentYear + _currentMonth + _day[index];
        break;
      default:
    }
  }

월이 변경될 때마다 day 리스트 값을 변경시키는 로직이다. 월에 따라 30일인 경우가 있고 31일인 경우가 있을 수 있으며, 2월은 28/29일 일수도 있다.

void _daySetting(bool initial) {
    int _month = int.parse(currentDate.value.substring(4, 6));
    List _thiryFirst = [1, 3, 5, 7, 8, 10, 12];
    int _selectedDayItem = !initial ? _dayController.selectedItem : 0;
    if (_thiryFirst.contains(_month)) {
      _day = List.generate(
          31, (index) => index < 9 ? "0${index + 1}" : "${index + 1}");
    } else if (_month == 2) {
      _leapYearChecked();
      if (_day.length <= _selectedDayItem) {
        _dayController.jumpToItem(_day.length - 1);
      }
    } else {
      _day = List.generate(
          30, (index) => index < 9 ? "0${index + 1}" : "${index + 1}");
      if (_selectedDayItem == 30) {
        _dayController.jumpToItem(29);
      }
    }
  }

윤년을 계산하는 로직이다. 윤년에는 조건들이 있는데, 연도를 4로 나눈 몫이 0이면 윤년이며, 해당 조건을 만족하더라도 100으로 나눈 몫이 0이면 평년이다. 그런데, 100으로 나눈 몫이 0이더라도, 400으로 나눈 몫이 0이면 유년이다.

윤년

  • 연도를 4로 나눈 몫이 0일 때.
  • 연도를 4로 나눈 몫이 0이면서, 100으로 나눈 몫이 0이 아닐 때.
  • 연도를 4로 나눈 몫이 0이면서, 100으로 나눈 몫이 0이 아닐 때에 400으로 나눈 몫이 0일 때.
void _leapYearChecked() {
    int _dayLength = 0;
    int _year = int.parse(currentDate.value.substring(0, 4));
	if (((_year % 4 == 0) && (_year % 100 != 0)) || (_year % 400 == 0)) {
      _dayLength = 29;
    } else {
      _dayLength = 28;
    }
    _day = List.generate(
        _dayLength, (index) => index < 9 ? "0${index + 1}" : "${index + 1}");
  }

if-else문을 아래와 같이 써도 결과는 동일하다.

if (_year % 4 == 0) {
       if (_year % 100 == 0) {
         if (_year % 400 == 0) {
           _dayLength = 29;
         } else {
           _dayLength = 28;
   	     }
         _dayLength = 28;
       } else {
         _dayLength = 29;
       }
     } else {
       _dayLength = 28;
     }

Result

Git

https://github.com/boglbbogl/flutter_velog_sample/tree/main/lib/app/date_picker

마무리

간단한 날짜 선택기를 만들어 보았는데, 윤년 계산하는 로직만 제대로 만들어주면, 달력 만드는 방법은 생각만큼 어렵지 않은 기능이다.

Git 저장소에 공유한 코드를 보고 참고하셔서 자신만의 UI로 만든 달력을 하나씩은 꼭 개발해보자 !

profile
Flutter Developer

0개의 댓글