日历控件CalendarListView解析与重新实现
项目地址: CalendarListview 。分析的版本:063952b
功能介绍
CalendarListview 是一个日历控件,效果如图
总体设计
CalendarListview 日历控件本质上是一个基于 RecyclerView 的列表控件。
DayPickerView 类直接继承 RecyclerView ,是控件的主类,表示日历。 SimpleMonthView 类继承 View ,是列表的单元控件,表示单个月份。
总体的类结构图为
详细设计
SimpleMonthView
SimpleMonthView 类的核心方法 onDraw 将月份的绘制划分为三个互相独立的部分,见下图。
每一部分的绘制都涉及两个问题 位置选择 与 文本内容 。除此之外,还要处理该控件的点击事件。
绘制位置
关于绘制位置的选择问题,在图中都用颜色块标出了,此外应该注意两个问题。
1.第三部分第一行中每月的第一天不一定是周日,所有要留出一定的偏移量,偏移量等于当月第一天与周日的差。采用 findDayOffset() 方法计算出该偏移量。
2.要求出每月的天数,才能准确结尾。在工具类 CalendarUtils 中解决,重点是处理闰年二月的天数。
此外对整体控件的尺寸要设置其高度与宽度。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mRowHeight * mNumRows + MONTH_HEADER_SIZE); } protected void onSizeChanged(int w, int h, int oldw, int oldh) { mWidth = w; }
文本内容
关于文本的内容涉及一些类。
1.标题绘制采用 DateUtils 工具类。
String DateUtils.formatDateRange(getContext(), millis, millis, flags);
简单的说,该类第二三两个参数表示起始和结束机器时间,第四个参数设置所显示的人类时间格式。
2.星期绘制采用 DateFormatSymbols 。
//获取星期标签,如“周日”,“周一”等 String[] getShortWeekdays();
点击事件
在触摸回调方法中,首先使用 getDayFromLocation 方法根据点击区域的得到某日( CalendarDay )。
public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_UP) { SimpleMonthAdapter.CalendarDay calendarDay = getDayFromLocation(event.getX(), event.getY()); if (calendarDay != null) { onDayClick(calendarDay); } } return true; }
而后设计接口 OnDayClickListener 将点击事件委托出去。
public static abstract interface OnDayClickListener { public abstract void onDayClick(SimpleMonthView simpleMonthView, SimpleMonthAdapter.CalendarDay calendarDay); }
SimpleMonthAdapter
SimpleMonthAdapter 是处理日期数据集的数据适配器,继承于 RecyclerView.Adapter 。首先要认识两个内部类。
-
CalendarDay 类表示单独的某一天,有三个参数表示年/月/日。
public static class CalendarDay { int day; int month; int year; }
-
SelectedDays<K> 表示一个时间段的首尾两天,用来处理用户的点击行为。
public static class SelectedDays<CalendarDay> { private CalendarDay first; private CalendarDay last; }
1.首先在 getItemCount() 方法中设置要显示的月份数目。
2.处理日期相关逻辑都放在 onBindViewHolder 方法中进行,完成数据和视图的绑定。在这个方法中要告诉视图 SimpleMonthView 该显示哪年哪月,使用下列方法为视图传递具体月份,并重绘。
@Override public void onBindViewHolder(ViewHolder viewHolder, int position){ SimpleMonthView v = viewHolder.simpleMonthView; HashMap<String, Integer> drawingParams = new HashMap<String, Integer>(); //传递年月参数给子视图 v.setMonthParams(drawingParams); v.invalidate(); }
3. SimpleMonthAdapter 类实现了子视图 SimpleMonthView 的 OnDayClickListener 接口,处理某日被选中后的事件。并给用户留下一个处理白点。
public void onDayClick(SimpleMonthView simpleMonthView, CalendarDay calendarDay) { mController.onDayOfMonthSelected(calendarDay.year, calendarDay.month, calendarDay.day); setSelectedDay(calendarDay); }
其中选中日期方法的处理流程如下
DayPickerView
在主类中,要完成数据适配器的设置之外,还要处理属性和用户接口。
-
TypedArray 在构造适配器时将该参数传递过去。
new SimpleMonthAdapter(getContext(), mController, typedArray);
-
DatePickerController 接口虽然为用户能直接接触和感知,但该接口仅仅是控件点击事件中留给用户的处理白点,不再多说。
一些工具类
DateFormatSymbols
java.textDate.FormatSymbols 类作用是封装一个局部时间,例如当月,年,星期。常见的 DateFormat 和 SimpleDateFormat 格式化类都基于该类。但不宜直接使用该类。
首先实例化 DateFormatSymbols 类,同时使用 Locale 类来本地化。
DateFormatSymbols dfs = DateFormatSymbols.getInstance(Locale.ENGLISH);
而后获取各类时间日期字符串
String[] ampm = dfs.getAmPmStrings(); String[] eras = dfs.getEras(); String[] months = dfs.getMonths(); String[] shortMonths = dfs.getShortMonths(); String[] weekdays = dfs.getWeekdays(); String[] shortWeekdays = dfs.getShortWeekdays();
DateFormat与SimpleDateFormat
专门负责日期格式,实现 Date 和 String 之间的转化。
DateUtils
android.text.format.DateUtils 类是一个日期相关工具,依赖 创建时间和日期跨度 有关的文本。基本使用方法如下
DateUtils.formatDateRange(Context context, long startMillis,long endMillis, int flags);
最关键的参数是第四个,可以控制时间与日期格式,形式为 FORMAT_* ,例如
FORMAT_SHOW_TIME //显示完整时间 FORMAT_SHOW_DATE //显示完整日期 FORMAT_SHOW_WEEKDAY //显示星期 FORMAT_SHOW_YEAR //显示年 FORMAT_NO_MONTH_DAY //不显示星期 FORMAT_NO_YEAR //不显示年
重新实现
首先看下初步效果
1.获取当月一共有多少天
LocalDate localDate = new LocalDate(); LocalDate firstDay = localDate.dayOfMonth().withMinimumValue(); LocalDate lastDay = localDate.dayOfMonth().withMaximumValue(); Period period = new Period(firstDay, lastDay); int dayCount = period.toStandardDays().getDays()+1;//注意使用toStandardDays()方法
2.获取当月第一天对上一个周末的偏移量
LocalDate lastWeekday = firstDay.minusWeeks(1).dayOfWeek().withMaximumValue(); Period p = new Period(lastWeekday, firstDay); int offset = p.getDays();