来看看效果图先,手把手教你实现一个简易,但高扩展度的日历控件,可自由扩展成签到,单选,多选日期。
首先我们来分析实现思路。对于上图的效果,很明显是一个6x7的表格。
我们可以两个for循环控制绘制每个元素,第一行特殊处理,绘制出星期符号。
接下来确定1号在星期几,这个月最大有几号,依次循环绘制就行。
对于日历控件,不可避免要采取这个类:Calendar 这是java为我们提供的日历相关,这是最简单的实现日历方案。
对于这个类,我们主要用到下面的方法
Calendar c = Calendar.getInstance();
c.set(Calendar.DATE,1);
int start = c.get(Calendar.DAY_OF_WEEK);//获得1号是星期几
int maxDay = c.getActualMaximum(Calendar.DATE);//获得当前月的最大日期数
c.get(Calendar.YEAR)//x年
c.get(Calendar.MONTH)+1;//x月
通过这些方法,我们拥有了1号是星期几,最大是几号。
获取到上面的参数后,我们可以开始正式的绘制
新建一个类,继承view,实现三个构造方法,重写onDraw
我们先在中间画一个正方形,边长为view宽和高两个之间较小那个。
然后分割为7x7的49个小正方形。(对于这里有问题的,参考我上一篇博客九宫格解锁那个)
然后,两个for循环~先把第一行星期标识符画出来
mPaint.setColor(Color.BLACK);
mPaint.setTextAlign(Paint.Align.CENTER);
mPaint.setTextSize(tWidth / 2);//字体大小,单位px
Paint.FontMetricsInt fontMetrics = mPaint.getFontMetricsInt();
//开始绘制
for(int j=0;j<7;j++){
int tY=startY+j*tWidth+tWidth/2;
for(int k=0;k<7;k++){
int tX=startX+k*tWidth+tWidth/2;
//tX,tY为每个cell的中心
if(j==0){//画星期标示符
int baseline=tY+((fontMetrics.bottom - fontMetrics.top)/2-fontMetrics.bottom);
canvas.drawText(weekString[k],tX,baseline,mPaint);
}
这里有一个难点,关于文字的绘制,请参考这篇博客:http://blog.youkuaiyun.com/hursing/article/details/18703599
写的很详细,也很好。
int nDay=2-start-7;//用这个获取到这个元素的"日期"
if(nDay>0 && nDay<=maxDay){//绘画日期
//回调接口获得cell的元素
CalendarCell cell=mOnDrawCellListener.onDrawCell(c.get(Calendar.YEAR),c.get(Calendar.MONTH)+1,nDay,k);
mPaint.setColor(cell.backgroundColor);
canvas.drawCircle(tX, tY, tWidth / 3, mPaint);//画背景圆圈
mPaint.setColor(cell.textColor);
int baseline=tY+((fontMetrics.bottom - fontMetrics.top)/2-fontMetrics.bottom);
canvas.drawText(cell.text, tX, baseline, mPaint);
}
nDay++;
根据nday变量控制绘制日期。这里有一个
mOnDrawCellListener.onDrawCell
这是为了自定义扩展所定义的接口,CalendarCell 这个类是这样的
class CalendarCell{
int backgroundColor;//背景色,背景画圆圈
int textColor;//文字颜色
String text;//画什么字
public CalendarCell(int backgroundColor, int textColor, String text) {
this.backgroundColor = backgroundColor;
this.textColor = textColor;
this.text = text;
}
}
用这个类定义每个元素的背景色,文字颜色,画什么文字上去
接口回调以后根据结果设置画笔绘制文字。
public CalendarCell defaultReturnCell(int year,int month,int day){
if (isToday(year,month,day)){
return new CalendarCell(Color.GRAY,Color.WHITE,"今");
}else{
return new CalendarCell(Color.WHITE,Color.BLACK,day+"");
}
}
这是一个默认的回调接口实现,所实现的功能就是上面截图所展示的。日期白底黑字,今天灰底白字。
正是这个接口的提供,所以我们的日历可以有很高的可扩展性。
ps:dayOfWeek表示星期几,0表示星期日,1表示星期一,2表示。。。。等等
接下来提供日期被点击以后的接口回调
@Override
public boolean onTouchEvent(MotionEvent event) {
int x= (int) event.getRawX();
int y= (int) event.getRawY();
//坐标转换
int[] location = new int[2] ;
getLocationInWindow(location);
x=x-location [0];
y=y-location [1];
// Log.e("绝对坐标",x+"--"+y);
int which=whichPath(x,y);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//触屏事件回调
which=which-start-6;
if (which>0 && which<=maxDay){
// Log.i("click","->"+which);
if(mOnCellClickListener!=null){
mOnCellClickListener.onCellClick(which);
}
}
break;
}
return true;
}
这里的坐标转换是因为触屏事件的坐标是以手机屏幕,除去最上面标题栏,就是你应用实际所占区域为起始点计算的。
而我们的控件可不是完全在界面左上的。需要得到坐标再转换为以最开始所画的大正方形为起始点。
通过坐标判断在哪行哪列,再用序号表示所在区域。
private int whichPath(int x, int y) {
if(x>startX+width | y>startY+width |x<startX |y<startY){
// Log.i("xy",x+"-"+y);
return 0;
}else {
//以起始点开始计算坐标
int nX = x - startX;
int nY = y - startY;
int hang = (int) nY / tWidth;//在哪行,从零开始
int lie = (int) nX / tWidth;//在哪列,从零开始
// Log.i("nXY",nX+"-"+nY);
int reInt = hang * 7 + 1 + lie;
return reInt;
}
}
至此,我们的日历显示和点击回调都完成了。然后再提供切换月份,年份,刷新,设置接口回调等等方法。
整个类的代码如下:
package com.toxicant.hua.mycalendarviewdemo;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import java.util.Calendar;
/**
* Created by hua on 2016/2/4.
*/
public class MyCalendarView extends View {
private int startX=0;
private int startY=0;
private int width=0;//大正方形的边长
private int tWidth=0;//小正方形的边长
private Paint mPaint=new Paint();//画笔
private Calendar c=Calendar.getInstance();//用于记录当前年月
Calendar today=Calendar.getInstance();//今天
int start=0;//一号是星期几
int maxDay=0;//最大日期
private String[] weekString={"日","一","二","三","四","五","六"};//标示符
//两个回调接口初始化
private OnDrawCellListener mOnDrawCellListener2;
private OnDrawCellListener mOnDrawCellListener=new OnDrawCellListener() {
@Override
public CalendarCell onDrawCell(int years, int month, int day, int dayOfWeek) {
return defaultReturnCell(years,month,day);
}
};
private OnCellClickListener mOnCellClickListener;
//绘制接口
public interface OnDrawCellListener{
CalendarCell onDrawCell(int year,int month,int day,int dayOfWeek);
}
//触屏接口
public interface OnCellClickListener{
void onCellClick(int day);
}
public MyCalendarView(Context context) {
super(context);
}
public MyCalendarView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyCalendarView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onDraw(Canvas canvas) {
//日历相关参数初始化
c.set(Calendar.DATE,1);
start = c.get(Calendar.DAY_OF_WEEK);//获得1号是星期几
maxDay = c.getActualMaximum(Calendar.DATE);//获得当前月的最大日期数
width=Math.min(getMeasuredHeight(),getMeasuredWidth());//获取正方形区域边长
//获取起始绘制点
startY=(getMeasuredHeight()-width)/2;
startX=(getMeasuredWidth()-width)/2;
//初始化画笔
mPaint.setColor(Color.WHITE);
mPaint.setStrokeWidth(2);
//绘制大正方形
canvas.drawRect(startX, startY, startX + width, startY + width, mPaint);
tWidth=width/7;
int nDay=2-start-7;
//画星期标识符的画笔设置
mPaint.setColor(Color.BLACK);
mPaint.setTextAlign(Paint.Align.CENTER);
mPaint.setTextSize(tWidth / 2);//字体大小,单位px
Paint.FontMetricsInt fontMetrics = mPaint.getFontMetricsInt();
//开始绘制
for(int j=0;j<7;j++){
int tY=startY+j*tWidth+tWidth/2;
for(int k=0;k<7;k++){
int tX=startX+k*tWidth+tWidth/2;
//tX,tY为每个cell的中心
if(j==0){//画星期标示符
int baseline=tY+((fontMetrics.bottom - fontMetrics.top)/2-fontMetrics.bottom);
canvas.drawText(weekString[k],tX,baseline,mPaint);
}
if(nDay>0 && nDay<=maxDay){//绘画日期
//回调接口获得cell的元素
CalendarCell cell=mOnDrawCellListener.onDrawCell(c.get(Calendar.YEAR),c.get(Calendar.MONTH)+1,nDay,k);
mPaint.setColor(cell.backgroundColor);
canvas.drawCircle(tX, tY, tWidth / 3, mPaint);//画背景圆圈
mPaint.setColor(cell.textColor);
int baseline=tY+((fontMetrics.bottom - fontMetrics.top)/2-fontMetrics.bottom);
canvas.drawText(cell.text, tX, baseline, mPaint);
}
nDay++;
}//for inside
}//for outside
}//onDraw
@Override
public boolean onTouchEvent(MotionEvent event) {
int x= (int) event.getRawX();
int y= (int) event.getRawY();
//坐标转换
int[] location = new int[2] ;
getLocationInWindow(location);
x=x-location [0];
y=y-location [1];
// Log.e("绝对坐标",x+"--"+y);
int which=whichPath(x,y);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//触屏事件回调
which=which-start-6;
if (which>0 && which<=maxDay){
// Log.i("click","->"+which);
if(mOnCellClickListener!=null){
mOnCellClickListener.onCellClick(which);
}
}
break;
}
return true;
}
/**
* 根据相对坐标计算所在区域
* @param x 坐标x
* @param y 坐标y
* @return 返回区域代号
*/
private int whichPath(int x, int y) {
if(x>startX+width | y>startY+width |x<startX |y<startY){
// Log.i("xy",x+"-"+y);
return 0;
}else {
//以起始点开始计算坐标
int nX = x - startX;
int nY = y - startY;
int hang = (int) nY / tWidth;//在哪行,从零开始
int lie = (int) nX / tWidth;//在哪列,从零开始
// Log.i("nXY",nX+"-"+nY);
int reInt = hang * 7 + 1 + lie;
return reInt;
}
}
/**
* 设置元素点击事件
* @param l
*/
public void setOnCellClickListener(OnCellClickListener l){
mOnCellClickListener=l;
}
/**
* 设置绘画每个元素的接口
* @param l
*/
public void setOnDrawCellListener(OnDrawCellListener l){
mOnDrawCellListener=l;
}
/**
* 判断是不是今天
* @param y 年
* @param m 月
* @param d 日
* @return 若是今天返回true
*/
public boolean isToday(int y,int m,int d){
return y==today.get(Calendar.YEAR) && m==today.get(Calendar.MONTH)+1 && d==today.get(Calendar.DATE);
}
/**
* 切换下一月视图
*/
public void nextMonth(){
c.add(Calendar.MONTH,1);
invalidate();
}
/**
* 返回上一月视图
*/
public void backMonth(){
c.add(Calendar.MONTH,-1);
invalidate();
}
/**
* 获取日历的年份
* @return 日历当前年份
*/
public int getYear(){
return c.get(Calendar.YEAR);
}
/**
* 获取日历的月份
* @return 日历当前月份
*/
public int getMonth(){
return c.get(Calendar.MONTH)+1;
}
/**
* 设置日历年份,例如2016年对应2016
* @param year 实际年份
*/
public void setYear(int year){
c.set(Calendar.YEAR,year);
invalidate();
}
/**
* 设置日历的月份,一月对应1,以此类推
* @param month 实际月份
*/
public void setMonth(int month){
c.set(Calendar.MONTH,month-1);
invalidate();
}
public void refresh(){
invalidate();
}
public CalendarCell defaultReturnCell(int year,int month,int day){
if (isToday(year,month,day)){
return new CalendarCell(Color.GRAY,Color.WHITE,"今");
}else{
return new CalendarCell(Color.WHITE,Color.BLACK,day+"");
}
}
/**
* 获得标示时间的文本,例如2016年2月1日,返回20160201
* @param year 年
* @param month 月
* @param day 日
* @return 返回日期戳
*/
public String getDateString(int year,int month,int day){
return (year*10000+month*100+day)+"";
}
}//class
class CalendarCell{
int backgroundColor;//背景色,背景画圆圈
int textColor;//文字颜色
String text;//画什么字
public CalendarCell(int backgroundColor, int textColor, String text) {
this.backgroundColor = backgroundColor;
this.textColor = textColor;
this.text = text;
}
}
复制粘贴就可以迅速使用到自己的项目中啦~
下面通过一个例子来讲下控件扩展成多选日期。
布局xml:日历,两个按钮
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.toxicant.hua.mycalendarviewdemo.MainActivity">
<com.toxicant.hua.mycalendarviewdemo.MyCalendarView
android:id="@+id/calendar"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Back"
android:id="@+id/button"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="New Button"
android:id="@+id/button2"
android:layout_alignBottom="@+id/button"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true" />
</RelativeLayout>
activity代码:
package com.toxicant.hua.mycalendarviewdemo;
import android.graphics.Color;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {
Button btnBack;
Button btnNext;
MyCalendarView calendarView;
List<String> dateList=new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
calendarView= (MyCalendarView) findViewById(R.id.calendar);
btnBack= (Button) findViewById(R.id.button);
btnNext= (Button) findViewById(R.id.button2);
calendarView.setOnCellClickListener(new MyCalendarView.OnCellClickListener() {
@Override
public void onCellClick(int day) {
String md=calendarView.getDateString(calendarView.getYear(),calendarView.getMonth(),day);
if (dateList.contains(md)){
dateList.remove(md);
}else{
dateList.add(md);
}
calendarView.refresh();
Toast.makeText(MainActivity.this,"click->"+day,Toast.LENGTH_SHORT).show();
}
});
calendarView.setOnDrawCellListener(new MyCalendarView.OnDrawCellListener() {
@Override
public CalendarCell onDrawCell(int year, int month, int day, int dayOfWeek) {
String md=calendarView.getDateString(year,month,day);
if (dateList.contains(md)){
return new CalendarCell(Color.GREEN,Color.WHITE,"√");
}
return calendarView.defaultReturnCell(year,month,day);
}
});
btnBack.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
calendarView.backMonth();
}
});
btnNext.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
calendarView.nextMonth();
}
});
}
}
点击日期后保存到list中,再次点击取消选择,从list中移除。然后刷新日历控件。
效果图如下:
博文到此结束,可以根据需求,自行扩展日历控件,单选多选,连续选择日期,签到啊,根据颜色显示每日活动频率都不在话下。
(如果你是学习自定义view的新人,可以根据思路自行实现周六和周日字体变灰的日历)