一、概述
接着上一篇文章:一步一步构建自己的简单日历控件 MySimpleCalendar(篇一)
上一篇的实现方式为:
- 自定义 ViewGroup(MonthCalendarView)
- 控件高度在 onMeasure() 方法中动态计算
- 日期 item 通过 addViewInLayout() 方法,将构建的一个一个 View 添加到 ViewGroup 中
- 日期 item 在 onMeasure() 方法中计算其宽高,并在 onLayout() 方法中布局坐标
现在换一种实现方式:
- 自定义 View(MonthCalendarView2)
- 控件高度在 onMeasure() 方法中根据宽度写死
- 日期 item 在 onSizeChanged() 方法里确定宽度
- 在 onDraw() 方法里根据日期的排布确定 item 的高度,再借助 Region 直接绘制每一个日期 item
二、自定义View
上一篇文章已经做了比较详细的解释,这一篇就只做重点分析,下面只列出关键代码
1、数据准备和工具类
数据准备和工具类直接沿用上一篇文章,工具类这里只新增了下面一个方法:
/**
* 获取当前月份的周数
* @param year
* @param month
* @return
*/
public int getWeekCountsOfMonth(int year, int month) {
int lastDayOfMonth = getDaysOfCertainMonth(year, month);
Calendar calendar = Calendar.getInstance();
calendar.set(year, month - 1, lastDayOfMonth);
return calendar.get(Calendar.WEEK_OF_MONTH);
}
其他的在这里将代码直接贴出来:
package com.example.deesonwoo.mysimplecalendar.calendar;
/**
* Created by deeson.woo
*/
public class MyCalendarBean {
private int year;
private int month;//1-12
private int day;//1-31
private boolean isCurrentMonth = true;//是否为当前月份的日期
public MyCalendarBean(int year, int month, int day) {
this.year = year;
this.month = month;
this.day = day;
}
public int getYear() {
return year;
}
public int getMonth() {
return month;
}
public int getDay() {
return day;
}
public boolean isCurrentMonth() {
return isCurrentMonth;
}
public void setCurrentMonth(boolean currentMonth) {
isCurrentMonth = currentMonth;
}
@Override
public String toString() {
return year + "/" + month + "/" + day;
}
}
package com.example.deesonwoo.mysimplecalendar.calendar;
import android.content.Context;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
/**
* Created by deeson.woo
*/
public class MyCalendarUtils {
private Context mContext;
public MyCalendarUtils(Context context) {
this.mContext = context;
}
/**
* 获取具体月份的最大天数
*
* @param year
* @param month
* @return
*/
public static int getDaysOfCertainMonth(int year, int month) {
Calendar calendar = Calendar.getInstance();
calendar.set(year, month - 1, 1);
return calendar.getActualMaximum(Calendar.DATE);
}
/**
* 获取当前月份的日期列表
*
* @param year
* @param month
* @return
*/
public List<MyCalendarBean> getDaysListOfMonth(int year, int month) {
List<MyCalendarBean> list = new ArrayList<>();
int daysOfMonth = getDaysOfCertainMonth(year, month);
//找到当前月第一天的星期,计算出前面空缺的上个月的日期个数,填充到当月日期列表中
int weekDayOfFirstDay = getWeekDayOnCertainDate(year, month, 1);
int preMonthDays = weekDayOfFirstDay - 1;
for (int i = preMonthDays; i > 0; i--) {
MyCalendarBean preMonthBean = generateCalendarBean(year, month, 1 - i);
preMonthBean.setCurrentMonth(false);
list.add(preMonthBean);
}
for (int i = 0; i < daysOfMonth; i++) {
MyCalendarBean monthBean = generateCalendarBean(year, month, i + 1);
monthBean.setCurrentMonth(true);
list.add(monthBean);
}
return list;
}
/**
* 构建具体一天的对象
*
* @param year
* @param month
* @param day
* @return
*/
public MyCalendarBean generateCalendarBean(int year, int month, int day) {
Calendar calendar = Calendar.getInstance();
calendar.set(year, month - 1, day);
year = calendar.get(Calendar.YEAR);
month = calendar.get(Calendar.MONTH) + 1;
day = calendar.get(Calendar.DATE);
return new MyCalendarBean(year, month, day);
}
/**
* 获取具体一天对应的星期
*
* @param year
* @param month
* @param day
* @return 1-7(周日-周六)
*/
private int getWeekDayOnCertainDate(int year, int month, int day) {
Calendar calendar = Calendar.getInstance();
calendar.set(year, month - 1, day);
return calendar.get(Calendar.DAY_OF_WEEK);
}
/**
* 获取当前月份的周数
* @param year
* @param month
* @return
*/
public int getWeekCountsOfMonth(int year, int month) {
int lastDayOfMonth = getDaysOfCertainMonth(year, month);
Calendar calendar = Calendar.getInstance();
calendar.set(year, month - 1, lastDayOfMonth);
return calendar.get(Calendar.WEEK_OF_MONTH);
}
/**
* 格式化标题展示
*
* @param year
* @param month
* @return
*/
public static String formatYearAndMonth(int year, int month) {
Calendar calendar = Calendar.getInstance();
calendar.set(year, month - 1, 1);
year = calendar.get(Calendar.YEAR);
month = calendar.get(Calendar.MONTH) + 1;
return year + "年" + month + "月";
}
/**
* 获取系统当前年月日
*
* @return
*/
public static int[] getNowDayFromSystem() {
Calendar cal = Calendar.getInstance();
cal.setTime(new Date());
return new int[]{cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DATE)};
}
/**
* 判断是否为系统当天
*
* @param bean
* @return
*/
public static boolean isToday(MyCalendarBean bean) {
int[] nowDay = getNowDayFromSystem();
return bean.getYear() == nowDay[0] && bean.getMonth() == nowDay[1] && bean.getDay() == nowDay[2];
}
public static int dp2px(Context context, float dp) {
float scale = context.getResources().getDisplayMetrics().density;
return (int) (dp * scale + 0.5f);
}
}
2、自定义View(MonthCalendarView2)
(1)onMeasure() 方法
- 一个月的日期,7列排布,最少4行,最多6行
- 将日历高度设为其宽度的 6/7
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
setMeasuredDimension(measureWidth, measureWidth * 6 / column);
}
(2)onSizeChanged() 方法
- 确定控件的宽高,以及 item 的宽度
@Override
protected void onSizeChanged(int w, int h, int oldW, int oldH) {
parentWidth = w;
parentHeight = h;
itemWidth = w / column;
}
(3)onDraw() 方法
- 获取当前月份的周数,根据周数确定日期 item 的高度
int weekCounts = calendarUtils.getWeekCountsOfMonth(currentYear, currentMonth);
itemHeight = parentHeight / weekCounts;
- 根据上面确定的 item 的高度,计算出每个 item 的 left, top, right, bottom,设置到 Region 里面
- 将日期绘制到 Region 的中间(这里获取到的当前月的日期列表中包含了个别上个月的数据,所以绘制的时候要做一个是否是当前月的判断)
currentMonthDays = calendarUtils.getDaysListOfMonth(currentYear, currentMonth);
for (int i = 0; i < currentMonthDays.size(); i++) {
int columnCount = i % column;
int rowCount = i / column;
Region region = new Region();
region.set(columnCount * itemWidth, rowCount * itemHeight, (columnCount * itemWidth) + itemWidth, (rowCount * itemHeight) + itemHeight);
MyCalendarBean bean = currentMonthDays.get(i);
if (bean.isCurrentMonth()) {
canvas.drawText(String.valueOf(bean.getDay()), region.getBounds().centerX(), region.getBounds().centerY(), mPaint);
}
}
(4)暴露一个初始化的方法
public void setMonth(int year, int month) {
currentYear = year;
currentMonth = month;
invalidate();
}
(5)获取当前显示的年月
public String getCurrentYearAndMonth() {
return MyCalendarUtils.formatYearAndMonth(currentYear, currentMonth);
}
3、在 SimpleCalendarView 中添加
- 跟上一篇文章的控件一样的用法,直接贴出代码:
//月历视图
LayoutParams monthParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
// monthCalendarView = new MonthCalendarView(context);
// initCalendarDate();
// monthCalendarView.setOnDatePickUpListener(this);
// addView(monthCalendarView, monthParams);
monthCalendarView2 = new MonthCalendarView2(context);
initCalendarDate2();
addView(monthCalendarView2, monthParams);
private void initCalendarDate2() {
int[] nowDay = MyCalendarUtils.getNowDayFromSystem();
monthCalendarView2.setMonth(nowDay[0], nowDay[1]);
updateTitle2();
}
private void updateTitle2() {
if (null != title && null != monthCalendarView2) {
title.setText(monthCalendarView2.getCurrentYearAndMonth());
}
}
效果跟上一篇文章是一样的:
4、补充前翻页、后翻页方法
方法跟上一篇也是一样,贴出代码:
/**
* 展示上一个月
*/
public void moveToPreMonth() {
currentMonth -= 1;
invalidate();
}
/**
* 展示下一个月
*/
public void moveToNextMonth() {
currentMonth += 1;
invalidate();
}
效果看下面动态图:
5、补充高亮显示系统当天日期
- 在 onDraw() 方法中绘制日期的同时,判断为系统当天时间,绘制一个圆形背景
@Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(getResources().getColor(R.color.white));
draw(canvas, currentYear, currentMonth);
}
private void draw(Canvas canvas, int year, int month) {
int weekCounts = calendarUtils.getWeekCountsOfMonth(year, month);
itemHeight = parentHeight / weekCounts;
currentMonthDays = calendarUtils.getDaysListOfMonth(year, month);
for (int i = 0; i < currentMonthDays.size(); i++) {
int columnCount = i % column;
int rowCount = i / column;
MyCalendarBean bean = currentMonthDays.get(i);
Region region = new Region();
region.set(columnCount * itemWidth, rowCount * itemHeight, (columnCount * itemWidth) + itemWidth, (rowCount * itemHeight) + itemHeight);
if (bean.isCurrentMonth()) {
drawTodayBG(canvas, region.getBounds(), bean);
drawText(canvas, region.getBounds(), bean);
}
}
}
private void drawText(Canvas canvas, Rect rect, MyCalendarBean bean) {
canvas.drawText(String.valueOf(bean.getDay()), rect.centerX(), rect.centerY() + (textSize / 4), mPaint);
}
private void drawTodayBG(Canvas canvas, Rect rect, MyCalendarBean bean) {
if (MyCalendarUtils.isToday(bean)) {
canvas.drawCircle(rect.centerX(), rect.centerY(), circleRadius, mTodayBGPaint);
}
}
效果出来:
6、补充点击日期效果
(1)点击回调
- onDraw() 方法
- 将当前月份的 Region 都添加到临时列表中
private void draw(Canvas canvas, int year, int month) {
//...
//...
for (int i = 0; i < currentMonthDays.size(); i++) {
//...
//...
Region region = new Region();
region.set(columnCount * itemWidth, rowCount * itemHeight, (columnCount * itemWidth) + itemWidth, (rowCount * itemHeight) + itemHeight);
tempRegions.add(region);
//...
//...
}
}
- 重写 onTouchEvent() 方法
- 根据点击的位置,找到对应的日期,回调
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
for (int i = 0; i < tempRegions.size(); i++) {
Region region = tempRegions.get(i);
if (region.contains((int) event.getX(), (int) event.getY())) {
MyCalendarBean bean = currentMonthDays.get(i);
if (bean.isCurrentMonth()) {
if (null != onDatePickUpListener) {
onDatePickUpListener.onDatePickUp2(bean);
}
}
}
}
break;
}
return true;
}
- 回调接口
private OnDatePickUpListener onDatePickUpListener;
public void setOnDatePickUpListener(OnDatePickUpListener onDatePickUpListener) {
this.onDatePickUpListener = onDatePickUpListener;
}
public interface OnDatePickUpListener {
void onDatePickUp2(MyCalendarBean bean);
}
效果出来,简单弹窗显示:
(2)高亮显示点击选中的日期
- 在 onTouchEvent() 方法中记录选中的 Region
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
for (int i = 0; i < tempRegions.size(); i++) {
Region region = tempRegions.get(i);
if (region.contains((int) event.getX(), (int) event.getY())) {
MyCalendarBean bean = currentMonthDays.get(i);
if (bean.isCurrentMonth()) {
//看这里
pickRegion.set(region);
invalidate();
if (null != onDatePickUpListener) {
onDatePickUpListener.onDatePickUp2(bean);
}
}
}
}
break;
}
return true;
}
- 在 onDraw() 方法中绘制选中的 Region 对应的圆形背景
private void draw(Canvas canvas, int year, int month) {
tempRegions.clear();
int weekCounts = calendarUtils.getWeekCountsOfMonth(year, month);
itemHeight = parentHeight / weekCounts;
currentMonthDays = calendarUtils.getDaysListOfMonth(year, month);
for (int i = 0; i < currentMonthDays.size(); i++) {
int columnCount = i % column;
int rowCount = i / column;
MyCalendarBean bean = currentMonthDays.get(i);
Region region = new Region();
region.set(columnCount * itemWidth, rowCount * itemHeight, (columnCount * itemWidth) + itemWidth, (rowCount * itemHeight) + itemHeight);
tempRegions.add(region);
if (bean.isCurrentMonth()) {
drawTodayBG(canvas, region.getBounds(), bean);
drawText(canvas, region.getBounds(), bean);
}
}
drawPickUpCircle(canvas);
pickRegion.setEmpty();
}
private void drawPickUpCircle(Canvas canvas) {
if(!pickRegion.isEmpty()){
canvas.drawCircle(pickRegion.getBounds().centerX(), pickRegion.getBounds().centerY(), circleRadius, mPickUpCirclePaint);
}
}
效果如下:
7、优化一下点击处理
- 将点击位置的判断放到 MotionEvent.ACTION_UP 处,并做滑动取消点击的判断处理
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
cancelClick = false;
lastX = event.getX();
lastY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
if ((Math.abs(lastX - event.getX()) >= moveLimit) || (Math.abs(lastY - event.getY()) >= moveLimit)) {
cancelClick = true;
}
break;
case MotionEvent.ACTION_UP:
if (!cancelClick) {
for (int i = 0; i < tempRegions.size(); i++) {
Region region = tempRegions.get(i);
if (region.contains((int) event.getX(), (int) event.getY())) {
MyCalendarBean bean = currentMonthDays.get(i);
if (bean.isCurrentMonth()) {
pickRegion.set(region);
invalidate();
if (null != onDatePickUpListener) {
onDatePickUpListener.onDatePickUp2(bean);
}
}
}
}
}
break;
}
return true;
}
三、MonthCalendarView2
- 将自定义View (MonthCalendarView2)全部代码放出来
package com.example.deesonwoo.mysimplecalendar.calendar;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Region;
import android.view.MotionEvent;
import android.view.View;
import com.example.deesonwoo.mysimplecalendar.R;
import java.util.ArrayList;
import java.util.List;
/**
* MonthCalendarView2
*/
public class MonthCalendarView2 extends View {
private MyCalendarUtils calendarUtils;
protected Paint mPaint = new Paint();
protected Paint mTodayBGPaint = new Paint();
protected Paint mPickUpCirclePaint = new Paint();
private int column = 7;
private int currentYear, currentMonth;
private int parentWidth, parentHeight;
private int itemWidth, itemHeight;
private int circleRadius;
private int textSize;
private List<MyCalendarBean> currentMonthDays;
private List<Region> tempRegions = new ArrayList<>();
private Region pickRegion = new Region();
private float lastX, lastY;
private float moveLimit = 25F;
private boolean cancelClick = false;
private OnDatePickUpListener onDatePickUpListener;
public MonthCalendarView2(Context context) {
super(context);
calendarUtils = new MyCalendarUtils(context);
textSize = MyCalendarUtils.dp2px(context, 15);
mPaint.setTextAlign(Paint.Align.CENTER);
mPaint.setTextSize(textSize);
mPaint.setColor(getResources().getColor(R.color.text_black));
mTodayBGPaint.setColor(getResources().getColor(R.color.theme_color));
mPickUpCirclePaint.setColor(getResources().getColor(R.color.theme_color));
mPickUpCirclePaint.setStyle(Paint.Style.STROKE);
mPickUpCirclePaint.setStrokeWidth(MyCalendarUtils.dp2px(context, 2));
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
setMeasuredDimension(measureWidth, measureWidth * 6 / column);
}
@Override
protected void onSizeChanged(int w, int h, int oldW, int oldH) {
parentWidth = w;
parentHeight = h;
itemWidth = w / column;
circleRadius = itemWidth * 3 / 8;
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(getResources().getColor(R.color.white));
draw(canvas, currentYear, currentMonth);
}
private void draw(Canvas canvas, int year, int month) {
tempRegions.clear();
int weekCounts = calendarUtils.getWeekCountsOfMonth(year, month);
itemHeight = parentHeight / weekCounts;
currentMonthDays = calendarUtils.getDaysListOfMonth(year, month);
for (int i = 0; i < currentMonthDays.size(); i++) {
int columnCount = i % column;
int rowCount = i / column;
MyCalendarBean bean = currentMonthDays.get(i);
Region region = new Region();
region.set(columnCount * itemWidth, rowCount * itemHeight, (columnCount * itemWidth) + itemWidth, (rowCount * itemHeight) + itemHeight);
tempRegions.add(region);
if (bean.isCurrentMonth()) {
drawTodayBG(canvas, region.getBounds(), bean);
drawText(canvas, region.getBounds(), bean);
}
}
drawPickUpCircle(canvas);
pickRegion.setEmpty();
}
private void drawText(Canvas canvas, Rect rect, MyCalendarBean bean) {
canvas.drawText(String.valueOf(bean.getDay()), rect.centerX(), rect.centerY() + (textSize / 4), mPaint);
}
private void drawTodayBG(Canvas canvas, Rect rect, MyCalendarBean bean) {
if (MyCalendarUtils.isToday(bean)) {
canvas.drawCircle(rect.centerX(), rect.centerY(), circleRadius, mTodayBGPaint);
}
}
private void drawPickUpCircle(Canvas canvas) {
if(!pickRegion.isEmpty()){
canvas.drawCircle(pickRegion.getBounds().centerX(), pickRegion.getBounds().centerY(), circleRadius, mPickUpCirclePaint);
}
}
public void setMonth(int year, int month) {
currentYear = year;
currentMonth = month;
invalidate();
}
public String getCurrentYearAndMonth() {
return MyCalendarUtils.formatYearAndMonth(currentYear, currentMonth);
}
/**
* 展示上一个月
*/
public void moveToPreMonth() {
currentMonth -= 1;
invalidate();
}
/**
* 展示下一个月
*/
public void moveToNextMonth() {
currentMonth += 1;
invalidate();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
cancelClick = false;
lastX = event.getX();
lastY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
if ((Math.abs(lastX - event.getX()) >= moveLimit) || (Math.abs(lastY - event.getY()) >= moveLimit)) {
cancelClick = true;
}
break;
case MotionEvent.ACTION_UP:
if (!cancelClick) {
for (int i = 0; i < tempRegions.size(); i++) {
Region region = tempRegions.get(i);
if (region.contains((int) event.getX(), (int) event.getY())) {
MyCalendarBean bean = currentMonthDays.get(i);
if (bean.isCurrentMonth()) {
pickRegion.set(region);
invalidate();
if (null != onDatePickUpListener) {
onDatePickUpListener.onDatePickUp2(bean);
}
}
}
}
}
break;
}
return true;
}
public void setOnDatePickUpListener(OnDatePickUpListener onDatePickUpListener) {
this.onDatePickUpListener = onDatePickUpListener;
}
public interface OnDatePickUpListener {
void onDatePickUp2(MyCalendarBean bean);
}
}
最后实现的效果跟上一篇文章的实现是一模一样的。
四、后续
- 控件还有很多可以扩展修改的地方,大家可以尽情扩展
- 源码链接在这里