이전글에서 ViewPager를 활용한 월단위 달력을 만들었는데, 여기서는 주 단위 달력을 만들어 보겠다.
참고 사이트 : http://hatti.tistory.com/entry/android-calendar
최종 소스 : 캘린더 소스
위의 달력을 만들기 위해서 필요한 구성은 이전 월단위 달력 구성과 차이가 없다. 다만, 달력 데이터를 주 단위로 구성하는데 그 차이가 있다.
# 주 단위 달력 데이터 만들기
# Fragment로 사용자가 정의한 주단위 달력 ViewGroup을 표시한다.
참고 사이트 : 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; } }