public abstract class DPCalendar {
protected final Calendar c = Calendar.getInstance();
public abstract String[][] buildMonthFestival(int year, int month);
public abstract Set buildMonthHoliday(int year, int month);
public boolean isLeapYear(int year) {
return (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0));
}
public boolean isToday(int year, int month, int day) {
Calendar c1 = Calendar.getInstance();
Calendar c2 = Calendar.getInstance();
c1.set(year, month - 1, day);
return (c1.get(Calendar.YEAR) == c2.get(Calendar.YEAR)) &&
(c1.get(Calendar.MONTH) == (c2.get(Calendar.MONTH))) &&
(c1.get(Calendar.DAY_OF_MONTH) == c2.get(Calendar.DAY_OF_MONTH));
}
.....
}
DPCNCalendar类简要实现:
public class DPCNCalendar extends DPCalendar {
private static final String[] NUMBER_CAPITAL = {"零", "一", "二", "三", "四", "五", "六", "七", "八", "九"};
private static final String[] LUNAR_HEADER = {"初", "十", "廿", "卅", "正", "腊", "冬", "闰"};
@Override
public String[][] buildMonthFestival(int year, int month) {
return buildMonthL(year, month);
}
private String[][] buildMonthL(int year, int month) {
....
}
/**
* 判断某年某月某日是否为节气
*
* @param year 公历年
* @param month 公历月
* @param day 公历日
* @return ...
*/
public boolean isSolarTerm(int year, int month, int day) {
return null == getSolarTerm(year, month, day);
}
......
}
DPUSCalendar类的简要设计实现:
public class DPUSCalendar extends DPCalendar {
........
@Override
public String[][] buildMonthFestival(int year, int month) {
String[][] gregorianMonth = buildMonthG(year, month);
String tmp[][] = new String[6][7];
for (int i = 0; i < tmp.length; i++) {
for (int j = 0; j < tmp[0].length; j++) {
tmp[i][j] = "";
String day = gregorianMonth[i][j];
if (!TextUtils.isEmpty(day)) {
tmp[i][j] = getFestivalG(month, Integer.valueOf(day));
}
}
}
return tmp;
}
@Override
public Set buildMonthHoliday(int year, int month) {
Set tmp = new HashSet<>();
if (year == 2015) {
Collections.addAll(tmp, HOLIDAY[month - 1]);
}
return tmp;
}
private String getFestivalG(int month, int day) {
String tmp = "";
int[] daysInMonth = FESTIVAL_G_DATE[month - 1];
for (int i = 0; i < daysInMonth.length; i++) {
if (day == daysInMonth[i]) {
tmp = FESTIVAL_G[month - 1][i];
}
}
return tmp;
}
}
上面就是三个类之间的缩略版,类之间的合理设计很有必要,也很重要,有时真的不是仅仅提取出来当道utils工具类中那么简单的一件事情。所以还是很有必须深入研究业务逻辑,针对类进行合理的设计,能让代码的整体性看起来很“舒心”。在calendars包中还有最后一个类SolarTerm类,该类是针对农历日期进行处理的类,没什么特别说的,毕竟怎么处理的不是我们研究的重点,有兴趣的可以下载源码研究下。
public class DPDecor {
/**
* 绘制当前日期区域左上角的装饰物
* Draw decor on Top left of current date area
*
* @param canvas 绘制图形的画布 Canvas of image drew
* @param rect 可以绘制的区域范围 Area you can draw
* @param paint 画笔对象 Paint
* @param data 日期
*/
public void drawDecorTL(Canvas canvas, Rect rect, Paint paint, String data) {
drawDecorTL(canvas, rect, paint);
}
/**
* @see #drawDecorTL(Canvas, Rect, Paint, String)
*/
public void drawDecorTL(Canvas canvas, Rect rect, Paint paint) {
}
public void drawDecorT(Canvas canvas, Rect rect, Paint paint, String data) {
drawDecorT(canvas, rect, paint);
}
public void drawDecorT(Canvas canvas, Rect rect, Paint paint) {
}
public void drawDecorTR(Canvas canvas, Rect rect, Paint paint, String data) {
drawDecorTR(canvas, rect, paint);
}
public void drawDecorTR(Canvas canvas, Rect rect, Paint paint) {
}
public void drawDecorL(Canvas canvas, Rect rect, Paint paint, String data) {
drawDecorL(canvas, rect, paint);
}
public void drawDecorL(Canvas canvas, Rect rect, Paint paint) {
}
public void drawDecorR(Canvas canvas, Rect rect, Paint paint, String data) {
drawDecorR(canvas, rect, paint);
}
public void drawDecorR(Canvas canvas, Rect rect, Paint paint) {
}
public void drawDecorBG(Canvas canvas, Rect rect, Paint paint, String data) {
drawDecorBG(canvas, rect, paint);
}
public void drawDecorBG(Canvas canvas, Rect rect, Paint paint) {
}
}
与我的[Android自定义控件之日历控件](http://blog.csdn.net/mr_dsw/article/details/48755993)对比会发现,这样做会大大减少View层中的代码量,我在[Android自定义控件之日历控件](http://blog.csdn.net/mr_dsw/article/details/48755993)中业务逻辑也掺杂在View的绘制中进行处理,代码臃肿性可想而知,这样做将这部分功能提取处理,只需要在View中持有该类的一个对象即可,用户可以通过继承该类进行自定义的“装饰”绘制,提高扩展性,当然如果有兴趣,也可以个用户实现几个通用常见的样式,方便使用。我个人理解,根据装饰类的设计思想,我们在进行设计时候完全可以把针对日历View的一些边角“装饰”的功能提取出来,提高扩展性,降低代码的臃肿。
public abstract class DPLManager {
private static DPLManager sLanguage;
/**
* 获取日历语言管理器
*
* Get DatePicker language manager
*
* @return 日历语言管理器 DatePicker language manager
*/
public static DPLManager getInstance() {
if (null == sLanguage) {
String locale = Locale.getDefault().getLanguage().toLowerCase();
if (locale.equals("zh")) {
sLanguage = new CN();
} else {
sLanguage = new EN();
}
}
return sLanguage;
}
/**
* 月份标题显示
*
* Titles of month
*
* @return 长度为12的月份标题数组 Array in 12 length of month titles
*/
public abstract String[] titleMonth();
/**
* 确定按钮文本
*
* Text of ensure button
*
* @return Text of ensure button
*/
public abstract String titleEnsure();
/**
* 公元前文本
*
* Text of B.C.
*
* @return Text of B.C.
*/
public abstract String titleBC();
/**
* 星期标题显示
*
* Titles of week
*
* @return 长度为7的星期标题数组 Array in 7 length of week titles
*/
public abstract String[] titleWeek();
}
中文系统下:
public class CN extends DPLManager {
@Override
public String[] titleMonth() {
return new String[]{"一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"};
}
@Override
public String titleEnsure() {
return "确定";
}
@Override
public String titleBC() {
return "公元前";
}
@Override
public String[] titleWeek() {
return new String[]{"日", "一", "二", "三", "四", "五", "六"};
}
}
英语系统下:
public class EN extends DPLManager {
@Override
public String[] titleMonth() {
return new String[]{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"};
}
@Override
public String titleEnsure() {
return "Ok";
}
@Override
public String titleBC() {
return "B.C.";
}
@Override
public String[] titleWeek() {
return new String[]{"MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"};
}
}
设计思想与上面是相同的。
public abstract class DPTheme {
/**
* 月视图背景色
*
* Color of MonthView's background
*
* @return 16进制颜色值 hex color
*/
public abstract int colorBG();
/**
* 背景圆颜色
*
* Color of MonthView's selected circle
*
* @return 16进制颜色值 hex color
*/
public abstract int colorBGCircle();
/**
* 标题栏背景色
*
* Color of TitleBar's background
*
* @return 16进制颜色值 hex color
*/
public abstract int colorTitleBG();
/**
* 标题栏文本颜色
*
* Color of TitleBar text
*
* @return 16进制颜色值 hex color
*/
public abstract int colorTitle();
/**
* 今天的背景色
*
* Color of Today's background
*
* @return 16进制颜色值 hex color
*/
public abstract int colorToday();
/**
* 公历文本颜色
*
* Color of Gregorian text
*
* @return 16进制颜色值 hex color
*/
public abstract int colorG();
/**
* 节日文本颜色
*
* Color of Festival text
*
* @return 16进制颜色值 hex color
*/
public abstract int colorF();
/**
* 周末文本颜色
*
* Color of Weekend text
*
* @return 16进制颜色值 hex color
*/
public abstract int colorWeekend();
/**
* 假期文本颜色
*
* Color of Holiday text
*
* @return 16进制颜色值 hex color
*/
public abstract int colorHoliday();
}
DPBaseTheme类的实现:
public class DPBaseTheme extends DPTheme {
@Override
public int colorBG() {
return 0xFFFFFFFF;
}
@Override
public int colorBGCircle() {
return 0x44000000;
}
@Override
public int colorTitleBG() {
return 0xFFF37B7A;
}
@Override
public int colorTitle() {
return 0xEEFFFFFF;
}
@Override
public int colorToday() {
return 0x88F37B7A;
}
@Override
public int colorG() {
return 0xEE333333;
}
@Override
public int colorF() {
return 0xEEC08AA4;
}
@Override
public int colorWeekend() {
return 0xEEF78082;
}
@Override
public int colorHoliday() {
return 0x80FED6D6;
}
}
(二)、cons包
/**
* 日期选择模式
* 支持单选和多选和展示
* Date select mode
* Support SINGLE or MULTIPLE or Display only.
*
* @author AigeStudio 2015-07-02
*/
public enum DPMode {
SINGLE, MULTIPLE, NONE
}
(三)、entities包
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包
public class DatePicker extends LinearLayout {
private DPTManager mTManager;// 主题管理器
private DPLManager mLManager;// 语言管理器
private MonthView monthView;// 月视图
private TextView tvYear, tvMonth;// 年份 月份显示
private TextView tvEnsure;// 确定按钮显示
.....
}
通过源码可知,我们的DatePicker其实继承自LinearLayout,然后在该View中包含了相关的View(monthView、tv_Year等),通过构造函数进行控件的布局设置,最后达到一个样式的展示。代码如下:
public DatePicker(Context context, AttributeSet attrs) {
super(context, attrs);
mTManager = DPTManager.getInstance();
mLManager = DPLManager.getInstance();
// 设置排列方向为竖向
setOrientation(VERTICAL);
LayoutParams llParams =
new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
// 标题栏根布局
RelativeLayout rlTitle = new RelativeLayout(context);
rlTitle.setBackgroundColor(mTManager.colorTitleBG());
int rlTitlePadding = MeasureUtil.dp2px(context, 10);
rlTitle.setPadding(rlTitlePadding, rlTitlePadding, rlTitlePadding, rlTitlePadding);
// 周视图根布局
LinearLayout llWeek = new LinearLayout(context);
llWeek.setBackgroundColor(mTManager.colorTitleBG());
llWeek.setOrientation(HORIZONTAL);
int llWeekPadding = MeasureUtil.dp2px(context, 5);
llWeek.setPadding(0, llWeekPadding, 0, llWeekPadding);
LayoutParams lpWeek = new LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
lpWeek.weight = 1;
// 标题栏子元素布局参数
RelativeLayout.LayoutParams lpYear =
new RelativeLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
lpYear.addRule(RelativeLayout.CENTER_VERTICAL);
RelativeLayout.LayoutParams lpMonth =
new RelativeLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
lpMonth.addRule(RelativeLayout.CENTER_IN_PARENT);
RelativeLayout.LayoutParams lpEnsure =
new RelativeLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
lpEnsure.addRule(RelativeLayout.CENTER_VERTICAL);
lpEnsure.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
// --------------------------------------------------------------------------------标题栏
// 年份显示
tvYear = new TextView(context);
tvYear.setText("2015");
tvYear.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
tvYear.setTextColor(mTManager.colorTitle());
// 月份显示
tvMonth = new TextView(context);
tvMonth.setText("六月");
tvMonth.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 20);
tvMonth.setTextColor(mTManager.colorTitle());
// 确定显示
tvEnsure = new TextView(context);
tvEnsure.setText(mLManager.titleEnsure());
tvEnsure.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
tvEnsure.setTextColor(mTManager.colorTitle());
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);
}
在构造函数中,首先进行DPTManager(样式管理器)、DPLManager(语言管理器)的初始化,然后设置该DatePicker的展示方向。接着定义了名为rlTitle的RelativeLayout对象和一个名为llWeek的LinearLayout对象,分别用于存放头部的月份标题控件和星期控件。中间涉及较多的就是LayoutParams布局参数的使用,动态进行控件的布局(通过代码实现布局)。这样就完成了整体的样式结构搭建。
@Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(mTManager.colorBG());
draw(canvas, width * indexMonth, (indexYear - 1) * height, topYear, topMonth);
draw(canvas, width * (indexMonth - 1), height * indexYear, leftYear, leftMonth);
draw(canvas, width * indexMonth, indexYear * height, centerYear, centerMonth);
draw(canvas, width * (indexMonth + 1), height * indexYear, rightYear, rightMonth);
draw(canvas, width * indexMonth, (indexYear + 1) * height, bottomYear, bottomMonth);
drawBGCircle(canvas);
}
我们可以看到在onDraw()方法中已经提前绘制了选中月份的上下左右对应月份的日期,所以我们通过Scroller进行滑动的时候就能将下月/上月的日期缓慢展示出来。这里需要在onTouchEvent中判断是左右滑动还是上下滑动,左右滑动是翻月份,上下滑动是翻年份。
@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;
}
这里的判断都是在onTouchEvent的ACTION_MOVE事件中进行处理,当水平方法滑动差值达到100的时候就认为是水平滑动,当竖直方向滑动差值达到50就认为是竖直滑动,然后调用smoothScrollTo()方法进行滑动处理。同样当我们滑动一部分手指抬起时的处理在ACTION_UP中进行处理。