[Flutter]커스텀 캘린더 피커

임효진·2024년 4월 3일
0

Flutter

목록 보기
15/20
원하는 캘린더 구현 조건에 부합하는 라이브러리가 많지 않았다, 
커스텀이 제한된 부분이 많아서 역시나 맘 편하게 커스텀으로 구현했다.

필요 부분

  1. 현재 월부터 다음 월까지 표시할 것.
  2. 오늘 날짜와 선택된 날짜 구분이 명확할 것
  3. 최대 7일까지 선택 가능하며, 시작일과 종료일을 지정하면 남은 일수는 자동 지정될 것

상세하게 주석을 남겼으니 필요한 분들은 참고를 하시면 좋겠습니다.(GetX기반)

class CustomCalendar extends StatelessWidget {
  final DateTime minDate; // 최소 선택 가능한 날짜
  final DateTime maxDate; // 최대 선택 가능한 날짜
  final TestController controller = Get.find(); // GetX 컨트롤러 인스턴스

  CustomCalendar({super.key, required this.minDate, required this.maxDate});

  
  Widget build(BuildContext context) {
    // 세로 스크롤을 위해 ListView.builder 사용
    return Expanded(
      child: ListView.builder(
        itemCount: (maxDate.year - minDate.year) * 12 +
            maxDate.month -
            minDate.month +
            1,
        itemBuilder: (context, index) {
          int year = minDate.year + (minDate.month + index - 1) ~/ 12;
          int month = (minDate.month + index - 1) % 12 + 1;
          // 각 달의 연도와 월을 표시하는 헤더와 달력 뷰를 생성
          return Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Padding(
                padding: const EdgeInsets.only(top: 20.0, bottom: 12.0),
                // 연도와 월 표시
                child: Text('$year년 $month월',
                    style: const TextStyle(
                        fontSize: 16, fontWeight: FontWeight.bold)),
              ),
              buildMonthView(year, month), // 달력 뷰 생성
            ],
          );
        },
      ),
    );
  }

  // 주어진 연도와 월에 대한 달력 뷰를 구성하는 메서드
   Widget buildMonthView(int year, int month) {
    List<Widget> dayWidgets = [];
    DateTime firstDayOfMonth = DateTime(year, month, 1);
    int daysInMonth = DateTime(year, month + 1, 0).day;

    // 일요일을 주의 시작으로 고려하여 인덱스 조정
    int weekdayOfFirstDay = firstDayOfMonth.weekday;
    // Dart의 DateTime에서 일요일은 7, 하지만 배열에서는 0으로 시작해야 함.
    int firstDayIndex = (weekdayOfFirstDay % 7);

    // 날짜가 시작되기 전의 빈 칸을 추가
    for (int i = 0; i < firstDayIndex; i++) {
      dayWidgets.add(Container()); // 빈 칸을 위한 컨테이너
    }

    // 달력의 날짜 채우기
    for (int day = 1; day <= daysInMonth; day++) {
      DateTime currentDay = DateTime(year, month, day);
      dayWidgets.add(_DayCell(
        controller: controller,
        day: day,
        currentDay: currentDay,
        maxDate: maxDate,
      ));
    }

    return GridView.count(
      crossAxisCount: 7,
      physics: const NeverScrollableScrollPhysics(),
      shrinkWrap: true,
      children: dayWidgets,
    );
  }
}

// 달력의 각 날짜 셀을 나타내는 위젯
class _DayCell extends StatelessWidget {
  final HomeWorkController controller; // GetX 컨트롤러
  final int day; // 날짜 번호
  final DateTime currentDay; // 현재 날짜 객체
  final DateTime maxDate; // 최대 선택 가능한 날짜

  const _DayCell({
    required this.controller,
    required this.day,
    required this.currentDay,
    required this.maxDate,
  });

  
  Widget build(BuildContext context) {
    // 오늘 날짜와 선택 가능 여부 확인
    bool isToday = DateTime.now().isSameDate(currentDay);
    bool isSelectable =
        currentDay.isAfter(DateTime.now()) && currentDay.isBefore(maxDate);

    // 날짜 선택 상태와 시작/종료 여부에 따라 UI 변경
    return Obx(() {
      bool isSelected = controller.isSelected(currentDay); // 선택된 날짜인지
      bool isStartOrEnd = controller.isStartOrEnd(currentDay); // 시작 또는 종료 날짜인지

      // 날짜 셀의 UI 구성
      return GestureDetector(
        onTap: () => controller.selectDate(currentDay), // 날짜 선택 이벤트 처리
        child: Container(
          margin: const EdgeInsets.all(4.0),
          decoration: BoxDecoration(
            color: isSelected
                ? (isStartOrEnd ? Colors.deepPurple : Colors.purple[200])
                : Colors.white,
            shape: BoxShape.circle,
            border: isToday
                ? Border.all(color: Colors.purple, width: 1)
                : null, // 오늘 날짜에는 보더 적용
          ),
          child: Center(
            child: Text(
              day.toString(), // 날짜 번호 표시
              style: TextStyle(
                fontWeight: isStartOrEnd ? FontWeight.bold : FontWeight.normal,
                color:
                    isSelected ? Colors.white : Colors.black, // 선택된 날짜는 흰색 텍스트
              ),
            ),
          ),
        ),
      );
    });
  }
}

// DateTime 확장 메서드 - 날짜만 비교
extension DateOnlyCompare on DateTime {
  bool isSameDate(DateTime other) {
    return year == other.year && month == other.month && day == other.day;
  }
}

Controller 부분

class TestController extends GetxController {
  var startDate = Rxn<DateTime>();
  var endDate = Rxn<DateTime>();
  var selectedDates = <DateTime>[].obs; // 선택된 날짜 목록을 관리하는 observable 리스트

  DateTime? minDate; // 선택 가능한 최소 날짜
  DateTime? maxDate; // 선택 가능한 최대 날짜

  // 날짜 선택 메소드
  void selectDate(DateTime date) {
    // 첫 번째 날짜 선택 로직
    // 만약 시작 날짜가 없거나 종료 날짜가 이미 설정되어 있다면, 새로운 시작 날짜로 설정
    if (selectedStartDate.value == null || selectedEndDate.value != null) {
      selectedStartDate.value = date;
      selectedEndDate.value = null; // 종료 날짜 초기화
      updateSelectedDates(); // 선택된 날짜 리스트 업데이트
      return; // 함수 종료
    }

    // 두 번째 날짜 선택 로직
    // 만약 시작 날짜가 이미 있고, 종료 날짜가 아직 설정되지 않았다면,
    if (selectedStartDate.value != null) {
      int difference = date.difference(selectedStartDate.value!).inDays;

      // 선택된 기간이 7일을 초과하지 않도록 검사
      if (difference >= 0) {
        selectedEndDate.value = (difference > 6)
            ? selectedStartDate.value!.add(const Duration(days: 6))
            : date;
      } else {
        // 선택한 날짜가 시작 날짜보다 이전인 경우, 새로운 시작 날짜로 설정
        selectedStartDate.value = date;
        selectedEndDate.value = null; // 종료 날짜 재설정을 위해 null로 설정
      }
      updateSelectedDates(); // 선택된 날짜 리스트 업데이트
    }
  }

  // 선택된 날짜 리스트 업데이트 메소드
  void updateSelectedDates() {
    selectedDates.clear(); // 기존 리스트 초기화
    if (selectedStartDate.value != null) {
      DateTime currentDate = selectedStartDate.value!;
      DateTime endDate = selectedEndDate.value ?? currentDate; // endDate가 null이면 currentDate를 사용
      while (currentDate.isBefore(endDate.add(const Duration(days: 1)))) {
        selectedDates.add(currentDate);
        currentDate = currentDate.add(const Duration(days: 1)); // 다음 날짜로 이동
      }
    }
  }

  // 특정 날짜가 선택되었는지 여부를 반환하는 메소드
  bool isSelected(DateTime date) {
    return selectedDates.contains(date); // 해당 날짜가 selectedDates 리스트에 포함되어 있는지 검사
  }

  // 특정 날짜가 시작 또는 종료 날짜인지 여부를 반환하는 메소드
  bool isStartOrEnd(DateTime date) {
    return date == selectedStartDate.value || date == selectedEndDate.value; // 시작 또는 종료 날짜와 일치하는지 검사
  }
}

구현 이미지

profile
네트워크 엔지니어에서 풀스택 개발자로

0개의 댓글