DatePicker前传
不知什么时候,AigeStudio侵入了我们这群android开发者的内心深处,有着一群不知姓什名谁的人xx(不想用小明指代,比较喜欢xx),不断地发出歇斯底里的叫声:
然而,在某天某时某刻,XX遇到需求如下:
商户可以通过app手动选择指定时间段/一些天数进行统计分析。
这一刻来临了,XX人蒙了,找不到头绪了,开始进各个群展开撒网战术,可怜兮兮的等待着群友的回复,总有那么些乐于助人又是Aige的死忠粉,会回复一句:爱哥开源的DatePicker可以实现,然而xx还想问”DatePicker在哪里啊,谁有下载链接啊“…找到了解决办法,XX终于可以安心睡觉了,此时此刻,我只想对xx说:你对Aige爱得不够深沉
DatePicker实战
效果图:
依赖导入
compile 'cn.aigestudio.datepicker:DatePicker:2.2.0'
DatePicker直接在xml布局使用,代码进行相关的初始化配置监听设置等。(setData方法必须调用进行初始化)
......
DatePicker datePicker = (DatePicker) findViewById(R.id.datePicker);
datePicker.setDate(mYear, mMonth);
datePicker.setOnDateSelectedListener(new DatePicker.OnDateSelectedListener() {
@Override
public void onDateSelected(List<String> date) {
}
});
......
DatePicker支持多种模式:单选、多选、正常模式,具体定义如下
public enum DPMode {
SINGLE, MULTIPLE, NONE
}
DatePicker不同的模式通过不同的监听回调事件,获取选中的时间值
setOnDateSelectedListener // 多选监听
setOnDatePickedListener // 单选监听
选择后的日期将会以列表(多选模式下)或字符串(单选模式下)的形式返回,日期字符串的格式为:
格式:yyyy-M-d
示例:2015-3-28
DatePicker有一套自己的默认UI,默认的Color配置,如果你使用Module导入工程,可以直接修改默认配置,关联类如下
DatePicker默认天朝的节日背景色
当然你也可以自己修改定制,同时还可以为特殊节日添加浮标,首先添加指定需要修改号数集,添加到缓存到对应map集合
List<String> tmpTR = new ArrayList<>();
tmpTR.add("2015-10-10");
tmpTR.add("2015-10-11");
tmpTR.add("2015-10-12");
tmpTR.add("2015-10-13");
tmpTR.add("2015-10-14");
tmpTR.add("2015-10-15");
tmpTR.add("2015-10-16");
DPCManager.getInstance().setDecorTR(tmpTR);
private static final HashMap<String, Set<String>> DECOR_CACHE_BG = new HashMap<>();
private static final HashMap<String, Set<String>> DECOR_CACHE_TL = new HashMap<>();
private static final HashMap<String, Set<String>> DECOR_CACHE_T = new HashMap<>();
private static final HashMap<String, Set<String>> DECOR_CACHE_TR = new HashMap<>();
private static final HashMap<String, Set<String>> DECOR_CACHE_L = new HashMap<>();
private static final HashMap<String, Set<String>> DECOR_CACHE_R = new HashMap<>();
完成上述步骤后,再通过setDPDecor方法绘制对应号数的浮标
picker.setDPDecor(new DPDecor() {
@Override
public void drawDecorTL(Canvas canvas, Rect rect, Paint paint, String data) {
super.drawDecorTL(canvas, rect, paint, data);
switch (data) {
case "2015-10-5":
case "2015-10-7":
case "2015-10-9":
case "2015-10-11":
paint.setColor(Color.GREEN);
canvas.drawRect(rect, paint);
break;
default:
paint.setColor(Color.RED);
canvas.drawCircle(rect.centerX(), rect.centerY(), rect.width() / 2, paint);
break;
}
}
@Override
public void drawDecorTR(Canvas canvas, Rect rect, Paint paint, String data) {
super.drawDecorTR(canvas, rect, paint, data);
switch (data) {
case "2015-10-10":
case "2015-10-11":
case "2015-10-12":
paint.setColor(Color.BLUE);
canvas.drawCircle(rect.centerX(), rect.centerY(), rect.width() / 2, paint);
break;
default:
paint.setColor(Color.YELLOW);
canvas.drawRect(rect, paint);
break;
}
}
});
绘制的浮标可以是不同形状不同颜色,具体怎么关联到月视图稍后再看,绘制的浮标可以是不同方向,具体使用参考DPDecor类的定义。(具体使用参考上面代码块实例)
绘制浮标效果如下图:
更为详尽的使用请参考Aige官方示例:https://github.com/AigeStudio/DatePicker
DataPicker源码浅析
calendars包内部提供方法都有中文注释,相信都能看懂,关于一些节日号数相关基础数据的获取判断,内部的一些算法,表示无能为力(我算法白痴),DPDecor定义方法上面有提到,具体使用不详解。
抽象类DPLManager是一个语言管理方面的,该库支持中英文的日历,根据CH标示采取不同的实现
theme模块关于UI配置方面的,上面有提到,具体代码设置color,请参考源码,头有中文注释一目了然。
日历数据实体,封装日历绘制时需要的数据(封装按照二维数组,每个月可能存在的行列进行封装)
public class DPInfo {
public String strG, strF;
public boolean isHoliday;
public boolean isToday, isWeekend;
public boolean isSolarTerms, isFestival, isDeferred;
public boolean isDecorBG;
public boolean isDecorTL, isDecorT, isDecorTR, isDecorL, isDecorR;
}
utils包里面就一个dp px的转换和一维数组转二维数组
整个库的UI核心是下面两个类
DatePicker
MonthView
DatePicker自定义的Layout容器,构造函数先添加标题栏显示控件和星期-到星期日的显示视图,最后添加月视图
public DatePicker(Context context, AttributeSet attrs) {
super(context, attrs);
//..............略过些许方法................
mTManager = DPTManager.getInstance();
mLManager = DPLManager.getInstance();
// 设置排列方向为竖向
setOrientation(VERTICAL);
// 标题栏根布局
RelativeLayout rlTitle = new RelativeLayout(context);
// 周视图根布局
LinearLayout llWeek = new LinearLayout(context);
// 标题栏子元素布局参数
RelativeLayout.LayoutParams lpYear =
new RelativeLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
// --------------------------------------------------------------------------------标题栏
// 年份显示
tvYear = new TextView(context);
// 月份显示
tvMonth = new TextView(context);
tvMonth.setText("六月");
// 确定显示
tvEnsure = new TextView(context);
tvEnsure.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (null != onDateSelectedListener) {
onDateSelectedListener.onDateSelected(monthView.getDateSelected());
}
}
});
rlTitle.addView(tvYear, lpYear);
rlTitle.addView(tvMonth, lpMonth);
rlTitle.addView(tvEnsure, lpEnsure);
addView(rlTitle, llParams);
// --------------------------------------------------------------------------------周视图
for (int i = 0; i < mLManager.titleWeek().length; i++) {
TextView tvWeek = new TextView(context);
tvWeek.setText(mLManager.titleWeek()[i]);
tvWeek.setGravity(Gravity.CENTER);
tvWeek.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14);
tvWeek.setTextColor(mTManager.colorTitle());
llWeek.addView(tvWeek, lpWeek);
}
addView(llWeek, llParams);
// ------------------------------------------------------------------------------------月视图
monthView = new MonthView(context);
monthView.setOnDateChangeListener(new MonthView.OnDateChangeListener() {
@Override
public void onMonthChange(int month) {
tvMonth.setText(mLManager.titleMonth()[month - 1]);
}
@Override
public void onYearChange(int year) {
String tmp = String.valueOf(year);
if (tmp.startsWith("-")) {
tmp = tmp.replace("-", mLManager.titleBC());
}
tvYear.setText(tmp);
}
});
addView(monthView, llParams);
}
设置日历选择模式如果支持多选则显示确定按钮
/**
* 设置日期选择模式
*
* @param mode ...
*/
public void setMode(DPMode mode) {
if (mode != DPMode.MULTIPLE) {
tvEnsure.setVisibility(GONE);
}
monthView.setDPMode(mode);
}
提供多个set方法,本质是调用了月视图View进行set,特别是对于DatePicker的监听绑定做了判断。Aige的使用文档是这样说的
MonthView默认多选模式,在没有设置模式的情况下直接设置单选监听会抛出异常,反之同理
/**
* 设置单选监听器
*
* @param onDatePickedListener ...
*/
public void setOnDatePickedListener(OnDatePickedListener onDatePickedListener) {
if (monthView.getDPMode() != DPMode.SINGLE) {
throw new RuntimeException(
"Current DPMode does not SINGLE! Please call setMode set DPMode to SINGLE!");
}
monthView.setOnDatePickedListener(onDatePickedListener);
}
/**
* 设置多选监听器
*
* @param onDateSelectedListener ...
*/
public void setOnDateSelectedListener(OnDateSelectedListener onDateSelectedListener) {
if (monthView.getDPMode() != DPMode.MULTIPLE) {
throw new RuntimeException(
"Current DPMode does not MULTIPLE! Please call setMode set DPMode to MULTIPLE!");
}
this.onDateSelectedListener = onDateSelectedListener;
}
MonthView月视图自定义控件,内部构造函数初始化一个Scroller实例用于辅助滑动,一个缩放动画监听(单选或者多选模式下,选中某一项会有个缩放动画的圆圈背景)
public MonthView(Context context) {
super(context);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
scaleAnimationListener = new ScaleAnimationListener();
}
mScroller = new Scroller(context);
mPaint.setTextAlign(Paint.Align.CENTER);
}
computeScroll:主要功能是计算拖动的位移量更新UI,重写computeScroll()的原因,调用startScroll()是不会有滚动效果的,只有在computeScroll()获取滚动情况,参考Scroller计算,做出滚动的响应,computeScroll在父控件执行drawChild时,会调用这个方法.要知道计算有没有终止,需要通过mScroller.computeScrollOffset()
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
} else {
requestLayout()
}
}
onTouch方法内判断滑动事件,如果还在滑动,down时结束滑动,并记录位置,MOVE时判断滑动距离,设置滑动方向mSlideMode (内部定义枚举类型),根据不同的Mode和计算出的距离,调用Scroller.startScroll方法配合computescroll实现滑动视图。而Touch的up事件如果造成月视图切换,可能月份年份的变化,这时候会调用到computeDate方法,该方法判断后根据条件执行回调,而回调函数在DatePicker内部实现了局部更新UI
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mScroller.forceFinished(true);
mSlideMode = null;
isNewEvent = true;
lastPointX = (int) event.getX();
lastPointY = (int) event.getY();
break;
case MotionEvent.ACTION_MOVE:
if (isNewEvent) {
if (Math.abs(lastPointX - event.getX()) > 100) {
mSlideMode = SlideMode.HOR;
isNewEvent = false;
} else if (Math.abs(lastPointY - event.getY()) > 50) {
mSlideMode = SlideMode.VER;
isNewEvent = false;
}
}
if (mSlideMode == SlideMode.HOR) {
int totalMoveX = (int) (lastPointX - event.getX()) + lastMoveX;
smoothScrollTo(totalMoveX, indexYear * height);
} else if (mSlideMode == SlideMode.VER) {
int totalMoveY = (int) (lastPointY - event.getY()) + lastMoveY;
smoothScrollTo(width * indexMonth, totalMoveY);
}
break;
case MotionEvent.ACTION_UP:
if (mSlideMode == SlideMode.VER) {
if (Math.abs(lastPointY - event.getY()) > 25) {
if (lastPointY < event.getY()) {
if (Math.abs(lastPointY - event.getY()) >= criticalHeight) {
indexYear--;
centerYear = centerYear - 1;
}
} else if (lastPointY > event.getY()) {
if (Math.abs(lastPointY - event.getY()) >= criticalHeight) {
indexYear++;
centerYear = centerYear + 1;
}
}
buildRegion();
computeDate();
smoothScrollTo(width * indexMonth, height * indexYear);
lastMoveY = height * indexYear;
} else {
defineRegion((int) event.getX(), (int) event.getY());
}
} else if (mSlideMode == SlideMode.HOR) {
if (Math.abs(lastPointX - event.getX()) > 25) {
if (lastPointX > event.getX() &&
Math.abs(lastPointX - event.getX()) >= criticalWidth) {
indexMonth++;
centerMonth = (centerMonth + 1) % 13;
if (centerMonth == 0) {
centerMonth = 1;
centerYear++;
}
} else if (lastPointX < event.getX() &&
Math.abs(lastPointX - event.getX()) >= criticalWidth) {
indexMonth--;
centerMonth = (centerMonth - 1) % 12;
if (centerMonth == 0) {
centerMonth = 12;
centerYear--;
}
}
buildRegion();
computeDate();
smoothScrollTo(width * indexMonth, indexYear * height);
lastMoveX = width * indexMonth;
} else {
defineRegion((int) event.getX(), (int) event.getY());
}
} else {
defineRegion((int) event.getX(), (int) event.getY());
}
break;
}
return true;
}
至于上面的defineRegion方法我也只了解一点(计算真心看不懂),基于v11版本区分单选模式和多选模式,执行缩放动画,加减速差值器效果(圆圈背景缩放),动画结束执行对应的回调函数,至于多选的值缓存方式使用的是List,个人认为还有更好的方案,具体参考AbsListView(看了别打我~~(>_<)~~)
private List<String> dateSelected = new ArrayList<>();
至于圆形背景怎么来,以前博客提过很多次,不再逼逼叨叨了
小结
Aige就是Aige,看完这个库收获良多!今后要加强算法方面的学习了,闰年判断都差不多忘了,时光飞逝,遥想当年学java,一个闰年判断的java的demo分分钟的事,现在还得百度一下!!!
DatePicker番外
一天,群里来了新人,漂亮的MM,在IT行业mm就是国宝啊,更何况是PL妹纸,xx纷纷跳出来要求爆照
妹纸羞答答的说:“你们先告诉我DatePicker怎么选择多个日期,我就发”
xx就开始逼逼叨叨先聊着,都没说到正点,此时一个非常活跃的oo告诉mm:”爱哥知道,你问他吧“
……….
………
………逼逼叨叨结束
妹子来了,Aige献身了:“setMode(DPMode.MULTIPLE);”
而后不再言语,事了拂衣去,深藏身与名,PL妹纸都还不知道Aige就是爱哥!!
关于点击切换月份的解决方案:
MonthView添加这两个方法,调用通过DatePicker get到MonthView就可以调用了。(之前考虑用setData经过仔细测试发现有问题的,最后参照onTouchEvent修改完成这两个方法)
public void showNectMonth(int distance) {
mSlideMode = SlideMode.HOR;
int totalMoveX = distance + lastMoveX;
smoothScrollTo(totalMoveX, indexYear * height);
indexMonth++;
centerMonth = (centerMonth + 1) % 13;
if (centerMonth == 0) {
centerMonth = 1;
centerYear++;
}
buildRegion();
computeDate();
smoothScrollTo(width * indexMonth, indexYear * height);
lastMoveX = width * indexMonth;
}
public void showPreviousMonth(int distance) {
mSlideMode = SlideMode.HOR;
int totalMoveX = distance + lastMoveX;
smoothScrollTo(totalMoveX, indexYear * height);
indexMonth--;
centerMonth = (centerMonth - 1) % 12;
if (centerMonth == 0) {
centerMonth = 12;
centerYear--;
}
buildRegion();
computeDate();
smoothScrollTo(width * indexMonth, indexYear * height);
lastMoveX = width * indexMonth;
}