赞
踩
实现相对简单,没有什么复杂的效果,支持滚动、点击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; // 对应的坐标
}
传递数据之后,就需要对数据进行计算对应拜访的位置了:
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返利最新优惠资讯
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。