自定义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,小米日历

相关推荐
阿巴斯甜13 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker14 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952715 小时前
Andorid Google 登录接入文档
android
黄林晴16 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_1 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android