본문 바로가기

IT/안드로이드 관련

[안드로이드] 안드로이드 커스텀 달력 예제 (Android Custom CalendarView Example)

안녕하세요 YTS 입니다. 

오늘은 제가 만든 Custom Calendar View에 대해 적어보려합니다.


기본적으로 Android 에서 제공하는 달력은 한계점이 많고 각 날짜에 꾸밈을 할 수없기 때문에 불편한점이 많습니다.


저의 방법은 RecyclerView를 이용하여 만든 방법입니다!

참고로 해당 방법을 이용하기위해선 RecyclerView와 RecyclerView.Adapter에 대한 사전지식이 조금 필요합니다.


1. RecyclerView에 ViewType 설정


2. StaggeredGridLayoutManager에 대한 Span 설정 ( 이 부분은 쉽습니다. )




사전 지식을 아는 개발자라면 쉽게 따라 하실 수 있도록 가이드하겠습니다.




1. Xml에 RecyclerView를 선언한다.


<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">


<!--헤더-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:background="@color/colorPrimary"
android:orientation="horizontal">

<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="@{(v)->model.startPicker(v)}"
android:padding="16dp"
android:text="@{model.mTitle}"
android:textSize="18sp" />

</LinearLayout>
<!--요일-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="20dp"
android:background="#eaeae9"
android:orientation="horizontal">

<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="일"
android:textSize="9sp" />

<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="월"
android:textColor="@color/black"
android:textSize="9sp" />

<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="화"
android:textColor="@color/black"
android:textSize="9sp" />

<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="수"
android:textColor="@color/black"
android:textSize="9sp" />

<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="목"
android:textColor="@color/black"
android:textSize="9sp" />

<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="금"
android:textColor="@color/black"
android:textSize="9sp" />

<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"

android:text="토"
android:textSize="9sp" />
</LinearLayout>
<!--달력뷰-->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/calendar"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

저는 헤더, 요일, 달력뷰 세가지로 나누어 구현하였습니다.


2. 달력에 구성을 생각하고 ViewType을 나누기




이렇게 저는 달력의 타입을 날짜타입, 비어있는 일자 타입, 일자 타입 총 세가지를 나누었습니다.



자 그렇다면 이제 이 ViewType 을 가지고 데이터를 만들어 볼까요? 


3. 달력에 넣을 데이터를 만들자


날짜 타입 = Long

비어있는 일자 타입 = String

일자 타입 = GregorianCalendar 

MutableLiveData<ArrayList<Object>> mCalendarList= new MutableLiveData<>();

public void setCalendarList() {


GregorianCalendar cal = new GregorianCalendar(); // 오늘 날짜

ArrayList<Object> calendarList = new ArrayList<>();

for (int i = -300; i < 300; i++) {
try {
GregorianCalendar calendar =
new GregorianCalendar(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + i, 1, 0, 0, 0);

calendarList.add(calendar.getTimeInMillis())
; //날짜 타입

int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK) - 1; //해당 월에 시작하는 요일 -1 을 하면 빈칸을 구할 수 있겠죠 ?
int max = calendar.getActualMaximum(Calendar.DAY_OF_MONTH); // 해당 월에 마지막 요일

for (int j = 0; j < dayOfWeek; j++) {
calendarList.add(Keys.
EMPTY); //비어있는 일자 타입
}
for (int j = 1; j <= max; j++) {
calendarList.add(
new GregorianCalendar(calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), j)); //일자 타입
}
}
catch (Exception e) {
e.printStackTrace()
;
}
}
mCalendarList.setValue(calendarList);
}




 RecyclerView.Adpater에 사용할 ItemView를 만들자.


4.1 날짜 타입 item_calendar_header.xml

<?xml version="1.0" encoding="utf-8"?>

<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">

<data class="CalendarHeaderBinding">

<import type="android.view.View" />

<variable
name="model"
type="com.yts.tsdiet.viewmodel.CalendarHeaderViewModel" />
</data>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

<androidx.appcompat.widget.AppCompatTextView
setCalendarHeaderText="@{model.mHeaderDate}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:textSize="16sp" />

</LinearLayout>

</layout>

4.2 비어있는 일자 타입 item_day_empty.xml

<?xml version="1.0" encoding="utf-8"?>

<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">

<data class="EmptyDayBinding">

<import type="android.view.View" />

<variable
name="model"
type="com.yts.tsdiet.viewmodel.EmptyViewModel" />
</data>

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

<View
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="2:3" />


</androidx.constraintlayout.widget.ConstraintLayout>

</layout>

4.3 일자 타입 item_day.xml


<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">

<data class="DayItemBinding">

<import type="android.view.View" />

<variable
name="model"
type="com.yts.tsdiet.viewmodel.CalendarViewModel" />
</data>

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:foreground="?android:selectableItemBackgroundBorderless"
android:onClick="@{(v)->model.startRecord(v)}"
android:orientation="vertical"
app:layout_constraintDimensionRatio="2:3">

<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/lineColor" />

<TextView
setDayText="@{model.mCalendar}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:textSize="9sp"
tools:text="1" />


</LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

</layout>


5. RecyclerView.Adpater를 만들자


이제는 실질적 각 ViewType에 맞는 View를 넣어 볼까요 


public class CalendarAdapter extends RecyclerView.Adapter {
private final int HEADER_TYPE = 0;
private final int EMPTY_TYPE = 1;
private final int DAY_TYPE = 2;

private List<Object> mCalendarList;

public CalendarAdapter(List<Object> calendarList) {
mCalendarList = calendarList;
}

public void setCalendarList(List<Object> calendarList) {
mCalendarList = calendarList;
notifyDataSetChanged();
}

@Override
public int getItemViewType(int position) { //뷰타입 나누기
Object item = mCalendarList.get(position);
if (item instanceof Long) {
return HEADER_TYPE; //날짜 타입
} else if (item instanceof String) {
return EMPTY_TYPE; // 비어있는 일자 타입
} else {
return DAY_TYPE; // 일자 타입

}
}


@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
if (viewType == HEADER_TYPE) { // 날짜 타입
CalendarHeaderBinding binding =
DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.item_calendar_header, parent, false);
StaggeredGridLayoutManager.LayoutParams params = (StaggeredGridLayoutManager.LayoutParams) binding.getRoot().getLayoutParams();
params.setFullSpan(true); //Span을 하나로 통합하기
binding.getRoot().setLayoutParams(params);
return new HeaderViewHolder(binding);
} else if (viewType == EMPTY_TYPE) { //비어있는 일자 타입
EmptyDayBinding binding =
DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.item_day_empty, parent, false);
return new EmptyViewHolder(binding);
}
DayItemBinding binding =
DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.item_day, parent, false);// 일자 타입
return new DayViewHolder(binding);
}

@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
int viewType = getItemViewType(position);
if (viewType == HEADER_TYPE) { //날짜 타입 꾸미기
HeaderViewHolder holder = (HeaderViewHolder) viewHolder;
Object item = mCalendarList.get(position);
CalendarHeaderViewModel model = new CalendarHeaderViewModel();
if (item instanceof Long) {
model.setHeaderDate((Long) item);
}
holder.setViewModel(model);
} else if (viewType == EMPTY_TYPE) { //비어있는 날짜 타입 꾸미기
EmptyViewHolder holder = (EmptyViewHolder) viewHolder;
EmptyViewModel model = new EmptyViewModel();
holder.setViewModel(model);
} else if (viewType == DAY_TYPE) { // 일자 타입 꾸미기
DayViewHolder holder = (DayViewHolder) viewHolder;
Object item = mCalendarList.get(position);
CalendarViewModel model = new CalendarViewModel();
if (item instanceof Calendar) {
model.initGoal(holder.itemView.getContext());
model.setCalendar((Calendar) item);
}
holder.setViewModel(model);
}
}

@Override
public int getItemCount() {
if (mCalendarList != null) {
return mCalendarList.size();
}
return 0;
}


private class HeaderViewHolder extends RecyclerView.ViewHolder { //날짜 타입 ViewHolder
private CalendarHeaderBinding binding;

private HeaderViewHolder(@NonNull CalendarHeaderBinding binding) {
super(binding.getRoot());
this.binding = binding;
}

private void setViewModel(CalendarHeaderViewModel model) {
binding.setModel(model);
binding.executePendingBindings();
}
}


private class EmptyViewHolder extends RecyclerView.ViewHolder { // 비어있는 요일 타입 ViewHolder
private EmptyDayBinding binding;

private EmptyViewHolder(@NonNull EmptyDayBinding binding) {
super(binding.getRoot());
this.binding = binding;
}

private void setViewModel(EmptyViewModel model) {
binding.setModel(model);
binding.executePendingBindings();
}

}

private class DayViewHolder extends RecyclerView.ViewHolder {// 요일 타입 ViewHolder
private DayItemBinding binding;

private DayViewHolder(@NonNull DayItemBinding binding) {
super(binding.getRoot());
this.binding = binding;
}

private void setViewModel(CalendarViewModel model) {
binding.setModel(model);
binding.executePendingBindings();
}
}
}


5. 만들어놓은 List를 Adapter에 연결하자.


model.mCalendarList.observe(this, new Observer<ArrayList<Object>>() {
@Override
public void onChanged(ArrayList<Object> objects) {
RecyclerView view = binding.pagerCalendar;
NewCalendarAdapter adapter = (NewCalendarAdapter) view.getAdapter();
if (adapter != null) {
adapter.setCalendarList(objects);
} else {
StaggeredGridLayoutManager manager = new StaggeredGridLayoutManager(7, StaggeredGridLayoutManager.VERTICAL);
adapter = new NewCalendarAdapter(objects);
view.setLayoutManager(manager);
view.setAdapter(adapter);
if (model.mCenterPosition >= 0) {
view.scrollToPosition(model.mCenterPosition); //센터 포지션
}
}
}
});


여기까지 따라오시고 이해를 하셨다면 충분히 여러분들도 예쁜 커스텀 달력을 만드실수 있을거예요!



이렇게 해서 만든 저만에 YTS 버젼 달력 뷰 입니다!

이번에 달력을 이쁘게 만들어 놨으니.. 다음부턴 재탕해서 사용해야겠네요!!! 



참고로 해당 달력뷰는 아래 링크로 들어가셔서 앱을 다운 받으시면 확인해 보실 수 있습니다.


https://play.google.com/store/apps/details?id=com.yts.tsdiet


 


 추가적으로 위와 동일한 달력부분만 추출하여 예제 소스를 만들었는데요. 밑에 링크에 들어가셔서 받아보시면됩니다! 스타도 한번 씩 눌러주세요 : )


https://github.com/YunTaeSik/Calendar_IOS



긴글을 읽어 주셔서 감사합니다.

추가적인 질문은 댓글로 해주시면 감사합니다.