티스토리 뷰

반응형

서론

 작년 이 맘때 즈음에 진행했던 앱 프로젝트 진행 중에 캘린더의 날짜 칸마다 사진과 날짜를 겹쳐 보여주는 캘린더뷰가 필요해 직접 캘린더뷰를 만들어 사용했었다. 작년 코드 리뷰도 해볼겸! 커스텀 캘린더뷰를 구현하는 방법을 기록으로 남겨두면 좋을 것 같아서 글로 작성해보았다. 안드로이드 개발에 사용한 언어는 Java이다.


본론

요구사항

내가 만들고 싶은 캘린더뷰는 다음 사진과 같다.

figma 초기 디자인


1. 아이템뷰에서 날짜와 이미지를 겹쳐 보여주도록 한다.

 

2. 세로와 가로 화면에서 같은 모양을 유지해야 한다.


구현

캘린더뷰 상단에서 보여줄 요일 리스트 아이템뷰를 먼저 만든다. (MON, TUE, WED, ..., SUN 이 부분!)

 

  • item_week.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/ll_week"
    android:layout_width="@dimen/gridWidth"
    android:layout_height="@dimen/gridHeight"
    android:layout_margin="@dimen/gridMargin"
    android:background="@drawable/week_bg_border">

        <TextView
            android:id="@+id/tv_day_of_week"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_gravity="center"
            android:gravity="center"
            android:text="@string/day"
            android:textColor="@color/textColor"
            android:textStyle="bold"
            android:fontFamily="@font/maruburittf"
            android:textSize="@dimen/textSize"/>

</LinearLayout>

 

 

  • item_calendar.xml

캘린더의 아이템뷰도 함께 만든다.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/rl_date"
    android:layout_width="@dimen/gridWidth"
    android:layout_height="@dimen/gridWidth"
    android:layout_margin="@dimen/gridMargin">

    <ImageView
        android:id="@+id/iv_date"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="centerCrop"
        android:background="@drawable/calendar_bg_border"
        android:layout_centerInParent="true" />

    <TextView
        android:id="@+id/tv_date"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_centerInParent="true"
        android:fontFamily="@font/maruburittf"
        android:gravity="center"
        android:text="@string/_0"
        android:textColor="@color/textColor"
        android:textSize="@dimen/textSize" />

</RelativeLayout>

 

 

  • activity.main.xml

activity_main.xml에 커스텀 캘린더뷰로 사용할 RecyclerView를 추가한다.

                <LinearLayout
                    android:id="@+id/ll_calendar"
                    android:layout_width="0dp"
                    android:layout_height="match_parent"
                    android:layout_alignParentTop="true"
                    android:layout_alignParentEnd="true"
                    android:layout_weight="6"
                    android:gravity="center"
                    android:layout_gravity="top"
                    android:orientation="vertical">

        	    <!-- 요일 리스트 -->
                    <androidx.recyclerview.widget.RecyclerView
                        android:id="@+id/rv_weeklist"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_marginTop="2dp"
                        android:layout_marginBottom="2dp"
                        android:numColumns="7"
                        android:orientation="horizontal"
                        app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
                        tools:itemCount="7"
                        tools:listitem="@layout/item_calendar_day_of_week" />

        	    <!-- 커스텀 캘린더뷰 -->
                    <androidx.recyclerview.widget.RecyclerView
                        android:id="@+id/rv_calendar"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_marginBottom="2dp"
                        android:numColumns="7"
                        app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
                        tools:itemCount="31"
                        tools:listitem="@layout/item_calendar_gridview" />
                </LinearLayout>

 

 

  • MainActivity.java

 MainActivity에 RecyclerView 관련 설정 코드를 작성한다.

public class MainActivity extends AppCompatActivity implements View.OnClickListener, CalendarAdapter.ItemClickListener {

    private TextView tv_month;

    // 캘린더뷰 어댑터
    private CalendarAdapter calendarAdapter;
    private WeekAdapter weeklistAdapter;

    // 요일 리스트
    private ArrayList<Day> dayList;
    private ArrayList<String> day_of_weekList;

    // 그리드뷰
    private RecyclerView rv_calendar;
    private RecyclerView rv_weeklist;

    private Calendar calendar;

    public static Context context;

    private static final String YEAR = "year";
    private static final String MONTH = "month";
    private static final String DATE = "date";
    
    private static final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        context = getApplicationContext();
        tv_month = findViewById(R.id.tv_month);
        
        rv_calendar = findViewById(R.id.rv_calendar);
        GridLayoutManager gridLayoutManager = new GridLayoutManager(this, 7);
        rv_calendar.setLayoutManager(gridLayoutManager);
        rv_calendar.setItemViewCacheSize(42);
        
        rv_weeklist = findViewById(R.id.rv_day_of_week);
        GridLayoutManager gridLayoutManager_week = new GridLayoutManager(this, 7);
        rv_weeklist.setLayoutManager(gridLayoutManager_week);
        rv_weeklist.setItemViewCacheSize(7);

        setWeeklistbar();
        setCalendarView(getCalendar(savedInstanceState));
    }

    // 캘린더 인스턴스 생성 또는 불러오기
    private Calendar getCalendar(Bundle savedInstanceState) {
        calendar = Calendar.getInstance();
        if (savedInstanceState != null) {
            calendar.set(savedInstanceState.getInt(YEAR), savedInstanceState.getInt(MONTH), 1);
        } else
            calendar.set(Calendar.DAY_OF_MONTH, 1);
            
        return calendar;
    }

    @Override
    protected void onResume() {
        super.onResume();
        calendar.set(Calendar.DAY_OF_MONTH, 1);
        setCalendarView(calendar);
    }

    // 요일 바 설정
    private void setWeeklistbar() {
        //gridview_day_of_week에 요일 리스트 표시
        day_of_weekList = new ArrayList<String>();
        day_of_weekList.add("SUN");
        day_of_weekList.add("MON");
        day_of_weekList.add("TUE");
        day_of_weekList.add("WED");
        day_of_weekList.add("THU");
        day_of_weekList.add("FRI");
        day_of_weekList.add("SAT");

        weeklistAdapter = new WeekAdapter(this, day_of_weekList);
        rv_weeklist.setAdapter(weeklistAdapter);

        dayList = new ArrayList<Day>();
    }

	// 달력 세팅
    private void setCalendarView(Calendar calendar) {
        int lastMonthStartDay;
        int dayOfMonth;
        int thisMonthLastDay;

        dayList.clear();

        // 이번달 시작 요일
        dayOfMonth = calendar.get(Calendar.DAY_OF_WEEK);
        thisMonthLastDay = calendar.getActualMaximum(Calendar.DAY_OF_MONTH);

        calendar.add(Calendar.MONTH, -1);

        // 지난달 마지막 일자
        lastMonthStartDay = calendar.getActualMaximum(Calendar.DAY_OF_MONTH);

        calendar.add(Calendar.MONTH, 1);

        lastMonthStartDay -= (dayOfMonth - 1) - 1;

        // 년월 표시
        tv_month.setText((this.calendar.get(Calendar.MONTH) + 1) + "");

        Day day;
        for (int i = 0; i < dayOfMonth - 1; i++) {
            int date = lastMonthStartDay + i;
            day = new Day();
            day.setDay(Integer.toString(date));
            day.setInMonth(false);

            dayList.add(day);
        }

        for (int i = 1; i <= thisMonthLastDay; i++) {
            day = new Day();
            day.setDay(Integer.toString(i));
            day.setInMonth(true);

            dayList.add(day);
        }

        for (int i = 1; i < 35 - (thisMonthLastDay + dayOfMonth) + 1; i++) {
            day = new Day();
            day.setDay(Integer.toString(i));
            day.setInMonth(false);
            dayList.add(day);
        }

        initCalendarAdapter();
    }

    // 캘린더뷰 어댑터 초기화
    private void initCalendarAdapter() {
        calendarAdapter = new CalendarAdapter(this, calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), dayList);
        calendarAdapter.setClickListener(this);
        rv_month.setAdapter(calendarAdapter);
    }

    // 캘린더뷰 아이템 클릭 리스너
    @Override
    public void onItemClick(View view, String day, boolean isInMonth) {
        Calendar itemCal = calendar;
        itemCal.set(Calendar.DAY_OF_MONTH, Integer.parseInt(day));
        String date = new SimpleDateFormat("yyyy.MM.dd.").format(itemCal.getTime());

        if (isInMonth) {
            Intent intent = new Intent(MainActivity.this, DiaryActivity.class);
            intent.putExtra(IS_TODAY, false);
            intent.putExtra(DATE, date);
            startActivity(intent);
        }
    }
}

블로그를 작성하는 와중에 고쳐야 할 부분이 많이 보인다 😇

 

 

  • CalendarAdapter.java

커스텀 캘린더뷰에 사용할 어댑터를 작성한다.

public class CalendarAdapter extends RecyclerView.Adapter<CalendarAdapter.ViewHolder> {
    // {0 ~ 30/31} 까지의 날짜 리스트
    private ArrayList<Day> list;

    private Context context;
    private final LayoutInflater inflater;

    Calendar cal = Calendar.getInstance();
    ItemClickListener mClickListener;

    public CalendarAdapter(Context context, int year, int month, ArrayList<Day> list) {
        this.context = context;
        this.list = list;
        this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

        // 캘린더 세팅
        this.cal.set(Calendar.YEAR, year);
        this.cal.set(Calendar.MONTH, month);
    }

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = inflater.inflate(R.layout.item_calendar, parent, false);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        Day day = list.get(position);
        if (day != null) holder.tv_item.setText(day.getDay());
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public int getItemCount() {
        return list.size();
    }

    public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
        TextView tv_item;
        ImageView iv_item;
        boolean isInMonth = true;

        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            tv_item = itemView.findViewById(R.id.tv_date);
            iv_item = itemView.findViewById(R.id.iv_date);
            itemView.setOnClickListener(this);
        }

        public void onClick(View view) {
            if (mClickListener != null) mClickListener.onItemClick(view, (String) tv_item.getText(), isInMonth);
        }
    }

    // 클릭리스너 인터페이스
    public interface ItemClickListener {
        void onItemClick(View view, String day, boolean isInMonth);
    }

    public void setClickListener(ItemClickListener itemClickListener) {
        this.mClickListener = itemClickListener;
    }

}

 

 

  • WeekAdapter.java

캘린더뷰 위에 보여줄 요일리스트의 어댑터도 작성한다. 요일리스트는 이 대신에 ListView로 구현하는게 더 좋을 듯!

/**
 * 그리드뷰 어댑터 - 요일 리스트
 */
public class WeekAdapter extends RecyclerView.Adapter<WeekAdapter.ViewHolder> {

    private ArrayList<String> list;
    private Context context;
    private final LayoutInflater inflater;

    /**
     * 생성자
     *
     * @param context
     * @param list
     */
    public WeekAdapter(Context context, ArrayList<String> list) {
        this.context = context;
        this.list = list;
        this.inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }

    @NonNull
    @Override
    public WeekAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = inflater.inflate(R.layout.item_calendar_day_of_week, parent, false);
        return new WeekAdapter.ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull WeekAdapter.ViewHolder holder, int position) {
        holder.tv_date_of_week.setText("" + list.get(position));
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public int getItemCount() {
        return list.size();
    }


    public class ViewHolder extends RecyclerView.ViewHolder {
        TextView tv_date_of_week;

        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            tv_date_of_week = itemView.findViewById(R.id.tv_day_of_week);
        }
    }
}

 

 

 

구현 결과물

최종 결과물

 

 

 

마무리하며 ..

 최근에는 안드로이드에 대부분 코틀린을 사용하고 있기도 하고, 거의 몇 주 간격으로 안드로이드가 업데이트되느라 위 코드에서 deprecated된 부분도 많다 ㅠ 다른 분들한테 도움은 못 될 것 같지만 그래도 ^__^ 스스로를 돌아보는 계기가 되었으니깐 .. 주먹구구식 구현만 하면 된다는 듯한 코드는 앞으로 작성하지 않는 걸로 ... 😔

 

반응형