安卓自定義日曆選擇器
哈哈,又要更新部落格了+_+!
這一次發一個日曆選擇器吧!
說實話,看到需求那一刻,我絕對是崩潰的,不過還好有github這個大佬罩著我,所以為了避免重複造輪子,我就去上面找了一下日曆選擇器,一搜一大把,剛開始挺高興的,結果後來越看臉越黑,沒一個符合我的需求嘛,怎麼搞得。。。((╯‵□′)╯︵┻━┻)後來沒辦法了,只能自己寫了,神啊,給我力量吧!!!
首先確定一下需要的效果:其實就是多選日期,聽起來挺簡單的,可是做起來真的要崩潰啊!嗷嗚~~(效果如下)
網上是挺多日期多選的三方,但是很多改起來費時間,而且還有用Fragment寫出來的,一下快取200個頁面,我的心裡有10000只XX馬奔騰而過啊,不過這個日曆還是很好做的。
1.首先,今天之前的日期不能選擇,並且呈灰色顯示
2.選中的日期,連續2個不顯示色帶,連續3個要顯示色帶
確定了這兩個要求,那麼久開始敲程式碼吧!
還是和之前的自定義View一樣,確定自定義屬性:
- 看圖片就知道選中顏色和色帶顏色這個是必須的吧
- 然後就是字型的大小
- 可以選擇的字型顏色
- 不可以選擇的字型顏色
右上角那個切換月份的ImageView可以忽略不寫進去,不然這個控制元件的邏輯就會變的複雜,通用性不強
先說一下畫控制元件的思路吧:
- 先用一行畫年月
- 畫星期
- 畫選中的圓形以及色帶
- 然後計算日期做成一堆List資料
- 然後根據資料使用for迴圈畫上去(ps:5.0的系統使用遞迴函式畫沒有問題,但是5.0以下的使用遞迴會丟擲堆疊溢位,原因是遞迴太深了,所以我改成了for迴圈)
- 丟擲一些方法
首先是定義的屬性:
private Context mContext;
//定義屬性
private final int TOTAL_COLUMS = 7;//列數
private int TOTAL_ROW = 0;//行數
private int mSelectColor;//預設紅色
private int num = 31;//定義可以選擇的天數
private int mColumWidth;//列寬
private int mColumHeith;//列高
private int textSize = 28;//字型大小
private int enableColor;//可編輯字型顏色
private int unenableColor;//不可編輯字型顏色
private int mBgSelectColor;//色帶顏色
private int MONTH = 0;
//定義畫筆
private Paint mSelectPaint;
private Paint mContiuousPaint;
private Paint mMonthPaint;
private Paint mWeekPaint;
private Paint mTextPaint;
private int mViewWidth;//檢視寬度
private int mViewHeigh;//檢視高度
//其他屬性
private int curYear;
private int curMonth;
private int MaxSize = 0;
private boolean isFirst = true;
private boolean canClick = true;
private boolean needClear = true;
private boolean canEdit = true;
private List<DateModel> mDatas = new ArrayList<>();
private List<String> selectDatas = new ArrayList<>();
private OnDateClickListener dateClickListener;
定義完屬性之後就開始重寫View的建構函式,用到自定義屬性,所以就要重寫3個建構函式
自定義的屬性:
<declare-styleable name="CalendarSelectView">
<attr name="selectColor" format="color"/>
<attr name="CalendartextSize" format="dimension"/>
<attr name="enableColor" format="color"/>
<attr name="unenableColor" format="color"/>
<attr name="bgSelectColor" format="color"/>
</declare-styleable>
重寫的建構函式:
public CalendarSelectView(Context context) {
this(context, null);
}
public CalendarSelectView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public CalendarSelectView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CalendarSelectView);
mSelectColor = a.getColor(R.styleable.CalendarSelectView_selectColor, Color.parseColor("#1FCD6D"));
textSize = a.getDimensionPixelOffset(R.styleable.CalendarSelectView_CalendartextSize,
DensityUtil.sp2px(context, 14));
enableColor = a.getColor(R.styleable.CalendarSelectView_enableColor, Color.BLACK);
unenableColor = a.getColor(R.styleable.CalendarSelectView_unenableColor, Color.LTGRAY);
mBgSelectColor = a.getColor(R.styleable.CalendarSelectView_bgSelectColor, Color.parseColor("#D6FFE9"));
a.recycle();
init();
}
然後是初始化畫筆:
public void init() {
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setTextSize(textSize);
mMonthPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mMonthPaint.setTextSize(textSize);
mMonthPaint.setColor(Color.GRAY);
mWeekPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mWeekPaint.setColor(Color.BLACK);
mWeekPaint.setStyle(Paint.Style.STROKE);
mWeekPaint.setTextSize(DensityUtil.sp2px(mContext, 14));
mSelectPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mSelectPaint.setStyle(Paint.Style.FILL);
mSelectPaint.setColor(mSelectColor);
mContiuousPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mContiuousPaint.setStyle(Paint.Style.FILL);
mContiuousPaint.setColor(mBgSelectColor);
setOnTouchListener(this);
}
搞定這些後就要開始測量佈局了:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
mViewWidth = widthSize;
mColumWidth = mViewWidth / TOTAL_COLUMS;
mColumHeith = mColumWidth - DensityUtil.dip2px(mContext, 5);
TOTAL_ROW = (DateUtils.getMonthOfAllDay(curYear, curMonth) / 7) + 2;
mViewHeigh = TOTAL_ROW * mColumHeith;
MaxSize = (TOTAL_ROW - 2) * 7;
setMeasuredDimension(mViewWidth, mViewHeigh);
}
首先解釋一下,寬度就是鋪滿螢幕的,受父佈局的控制,然後就是計算每一列的列寬用整個控制元件的寬度直接除7得到平均的寬,然後列高就是列寬 - 5dp轉成的px,然後才開始算總的行數,因為年月和星期用了2行,所以要加上2,日期需要的行數就是當月的天數 / 7,這個應該不難吧,然後就開始算控制元件的高度了,這個簡單啦,直接用 行數 * 列高,最後呼叫一下setMeasureDimension();就打算測量結束了。
長話短說吧,測量完了就開始要畫了,那麼就直接重寫onDraw(Canvas canvas)就好了:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int week = DateUtils.getDayofWeek(calendar, 1);
calendar.set(Calendar.DATE, 1);
calendar.add(Calendar.DAY_OF_WEEK, -week + 1);
drawMonth(canvas);
drawWeek(canvas);
drawRow(canvas);
}
解釋一下:
drawMonth就是畫年月的
private void drawMonth(Canvas canvas) {
Rect c = new Rect();
String text = String.valueOf(curMonth + 1) + "月 " + String.valueOf(curYear);
mMonthPaint.getTextBounds(text, 0, text.length(), c);
canvas.drawText(text, 30, (mColumWidth + c.height()) / 3, mMonthPaint);
}
drawWeek就是畫星期的
String[] week = new String[]{"日", "一", "二", "三", "四", "五", "六"};
private void drawWeek(Canvas canvas) {
for (int i = 0; i < week.length; i++) {
Rect bound = new Rect();
mWeekPaint.getTextBounds(week[i], 0, week[i].length(), bound);
int x = i * mColumWidth + (mColumWidth - bound.width()) / 2;
canvas.drawText(week[i], x, mColumHeith / 2 + mColumHeith, mWeekPaint);
}
}
drawRow就是畫日期的
private void drawRow(Canvas canvas) {
for (int i = 2; i < TOTAL_ROW; i++) {
calendar.setTime(calendar.getTime());
if (mDatas.size() < MaxSize) {
initData(calendar, 1, i);
}
}
if (needClear) {
clearData(mDatas);
needClear = false;
}
if (selectDatas.size() > 0) {
if (!canEdit) {
findSelectData();
}else{
if (isFirst) {
// dealData(selectDatas, 0, 0);
dealData();
isFirst = false;
}
}
}
drawDay(canvas);
}
//處理資料
private void dealData(){
for (int i=0;i<mDatas.size();i++){
for (int j=0;j<selectDatas.size();j++){
if (mDatas.get(i).getDate().equals(selectDatas.get(j))){
mDatas.get(i).setSelect(true);
break;
}else{
if (j == selectDatas.size() - 1){
mDatas.get(i).setSelect(false);
}
}
}
}
}
//清除資料
//清理data的選中資料
private void clearData(List<DateModel> datas){
for (int i=0;i<datas.size();i++){
datas.get(i).setSelect(false);
}
}
可能大家會有點看不動這個drawRow裡面的東西,其實這個方法我是後面改過來了,重要的地方是迴圈和drawDay這個兩個方法,迴圈裡面其實是為了算日期的,按照順序把日期算出來,然後存成一個List:
private void initData(Calendar time, int colum, int row) {
DateModel model = new DateModel(time.getTime());
model.setColums(colum);
model.setRow(row);
long diff = time.getTime().getTime() - System.currentTimeMillis();
long day = (long) Math.ceil(diff / (3600 * 24 * 1000));//天數距離當前天數
boolean isCurrentMonth = DateUtils.isCurrentMonthDay(time, curYear, MONTH);
if (day >= 0 && day < num - 1 && isCurrentMonth) {//當天或者最大可選擇數目之間則可編輯
model.setCanSelect(true);
} else {
model.setCanSelect(false);
}
if (!DateUtils.isCurrentMonthDay(time, curYear, MONTH)) {
model.setCurrentMonth(false);
} else {
model.setCurrentMonth(true);
}
mDatas.add(model);//儲存到list
time.add(Calendar.DATE, 1);
if (colum != 7) {
colum += 1;
initData(time, colum, row);
}
}
其實每一個日期都是一個實體,根據當天日期和傳入的可選擇的最大天數去判斷是否可以選擇,可以的就把實體的canselect修改成true,否則就是false,這樣方便我們記錄它的資訊:
private class DateModel {
private String date;
private boolean canSelect;
private int colums;
private int row;
private boolean isSelect;
private boolean isCurrentMonth;
private int selectColor = Color.parseColor("#F24949");
/**
* 狀態標記值
* 1 左邊開始圓形帶色帶
* -1 色帶
* 2 右邊結束圓形帶色帶
* 3 不帶色帶的圓形
*/
private int area;
public DateModel(Date date) {
SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd");
this.date = format.format(date);
}
public boolean isCurrentMonth() {
return isCurrentMonth;
}
public void setCurrentMonth(boolean currentMonth) {
isCurrentMonth = currentMonth;
}
public int getArea() {
return area;
}
public void setArea(int area) {
this.area = area;
}
public int getDay() {
if (TextUtils.isEmpty(date)) {
return 0;
} else {
return Integer.parseInt(date.substring(6, date.length()));
}
}
public int getSelectColor() {
return selectColor;
}
public void setSelectColor(int selectColor) {
this.selectColor = selectColor;
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
public boolean isSelect() {
return isSelect;
}
public void setSelect(boolean select) {
isSelect = select;
}
public int getRow() {
return row;
}
public void setRow(int row) {
this.row = row;
}
public int getColums() {
return colums;
}
public void setColums(int colums) {
this.colums = colums;
}
public boolean isCanSelect() {
return canSelect;
}
public void setCanSelect(boolean canSelect) {
this.canSelect = canSelect;
}
public String toString() {
return date;
}
}
計算好了之後就開始drawDay()了,在drawDay()中我迴圈這個算好的日期的list,然後一邊算每一個日期的位置然後畫出來:
private void drawDay(Canvas canvas) {
for (int i = 0; i < mDatas.size(); i++) {
Rect bound = new Rect();
String text = String.valueOf(mDatas.get(i).getDay() == 0 ? "" : mDatas.get(i).getDay());
mTextPaint.getTextBounds(text, 0, text.length(), bound);
int x = (mDatas.get(i).getColums() - 1) * mColumWidth + (mColumWidth - bound.width()) / 2;
int y = 2 * mColumHeith + bound.height() + ((mDatas.get(i).getRow() - 2) * mColumHeith);
mTextPaint.setColor(mDatas.get(i).isCanSelect() ? enableColor : unenableColor);
int centerx = (mDatas.get(i).getColums() - 1) * mColumWidth + (mColumWidth / 2);
int centery = 2 * mColumHeith + (bound.height() / 2) + ((mDatas.get(i).getRow() - 2) * mColumHeith);
if (mDatas.get(i).isCanSelect()) {
if (canEdit) {
if (!mDatas.get(i).isSelect()) {
mSelectPaint.setColor(Color.TRANSPARENT);
mTextPaint.setColor(enableColor);
} else {
mSelectPaint.setColor(mSelectColor);
mTextPaint.setColor(Color.WHITE);
}
drawCircle(canvas, centerx, centery, mSelectPaint);
} else {
int left = (mDatas.get(i).getColums() - 1) * mColumWidth;
int top = 2 * mColumHeith - (mColumHeith / 2) + (bound.height() / 2) +
((mDatas.get(i).getRow() - 2) * mColumHeith);
mContiuousPaint.setColor(mBgSelectColor);
if (mDatas.get(i).isSelect()) {
if (mDatas.get(i).getArea() == -1) {
mTextPaint.setColor(enableColor);
//畫色帶
drawRibbon(canvas, left, top, mDatas.get(i).getArea(), mContiuousPaint);
} else if (mDatas.get(i).getArea() == 1 || mDatas.get(i).getArea() == 2
|| mDatas.get(i).getArea() == 3) {
//畫圓
mSelectPaint.setColor(mSelectColor);
mTextPaint.setColor(Color.WHITE);
drawRibbon(canvas, left, top, mDatas.get(i).getArea(), mContiuousPaint);
drawCircle(canvas, centerx, centery, mSelectPaint);
}
}
}
}
if (mDatas.get(i).isCurrentMonth()) {
canvas.drawText(text, x, y, mTextPaint);
} else {
canvas.drawText("", x, y, mTextPaint);
}
}
}
通過實體裡面記錄的colums以及rows去計算位置並且判斷哪些日期可以被選擇,哪些不可以,然後把畫筆的顏色根據狀態去改變,最後畫出來,在畫的時候還要判斷是否可以編輯(canEdit)狀態,true的話我們就要判斷這個實體的是否被選中,選中的話,那麼畫筆就要改變顏色,然後我們先畫圓跟色帶,最後才畫文字,不然文字會被圓或者色帶給遮擋住,在畫圓和色帶的時候我們需要判斷每個實體的area,根據area的值去畫,具體的在實體類上面有寫著area的解釋
畫圓和色帶的方法我也貼上來:
private void drawCircle(Canvas canvas, int centerX, int centerY, Paint paint) {
canvas.drawCircle(centerX, centerY, (float) (mColumHeith / 2 * 0.85), paint);
}
private void drawRibbon(Canvas canvas, int x, int y, int type, Paint paint) {
Rect ribbon = new Rect();
int interval = (int) ((mColumHeith - (mColumHeith * 0.7)) / 2);
if (type == -1 || type == 2) {
ribbon.left = x;
} else if (type == 1) {
//畫右邊一半的色帶
ribbon.left = x + (mColumWidth / 2);
}
ribbon.top = y + interval;
if (type == -1 || type == 1) {
ribbon.right = x + mColumWidth;
} else if (type == 2) {
//畫左邊一半的色帶
ribbon.right = x + (mColumWidth / 2);
}
ribbon.bottom = y + mColumHeith - interval;
canvas.drawRect(ribbon, paint);
}
處理選中資料的方法:
private void findSelectData(){
for(int i=0;i<mDatas.size();i++){
for(int j = 0;j < selectDatas.size();j++){
if (mDatas.get(i).toString().equals(selectDatas.get(j))){
mDatas.get(i).setSelect(true);
if (i != 0){
if (mDatas.get(i - 1).isSelect()){
if (mDatas.get(i - 1).getArea() != 2){
mDatas.get(i).setArea(-1);
}else{
mDatas.get(i).setArea(1);
}
if (mDatas.get(i).isCanSelect() && mDatas.get(i - 1).isCanSelect() && mDatas.get(i - 1).getArea() == 3) {
mDatas.get(i - 1).setArea(1);
}
}else{
mDatas.get(i).setArea(3);
}
}else{
mDatas.get(i).setArea(3);
}
break;
}else{
if (i != 0) {
if (mDatas.get(i - 1).isSelect()) {
if (j == selectDatas.size() - 1) {
mDatas.get(i).setSelect(false);
if (i > 1) {
if (mDatas.get(i - 2).isSelect() && mDatas.get(i - 2).getArea() == -1) {
mDatas.get(i - 1).setArea(2);
} else {
mDatas.get(i - 1).setArea(3);
if (mDatas.get(i - 2).isSelect()) {
mDatas.get(i - 2).setArea(3);
}
}
} else {
if (mDatas.get(i - 1).isSelect()) {
mDatas.get(i - 1).setArea(3);
}
}
break;
}
}
}
}
}
}
}
然後現在主要就是丟擲方法了,從外面傳資料進來控制元件需要處理的方法:
/**
* 重新整理日期
*/
public void setDate(Calendar calendar, List<String> datas) {
this.calendar = calendar;
selectDatas.clear();
selectDatas.addAll(datas);
fillData(selectDatas);
curYear = calendar.get(Calendar.YEAR);
curMonth = calendar.get(Calendar.MONTH);
MONTH = curMonth;
mDatas.clear();
//重新測量佈局
requestLayout();
}
//排序List
public void fillData(List<String> data) {
for (int i = 0; i < data.size(); i++) {
for (int j = i + 1; j < data.size(); j++) {
if (Integer.parseInt(data.get(i)) > Integer.parseInt(data.get(j))) {
String temp = data.get(i);
data.set(i, data.get(j));
data.set(j, temp);
}
}
}
}
恩~差點漏了,還有一個重點,就是控制元件的觸控事件:
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
if (canEdit && canClick) {
int colums = (int) Math.ceil(event.getX() / mColumWidth);
int row = (int) Math.ceil(event.getY() / mColumHeith) - 1;
for (int i = 0; i < mDatas.size(); i++) {
if (mDatas.get(i).getColums() == colums && mDatas.get(i).getRow() == row
&& mDatas.get(i).isCanSelect()) {
if (mDatas.get(i).isSelect()) {
mDatas.get(i).setSelect(false);
if (dateClickListener != null) {
dateClickListener.onUnSelect(mDatas.get(i).toString());
}
} else {
mDatas.get(i).setSelect(true);
if (dateClickListener != null) {
dateClickListener.onSelect(mDatas.get(i).toString());
}
}
postInvalidate();
}
}
}
break;
}
return true;
}
我這裡直接用了手指擡起的事件,在擡起的時候獲取X和Y,然後計算當前的所點選的行和列,然後對應到實體的行和列,符合就標記為選中狀態,然後呼叫postInvalidate()重新整理控制元件,然後再控制元件裡面寫一個點選的回撥OnDateClickListener,然後在Touch事件中呼叫,就可以了
public interface OnDateClickListener {
void onSelect(String date);
void onUnSelect(String date);
}
大功告成 = =,終於寫完了,繼續敲程式碼去了(=^ ^=)
一直忘了還有一個工具類沒有放上來,是我的鍋~~,重新補上了
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
/**
* Date工具類
* Created by Thong on 2017/6/8.
*/
public class DateUtils {
/**
* 獲取月份第一天的星期
*/
public static int getFirstDayofWeek(){
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.DAY_OF_MONTH,1);
return calendar.get(Calendar.DAY_OF_WEEK);
}
/**
* 獲取某一天的星期
*/
public static int getDayofWeek(Calendar calendar, int day){
calendar.set(Calendar.DAY_OF_MONTH,day);
return calendar.get(Calendar.DAY_OF_WEEK);
}
/**
* 獲取某個月的第一天的星期
*/
public static int getMonthDayOfWeek(Date date){
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.set(Calendar.DAY_OF_MONTH,1);
return calendar.get(Calendar.DAY_OF_WEEK);
}
/**
* 獲取當前日期是該月的第幾天
*
* @return
*/
public static int getCurrentDayOfMonth() {
return Calendar.getInstance().get(Calendar.DAY_OF_MONTH);
}
/**
* 獲取當前日期是該周的第幾天
*
* @return
*/
public static int getCurrentDayOfWeek() {
return Calendar.getInstance().get(Calendar.DAY_OF_WEEK);
}
/**
* 根據傳入的年份和月份,判斷上一個月有多少天
*
* @param year
* @param month
* @return
*/
public static int getLastDaysOfMonth(int year, int month) {
int lastDaysOfMonth = 0;
if (month == 1) {
lastDaysOfMonth = getDaysOfMonth(year - 1, 12);
} else {
lastDaysOfMonth = getDaysOfMonth(year, month - 1);
}
return lastDaysOfMonth;
}
/**
* 根據傳入的年份和月份,判斷當前月有多少天
*
* @param year
* @param month
* @return
*/
public static int getDaysOfMonth(int year, int month) {
switch (month) {
case 0:
case 2:
case 4:
case 6:
case 7:
case 9:
case 11:
return 31;
case 1:
if (isLeap(year)) {
return 29;
} else {
return 28;
}
case 3:
case 5:
case 8:
case 10:
return 30;
}
return -1;
}
/**
* 判斷是否為閏年
*
* @param year
* @return
*/
public static boolean isLeap(int year) {
if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) {
return true;
}
return false;
}
public static int getYear() {
return Calendar.getInstance().get(Calendar.YEAR);
}
public static int getMonth() {
return Calendar.getInstance().get(Calendar.MONTH) + 1;
}
public static int getCurrentMonthDay() {
return Calendar.getInstance().get(Calendar.DAY_OF_MONTH);
}
public static boolean isToday(){
SimpleDateFormat dateFormat = new SimpleDateFormat("dd");
String day = dateFormat.format(System.currentTimeMillis());
int curDay = Calendar.getInstance().get(Calendar.DAY_OF_MONTH);
return(curDay == Integer.parseInt(day));
}
public static boolean isCurrentMonth(){
SimpleDateFormat dateFormat = new SimpleDateFormat("MM");
String month = dateFormat.format(System.currentTimeMillis());
int curMonth = Calendar.getInstance().get(Calendar.MONTH) + 1;
return(curMonth == Integer.parseInt(month));
}
/**
* 判斷當前日期是否是當前月的日期
* @param calendar
* @return
*/
public static boolean isCurrentMonthDay(Calendar calendar, int year, int month) {
return calendar.get(Calendar.YEAR) == year && calendar.get(Calendar.MONTH) == month;
}
public static int getMonthOfAllDay(int year,int month){
Calendar calendar = Calendar.getInstance();
calendar.set(year,month,1);
int thismonthdays = getDaysOfMonth(year,month);
int week = calendar.get(Calendar.DAY_OF_WEEK);
int lastmonthdayInthismonth = week - 1;
calendar.set(year,month,thismonthdays);
week = calendar.get(Calendar.DAY_OF_WEEK);
int nextmonthdayInthismonth = 7-week;
return thismonthdays + lastmonthdayInthismonth + nextmonthdayInthismonth;
}
}