自定义View,实现日历展示事件,周和月两种。先上个效果图:
实现相对简单,没有什么复杂的效果,支持滚动、点击item项目。
下面说下自定义流程,月和周的类似,就说个周的,月的基本就在周的基础上进行修改即可。
开始着手
首先进行分析界面
界面包含以下元素:
周期一行
周期对应的当前日期一行
每个时间点为一行
1、每一列的宽度是固定的,左侧的时间栏宽度也是固定的
2、内容区域每一行具有默认高度,当超过默认高度之后会继续向下撑,让这一行高度进行适配内容总高度
3、如果总的表列宽度大于屏幕宽度,高度大于屏幕高度,需要进行滑动适配
4、每个内容item项目支持点击
一、基础属性设置
private int mWidth, mHeight, mTotalWidth, mTotalHeight; // 整个试图的宽高,UI全绘制后总的宽高
private int mAxisWidth = 120; // 左侧坐标轴列的宽度
private float mItemHorizontalCount = 2.8f; // 横向显示多少个方格item
private int mItemWidth, mItemHeight = 150; // 每一个方格item的宽高,宽度取决于mItemHorizontalCount,高度存在最小值固定
private int mItemPadding = 2; // 方格item内间距
private int mWorkWidth, mWorkHeight; // 内容的宽高,宽度取决于ItemWidth,高度取决于文字高度+mWorkPaddingVertical * 2
private int mWorkPaddingHorizontal = 6; // 内容的横向内间距
private int mWorkPaddingVertical = 6; // 内容的纵向间距
private final int mStrokeWidth = 2; // 线宽
private int mAxisTextSize = 33; // 坐标轴文字大小
private int mWorkTextSize = 33; // 内容文字大小
private boolean isVerticalLineToTop = true;
private int mWeekMarginVertical = 30;
private int mDateCircleMarginVertical = 20;
private int mWeekHeight; // 顶部第一行,周和日期行的高度
private int mCircleRadius = mAxisTextSize;
private boolean isShowEllipsis, isShowWholeDay;
private static final int TITLE_INTERVAL_TO_SUB_TITLE = 10;
以上属性都相应会在attrs进行设置,做成自定义属性
二、数据配置
```
public String[] setDate(String date) {
date = date.replace("-", "/");
String[] split = date.split("/");
if (split.length == 3) {
try {
int year = Integer.parseInt(split[0]);
int month = Integer.parseInt(split[1]);
int day = Integer.parseInt(split[2]);
return setDate(year, month, day);
} catch (Exception ignored) {
}
}
return null;
}
```
// 返回第一天和最后一天的日期
public String[] setDate(int year, int month, int day) {
String[] firstLastDate = new String[2];
mYear = year;
mMonth = month;
mDay = day;
List<String> weekDayList = TimeUtils.getWeekDayList(String.format(Locale.getDefault(), "%4d-%02d-%02d", year, month, day));
firstLastDate[0] = weekDayList.get(0);
firstLastDate[1] = weekDayList.get(weekDayList.size() - 1);
for (int i = 0; i < weekDayList.size(); i++) {
String date = weekDayList.get(i);
String[] split = date.split("/");
if (split.length == 3) {
try {
mDate[i + 1] = Integer.parseInt(split[2]);
} catch (NumberFormatException ignored) {
}
}
}
return firstLastDate;
}
由外部传递一个日期,然后在内部进行计算获取一周的所有日期,用于绘制
数据模型至少需要包含坐标轴左侧需要的分类信息-时间、列分类信息-日期、内容标题
至于内容item的背景颜色之类的,需要进行区分等则同样可以由数据模型传递进行设置
// 数据模型
public class TimeWork {
private String mId;
private int mBgColor;
private String mTitle;
private String mDate; // 年月日
private String mTime; // 00 00:00 00:00:00 三种都可以,只使用到小时,月日历可以不进行设置
// UI绘制使用
private RectF rectF; // 对应的坐标
}
三、计算各种路径位置
传递数据之后,就需要对数据进行计算对应拜访的位置了:
首先计算所有XY轴线的位置:
private Map<String, Integer> mPathXMap; // 记录每一根竖线的X轴,用于work绘制时设置X轴起点,key位mDate
private Map<String, Integer> mPathYMap; // 记录每一根横线的Y轴,用于work绘制时设置Y轴起点,key位mTime
每一列宽度位置:因为每一列都是固定宽度的,只需要宽度进行相加,就可以获取到相应的宽度,然后将宽度对应保存进mPathYMap内,以方便后续绘制读取使用。
每一行高度位置:由TimeWork的mDate和mTime可以计算出位于表格的哪个item项目内,高度则需要计算总的有多少个在item内,然后进行叠加,计算后和默认的高度对比,取大值为item的最终高度。最后这一行的高度,则需要取这一行所有item的最大高度。然后保存在mPathXMap内,以方便后续绘制读取使用。
同时在遍历计算XY轴的时候,也把每个TimeWork摆放的位置也计算出来,存储到TimeWork的RectF内,后续绘制可以直接使用
四、开始布局绘制
按步骤由底部开始向上绘制
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 绘制工作项的背景
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(mBackgroundColor);
canvas.drawRect(mAxisWidth + mStrokeWidth, mWeekHeight + mStrokeWidth / 2f, mWidth, mHeight, mPaint);
// 绘制工作项
drawWork(canvas);
// 盖一层白色背景在时间后面,遮住下面的内容
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(mAxisBackgroundColor);
canvas.drawRect(0, mWeekHeight + mStrokeWidth / 2f, mAxisWidth, mHeight, mPaint);
// 盖一层白色背景在周和日期后面,遮住下面的内容
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(mAxisBackgroundColor);
canvas.drawRect(mAxisWidth + mStrokeWidth, 0, mWidth, mWeekHeight - mStrokeWidth / 2f, mPaint);
// 绘制横线
drawHorizontalAxis(canvas);
// 绘制竖线
drawVerticalAxis(canvas);
// 绘制左侧时间
drawTime(canvas);
// 绘制周和日期
drawWeek(canvas);
}
首先绘制出背景,之后绘制工作项,因为考虑后续移动,会导致工作项移动到坐标轴顶部和左边的日期时间区域,所以工作项需要先进行绘制,之后再绘制坐标轴和顶部左边的日期时间,层级才不会乱。
对于绘制的位置,基本在第三步已经计算完毕,这里只需要根据计算的位置,进行绘制即可,内容标题则需要根据列item宽度进行缩短显示:
subTitleWidth = mPaint.measureText(subTitle);
int maxWidth = mWorkWidth - 2 * mWorkPaddingHorizontal;
boolean isOver = false;
while (subTitleWidth > maxWidth && subTitle.length() > 0) {
isOver = true;
subTitle = subTitle.substring(0, subTitle.length() - 1);
subTitleWidth = mPaint.measureText(subTitle);
}
if (isShowEllipsis && isOver && subTitle.length() >= 2) {
subTitle = subTitle.substring(0, subTitle.length() - 1) + "...";
subTitleWidth = mPaint.measureText(subTitle);
}
canvas.drawText(subTitle, left + mWorkWidth - mWorkPaddingHorizontal - subTitleWidth / 2f, titleY, mPaint);
五、滑动和点击支持
让自定义View进行实现GestureDetector.OnGestureListener接口:
`
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
if (gestureDetector.onTouchEvent(event))
return true;
else
return super.onTouchEvent(event);
}
@Override
public boolean onDown(@NonNull MotionEvent e) {
scroller.forceFinished(true);
return true;
}
@Override
public void onShowPress(@NonNull MotionEvent e) {
}
@Override
public boolean onSingleTapUp(@NonNull MotionEvent e) {
// 点击事件处理
...
return true;
}
@Override
public boolean onScroll(@NonNull MotionEvent e1, @NonNull MotionEvent e2, float distanceX, float distanceY) {
mXOffset -= distanceX;
mYOffset -= distanceY;
checkOffset();
refresh();
return false;
}
@Override
public void onLongPress(@NonNull MotionEvent e) {
}
@Override
public boolean onFling(@NonNull MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY) {
Log.i("TimeTable", "onFling mXOffset: " + mXOffset + " mYOffset: " + mYOffset +
" minX: " + (mWidth - mTotalWidth) + " maxX: " + 0 + " velocityX:" + velocityX +
" minY: " + (mHeight - mTotalHeight) + " maxY: " + 0 + " velocityY:" + velocityY);
scroller.fling(
mXOffset, mYOffset, (int) velocityX, (int) velocityY,
mWidth - mTotalWidth, 0,
mHeight - mTotalHeight, 0);
invalidate();
return true;
}
@Override
public void computeScroll() {
super.computeScroll();
Log.i("TimeTable", "currY: " + scroller.getCurrY());
if (scroller.computeScrollOffset()) {
mXOffset = scroller.getCurrX();
mYOffset = scroller.getCurrY();
checkOffset();
refresh();
}
}
private boolean checkOffset() {
Log.i("TimeTable", "mXOffset pre: " + mXOffset + " mYOffset pre: " + mYOffset);
boolean isOver = false;
if (mXOffset > 0) {
mXOffset = 0;
isOver = true;
} else if (mXOffset < mWidth - mTotalWidth) {
mXOffset = mWidth - mTotalWidth;
isOver = true;
}
if (mYOffset > 0) {
mYOffset = 0;
isOver = true;
} else if (mYOffset < mHeight - mTotalHeight) {
mYOffset = mHeight - mTotalHeight;
isOver = true;
}
Log.i("TimeTable", "mXOffset: " + mXOffset + " mYOffset: " + mYOffset);
return isOver;
}
1、滚动事件:
基于接口的onScroll方法,进行设置XY轴的偏移量进行绘制,关键在于偏移量设置之后,需要去检测一次边界,避免偏移量超出试图边界。而onFling方法用于快速滑动后手指离开界面的持续滚动,同样计算好XY轴偏移量即可。
设置完XY轴偏移量之后进行刷新重新从三开始计算各种属性然后重新绘制。
2、点击事件:
基于接口的onSingleTapUp方法,可以获取到点击的XY轴坐标位置,再根据位置,可以通过
mPathXMap、mPathYMap判断是在哪一列、哪一行,还可以通过TimeWork带有的坐标信息RectF判断是点了哪个项目
完结啦
整体来说,实现相对简单,没有什么花里胡哨的功能
项目源码:Calendar: 周日历、月日历 用于记录展示事件列表 (gitee.com)
项目里面的ncalendar模块,使用的是GitHub - yannecer/NCalendar: 一款安卓日历,仿miui,钉钉,华为的日历,万年历、365、周日历,月日历,月视图、周视图滑动切换,农历,节气,Andriod Calendar , MIUI Calendar,小米日历
微信搜索“A查佣利小助手”,获取支付宝红包、TB/JD/PDD返利最新优惠资讯