[Android]自定義簡易版日曆控件

先來看看效果圖,看看是不是各位大佬想要的:

特別的功能並不多,重點是講解簡易日曆該如何構造,假若是項目着急要用的話,最好還是找一下其它人寫好的日曆(附加滑動改變日曆日期等功能)


---------------------------------------------------------------------------------------華麗的分割線-------------------------------------------------------------------------------------


根據所輸入的年份以及月份顯示那個月份視圖,並且能監聽日期的點擊,這便是該日曆需要實現的功能。

和以往一樣,當我們拿到一份需求時,先彆着急敲代碼,先思考功能的實現分爲多少部分,然後再一步步地去實現,在這裏,我大致將其分爲以下這幾個功能:

1、獲得某月份的日期分佈;

2、將獲得的日期分佈繪畫在畫布上;

3、監聽日期點擊事件。

咋看功能也不多,也就三個,下面將一步步講解:


(1)獲得某月份的日期分佈

在實現這個功能時,我選擇了Calendar這個類,翻譯過來就是日曆類,它是個抽象類,但是卻提供了以下方法讓我們獲取一下日曆值,例如獲取得到下列日曆值:

		int year = Calendar.getInstance().get(Calendar.YEAR);//獲取當前年份
		int month = Calendar.getInstance().get(Calendar.MONTH) + 1;// +1是因爲返回來的值並不是代表月份,而是對應於Calendar.MAY常數的值,
		// Calendar在月份上的常數值從Calendar.JANUARY開始是0,到Calendar.DECEMBER的11
		int dayOfMonth = Calendar.getInstance().get(Calendar.DAY_OF_MONTH);// 獲取當前的時間爲該月的第幾天
		int dayOfWeek = Calendar.getInstance().get(Calendar.DAY_OF_WEEK);//獲取當前的時間爲該周的第幾天(需要注意的是,一週的第一天爲週日,值爲1)
其實,使用以上這些便足夠可以求出我們這部分所需要的功能了。

不過,就是上面的代碼都是獲得當日的日曆類,因此所獲得的日期都是本日的,假如我們想看之前的日期或之後的日期呢,這就需要我們去手動改變它了:

		Calendar calendar = Calendar.getInstance();
		calendar.set(year, month, day);

假如我們calendar.set(year, month, day);中的day設置爲1,然後便可以通過int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);方式獲得該月的第一天是周幾,於是第一部分我們想要解決的功能就完成!

嗯?不懂?好像說得有些快,我再仔細說說:

每個月的天數差不多是固定的(除了閏年的二月變成29),都是(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)

因此我們可以很明確的通過月份確定這個月的天數,然後,假若我們知道了這個月的第一天,然後在這天后不斷加上天數,直到填滿這個月的天數,這樣,我們就可以知道這個月的日期排布了。例如:

具體的實現代碼,我封裝了一下,大家可以參考一下:DateUtil.java

調用public static int[][] getMonthNumFromDate(int year, int month)方法獲得日曆日期數組,大家可以輸出測試看看

package com.xiaoyan.mycalendar;

import java.util.Calendar;

/**
 * 用於處理日期工具類
 * 
 * @author xiejinxiong
 * 
 */
public class DateUtil {

	/**
	 * 獲取當前年份
	 * 
	 * @return
	 */
	public static int getYear() {
		return Calendar.getInstance().get(Calendar.YEAR);
	}

	/**
	 * 獲取當前月份
	 * 
	 * @return
	 */
	public static int getMonth() {
		return Calendar.getInstance().get(Calendar.MONTH) + 1;// +1是因爲返回來的值並不是代表月份,而是對應於Calendar.MAY常數的值,
		// Calendar在月份上的常數值從Calendar.JANUARY開始是0,到Calendar.DECEMBER的11
	}

	/**
	 * 獲取當前的時間爲該月的第幾天
	 * 
	 * @return
	 */
	public static int getCurrentMonthDay() {
		return Calendar.getInstance().get(Calendar.DAY_OF_MONTH);
	}

	/**
	 * 獲取當前的時間爲該周的第幾天
	 * 
	 * @return
	 */
	public static int getWeekDay() {
		return Calendar.getInstance().get(Calendar.DAY_OF_WEEK);
	}

	/**
	 * 獲取當前時間爲該天的多少點
	 * 
	 * @return
	 */
	public static int getHour() {
		return Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
		// Calendar calendar = Calendar.getInstance();
		// System.out.println(calendar.get(Calendar.HOUR_OF_DAY)); // 24小時制
		// System.out.println(calendar.get(Calendar.HOUR)); // 12小時制
	}

	/**
	 * 獲取當前的分鐘時間
	 * 
	 * @return
	 */
	public static int getMinute() {
		return Calendar.getInstance().get(Calendar.MINUTE);
	}

	/**
	 * 通過獲得年份和月份確定該月的日期分佈
	 * 
	 * @param year
	 * @param month
	 * @return
	 */
	public static int[][] getMonthNumFromDate(int year, int month) {
		Calendar calendar = Calendar.getInstance();
		calendar.set(year, month - 1, 1);// -1是因爲賦的值並不是代表月份,而是對應於Calendar.MAY常數的值,

		int days[][] = new int[6][7];// 存儲該月的日期分佈

		int firstDayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);// 獲得該月的第一天位於周幾(需要注意的是,一週的第一天爲週日,值爲1)

		int monthDaysNum = getMonthDaysNum(year, month);// 獲得該月的天數
		// 獲得上個月的天數
		int lastMonthDaysNum = getLastMonthDaysNum(year, month);

		// 填充本月的日期
		int dayNum = 1;
		int lastDayNum = 1;
		for (int i = 0; i < days.length; i++) {
			for (int j = 0; j < days[i].length; j++) {
				if (i == 0 && j < firstDayOfWeek - 1) {// 填充上個月的剩餘部分
					days[i][j] = lastMonthDaysNum - firstDayOfWeek + 2 + j;
				} else if (dayNum <= monthDaysNum) {// 填充本月
					days[i][j] = dayNum++;
				} else {// 填充下個月的未來部分
					days[i][j] = lastDayNum++;
				}
			}
		}

		return days;

	}

	/**
	 * 根據年數以及月份數獲得上個月的天數
	 * 
	 * @param year
	 * @param month
	 * @return
	 */
	public static int getLastMonthDaysNum(int year, int month) {

		int lastMonthDaysNum = 0;

		if (month == 1) {
			lastMonthDaysNum = getMonthDaysNum(year - 1, 12);
		} else {
			lastMonthDaysNum = getMonthDaysNum(year, month - 1);
		}
		return lastMonthDaysNum;

	}

	/**
	 * 根據年數以及月份數獲得該月的天數
	 * 
	 * @param year
	 * @param month
	 * @return 若返回爲負一,這說明輸入的年數和月數不符合規格
	 */
	public static int getMonthDaysNum(int year, int month) {

		if (year < 0 || month <= 0 || month > 12) {// 對於年份與月份進行簡單判斷
			return -1;
		}

		int[] array = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };// 一年中,每個月份的天數

		if (month != 2) {
			return array[month - 1];
		} else {
			if (year % 4 == 0 && year % 100 != 0 || year % 400 == 0) {// 閏年判斷
				return 29;
			} else {
				return 28;
			}
		}

	}
}


(2)將獲得的日期分佈繪畫在畫布上

首先,我們還是複習一下比較基礎的知識:

		Paint paintText = new Paint();
		paintText.setTextSize(30);
		paintText.setColor(Color.BLACK);
		canvas.drawText("25", 100, 100, paintText);
從以上代碼便可以繪畫出一個數字:

但是,這樣還是不行的,因爲繪畫文字的時候,它並不是以繪畫的那個中心點爲中心繪製,而是在中心點的右上方。(呃,好像這樣有些難看出,我加兩條直線比較一下:)

		Paint paintText = new Paint();
		paintText.setTextSize(30);
		paintText.setColor(Color.BLACK);
		canvas.drawText("25", 100, 100, paintText);
		canvas.drawLine(0, 100, 200, 100, paintText);
		canvas.drawLine(100, 0, 100, 200, paintText);

這樣就非常明顯,假如我們不加改動便把日期數字繪畫上去,將會發現日曆表格的數字是偏右的,爲了杜絕這個問題,我們需要知道所繪製的文字的寬高,已便於對其進行適當的偏移:

a.獲得文字的寬度:

paintText.measureText("25");

b.獲得文字的高度:

		FontMetrics fm = paintText.getFontMetrics();
		float fontHeight = (float) Math.ceil(fm.descent - fm.top)/2;

由上面這些,我們便可以非常容易的將數字居中顯示了:

		Paint paintText = new Paint();
		paintText.setTextSize(30);
		paintText.setColor(Color.BLACK);
		FontMetrics fm = paintText.getFontMetrics();
		float fontHeight = (float) Math.ceil(fm.descent - fm.top)/2;
		canvas.drawText("25", 100-paintText.measureText("25")/2f, 100+fontHeight/2, paintText);
		canvas.drawLine(0, 100, 200, 100, paintText);
		canvas.drawLine(100, 0, 100, 200, paintText);

好了,數字居中的問題解決了,但是,日曆表格的大小還未確定,還需要動態獲取控件大小以計算出表格大小:

	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		// TODO Auto-generated method stub
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
		// 獲得控件寬度
		width = getMeasuredWidth();
		// 計算日曆表格寬度
		dateNumWidth = width / 7.0f;
		// 計算日曆高度
		height = (int) (dateNumWidth * 6);
		// 設置控件寬高
		setMeasuredDimension(width, height);
	}
ok,所需要的重要東西都拿到了,現在只要根據所獲得的日曆數組,進行遍歷繪製日期數字即可。相同的,我也進行封裝,大家也可以參考一下:

package com.example.calendartest;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.FontMetrics;
import android.util.AttributeSet;
import android.view.View;

public class CalendarViewTest extends View {

	/**
	 * 使用枚舉表示日期狀態(今天、本月、非本月)
	 * 
	 * @author xiejinxiong
	 * 
	 */
	public static enum CalendarState {
		TODAY, CURRENT_MONTH, NO_CURRENT_MONTH
	}

	/** 屏幕寬度 */
	private int width;
	/** 屏幕高度 */
	private int height;
	/** 日曆數組 */
	private int[][] dateNum;
	/** 日曆日期狀態數組 */
	private CalendarState[][] calendarStates;
	/** 年 */
	private int year;
	/** 月 */
	private int month;
	/** 繪畫類 */
	private DrawCalendar drawCalendar;
	/** 日曆表格寬度 */
	private float dateNumWidth;

	public CalendarViewTest(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		initUI(context);
	}

	public CalendarViewTest(Context context, AttributeSet attrs) {
		super(context, attrs);
		initUI(context);
	}

	public CalendarViewTest(Context context) {
		super(context);
		initUI(context);
	}


	/**
	 * 初始化UI
	 * 
	 * @param context
	 */
	private void initUI(Context context) {
		// 初始化日期
		year = DateUtil.getYear();
		month = DateUtil.getMonth();

		calendarStates = new CalendarState[6][7];

		drawCalendar = new DrawCalendar(year, month);

	}

	/**
	 * 設置日曆時間並刷新日曆視圖
	 * 
	 * @param year
	 * @param month
	 */
	public void setYearMonth(int year, int month) {
		this.year = year;
		this.month = month;
		drawCalendar = new DrawCalendar(year, month);
		invalidate();
	}

	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		// TODO Auto-generated method stub
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
		// 獲得控件寬度
		width = getMeasuredWidth();
		// 計算日曆表格寬度
		dateNumWidth = width / 7.0f;
		// 計算日曆高度
		height = (int) (dateNumWidth * 6);
		// 設置控件寬高
		setMeasuredDimension(width, height);
	}

	@Override
	protected void onDraw(Canvas canvas) {
		// TODO Auto-generated method stub
		super.onDraw(canvas);
		drawCalendar.drawCalendarCanvas(canvas);

	}

	/**
	 * 封裝繪畫日曆方法的繪畫類
	 * 
	 * @author xiejinxiong
	 * 
	 */
	class DrawCalendar {

		/** 繪畫日期畫筆 */
		private Paint mPaintText;
		/** 繪畫本日的藍圓 背景的畫筆 */
		private Paint mPaintCircle;
		/** 字體高度 */
		private float fontHeight;

		public DrawCalendar(int year, int month) {
			// 獲得月份日期排布數組
			dateNum = DateUtil.getMonthNumFromDate(year, month);
			// 初始化繪畫文本的畫筆
			mPaintText = new Paint();
			mPaintText.setTextSize(25);
			mPaintText.setColor(Color.GRAY);// 設置灰色
			mPaintText.setAntiAlias(true);// 設置畫筆的鋸齒效果。
			// 獲得字體高度
			FontMetrics fm = mPaintText.getFontMetrics();
			fontHeight = (float) Math.ceil(fm.descent - fm.top) / 2;

			// 初始化繪畫圓圈的畫筆
			mPaintCircle = new Paint();
			mPaintCircle.setColor(Color.argb(100, 112, 199, 244));// 設置藍色
			mPaintCircle.setAntiAlias(true);// 設置畫筆的鋸齒效果。
		}

		/**
		 * 繪畫日曆
		 * 
		 * @param canvas
		 */
		public void drawCalendarCanvas(Canvas canvas) {
			// canvas.drawCircle(width/2, width/2, width/2, mPaint);// 畫圓
			for (int i = 0; i < dateNum.length; i++) {
				for (int j = 0; j < dateNum[i].length; j++) {

					if (i == 0 && dateNum[i][j] > 20) {// 上個月的日期
						drawCalendarCell(i, j, CalendarState.NO_CURRENT_MONTH,
								canvas);
					} else if ((i == 5 || i == 4) && dateNum[i][j] < 20) {// 下個月的日期
						drawCalendarCell(i, j, CalendarState.NO_CURRENT_MONTH,
								canvas);
					} else {// 本月日期
						if (dateNum[i][j] == DateUtil.getCurrentMonthDay()) {// 是否爲今天的日期號
							if (year == DateUtil.getYear()
									&& month == DateUtil.getMonth()) {// 是否爲今年今月
								drawCalendarCell(i, j, CalendarState.TODAY,
										canvas);
							}
							drawCalendarCell(i, j, CalendarState.CURRENT_MONTH,
									canvas);
						} else {
							drawCalendarCell(i, j, CalendarState.CURRENT_MONTH,
									canvas);
						}
					}
				}
			}
		}

		/**
		 * 繪畫日曆表格
		 * 
		 * @param i
		 *            橫序號
		 * @param j
		 *            列序號
		 * @param state
		 *            狀態
		 * @param canvas
		 *            畫布
		 */
		private void drawCalendarCell(int i, int j, CalendarState state,
				Canvas canvas) {
			switch (state) {
			case TODAY:// 今天
				calendarStates[i][j] = CalendarState.TODAY;
				mPaintText.setColor(Color.WHITE);
				canvas.drawCircle(dateNumWidth * j + dateNumWidth / 2,
						dateNumWidth * i + dateNumWidth / 2, dateNumWidth / 2,
						mPaintCircle);
				break;
			case CURRENT_MONTH:// 本月
				calendarStates[i][j] = CalendarState.CURRENT_MONTH;
				mPaintText.setColor(Color.BLACK);
				break;
			case NO_CURRENT_MONTH:// 非本月
				calendarStates[i][j] = CalendarState.NO_CURRENT_MONTH;
				mPaintText.setColor(Color.GRAY);
				break;
			default:
				break;
			}
			// 繪畫日期
			canvas.drawText(dateNum[i][j] + "", dateNumWidth * j + dateNumWidth
					/ 2 - mPaintText.measureText(dateNum[i][j] + "") / 2,
					dateNumWidth * i + dateNumWidth / 2 + fontHeight / 2.0f,
					mPaintText);
		}
	}
}
(3)監聽日期點擊事件

監聽日期點擊是比較簡單的,只要我們監聽到所點擊的位置(x,y),然後將該位置除以日曆表格寬度,便可以得到日曆表格數組的i,j下標,由此,便可以輕而易舉地獲得所點擊的日期。

不過,有些需要注意的是,由於手指點下去的位置和手指鬆開的位置或許有些不同,因此,我們需要設置一個距離,在一個合適的距離內,我們便可以認爲是一種點擊事件。這設置這個距離時,我使用的是ViewConfiguration.get(context).getScaledTouchSlop(),這便是滑動的最小距離,因爲一般日曆都具有滑動的功能,使用該距離,可以避免與滑動事件有衝突。

當然,只是監聽還是不夠的,我們要提供一個回調接口供用戶使用,便於執行點擊事件的相關代碼。

 touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();

	@Override
	public boolean onTouchEvent(MotionEvent event) {

		switch (event.getAction()) {
		case MotionEvent.ACTION_DOWN:
			// 記錄點擊的座標
			touchX = event.getX();
			touchY = event.getY();
			break;
		case MotionEvent.ACTION_UP:
			float touchLastX = event.getX();
			float touchLastY = event.getY();
			if (Math.abs(touchLastX - touchX) < touchSlop
					&& Math.abs(touchLastY - touchY) < touchSlop) {// 判斷是否符合正常點擊
				// 計算出所點擊的數組序列
				int dateNumX = (int) (touchLastX / dateNumWidth);
				int dateNumY = (int) (touchLastY / dateNumWidth);
				// 使用回調函數響應點擊日曆日期
				onCalendarClickListener.onCalendaeClick(
						dateNum[dateNumY][dateNumX],
						calendarStates[dateNumY][dateNumX]);
			}
			break;
		default:
			break;
		}
		return true;
	}
	/**
	 * 日曆監聽類
	 * 
	 * @author xiejinxiong
	 * 
	 */
	public interface OnCalendarClickListener {

		/**
		 * 日曆日期點擊監聽
		 * 
		 * @param dateNum
		 *            日期數字
		 * @param calendarState
		 *            日期狀態
		 */
		public void onCalendaeClick(int dateNum, CalendarState calendarState);

	}

通過以上內容,估計你對於日曆的製作應該會有着自己的想法,你可以按照這種思路去設計自己想要的自定義日曆。

源碼:http://download.csdn.net/detail/u011596810/9478962









發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章