2016년 12월 29일 목요일

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

Standard
이전글에서 ViewPager를 활용한 월단위 달력을 만들었는데, 여기서는 주 단위 달력을 만들어 보겠다.

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

[달력 결과물]

위의 달력을 만들기 위해서 필요한 구성은 이전 월단위 달력 구성과 차이가 없다. 다만, 달력 데이터를 주 단위로 구성하는데 그 차이가 있다.

# 주 단위 달력 데이터 만들기
public void setNumOfWeek(int numOfWeeks) {
        this.numOfMonth = numOfMonth;

        Calendar calendar = Calendar.getInstance();
        // 현재주를 일요일로 설정
        calendar.set(Calendar.DAY_OF_WEEK,Calendar.SUNDAY);
        // 현재 주에서 주 범위를 설정하기 위해서 시작 주를 세팅
        calendar.add(Calendar.WEEK_OF_MONTH, -numOfWeeks);
        // 주 범위는 -numofWeeks ~ +numofWeeks 까지 달력 데이터를 세팅
        for (int i = 0; i < numOfMonth * 2 + 1; i++) {
            listMonthByMillis.add(calendar.getTimeInMillis());
            // 1주 씩 증가
            calendar.add(Calendar.WEEK_OF_MONTH,1);
        }
        // 변경된 데이터를 viewPager에 반영
        notifyDataSetChanged();
    } 
다시 한번 달력구성은 아래 그림과 같이 이루어진다. 여기서는 ViewPager를 제외한 나머지 부분의 소스코드에 대해서 알아 본다.
# Fragment로 사용자가 정의한 주단위 달력 ViewGroup을 표시한다.
public class TaTCalendarWeekFragment extends Fragment {
    private int position;
    private long timeByMillis;
    private TaTCalendarWeekFragment.OnFragmentListener onFragmentListener;
    private View mRootView;
    private TaTCalendarWeekView calendarView;

    public void setOnFragmentListener(TaTCalendarWeekFragment.OnFragmentListener onFragmentListener) {
        this.onFragmentListener = onFragmentListener;
    }

    public interface OnFragmentListener{
        public void onFragmentListener(View view);
    }

    public static TaTCalendarWeekFragment newInstance(int position) {
        TaTCalendarWeekFragment frg = new TaTCalendarWeekFragment();
        Bundle bundle = new Bundle();
        bundle.putInt("position", position);
        frg.setArguments(bundle);
        return frg;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        position = getArguments().getInt("poisition");
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        mRootView = inflater.inflate(R.layout.fragment_tat_calendar_week, null);
        calendarView = (TaTCalendarWeekView) mRootView.findViewById(R.id.calendarweekview);
        Calendar calendar = Calendar.getInstance();
        calendar.setTimeInMillis(timeByMillis);

        for (int i = 0; i < 14; i++) {
            TaTCalendarWeekItemView child = new TaTCalendarWeekItemView(getActivity().getApplicationContext());
            child.setDate(calendar.getTimeInMillis());
            if (i < 7) {
                child.setDayOfWeek(i);
            } else {
                calendar.add(Calendar.DATE, 1);
            }
            calendarView.addView(child);
        }
        return mRootView;
    }

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        if (isVisibleToUser && onFragmentListener != null && mRootView != null) {
            onFragmentListener.onFragmentListener(mRootView);
        }
        super.setUserVisibleHint(isVisibleToUser);
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        if (getUserVisibleHint()) {

            mRootView.post(new Runnable() {
                @Override
                public void run() {
                    // TODO Auto-generated method stub
                    onFragmentListener.onFragmentListener(mRootView);
                }
            });

        }
    }

    public void setTimeByMillis(long timeByMillis) {
        this.timeByMillis = timeByMillis;
    }
}

# ViewGroup을 이용한 달력 레이아웃 만들기 :: Fragment에 담을 사용자 ViewGroup
public class TaTCalendarWeekView extends ViewGroup {
    private final int mScreenWidth; ///< 스크린 가로 사이즈
    private final int mWidthDate; ///< 달력의 한 칸 가로 사이즈
    private long mMillis; ///< 달력 시간 데이터
    private int mDefaultTextSize = 40; ///< 기본 텍스트 크기

    public static String[] DAY_OF_WEEK = null; ///< 요일 리스트[일,월,화,수,목,금,토]

    public TaTCalendarWeekView(Context context, AttributeSet attrs) {
        super(context,attrs);
        mScreenWidth = getResources().getDisplayMetrics().widthPixels;
        mWidthDate = mScreenWidth / 7;
        DAY_OF_WEEK = getResources().getStringArray(R.array.day_of_week);
    }

    /**
     * 자식 view 사이즈(width/height)를 재 설정하는 함수
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();
        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 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;

            ///< 자식뷰의 사이즈를 측정
            measureChild(child,widthMeasureSpec,heightMeasureSpec);

            ///< 이전 자식뷰 상태를 합쳐서 두 상태의 조합을 반영하는 새로운 정수를 반환
            childState = combineMeasuredStates(childState, child.getMeasuredState());
        }
        // 요일과 일자로 항상 7칸 2줄로 이루어짐(mWidthDate 폰 가로 사이즈 / 7 한 크기)
        maxHeight = (int) (2 * (mWidthDate * 0.75));
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
        // 제공된 크기와 모드에 따라 측정 규격을 작성합니다.
        // MEASURED_SIZE_MASK :  getMeasuredWidthAndState()와 getMeasuredWidthAndState()그 실제 측정 된 크기를 제공합니다
        // AT_MOST : 사양 측정 모드 : 그것은 지정된 크기까지 원하는대로 아이로 클 수 있습니다.(wrap_content)
        int expandSpec = MeasureSpec.makeMeasureSpec(MEASURED_SIZE_MASK, MeasureSpec.AT_MOST);
        /**
         * onMeasure 메소드를 오버라이드 하면, setMeasuredDimension 메소드가 호출이 되지 않으므로, 직접 호출해줘야 함
         * setMeasuredDimension(int width, int height)
         * - width : 가로크기
         * - height : 세로크기
         * resolveSizeAndState(int size, int measureSpec, int childMeasuredState)
         * - size : 뷰 가로 크기
         * - measureSpec : 부모에 의해서 측정된 값
         * - childMeasuredState : 뷰의 자식 크기 정보
         * - MEASURED_HEIGHT_STATE_SHIFT :  MEASURED_STATE_MASK 높이에 도착하는 같은 단일의 int로 폭과 높이를 모두 결합 기능에 대한 비트
         *
         * 여기서는 계산된 크기를 부모 뷰에 맞게 사이즈를 조정하도록 값을 설정하였다.
         */
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, expandSpec, childState << MEASURED_HEIGHT_STATE_SHIFT));

        // 최종 측정된 높이 값을 레이아웃에 설정한다.
        LayoutParams params = getLayoutParams();
        params.height = getMeasuredHeight();

    }

    /**
     * 뷰가 그 자식들에게 크기와 위치를 할당할 때 호출됩니다.
     * @param b
     * @param i
     * @param i1
     * @param i2
     * @param i3
     */
    @Override
    protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
        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 h = 0; h < count; h++) {
            View child = getChildAt(h);

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

            /**
             * 제공된 크기와 모드에 따라 측정 규격을 작성합니다.
             * childWidth :  계산된 뷰의 가로 크기
             * childHeight : 계산된 뷰의 세로 크기
             * AT_MOST : 사양 측정 모드 : 그것은 지정된 크기까지 원하는대로 아이로 클 수 있습니다.(wrap_content)
             */
            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;
            }

            /**
             * 뷰의 크기 및 위치를 할당하는 메소드 호출
             * 크기를 측정하고 나서 호출
             * laout(int l, int t, int r, int b)
             * - l : 부모기준으로 왼쪽 위치
             * - t : 부모기준으로 위쪽 위치
             * - r : 부모기준으로 오른쪽 위치
             * - b : 부모기준으로 아래 위치
             */
            child.layout(curLeft, curTop, curLeft + curWidth, curTop + curHeight);

            ///< 현재 세로크기는 최대 세로크기보다 클수 없음
            if (maxHeight < curHeight) {
                maxHeight = curHeight;
            }

            ///< 뷰를 우측으로 정렬하기 위해서 가로크기 만큼 좌페딩값을 더함
            curLeft += curWidth;
        }
    }

    public void setDate(long millis) {
        mMillis = millis;
        invalidate();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    }

    private Paint makePaint(int color) {
        Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
        p.setColor(color);
        p.setTextSize(mDefaultTextSize);
        return p;
    }

    /**
     * 선택된 View의 별도의 이벤트를 처리하기 위한 함수
     * - 여기서는 선택된 View의 색을 변경
     * @param view
     */
    public void setCurrentSelectedView(View view) {
        if (getParent() instanceof ViewPager) {
            ViewPager pager = (ViewPager) getParent();
            View tagView = (View) pager.getTag();
            if (tagView != null) {
                long time = (long) tagView.getTag();
                Calendar c = Calendar.getInstance();
                c.setTimeInMillis(time);
                for (int i = 0; i < pager.getChildCount(); i++) {
                    for (int j = 0; j < getChildCount(); j++) {
                        TaTCalendarWeekItemView child = (TaTCalendarWeekItemView) ((TaTCalendarWeekView) pager.getChildAt(i)).getChildAt(j);
                        if (child == null) {
                            continue;
                        }
                        if (child.isStaticText()) {
                            continue;
                        }
                        if (child.isSameDay((Long) child.getTag(), (Long) tagView.getTag())) {
                            child.invalidate();
                            break;
                        }
                    }
                }
            }
            if (tagView == view) {
                pager.setTag(null);
                return;
            }
            long time = (long) view.getTag();
            Calendar cal = Calendar.getInstance();
            cal.setTimeInMillis(time);
            pager.setTag(view);
            view.invalidate();

        }
    }
}

# 사용자 ViewGroup을 생성하기 위한 XML 정의




# View를 이용한 달력의 구성 Cell 그리기
public class TaTCalendarWeekItemView extends View {
    Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    Paint mPaintBackground = new Paint(Paint.ANTI_ALIAS_FLAG);
    Paint mPaintBackgroundToday = new Paint(Paint.ANTI_ALIAS_FLAG);
    Paint mPaintBackgroundEvent = new Paint(Paint.ANTI_ALIAS_FLAG);
    private Rect rect;
    private long millis;
    private int dp11;
    private int dp16;

    private int dayOfWeek = -1;
    private boolean isStaticText = false;
    private boolean isTouchMode;
    private boolean hasEvent = false;
    private int[] mColorEvents;

    public TaTCalendarWeekItemView(Context context) {
        super(context);
        initialize();
    }

    private void initialize() {
        dp11 = (int) dp2px(getContext(),11);
        dp16 = (int) dp2px(getContext(),16);

        mPaint.setColor(Color.BLACK);
        mPaint.setTextSize(dp11);

        if (Build.VERSION.SDK_INT >= 23) {
            mPaintBackground.setColor(ContextCompat.getColor(getContext(), R.color.colorPrimaryDark));
            mPaintBackgroundToday.setColor(ContextCompat.getColor(getContext(),R.color.colorAccent));
            mPaintBackgroundEvent.setColor(ContextCompat.getColor(getContext(),R.color.colorPrimary));
        }else{
            mPaintBackground.setColor(getContext().getResources().getColor(R.color.colorPrimaryDark));
            mPaintBackgroundToday.setColor(getContext().getResources().getColor(R.color.colorAccent));
            mPaintBackgroundEvent.setColor(getContext().getResources().getColor(R.color.colorPrimary));
        }
        setClickable(true);
        setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                int action = event.getAction();
                Log.d("hatti.onTouchEvent", event.getAction() + "");
                switch (action) {
                    case MotionEvent.ACTION_DOWN:
                        rect = new Rect(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());
                        isTouchMode = true;
                        break;
                    case MotionEvent.ACTION_UP:
                        if (isTouchMode) {
                            ((TaTCalendarWeekView) getParent()).setCurrentSelectedView(v);
                            isTouchMode = false;
                        }
                        break;
                    case MotionEvent.ACTION_CANCEL:
                        isTouchMode = false;
                        break;
                    case MotionEvent.ACTION_MOVE:
                        if (!rect.contains(v.getLeft() + (int) event.getX(), v.getTop() + (int) event.getY())) {
                            isTouchMode = false;
                            return true;
                        }
                        break;
                }
                return false;
            }
        });
        setPadding(30, 0, 30, 0);
    }
    @Override
    public void setBackgroundResource(int resid) {
        super.setBackgroundResource(resid);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        /**
         * Canvas에 중점 구하기.
         * Paint는 Canvas에 그리는 작업을 하는 객체
         * - ascent 는 baseline 위로의 크기이며 descent 는 밑으로의 크기, 두개를 합치면 높이
         */
        int xPos = canvas.getWidth() / 2;
        ///< 실제 그리는 문자나 숫자의 중간이 중점에 위치하기 위해서 Paint의 높이의 절반 만큼을 뺀다.
        int yPos = (int)((canvas.getHeight() /2) - ((mPaint.descent() + mPaint.ascent()) / 2));
        ///< 문자는 가운데 정렬로 그린다.
        mPaint.setTextAlign(Paint.Align.CENTER);
        Calendar calendar = Calendar.getInstance();
        calendar.setTimeInMillis(millis);

        TaTCalendarWeekView taTCalendarWeekView = (TaTCalendarWeekView) getParent();
        if(taTCalendarWeekView.getParent() instanceof ViewPager) {
            ViewGroup parent = (ViewPager) taTCalendarWeekView.getParent();
            TaTCalendarWeekItemView itemView = (TaTCalendarWeekItemView) parent.getTag();
            if (!isStaticText && itemView != null && itemView.getTag() != null && itemView.getTag() instanceof Long) {
                long millis = (long) itemView.getTag();
                if (isSameDay(millis, this.millis)) {
                    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                        canvas.drawRoundRect(xPos - dp16, getHeight() / 2 - dp16, xPos + dp16, getHeight() / 2 + dp16, 50f, 50f, mPaintBackground);
                    }else{
                        canvas.drawRect(xPos - dp16, getHeight() / 2 - dp16, xPos + dp16, getHeight() / 2 + dp16, mPaintBackground);
                    }
                }
            }
        }
        if (!isStaticText && isToday(millis)) {
            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                canvas.drawRoundRect(xPos - dp16, getHeight() / 2 - dp16, xPos + dp16, getHeight() / 2 + dp16, 50f, 50f, mPaintBackgroundToday);
            }else{
                canvas.drawRect(xPos - dp16, getHeight() / 2 - dp16, xPos + dp16, getHeight() / 2 + dp16,  mPaintBackgroundToday);
            }
        }

        if (isStaticText) {
            // 요일 표시
            mPaint.setTypeface(Typeface.create((String)null, Typeface.BOLD));
            canvas.drawText(TaTCalendarWeekView.DAY_OF_WEEK[dayOfWeek], xPos, yPos, mPaint);
        } else {
            // 날짜 표시
            mPaint.setTypeface(Typeface.create(Typeface.DEFAULT_BOLD, Typeface.NORMAL));
            mPaint.setColor(Color.BLACK);

            canvas.drawText(calendar.get(Calendar.DATE) + "", xPos, yPos, mPaint);
        }

        if (hasEvent) {
            if (Build.VERSION.SDK_INT >= 23) {
                mPaintBackgroundEvent.setColor(ContextCompat.getColor(getContext(),mColorEvents[0]));
            }else{
                mPaintBackgroundEvent.setColor(getResources().getColor(mColorEvents[0]));
            }

            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                canvas.drawRoundRect(xPos - 5, getHeight() / 2 + 20, xPos + 5, getHeight() / 2 + 30, 50f, 50f, mPaintBackground);
            }else{
                canvas.drawRect(xPos - 5, getHeight() / 2 + 20, xPos + 5, getHeight() / 2 + 30, mPaintBackground);
            }
        }
    }

    private boolean isToday(long millis) {
        Calendar cal1 = Calendar.getInstance();
        return isSameDay(cal1.getTimeInMillis(), millis);

    }

    public void setDate(long millis) {
        this.millis = millis;
        setTag(millis);
    }

    public void setDayOfWeek(int dayOfWeek) {
        this.dayOfWeek = dayOfWeek;
        isStaticText = true;
    }

    public void setEvent(int... resid) {
        hasEvent = true;
        mColorEvents = resid;
    }
    public boolean isStaticText() {
        return isStaticText;
    }

    public boolean isSameDay(long millis1, long millis2) {
        Calendar cal1 = Calendar.getInstance();
        Calendar cal2 = Calendar.getInstance();
        cal1.setTimeInMillis(millis1);
        cal2.setTimeInMillis(millis2);
        return (cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) && cal1.get(Calendar.MONTH) == cal2.get(Calendar.MONTH) && cal1.get(Calendar.DATE) == cal2.get(Calendar.DATE));
    }

    public static float dp2px(Context context, float dp) {
        return dp * context.getResources().getDisplayMetrics().density;
    }
}

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);
        }

    }