본문 바로가기
프로그래밍

새롭게 안드로이드 달력 만들기 3탄 (스크롤이 가능한 달력 뷰)

by hansoo.labs 한수댁 2014. 11. 10.

Making Calendar in Android. chapter 3 : Scrollable MonthlyFragment

Enable infinite paging with Android ViewPager


무한 스크롤을 어떻게 할 것인지 고민에 빠졌다. 커스텀 클래스를 만들자니 시간도 없고 기존 안드로이드 기본 클래스들과 어떻게 맞춰야 할지 어려웠다. 안드로이드에서 페이지마다 스크롤을 하는 데에는 ViewPager가 가장 적합 할 것 같았다. 그렇지만 동적으로 페이지를 생성하기에는 ViewPager(AdapterView)는 알맞지 않은 형태라고 생각되었다. 이런 고민 속에 구글 검색에 돌입~!!! 삐리삐리~~
이런 역시나 마땅한 방법이 없어보였다... 무한 스크롤을 만들어 내기 위해 2가지 방법을 사용하는 것으로 압축되었다.

  1. 하나는, 아답터의 데이터를 3개 정도로 미리 만들어 놓고 페이지가 바뀌면 ViewPager.setCurrentItem(1) 을 실행하는 것이다. 즉, 페이지를 1로 지정함으로써 항상 2번째 화면이 보이게 만드는 것이다. 페이지의 데이터는 인덱스 값을 보고 업데이트 시켜준다.
  2. 다른 방법은, 아답터의 데이터 개수를 무수히 많이 (1000개 정도) 지정하는 것이다. 무식한 방법으로 진짜 페이지 수를 많이 해놓는 것이다. 1000번 넘게 페이지를 넘겨볼 사람은 없다는 상식 아닌 상식으로. 페이지의 데이터는 인덱스 값을 보고 동적으로 업데이트 시켜준다. ViewPager.setOffscreenPageLimit 함수로 메모리에 유지해야 할 페이지 수를 지정해서 데이터 증가 부담을 줄여준다.

첫번째 방법으로 시도해보았는데, 역시 퍼포먼스에서 문제가 됐다. 페이지 변경이 되고 난 후 한번 더 강제로 페이지를 1로 변경한다는 점이 시간을 잡아먹었다. 데이터 업데이트를 해야 할 때도 방어코드가 많아질 게 분명했다. 역시 무식하지만 두번째 방법이 낫겠다 싶었다.

무한 스크롤을 위한 방법은 여기 코드를 참고했다 : SimpleInfiniteCarousel
페이지 수를 1000개 이상 지정하고 첫페이지를 그 중간 값으로 지정하는 것으로 무한 스크롤을 할 수 있겠다.

다음으로 고민할 것은 특정 날짜와 특정 페이지 번호를 1:1 대응 시키는 것이다. 예를 들어, 500번 페이지가 이번 달을 요청하는 것이고 거꾸로 이번달에 해당하는 페이지 번호를 요청하면 500을 얻을 수 있어야 한다. 그래서 기준이 되는 날짜를 지정하고 요청하는 값과 차이를 계산했다.
/** 위치계산을 위한 기준 년 */
final static int BASE_YEAR = 2015;
/** 위치계산을 위한 기준 월 */
final static int BASE_MONTH = Calendar.JANUARY;
/** 기준 위치, 기준 날짜에 해당하는 위치 */
final static int BASE_POSITION = PAGES * LOOPS / 2;
/** 기준 날짜를 기반한 Calendar */
final Calendar BASE_CAL;
...
//기준 Calendar 지정
Calendar base = Calendar.getInstance();
base.set(BASE_YEAR, BASE_MONTH, 1);
BASE_CAL = base;
...

특정 위치에 해당하는 날짜 구하기

주어진 위치(position)와 기본 위치(BASE_POSITION)의 차이만큼 기준 캘린더를 월 이동 시켰다.
/**
 * 년월이 구하기
 * @param position 페이지 위치
 * @return position 위치에 해당하는 년월이
 */
public YearMonth getYearMonth(int position) {
    Calendar cal = (Calendar)BASE_CAL.clone();
    cal.add(Calendar.MONTH, position - BASE_POSITION);
    return new YearMonth(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH));
}

특정 날짜에 해당하는 위치 구하기

기준 날짜와 특정 날짜가 몇 달 차이가 나는지 계산하여 위치를 구한다.
/**
 * 페이지 위치 구하기
 * @param year 년
 * @param month 월
 * @return 페이지 위치
 */
public int getPosition(int year, int month) {
    Calendar cal = Calendar.getInstance();
    cal.set(year, month, 1);
    return BASE_POSITION + howFarFromBase(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH));
}

/**
 * 기준 날짜를 기준으로 몇달이 떨어져 있는지 확인
 * @param year 비교할 년
 * @param month 비교할 월
 * @return 달 수, count of months
 */
private int howFarFromBase(int year, int month) {
    
    int disY = (year - BASE_YEAR) * 12;
    int disM = month - BASE_MONTH;
    
    return disY + disM;
}


이렇게 가장 중요한 문제를 모두 해결할 수 있게 됐다.
  • 무한 뷰페이저(Infinite ViewPager)를 만들 수 있게 됐다. (가짜지만 그렇게 느껴지는..)
  • 날짜와 페이지 번호를 매칭시킬 수 있다.
참, 세로로 스크롤시키기 위해 이 라이브러리를 포함시켰다. : VerticalViewPager


그럭저럭 잘 된다~!

이렇게 달력만들기 초간단 프로젝트를 마친다능.
프로젝트 파일(이클립스용): calendar_proto.zip

그리고 GitHub에 최신 버전을 유지하고 있습니다.
Android Vertically Scrollable Calendar Prototype
관련글:
새롭게 안드로이드 달력 만들기 1탄(하루뷰)
새롭게 안드로이드 달력 만들기 2탄(한달뷰)

댓글49

    이전 댓글 더보기
  • 강하라 2015.03.27 01:17

    안녕하세요 잘 봤습니다! 1000개의 어댑터를 사용하게 되면 1000개의 어댑터 그리고 캘린더 옵젝트가 다 메모리에 올라가게 되나요? 아니면 안드로이드에서 1000개의 페이지를 가진 ViewPager중에서 몇개만 메모리에 올리는 최적화를 알아서 하나요? 궁금해서 질문 남깁니다!
    답글

    • 안녕하세요. 1000개의 아답터가 아니라 1000개의 페이지라고 보는게 맞겠죠. 모든 오브젝트(Fragment)가 메모리에 올라가는 것은 아니고 ViewPager가 필요에 따라서 생성하고 삭제합니다. Fragment에 onCreate(), onDestroy()를 로그로 확인해보시면 될꺼에요.

  • 조경진 2015.04.24 18:30

    와 이거 진짜 만들어보고 싶었는데 감사히 잘 쓸게요!!

    스토어에 앱 올리면 다시 댓글달러 오겠습니다!!
    답글

  • 이동문 2015.08.31 00:46

    안녕하세요. 먼저 좋은 소스코드 올려주셔서 감사합니다. 이 소스를 기반으로 캘린더를 개발을 하다가 에러사항이 있어서 이렇게 댓글 남깁니다. 저는 mothlyFragment를 MainActivity에 올려서 사용하는 것이 아니라 Activity에 포함된 HomeFragment 위에 MonthlyFragment를 올려서 구현중입니다. HomeFragment위에 버튼을 누르면 MonthlyFragment가 childFragmentManager를 통해 replace 되는 방법으로 구현했습니다.

    여기서 기능적으로는 문제 없이 잘 동작을 하는데 0.2초 정도의 딜레이가 걸립니다. 그래서 어떤 부분에서 딜레이가 생기는지 확인했더니
    OneMonthView를 만들어 주는 생성자 부분에서 주와 일 레이아웃을 프로그래밍적으로 만들어서 컨테이너에 add해 줄 때 딜레이가 걸리는 것을 확인했습니다. 이 딜레이를 줄이긴 위한 좋은 방법이나 방향성을 알려주실수 있을까해서 이렇게 댓글남깁니다.

    답글

    • 안녕하세요. 이렇게 어려운 질문을 주시다니..! 프로젝트가 끝까지 진행되지 못해서 제가 실제 속도 테스트까지 못했는데, 그런 문제가 있군요.

      제 생각에는 아무래도 속도를 높이는 방법에는
      1. 미리 만들어 놓는 방법이 가장 효과가 좋을 테고,
      2. 가능한 뷰 사이즈나 형태가 변하지 않도록 하는 것이라고 생각됩니다.

      1번 방법으로 OneMonthView를 미리 만들어 놓을 수 있는지 확인해볼 수도 있고, MonthlyFragment 가 재생성되는 동안 불필요하게 다시 만드는 인스턴스는 없는지 살펴볼 수도 있겠습니다. 아무튼 가능한 적정한 수준으로 미리 만들어놓고 재사용하도록 하는 게 속도향상에 좋으니깐요.

      2번 방법은 뷰가 addView 하면서 레이아웃을 계산하게 되는 데 이시간을 줄 일 수 있는 방법은 없는지 찾아보아야 한다는 것입니다. 코드로 된 OneMonthView 안의 OneDayView 생성부분을 대신해 xml로 한달 화면을 만들어서 테스트해봐도 좋을 듯 합니다. 또는 리니어뷰 말고 그리드 뷰나 커스텀 뷰를 사용해보는 것도 좋을 것 같습니다.

      음.. 이 소스코드 내용을 벗어나는 이야기지만 뷰를 캔버스에 그려서 만들어도 빨라질 것 같긴 합니다.

      확답은 드리기 힘들군요. 다 아시는 방법이라고 생각됩니다.^
      혹시 방법을 찾으시면 공유해주시면 좋겠군요.

  • 이동문 2015.08.31 11:57

    우와 ... 이렇게 바로 답변 남겨주셔서 감사합니다. 알려주신 방향으로 속도가 빨라졌습니다.

    일단 1번 방법을 통해 MainActivity의 onCreate함수에서 OneMonthView를 미리 만들어서 HomeFragment의 생성자를 통해 값을 전달한 후 MonthlyFragment에 전달하는 식으로 해서 oneMonthView가 불필요하게 생성되는 시간을 줄일 수 있었습니다.

    2 방법을 도입하면 조금더 속도가 빨라 질 것 같은데 아직 시도해 보진 못했습니다. 2번 방법중 xml파일을 만들어서 inflate 해보는 방식으로 진행해 보려고 합니다. GridView나 커스텀 뷰 방식, 캔버스에 그리는 방식은 해본적이 없어서 .. 이방법 실패하면 공부해서 진행 해보려고 합니다.

    좋은 방향성 알려주셔서 감사합니다.~~~~~~
    답글

  • Lisa 2015.09.23 04:33

    감사합니다! 정말 저에게 딱맞는 정보이고, 많은 도움이 되었습니다 ㅎㅎ
    많은 시간을 들여 코드를 이해하니 금쪽같은 정보에요! 정말 감사합니다
    답글

  • ssh 2016.02.28 03:52

    다른 방식으로 하다 이글 보고 많이 도움됐습니다. 감사의 말씀 드립니다
    한가지 조언 부탁드리고 싶은게 있는데, 클릭리스너로 다이얼로그띄워서 해당날짜의 2주전기준으로 4일(15일 선택시 1,2,3,4일) 의 데이터를 db에 저장하고 달력에 넣어주려 합니다. 클릭한 날짜 하루의 데이터를 넣어주는건 쉬운데, 저번달로 넘어갈 수도 있고.. 다른 포지션의 뷰의 객체를 받을 수 있는 방법이 있을까요? onPageScrolled에 db 칼럼 불러와 넣어주는방법밖엔 없을까요?
    답글

    • 감사합니다~ /
      하고자 하는 목적이 클릭한 날짜 이외에 다른 날짜의 데이터들을 저장하고 싶은 거죠? 일단 데이터는 날짜를 기준으로 저장되어있을테니.. 클릭한 날짜에서 2주 전으로 Date값을 변경하여 저장하면 되겠죠!!? Calendar 클래스의 add라는 함수가 있으니 살펴보세요. 데이터 저장을 위해 다른 뷰의 객체를 참고하는 것은 좋은 방향은 아닌 듯 합니다.

  • ssh 2016.03.01 17:36

    제가 말을 좀 뱅뱅 돌려서 이해하시기 힘드셨을겁니다.ㅠ
    다이얼로그를 통해 db에 저장하고. db를 불러온 다음
    불러온 Arraylist를 적용하기위해 페이지의 뷰를 어떻게 리플래시 시켜줘야 하는지 궁금합니다.
    한번에 범위 날짜의 데이터가 변경되기때문에 한달뷰 전체를 리플래시시켜줘야 할 것 같습니다.
    답글

    • 그런 뜻이였군요. 일일뷰가 어떻게 갱신되는지 확인하고 변경된 데이터를 적용하면 되겠죠. 너무 일반적인 대답인가요? ;; 텍스트뷰나 이미지뷰 같은 것은 set 함수만으로도 갱신이 되니까 필요에 따라 요청만하면 될 것 같아요. 특정 뷰들만 갱신하려면 그 뷰들을 콜랙션에 담아 일괄적용하면 되고요

  • ssh 2016.03.04 04:34

    제가 코드 이해와 실력이 많이 부족했습니다. 현재는 db가 변경되면 notifyDataSetChanged()를 통해 메모리에 올라간 뷰들 전부 dayView에서 가져와 동적으로 업데이트 시켜주고 있습니다. 도움 정말 많이되었습니다 감사합니다.^^
    답글

  • hyun 2016.05.03 16:24

    안녕하세요

    이 소스들을 수정하여

    연달력을 만들어야하는데 어떻게 해야할지 알려주실수있나여?
    답글

  • JSY 2016.12.01 21:08

    안녕하세요, 이 소스코드로 스케쥴어플 공부를 하고 있는 학생입니다. 어플리케이션을 실행시키면 액션바가 나오지 않는데 혹시 이유를 알 수 있을까요..? 도무지 모르겠네요..ㅠ
    답글

  • 호호 2016.12.02 14:32

    스크롤로 이동하지않고 날짜선택다이얼로그를 띄워서 이동하고싶은데 어떻게 해야돼나요ㅠㅠ?? 제발 알려주시면 정말 감사하겠습니다 ㅠㅠ
    답글

    • 음.. 제가 특정 년월일로 position 값을 구하는 함수를 Adapter에 넣어 놨어요. 확인해보세요~

    • 호호 2016.12.02 20:00

      감사합니다! 날짜 이동 해결했습니다!!근데 하나만 더 여쭤볼게요.. 달력에다가 오늘날짜인 뷰는 배경색을 다르게 주고싶은데요. 배경색을 지정하는거는 알겠는데 오늘날짜인 뷰를 어떻게 알아내서 코드로 짜야되나요 ?ㅠ OneDayView클래스에서 코드 짜는건가요??

  • JSY 2016.12.04 21:35

    헤헤 액션바는 어찌저찌 해결했습니다. 우선 댓글달아주셔서 감사합니다. 한가지 더 궁금한게 생겼는데.. 혹시 전달과 다음달에 해당하는 셀은 배경색을 회색으로 지정하고 싶은데.. 이 방법에 대해서 혹시 아시나요..ㅠㅠ?
    답글

    • 이전달과 다음달은 OneDayView 의 속성값으로 알 수 있어요. 이번달 수와 비교하면 될거에요.

    • JSY 2016.12.07 19:37

      if(one.get(Calendar.DAY_OF_MONTH) != one.get(Calendar.MONTH))
      {
      dayTv.setTextColor(Color.Grey);
      }

      이렇게 하면.. 이번달이 아닌 일은 텍스트컬러를 회색으로 한다.. 맞지 않나요..ㅠㅠㅠㅠㅠㅠ?

    • ㅠㅠ 어떻게 설명해드려야 할지.. 더 코멘트해드리기가 어렵군요.

  • SRH 2016.12.06 13:17

    github가서 코드를 보니깐
    final static int PAGES = 5;
    final static int LOOPS = 1000;
    final static int BASE_POSITION = PAGES * LOOPS / 2;

    이렇게 되있는데요.. PAGES변수가 뭔가요?? BASE_POSITION 변수도 뭘 의미하는지 알려주세요..ㅠㅠ 죄송합니당 ㅠㅠ
    답글

  • SRH 2016.12.06 17:44

    제가 달력뷰에다가 이미지를 보여주고 있는데요 PAGES=5로 하니깐 5달이전에 똑같은 이미지가 뜨네요.. 5달 이전에 안뜨게 하는법좀 알려주세요ㅠㅠ
    답글

  • 어흥 2017.04.25 17:44

    현재 월로 이동하는 기능을 구현하고 싶은데...
    어떻게 구현해야 되나요..ㅜㅜ

    위에 남기신 position 을 어떻게 이용해야 될지 모르겠네요 OTL

    아래처럼 소스 적용하면 에러 나면서 종료되버리네요 ㅜㅜ

    가르침 부탁드립니다

    vvPager.setCurrentItem(adapter.getPosition(currentYear, currentMonth));
    답글

  • 민승기 2017.05.25 17:38

    안녕하세요.
    좋은 소스 올려주셔서 감사드립니다.
    그런데 실행해보니 일정 추가 및 밑의 액션바 자체가 작동을 안하더군요.
    소스를 들어가서 보니 관련 처리한 부분도 없는 것 같구요.
    혹 제가 못본 부분인가요?
    원래 일정 추가 기능이 작동해야되는게 맞는 건가요?
    제가 만들려고 하는 캘린더는 일정 추가 저장으로 데이터 베이스로 연동해서 만들고자 합니다.
    도움 부탁드립니다.
    답글

  • 초보 2017.05.30 19:54

    안녕하세요 좋은자료 보고갑니다. 질문이 있는데요 서버랑 통신해서 어떤데이터를 서버로부터 받아와 textview에 뿌려준다음 저장버튼을 통해 원하는 날짜에 받아온 데이터들을 저장하고싶은데 어떤방법을 쓰면 좋을까요? 감이 안잡히네요 ㅠㅠ
    답글

  • 감사합니다. 2017.08.25 04:58

    안녕하세요. 안드로이드 초보 개발자입니다. 너무 좋은 자료라 도움이 많이 되어 감사글을 남깁니다.
    소스 전체적으로 너무 예쁘게 코딩하셨더라구요.. 주석도 알아듣기 쉽게 해주시고 정말 감사합니다!
    답글

  • 왕초보 2017.10.17 16:11

    지금 만드려고 하는 캘린더의 구조를 아무리 구글링하고 해도 원하는게 없었는데 거의 근접한 훌륭한 포스팅을 찾아 감사해 하고 있습니다.
    초보라 소스 코드에서 어떤 부분을 봐야 세로로 스크롤되는것을 가로 횡 스크롤로 한달 단위 이동으로 변경할수 있을지 몰라 질문드립니다.
    답글

  • KYH 2018.06.28 17:35

    우선 처음 해보는 어플리케이션 만들기에 큰 도움을 주셔서 감사합니다.
    만드려고 하는 페이지에 횡스크롤 캘린더가 더 어울릴 것 같아서 횡스크롤로 바꾸고 싶은데,
    바로 위의 댓글에서 횡으로 스크롤 하기 위해선 안드로이드에서 제공하는 ViewPager를 이용하라고 하셨는데 어떤 부분을 바꿔야 횡스크롤로 바꿀 수 있을까요?
    안드로이드와 자바를 접한지 얼마 안되서 그런지 어떻게 해야하는지 몰라 질문드립니다.
    답글