我们的态度是:每天进步一点点,理想终会被实现。
前言
可能很多人都会说,你还自己撸一个日历控件,GitHub有那么多好的开源项目,比如:material-calendarview(https://github.com/prolificinteractive/material-calendarview)4K多的star,而且人家的扩展性也很强,我干嘛要自己撸。我就是个不喜欢用别人的,想着别人能做出来的,自己干嘛不能做出来,再说要是后面的需求越改越多,要是满足不了了,那我们该怎么办?这时就需要我们自己撸了。
还有种情况就是,当我们在赶项目的时候,可能项目初期只需要一个基本的日历控件,其他的暂时用不上,需要我们快速的撸一个。这个时候就派上用场了,教你最快的撸一个日历控件。
看看我们几分钟实现的效果:
Calender日历控件
分析
自定义View的实现方式一般就三种:
- 继承系统控件
- 组合系统控件
- 自定义绘制控件
具体这三种方式的详细我就不讲了,不清楚的小伙伴可以查看:
HenCoder Android 自定义 View 1-6: 属性动画(上手篇)(https://juejin.im/post/59af4b415188252427260c3d)
【HenCoder Android 开发进阶】自定义 View 1-7:属性动画(进阶篇)(https://juejin.im/post/59b5fe19f265da06710d8116)
既然我们讲的是快速构建一个日历控件,我们就不完全采用第三种方式绘制。我们主要采用组合系统控件。
分析需求
我们看下面这张图来分析:
从图中我们看出,我们将日历分为三个部分:
第一部分:左右箭头,年月显示
第二部分:星期几
第三部分:日历显示
这里我采用ConstrainLayout约束布局来讲这个布局显示出来,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.AppCompatImageButton
android:id="@+id/preMonth"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="8dp"
android:src="@drawable/ic_arrow_back_black_24dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<android.support.v7.widget.AppCompatImageButton
android:id="@+id/nextMonth"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="32dp"
android:layout_marginTop="8dp"
android:src="@drawable/ic_arrow_forward_black_24dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<android.support.v7.widget.AppCompatTextView
android:id="@+id/tv_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="2018年5月"
app:layout_constraintBottom_toBottomOf="@+id/preMonth"
app:layout_constraintEnd_toStartOf="@+id/nextMonth"
app:layout_constraintStart_toEndOf="@+id/preMonth"
app:layout_constraintTop_toTopOf="@+id/preMonth"/>
<android.support.v7.widget.AppCompatTextView
android:id="@+id/sunday"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="周天"
android:textColor="@android:color/black"
android:textSize="18sp"
app:layout_constraintEnd_toStartOf="@+id/monday"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/preMonth"/>
<android.support.v7.widget.AppCompatTextView
android:id="@+id/monday"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textColor="@android:color/black"
android:textSize="18sp"
android:text="周一"
app:layout_constraintEnd_toStartOf="@+id/tuesday"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toEndOf="@+id/sunday"
app:layout_constraintTop_toBottomOf="@+id/preMonth"/>
<android.support.v7.widget.AppCompatTextView
android:id="@+id/tuesday"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textColor="@android:color/black"
android:textSize="18sp"
android:text="周二"
app:layout_constraintEnd_toStartOf="@+id/wednesday"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toEndOf="@+id/monday"
app:layout_constraintTop_toBottomOf="@+id/preMonth"/>
<android.support.v7.widget.AppCompatTextView
android:id="@+id/wednesday"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textColor="@android:color/black"
android:textSize="18sp"
android:text="周三"
app:layout_constraintEnd_toStartOf="@+id/thursday"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toEndOf="@+id/tuesday"
app:layout_constraintTop_toBottomOf="@+id/preMonth"/>
<android.support.v7.widget.AppCompatTextView
android:id="@+id/thursday"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/black"
android:textSize="18sp"
android:layout_marginTop="8dp"
android:text="周四"
app:layout_constraintEnd_toStartOf="@+id/friday"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toEndOf="@+id/wednesday"
app:layout_constraintTop_toBottomOf="@+id/nextMonth"/>
<android.support.v7.widget.AppCompatTextView
android:id="@+id/friday"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="周五"
android:textColor="@android:color/black"
android:textSize="18sp"
app:layout_constraintEnd_toStartOf="@+id/saturday"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toEndOf="@+id/thursday"
app:layout_constraintTop_toBottomOf="@+id/nextMonth"/>
<android.support.v7.widget.AppCompatTextView
android:id="@+id/saturday"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="周六"
android:textColor="@android:color/black"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toEndOf="@+id/friday"
app:layout_constraintTop_toBottomOf="@+id/nextMonth"/>
<android.support.constraint.Group
android:id="@+id/group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<android.support.constraint.Group
android:id="@+id/group2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<android.support.constraint.Barrier
android:id="@+id/barrier2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="top"/>
<GridView
android:id="@+id/gv_calendar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:numColumns="7"
android:verticalSpacing="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/wednesday"/>
</android.support.constraint.ConstraintLayout>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
界面效果:
基本界面就已经搭建完成了。我们要简单快速的实现日历,我们的日历采用的GridView控件来显示,不熟悉GridView的老铁可以先搜索学习下。
处理业务逻辑
这里最核心的业务逻辑:
- 计算当前月份的第一天星期几
- 计算当前月份第一天前还要显示上个月几天
- 计算要显示下个月几天
那么具体代码如下:
public class MyCalendar extends LinearLayout implements View.OnClickListener {
private LayoutInflater mInflater;
//上一个月、下一个月
private AppCompatImageButton mPreMonth, mNextMonth;
/**
* 顶部年月
*/
private AppCompatTextView mDate;
/**
* 日历列表
*/
private GridView mGridView;
private Calendar mCalendar;
public MyCalendar(Context context) {
super(context);
init();
}
public MyCalendar(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public MyCalendar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
/**
* 初始化
*/
private void init() {
initView();
initListener();
initCalenderCell();
}
private void initListener() {
mNextMonth.setOnClickListener(this);
mPreMonth.setOnClickListener(this);
}
private void initView() {
mInflater = LayoutInflater.from(getContext());
mInflater.inflate(R.layout.calender_view, this, true);
mPreMonth = findViewById(R.id.preMonth);
mNextMonth = findViewById(R.id.nextMonth);
mDate = findViewById(R.id.tv_date);
mGridView = findViewById(R.id.gv_calendar);
mCalendar = Calendar.getInstance();
}
@Override
public void onClick(View v) {
switch (v.getId()) {
//下月
case R.id.nextMonth:
//月份+1
mCalendar.add(Calendar.MONTH, 1);
initCalenderCell();
break;
//上月
case R.id.preMonth:
//月份-1
mCalendar.add(Calendar.MONTH, -1);
initCalenderCell();
break;
default:
break;
}
}
/**
* 计算列表Cell的值
*/
private void initCalenderCell() {
//设置顶部年月
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月", Locale.getDefault());
mDate.setText(sdf.format(mCalendar.getTime()));
/**
* 1.计算我们的cell的个数 7*6=42 任何一个月的天数都能包含在里面
*
* 2.计算这个当前月1号所在星期几,计算上个月显示的天数
*
* 3.将cell里面添加值
*/
//日历每天的cell
ArrayList<Date> cells = new ArrayList<>();
//总的天数
int count = 7 * 6;
//克隆下calender
Calendar calendar = (Calendar) mCalendar.clone();
//将calender置于当月第一天
calendar.set(Calendar.DAY_OF_MONTH, 1);
//计算当月1号之前还有几天
int preDays = calendar.get(Calendar.DAY_OF_WEEK) - 1;
//将当前日期向前移动preDays
calendar.add(Calendar.DAY_OF_MONTH, -preDays);
//填满42个cells
while (cells.size() < count) {
//填充cell
cells.add(calendar.getTime());
//填充一次之后,向后移动一天
calendar.add(Calendar.DAY_OF_MONTH, 1);
}
mGridView.setAdapter(new CalenderAdapter(getContext(), cells));
}
private class CalenderAdapter extends ArrayAdapter<Date> {
private LayoutInflater mInflater;
CalenderAdapter(@NonNull Context context, ArrayList<Date> dates) {
super(context, R.layout.cell_layout, dates);
mInflater = LayoutInflater.from(context);
}
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
final Date date = getItem(position);
if (convertView == ) {
convertView = mInflater.inflate(R.layout.cell_layout, parent, false);
}
if (date != ) {
int day = date.getDate();
((TextView) convertView).setText(String.valueOf(day));
convertView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(getContext(), new SimpleDateFormat("yyyy年MM月dd", Locale.getDefault()
).format(date), Toast.LENGTH_SHORT).show();
}
});
}
//获取当月日期
Date now = new Date();
boolean isSameMonth = false;
//判断是否是本月
if (date.getMonth() == now.getMonth()) {
isSameMonth = true;
}
if (isSameMonth) {
((MyTextView) convertView).setTextColor(Color.BLACK);
} else {
((MyTextView) convertView).setTextColor(Color.GRAY);
}
//判断是否是今日
if (date.getDate() == now.getDate() && date.getMonth() == now.getMonth() && date.getYear() == now.getYear()) {
((MyTextView) convertView).isToday = true;
((MyTextView) convertView).setTextColor(Color.RED);
}
return convertView;
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
细心的朋友可能看到了MyTextView,这个是自定义的TextView,为当天的日期画个红色的圈,代码如下:
public class MyTextView extends AppCompatTextView {
private Paint mPaint;
public boolean isToday = false;
public MyTextView(Context context) {
super(context);
init();
}
public MyTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public MyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (isToday) {
canvas.drawCircle(getWidth() / 2, getHeight() / 2, getWidth() / 2, mPaint);
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
最后使用就只需要的在XML中添加我们的自定义的MyCalendar即可:
这样就大功告成了,我们可以看到非常简单的自定义View显示日历就完成了,代码量很少,而且思路很清晰,很适合快速的开发。
这里还没有补充的就是那就是:比如箭头的图片、年月的显示格式颜色大小、星期几的文字颜色大小等这些都是默认,加入后期开发的时候要求可以改变,那么我们就在values文件下创建attrs的xml文件,配置相应的属性即可。
自己撸一个和用别人的东西还是不一样的感觉,至少我是这么认为的。
demo已经上传到github:https://github.com/lt13982250340/MyCalender
一句话总结
本文旨在快速的开发一个日历控件,比如GridView可以换成我们常用的recyclerview,这些都是后期再优化的细节。熟悉API或者流程的话,估计几分钟就可以撸一个日历控件。
在这个基础上再改造,就能得到你想要的效果,符合产品的需求。
温馨提示:
我创建了一个技术交流群,群里有各个行业的大佬都有,大家可以在群里畅聊技术方面内容,以及文章推荐;如果有想加入的伙伴加我微信号【luotaosc】备注一下“加群”
另外关注公众号,还有一些个人收藏的视频:
回复“Android” ,获取Android视频链接。
回复“Java” ,获取Java视频链接。
回复“C++” ,获取C++视频链接。
回复“C” ,获取C视频链接。
回复“Python” ,获取Python视频链接等等。
原创文章不易,如果觉得写得好,扫码关注一下点个赞,是我最大的动力。
关注我,一定会有意想不到的东西等你:
每天专注分享Android、JAVA干货
备注:程序圈LT
<link rel="stylesheet" href="https://csdnimg.cn/release/phoenix/template/css/markdown_views-ea0013b516.css">
</div>