手把手教你最快撸一个日历控件


我们的态度是:每天进步一点点,理想终会被实现。

前言

可能很多人都会说,你还自己撸一个日历控件,GitHub有那么多好的开源项目,比如:material-calendarview(https://github.com/prolificinteractive/material-calendarview)4K多的star,而且人家的扩展性也很强,我干嘛要自己撸。我就是个不喜欢用别人的,想着别人能做出来的,自己干嘛不能做出来,再说要是后面的需求越改越多,要是满足不了了,那我们该怎么办?这时就需要我们自己撸了。

还有种情况就是,当我们在赶项目的时候,可能项目初期只需要一个基本的日历控件,其他的暂时用不上,需要我们快速的撸一个。这个时候就派上用场了,教你最快的撸一个日历控件。

看看我们几分钟实现的效果:

Calender日历控件

1.分析

自定义View的实现方式一般就三种:

  • 继承系统控件
  • 组合系统控件
  • 自定义绘制控件

具体这三种方式的详细我就不讲了,不清楚的小伙伴可以查看:

HenCoder Android 自定义 View 1-6: 属性动画(上手篇)(https://juejin.im/post/59af4b415188252427260c3d)
【HenCoder Android 开发进阶】自定义 View 1-7:属性动画(进阶篇)(https://juejin.im/post/59b5fe19f265da06710d8116)

既然我们讲的是快速构建一个日历控件,我们就不完全采用第三种方式绘制。我们主要采用组合系统控件。

2.分析需求

我们看下面这张图来分析:

从图中我们看出,我们将日历分为三个部分:

  • 第一部分:左右箭头,年月显示
  • 第二部分:星期几
  • 第三部分:日历显示

这里我采用ConstrainLayout约束布局来讲这个布局显示出来,代码如下:

  1<?xml version="1.0" encoding="utf-8"?>
2<android.support.constraint.ConstraintLayout
3 xmlns:android="http://schemas.android.com/apk/res/android"
4 xmlns:app="http://schemas.android.com/apk/res-auto"
5 xmlns:tools="http://schemas.android.com/tools"
6 android:layout_width="match_parent"
7 android:layout_height="match_parent">

8 <android.support.v7.widget.AppCompatImageButton
9 android:id="@+id/preMonth"
10 android:layout_width="wrap_content"
11 android:layout_height="wrap_content"
12 android:layout_marginStart="32dp"
13 android:layout_marginTop="8dp"
14 android:src="@drawable/ic_arrow_back_black_24dp"
15 app:layout_constraintStart_toStartOf="parent"
16 app:layout_constraintTop_toTopOf="parent"/>

17 <android.support.v7.widget.AppCompatImageButton
18 android:id="@+id/nextMonth"
19 android:layout_width="wrap_content"
20 android:layout_height="wrap_content"
21 android:layout_marginEnd="32dp"
22 android:layout_marginTop="8dp"
23 android:src="@drawable/ic_arrow_forward_black_24dp"
24 app:layout_constraintEnd_toEndOf="parent"
25 app:layout_constraintTop_toTopOf="parent"/>

26 <android.support.v7.widget.AppCompatTextView
27 android:id="@+id/tv_date"
28 android:layout_width="wrap_content"
29 android:layout_height="wrap_content"
30 android:layout_marginBottom="8dp"
31 android:layout_marginEnd="8dp"
32 android:layout_marginLeft="8dp"
33 android:layout_marginRight="8dp"
34 android:layout_marginStart="8dp"
35 android:layout_marginTop="8dp"
36 android:text="2018年5月"
37 app:layout_constraintBottom_toBottomOf="@+id/preMonth"
38 app:layout_constraintEnd_toStartOf="@+id/nextMonth"
39 app:layout_constraintStart_toEndOf="@+id/preMonth"
40 app:layout_constraintTop_toTopOf="@+id/preMonth"/>

41 <android.support.v7.widget.AppCompatTextView
42 android:id="@+id/sunday"
43 android:layout_width="wrap_content"
44 android:layout_height="wrap_content"
45 android:layout_marginTop="8dp"
46 android:text="周天"
47 android:textColor="@android:color/black"
48 android:textSize="18sp"
49 app:layout_constraintEnd_toStartOf="@+id/monday"
50 app:layout_constraintHorizontal_bias="0.5"
51 app:layout_constraintHorizontal_chainStyle="spread_inside"
52 app:layout_constraintStart_toStartOf="parent"
53 app:layout_constraintTop_toBottomOf="@+id/preMonth"/>

54 <android.support.v7.widget.AppCompatTextView
55 android:id="@+id/monday"
56 android:layout_width="wrap_content"
57 android:layout_height="wrap_content"
58 android:layout_marginTop="8dp"
59 android:textColor="@android:color/black"
60 android:textSize="18sp"
61 android:text="周一"
62 app:layout_constraintEnd_toStartOf="@+id/tuesday"
63 app:layout_constraintHorizontal_bias="0.5"
64 app:layout_constraintHorizontal_chainStyle="spread"
65 app:layout_constraintStart_toEndOf="@+id/sunday"
66 app:layout_constraintTop_toBottomOf="@+id/preMonth"/>

67 <android.support.v7.widget.AppCompatTextView
68 android:id="@+id/tuesday"
69 android:layout_width="wrap_content"
70 android:layout_height="wrap_content"
71 android:layout_marginTop="8dp"
72 android:textColor="@android:color/black"
73 android:textSize="18sp"
74 android:text="周二"
75 app:layout_constraintEnd_toStartOf="@+id/wednesday"
76 app:layout_constraintHorizontal_bias="0.5"
77 app:layout_constraintHorizontal_chainStyle="spread"
78 app:layout_constraintStart_toEndOf="@+id/monday"
79 app:layout_constraintTop_toBottomOf="@+id/preMonth"/>

80 <android.support.v7.widget.AppCompatTextView
81 android:id="@+id/wednesday"
82 android:layout_width="wrap_content"
83 android:layout_height="wrap_content"
84 android:layout_marginTop="8dp"
85 android:textColor="@android:color/black"
86 android:textSize="18sp"
87 android:text="周三"
88 app:layout_constraintEnd_toStartOf="@+id/thursday"
89 app:layout_constraintHorizontal_bias="0.5"
90 app:layout_constraintHorizontal_chainStyle="spread"
91 app:layout_constraintStart_toEndOf="@+id/tuesday"
92 app:layout_constraintTop_toBottomOf="@+id/preMonth"/>

93 <android.support.v7.widget.AppCompatTextView
94 android:id="@+id/thursday"
95 android:layout_width="wrap_content"
96 android:layout_height="wrap_content"
97 android:textColor="@android:color/black"
98 android:textSize="18sp"
99 android:layout_marginTop="8dp"
100 android:text="周四"
101 app:layout_constraintEnd_toStartOf="@+id/friday"
102 app:layout_constraintHorizontal_bias="0.5"
103 app:layout_constraintHorizontal_chainStyle="spread"
104 app:layout_constraintStart_toEndOf="@+id/wednesday"
105 app:layout_constraintTop_toBottomOf="@+id/nextMonth"/>

106 <android.support.v7.widget.AppCompatTextView
107 android:id="@+id/friday"
108 android:layout_width="wrap_content"
109 android:layout_height="wrap_content"
110 android:layout_marginTop="8dp"
111 android:text="周五"
112 android:textColor="@android:color/black"
113 android:textSize="18sp"
114 app:layout_constraintEnd_toStartOf="@+id/saturday"
115 app:layout_constraintHorizontal_bias="0.5"
116 app:layout_constraintHorizontal_chainStyle="spread"
117 app:layout_constraintStart_toEndOf="@+id/thursday"
118 app:layout_constraintTop_toBottomOf="@+id/nextMonth"/>

119 <android.support.v7.widget.AppCompatTextView
120 android:id="@+id/saturday"
121 android:layout_width="wrap_content"
122 android:layout_height="wrap_content"
123 android:layout_marginTop="8dp"
124 android:text="周六"
125 android:textColor="@android:color/black"
126 android:textSize="18sp"
127 app:layout_constraintEnd_toEndOf="parent"
128 app:layout_constraintHorizontal_bias="0.5"
129 app:layout_constraintHorizontal_chainStyle="spread"
130 app:layout_constraintStart_toEndOf="@+id/friday"
131 app:layout_constraintTop_toBottomOf="@+id/nextMonth"/>

132 <android.support.constraint.Group
133 android:id="@+id/group"
134 android:layout_width="wrap_content"
135 android:layout_height="wrap_content"/>

136 <android.support.constraint.Group
137 android:id="@+id/group2"
138 android:layout_width="wrap_content"
139 android:layout_height="wrap_content"/>

140 <android.support.constraint.Barrier
141 android:id="@+id/barrier2"
142 android:layout_width="wrap_content"
143 android:layout_height="wrap_content"
144 app:barrierDirection="top"/>

145 <GridView
146 android:id="@+id/gv_calendar"
147 android:layout_width="0dp"
148 android:layout_height="wrap_content"
149 android:layout_marginTop="16dp"
150 android:numColumns="7"
151 android:verticalSpacing="8dp"
152 app:layout_constraintEnd_toEndOf="parent"
153 app:layout_constraintStart_toStartOf="parent"
154 app:layout_constraintTop_toBottomOf="@+id/wednesday"/>

155</android.support.constraint.ConstraintLayout>
复制代码

界面效果:

基本界面就已经搭建完成了。我们要简单快速的实现日历,我们的日历采用的GridView控件来显示,不熟悉GridView的老铁可以先搜索学习下。

3.处理业务逻辑

这里最核心的业务逻辑:

  • 计算当前月份的第一天星期几
  • 计算当前月份第一天前还要显示上个月几天
  • 计算要显示下个月几天

那么具体代码如下:

  1public class MyCalendar extends LinearLayout implements View.OnClickListener {
2 private LayoutInflater mInflater;
3 //上一个月、下一个月
4 private AppCompatImageButton mPreMonth, mNextMonth;
5 /**
6 * 顶部年月
7 */

8 private AppCompatTextView mDate;
9 /**
10 * 日历列表
11 */

12 private GridView mGridView;
13 private Calendar mCalendar;
14 public MyCalendar(Context context) {
15 super(context);
16 init();
17 }
18 public MyCalendar(Context context, @Nullable AttributeSet attrs) {
19 super(context, attrs);
20 init();
21 }
22 public MyCalendar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
23 super(context, attrs, defStyleAttr);
24 init();
25 }
26 /**
27 * 初始化
28 */

29 private void init() {
30 initView();
31 initListener();
32 initCalenderCell();
33 }
34 private void initListener() {
35 mNextMonth.setOnClickListener(this);
36 mPreMonth.setOnClickListener(this);
37 }
38 private void initView() {
39 mInflater = LayoutInflater.from(getContext());
40 mInflater.inflate(R.layout.calender_view, this, true);
41 mPreMonth = findViewById(R.id.preMonth);
42 mNextMonth = findViewById(R.id.nextMonth);
43 mDate = findViewById(R.id.tv_date);
44 mGridView = findViewById(R.id.gv_calendar);
45 mCalendar = Calendar.getInstance();
46 }
47 @Override
48 public void onClick(View v) {
49 switch (v.getId()) {
50 //下月
51 case R.id.nextMonth:
52 //月份+1
53 mCalendar.add(Calendar.MONTH, 1);
54 initCalenderCell();
55 break;
56 //上月
57 case R.id.preMonth:
58 //月份-1
59 mCalendar.add(Calendar.MONTH, -1);
60 initCalenderCell();
61 break;
62 default:
63 break;
64 }
65 }
66 /**
67 * 计算列表Cell的值
68 */

69 private void initCalenderCell() {
70 //设置顶部年月
71 SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月", Locale.getDefault());
72 mDate.setText(sdf.format(mCalendar.getTime()));
73 /**
74 * 1.计算我们的cell的个数 7*6=42 任何一个月的天数都能包含在里面
75 *
76 * 2.计算这个当前月1号所在星期几,计算上个月显示的天数
77 *
78 * 3.将cell里面添加值
79 */

80 //日历每天的cell
81 ArrayList<Date> cells = new ArrayList<>();
82 //总的天数
83 int count = 7 * 6;
84 //克隆下calender
85 Calendar calendar = (Calendar) mCalendar.clone();
86 //将calender置于当月第一天
87 calendar.set(Calendar.DAY_OF_MONTH, 1);
88 //计算当月1号之前还有几天
89 int preDays = calendar.get(Calendar.DAY_OF_WEEK) - 1;
90 //将当前日期向前移动preDays
91 calendar.add(Calendar.DAY_OF_MONTH, -preDays);
92 //填满42个cells
93 while (cells.size() < count) {
94 //填充cell
95 cells.add(calendar.getTime());
96 //填充一次之后,向后移动一天
97 calendar.add(Calendar.DAY_OF_MONTH, 1);
98 }
99 mGridView.setAdapter(new CalenderAdapter(getContext(), cells));
100 }
101 private class CalenderAdapter extends ArrayAdapter<Date> {
102 private LayoutInflater mInflater;
103 CalenderAdapter(@NonNull Context context, ArrayList<Date> dates) {
104 super(context, R.layout.cell_layout, dates);
105 mInflater = LayoutInflater.from(context);
106 }
107 @NonNull
108 @Override
109 public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
110 final Date date = getItem(position);
111 if (convertView == null) {
112 convertView = mInflater.inflate(R.layout.cell_layout, parent, false);
113 }
114 if (date != null) {
115 int day = date.getDate();
116 ((TextView) convertView).setText(String.valueOf(day));
117 convertView.setOnClickListener(new OnClickListener() {
118 @Override
119 public void onClick(View v) {
120 Toast.makeText(getContext(), new SimpleDateFormat("yyyy年MM月dd", Locale.getDefault()
121 ).format(date), Toast.LENGTH_SHORT).show();
122 }
123 });
124 }
125 //获取当月日期
126 Date now = new Date();
127 boolean isSameMonth = false;
128 //判断是否是本月
129 if (date.getMonth() == now.getMonth()) {
130 isSameMonth = true;
131 }
132 if (isSameMonth) {
133 ((MyTextView) convertView).setTextColor(Color.BLACK);
134 } else {
135 ((MyTextView) convertView).setTextColor(Color.GRAY);
136 }
137 //判断是否是今日
138 if (date.getDate() == now.getDate() && date.getMonth() == now.getMonth() && date.getYear() == now.getYear()) {
139 ((MyTextView) convertView).isToday = true;
140 ((MyTextView) convertView).setTextColor(Color.RED);
141 }
142 return convertView;
143 }
144 }
145}
复制代码

细心的朋友可能看到了MyTextView,这个是自定义的TextView,为当天的日期画个红色的圈,代码如下:

 1public class MyTextView extends AppCompatTextView {
2 private Paint mPaint;
3 public boolean isToday = false;
4 public MyTextView(Context context) {
5 super(context);
6 init();
7 }
8 public MyTextView(Context context, @Nullable AttributeSet attrs) {
9 super(context, attrs);
10 init();
11 }
12 public MyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
13 super(context, attrs, defStyleAttr);
14 init();
15 }
16 private void init() {
17 mPaint = new Paint();
18 mPaint.setColor(Color.RED);
19 mPaint.setStyle(Paint.Style.STROKE);
20 mPaint.setAntiAlias(true);
21 }
22 @Override
23 protected void onDraw(Canvas canvas) {
24 super.onDraw(canvas);
25 if (isToday) {
26 canvas.drawCircle(getWidth() / 2, getHeight() / 2, getWidth() / 2, mPaint);
27 }
28 }
29}
复制代码

最后使用就只需要的在XML中添加我们的自定义的MyCalendar即可:

这样就大功告成了,我们可以看到非常简单的自定义View显示日历就完成了,代码量很少,而且思路很清晰,很适合快速的开发。

这里还没有补充的就是那就是:比如箭头的图片、年月的显示格式颜色大小、星期几的文字颜色大小等这些都是默认,加入后期开发的时候要求可以改变,那么我们就在values文件下创建attrs的xml文件,配置相应的属性即可。

自己撸一个和用别人的东西还是不一样的感觉,至少我是这么认为的。
demo已经上传到github:https://github.com/lt13982250340/MyCalender

一句话总结

本文旨在快速的开发一个日历控件,比如GridView可以换成我们常用的recyclerview,这些都是后期再优化的细节。熟悉API或者流程的话,估计几分钟就可以撸一个日历控件。

在这个基础上再改造,就能得到你想要的效果,符合产品的需求。

温馨提示:
我创建了一个技术交流群,群里有各个行业的大佬都有,大家可以在群里畅聊技术方面内容,以及文章推荐;如果有想加入的伙伴加我微信号【luotaosc】
备注一下“加群”
另外公众号还有一些个人收藏的视频:

原创文章不易,如果觉得写得好,扫码关注一下点个赞,是我最大的动力。

关注我,一定会有意想不到的东西等你:
每天专注分享Android、JAVA干货

备注:程序圈LT

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值