自定义View,实现日历展示事件

自定义View,实现日历展示事件,周和月两种。先上个效果图:

实现相对简单,没有什么复杂的效果,支持滚动、点击item项目。

下面说下自定义流程,月和周的类似,就说个周的,月的基本就在周的基础上进行修改即可。

开始着手

首先进行分析界面

界面包含以下元素:

周期一行

周期对应的当前日期一行

每个时间点为一行

1、每一列的宽度是固定的,左侧的时间栏宽度也是固定的

2、内容区域每一行具有默认高度,当超过默认高度之后会继续向下撑,让这一行高度进行适配内容总高度

3、如果总的表列宽度大于屏幕宽度,高度大于屏幕高度,需要进行滑动适配

4、每个内容item项目支持点击

一、基础属性设置

ini 复制代码
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进行设置,做成自定义属性

二、数据配置

ini 复制代码
```
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的背景颜色之类的,需要进行区分等则同样可以由数据模型传递进行设置

arduino 复制代码
// 数据模型
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轴线的位置:

typescript 复制代码
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内,后续绘制可以直接使用

四、开始布局绘制

按步骤由底部开始向上绘制

scss 复制代码
@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宽度进行缩短显示:

ini 复制代码
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接口: `

less 复制代码
@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,小米日历

相关推荐
深海呐4 小时前
Android AlertDialog圆角背景不生效的问题
android
ljl_jiaLiang4 小时前
android10 系统定制:增加应用使用数据埋点,应用使用时长统计
android·系统定制
花花鱼4 小时前
android 删除系统原有的debug.keystore,系统运行的时候,重新生成新的debug.keystore,来完成App的运行。
android
落落落sss5 小时前
sharding-jdbc分库分表
android·java·开发语言·数据库·servlet·oracle
消失的旧时光-19438 小时前
kotlin的密封类
android·开发语言·kotlin
服装学院的IT男9 小时前
【Android 13源码分析】WindowContainer窗口层级-4-Layer树
android
CCTV果冻爽10 小时前
Android 源码集成可卸载 APP
android
码农明明10 小时前
Android源码分析:从源头分析View事件的传递
android·操作系统·源码阅读
秋月霜风11 小时前
mariadb主从配置步骤
android·adb·mariadb
Python私教12 小时前
Python ORM 框架 SQLModel 快速入门教程
android·java·python