2016년 12월 28일 수요일

[Android]달력만들기 - 월단위

Standard
안드로이드에서 제공하는 CalendarView를 이용하여 달력을 쉽게 만들 수 있다.
하지만, 색을 칠하거나, 문자 또는 이미지를 달력안에 표시 하기 위해서는 많은 부분을 손 되어야 한다.
앞으로 만들 서비스는 달력이 기본이 되는 서비스 이므로 직접 달력을 구현하고자 검색한 결과
아래 사이트에 괜찮은 소스가 있어, 참고하였다.

참고 사이트 : http://hatti.tistory.com/entry/android-calendar
최종 소스 : 캘린더 소스

[달력 결과물]


달력을 구현하기 위해서 사용된 항목
1. ViewPager & FragmentStatePagerAdapter
- https://developer.android.com/reference/android/support/v4/view/ViewPager.html
- https://developer.android.com/reference/android/support/v4/app/FragmentStatePagerAdapter.html
- 좌우스크롤 시 달력의 월을 변경하기 위해서 ViewPager를 사용한다.


- 초기 달력 데이터를 생성하기 위해서 FragmentStatePagerAdapter에서 달력 데이터를 생성한다. 참고사이트의 소스에서는 25개월 치를 미리 생성한다.
private HashMap frgMap;
    private ArrayList listMonthByMillis = new ArrayList<>();
    private int numOfMonth;
    
    @Override
    public Fragment getItem(int position) {
        FrgCalendar frg = null;
        if (frgMap.size() > 0) {
            frg = frgMap.get(position);
        }
        if (frg == null) {
            frg = FrgCalendar.newInstance(position);
            frg.setOnFragmentListener(onFragmentListener);
            frgMap.put(position, frg);
        }
        frg.setTimeByMillis(listMonthByMillis.get(position));

        return frg;
    }

    @Override
    public int getCount() {
        return listMonthByMillis.size();
    }

    // 초기 달력 데이터 생성 메소드
    public void setNumOfMonth(int numOfMonth) {
        this.numOfMonth = numOfMonth;

        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.MONTH, -numOfMonth);
        calendar.set(Calendar.DATE, 1);

        for (int i = 0; i < numOfMonth * 2 + 1; i++) {
            listMonthByMillis.add(calendar.getTimeInMillis());
            calendar.add(Calendar.MONTH, 1);
        }

        notifyDataSetChanged();
    }
// Fragment에서 ViewGroup 및 View 생성 :: ViewGroup은 XML 설정하거나, 직접 생성해줘야 함
    protected void initView() {

        Calendar calendar = Calendar.getInstance();
        calendar.setTimeInMillis(timeByMillis);
        calendar.set(Calendar.DATE, 1);
        // 1일의 요일
        int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
        //이달의 마지막 날
        int maxDateOfMonth = calendar.getActualMaximum(Calendar.DAY_OF_MONTH);
        calendarView.initCalendar(dayOfWeek, maxDateOfMonth);
        // 요일 및 날짜 만큼 View를 생성하여 ViewGroup에 추가하는 로직
        for (int i = 0; i < maxDateOfMonth + 7; i++) {
            CalendarItemView child = new CalendarItemView(getActivity().getApplicationContext());
            if (i == 20) {
                child.setEvent(R.color.colorPrimaryDark);
            }
            child.setDate(calendar.getTimeInMillis());
            if (i < 7) {
                child.setDayOfWeek(i);
            } else {
                calendar.add(Calendar.DATE, 1);
            }
            calendarView.addView(child);
        }
    }
2. ViewGroup

- https://developer.android.com/training/basics/firstapp/building-ui.html - http://bcho.tistory.com/1043

ViewGroup은 위 링크에서 설명하고 있는 것 처럼 다수의 ViewGroup과 View를 담기 위한 공간이다. 여기서는 달력의 요일과 일자를 그린 View를 담기 위해서 사용된다. 또한 월 단위로 표시되는 ViewGroup은 Fragment에 표현되며, Fragment는 FragmentStatePagerAdapter에 의해서 ViewPager에 표현된다.

ViewGroup에서는 onMeasure() - 크기설정 메소드와 onLayout() - 좌표설정 메소드를 오버라이드 해서 View를 ViewGroup에 그린다.


아래 사이트 참조

- http://i5on9i.blogspot.kr/2013/05/android-view-onmeasure-onlayout.html

- http://kingorihouse.tumblr.com/post/86806256119/android-viewgroup-%EC%A7%81%EC%A0%91-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0


여기서 각 View의 크기와 좌표를 계산하기 위해서 사용되는 변수는 ViewPager에서 생성한 달력 데이터를 이용한다.

public CalendarView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mScreenWidth = getResources().getDisplayMetrics().widthPixels; ///< 스마트폰의 가로 화면 사이즈
        mWidthDate = mScreenWidth / 7; // 스마트폰 화면 사이즈에서 7(월~일 요일)로 나누어 View의 Width를 구함
        DAY_OF_WEEK = getResources().getStringArray(R.array.day_of_week);
    }
    
    // View의 크기를 설정함
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();
        // Measurement will ultimately be computing these values.
        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;
        int mLeftWidth = 0;
        int rowCount = 0;
        Calendar calendar = Calendar.getInstance();
        calendar.setTimeInMillis(mMillis);

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);

            if (child.getVisibility() == GONE)
                continue;

            // Measure the child.
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            maxWidth += Math.max(maxWidth, child.getMeasuredWidth());
            mLeftWidth += child.getMeasuredWidth();

            if ((mLeftWidth / mScreenWidth) > rowCount) {
                maxHeight += child.getMeasuredHeight();
                rowCount++;
            } else {
                maxHeight = Math.max(maxHeight, child.getMeasuredHeight());
            }
            childState = combineMeasuredStates(childState, child.getMeasuredState());
        }

        maxHeight = (int) (Math.ceil((count + mDateOfWeek - 1) / 7d) * (mWidthDate * 0.75));// 요일중 일요일이 1부터 시작하므로 1을 빼줌
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
        int expandSpec = MeasureSpec.makeMeasureSpec(MEASURED_SIZE_MASK, MeasureSpec.AT_MOST);

        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, expandSpec, childState << MEASURED_HEIGHT_STATE_SHIFT));

        LayoutParams params = getLayoutParams();
        params.height = getMeasuredHeight();
    }

    // View의 위치를 설정함
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {


        final int count = getChildCount();
        int curWidth, curHeight, curLeft, curTop, maxHeight;

        final int childLeft = this.getPaddingLeft();
        final int childTop = this.getPaddingTop();
        final int childRight = this.getMeasuredWidth() - this.getPaddingRight();
        final int childBottom = this.getMeasuredHeight() - this.getPaddingBottom();
        final int childWidth = childRight - childLeft;
        final int childHeight = childBottom - childTop;

        maxHeight = 0;
        curLeft = childLeft;
        curTop = childTop;

        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);

            if (child.getVisibility() == GONE)
                return;

            child.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.AT_MOST));
            curWidth = mWidthDate;
            curHeight = (int) (mWidthDate * 0.75);

            if (curLeft + curWidth >= childRight) {
                curLeft = childLeft;
                curTop += maxHeight;
                maxHeight = 0;
            }
            if (i == 7) {
                curLeft = (mDateOfWeek - 1) * curWidth;
            }
            child.layout(curLeft, curTop, curLeft + curWidth, curTop + curHeight);

            if (maxHeight < curHeight) {
                maxHeight = curHeight;
            }
            curLeft += curWidth;
        }
    }
    
    // 변경된 달력 정보로 초기 값을 변경함
    public void setDate(long millis) {
        mMillis = millis;
        Calendar calendar = Calendar.getInstance();
        calendar.setTimeInMillis(millis);
        calendar.set(Calendar.DATE, 1);

        mDateOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
        mMaxtDateOfMonth = calendar.getActualMaximum(Calendar.DAY_OF_MONTH);

        invalidate();
    }

3. View
- https://developer.android.com/reference/android/view/View.html
View에서는 onDraw() 메소드를 오버라이드 해서 canvas에 달력 데이터를 그리고, 색을 칠하거나 하는 작업을 한다.
// Canvas에 달력 데이터를 그림
    @TargetApi(Build.VERSION_CODES.LOLLIPOP) 
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int xPos = (canvas.getWidth() / 2);
        int yPos = (int) ((canvas.getHeight() / 2) - ((mPaint.descent() + mPaint.ascent()) / 2));
        mPaint.setTextAlign(Paint.Align.CENTER);
        Calendar calendar = Calendar.getInstance();
        calendar.setTimeInMillis(millis);

        CalendarView calendarView = (CalendarView) getParent();
        if (calendarView.getParent() instanceof ViewPager) {
            ViewGroup parent = (ViewPager) calendarView.getParent();
            CalendarItemView tagView = (CalendarItemView) parent.getTag();

            if (!isStaticText && tagView != null && tagView.getTag() != null && tagView.getTag() instanceof Long) {
                long millis = (long) tagView.getTag();
                if (isSameDay(millis, this.millis)) {
                    canvas.drawRoundRect(xPos - dp16, getHeight() / 2 - dp16, xPos + dp16, getHeight() / 2 + dp16, 50f, 50f, mPaintBackground);
                }
            }
        }

        if (!isStaticText && isToday(millis)) {
            canvas.drawRoundRect(xPos - dp16, getHeight() / 2 - dp16, xPos + dp16, getHeight() / 2 + dp16, 50f, 50f, mPaintBackgroundToday);
        }

        if (isStaticText) {
            // 요일 표시
            canvas.drawText(CalendarView.DAY_OF_WEEK[dayOfWeek], xPos, yPos, mPaint);
        } else {
            // 날짜 표시
            canvas.drawText(calendar.get(Calendar.DATE) + "", xPos, yPos, mPaint);
        }

        if (hasEvent) {
            mPaintBackgroundEvent.setColor(getResources().getColor(mColorEvents[0]));
            canvas.drawRoundRect(xPos - 5, getHeight() / 2 + 20, xPos + 5, getHeight() / 2 + 30, 50f, 50f, mPaintBackground);
        }

    }

댓글 1개:

  1. 질문드립니다..
    혹시 위젯에서도 onDraw를 사용해서 월간 달력 그릴 수 있나요??

    답글삭제