원하는 캘린더 구현 조건에 부합하는 라이브러리가 많지 않았다,
커스텀이 제한된 부분이 많아서 역시나 맘 편하게 커스텀으로 구현했다.
상세하게 주석을 남겼으니 필요한 분들은 참고를 하시면 좋겠습니다.(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;
}
}
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; // 시작 또는 종료 날짜와 일치하는지 검사
}
}