标签:
在一些项目开发中,会使用日历去标识事务,所以根据美工出的效果图,我们可以采用不同的方法去实现。比如通过GridView扣扣你敢、自定义View实现日历控件,这些都是我们解决问题的手段,我也实现过一个自定义日历控件(Android自定义控件之日历控件55993)),由于我只是粗糙的进行实现,并没有进行过多的在控件的可扩展性上进行打磨设计,所以在本篇文章中,我秉着学习的态度分析下爱哥的鼎力巨作DatePicker-DatePicker。public abstract class DPCalendar { protected final Calendar c = Calendar.getInstance(); public abstract String[][] buildMonthFestival(int year, int month); public abstract Set<String> 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<String> buildMonthHoliday(int year, int month) { Set<String> 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中进行处理。
标签:
原文地址:http://blog.csdn.net/mr_dsw/article/details/51334404