안녕하세요 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);
}
4 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
긴글을 읽어 주셔서 감사합니다.
추가적인 질문은 댓글로 해주시면 감사합니다.
'IT > 안드로이드 관련' 카테고리의 다른 글
[안드로이드] 안드로이드 지문인식 변경점 (9) | 2019.01.09 |
---|---|
[안드로이드] ROOM 라이브러리 사용하기 , 코루틴 (3) | 2018.12.28 |
[안드로이드] MPAndroidChart LineChart 속성 정리 (Example) (11) | 2018.12.07 |
[안드로이드] 안드로이드 클래스 다이어그램(Class Diagram) 만들기 (0) | 2018.11.29 |
[안드로이드] 안드로이드 스낵바(SnackBar) (0) | 2018.10.26 |