哈哈,又要更新博客了+_+!
这一次发一个日历选择器吧!
说实话,看到需求那一刻,我绝对是崩溃的,不过还好有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;
}
}