Android自定义日历控件实例详解

为什么要自定义控件

有时,原生控件不能满足我们对于外观和功能的需求,这时候可以自定义控件来定制外观或功能;有时,原生控件可以通过复杂的编码实现想要的功能,这时候可以自定义控件来提高代码的可复用性。

如何自定义控件

下面我通过我在github上开源的Android-CalendarView项目为例,来介绍一下自定义控件的方法。该项目中自定义的控件类名是CalendarView。这个自定义控件覆盖了一些自定义控件时常需要重写的一些方法。

构造函数

为了支持本控件既能使用xml布局文件声明,也可在java文件中动态创建,实现了三个构造函数。

public CalendarView(Context context, AttributeSet attrs, int defStyle);
public CalendarView(Context context, AttributeSet attrs);
public CalendarView(Context context);

可以在参数列表最长的第一个方法中写上你的初始化代码,下面两个构造函数调用第一个即可。

public CalendarView(Context context, AttributeSet attrs) {
  this(context, attrs, 0);
}
public CalendarView(Context context) {
  this(context, null);
}

那么在构造函数中做了哪些事情呢?

1 读取自定义参数

读取布局文件中可能设置的自定义属性(该日历控件仅自定义了一个mode参数来表示日历的模式)。代码如下。只要在attrs.xml中自定义了属性,就会自动创建一些R.styleable下的变量。

代码如下:

TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CalendarView);
mode = typedArray.getInt(R.styleable.CalendarView_mode, Constant.MODE_SHOW_DATA_OF_THIS_MONTH);

然后附上res目录下values目录下的attrs.xml文件,需要在此文件中声明你自定义控件的自定义参数。

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <declare-styleable name="CalendarView">
    <attr name="mode" format="integer" />
  </declare-styleable>
</resources>

2 初始化关于绘制控件的相关参数

如字体的颜色、尺寸,控件各个部分尺寸。

3 初始化关于逻辑的相关参数

对于日历来说,需要能够判断对应于当前的年月,日历中的每个单元格是否合法,以及若合法,其表示的day的值是多少。未设定年月之前先用当前时间来初始化。实现如下。

/**
 * calculate the values of date[] and the legal range of index of date[]
 */
private void initial() {
  int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
  int monthStart = -1;
  if(dayOfWeek >= 2 && dayOfWeek <= 7){
    monthStart = dayOfWeek - 2;
  }else if(dayOfWeek == 1){
    monthStart = 6;
  }
  curStartIndex = monthStart;
  date[monthStart] = 1;
  int daysOfMonth = daysOfCurrentMonth();
  for (int i = 1; i < daysOfMonth; i++) {
    date[monthStart + i] = i + 1;
  }
  curEndIndex = monthStart + daysOfMonth;
  if(mode == Constant.MODE_SHOW_DATA_OF_THIS_MONTH){
    Calendar tmp = Calendar.getInstance();
    todayIndex = tmp.get(Calendar.DAY_OF_MONTH) + monthStart - 1;
  }
}

其中date[]是一个整型数组,长度为42,因为一个日历最多需要6行来显示(6*7=42),curStartIndex和curEndIndex决定了date[]数组的合法下标区间,即前者表示该月的第一天在date[]数组的下标,后者表示该月的最后一天在date[]数组的下标。

4 绑定了一个OnTouchListener监听器

监听控件的触摸事件。

onMeasure方法

该方法对控件的宽和高进行测量。CalendarView覆盖了View类的onMeasure()方法,因为某个月的第一天可能是星期一到星期日的任何一个,而且每个月的天数不尽相同,因此日历控件的行数会有多变化,也导致控件的高度会有变化。因此需要根据当前的年月计算控件显示的高度(宽度设为屏幕宽度即可)。实现如下。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(screenWidth, View.MeasureSpec.EXACTLY);
  heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(measureHeight(), View.MeasureSpec.EXACTLY);
  setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
  super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

其中screenWidth是构造函数中已经获取的屏幕宽度,measureHeight()则是根据年月计算控件所需要的高度。实现如下,已经写了非常详细的注释。

/**
 * calculate the total height of the widget
 */
private int measureHeight(){
  /**
   * the weekday of the first day of the month, Sunday's result is 1 and Monday 2 and Saturday 7, etc.
   */
  int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
  /**
   * the number of days of current month
   */
  int daysOfMonth = daysOfCurrentMonth();
  /**
   * calculate the total lines, which equals to 1 (head of the calendar) + 1 (the first line) + n/7 + (n%7==0?0:1)
   * and n means numberOfDaysExceptFirstLine
   */
  int numberOfDaysExceptFirstLine = -1;
  if(dayOfWeek >= 2 && dayOfWeek <= 7){
    numberOfDaysExceptFirstLine = daysOfMonth - (8 - dayOfWeek + 1);
  }else if(dayOfWeek == 1){
    numberOfDaysExceptFirstLine = daysOfMonth - 1;
  }
  int lines = 2 + numberOfDaysExceptFirstLine / 7 + (numberOfDaysExceptFirstLine % 7 == 0 ? 0 : 1);
  return (int) (cellHeight * lines);
}

onDraw方法

该方法实现对控件的绘制。其中drawCircle给定圆心和半径绘制圆,drawText是给定一个坐标x,y绘制文字。

/**
 * render
 */
@Override
protected void onDraw(Canvas canvas) {
 super.onDraw(canvas);
 /**
 * render the head
 */
 float baseline = RenderUtil.getBaseline(0, cellHeight, weekTextPaint);
 for (int i = 0; i < 7; i++) {
 float weekTextX = RenderUtil.getStartX(cellWidth * i + cellWidth * 0.5f, weekTextPaint, weekText[i]);
 canvas.drawText(weekText[i], weekTextX, baseline, weekTextPaint);
 }
 if(mode == Constant.MODE_CALENDAR){
 for (int i = curStartIndex; i < curEndIndex; i++) {
  drawText(canvas, i, textPaint, "" + date[i]);
 }
 }else if(mode == Constant.MODE_SHOW_DATA_OF_THIS_MONTH){
 for (int i = curStartIndex; i < curEndIndex; i++) {
  if(i < todayIndex){
  if(data[date[i]]){
   drawCircle(canvas, i, bluePaint, cellHeight * 0.37f);
   drawCircle(canvas, i, whitePaint, cellHeight * 0.31f);
   drawCircle(canvas, i, blackPaint, cellHeight * 0.1f);
  }else{
   drawCircle(canvas, i, grayPaint, cellHeight * 0.1f);
  }
  }else if(i == todayIndex){
  if(data[date[i]]){
   drawCircle(canvas, i, bluePaint, cellHeight * 0.37f);
   drawCircle(canvas, i, whitePaint, cellHeight * 0.31f);
   drawCircle(canvas, i, blackPaint, cellHeight * 0.1f);
  }else{
   drawCircle(canvas, i, grayPaint, cellHeight * 0.37f);
   drawCircle(canvas, i, whitePaint, cellHeight * 0.31f);
   drawCircle(canvas, i, blackPaint, cellHeight * 0.1f);
  }
  }else{
  drawText(canvas, i, textPaint, "" + date[i]);
  }
 }
 }
}

需要说明的是,绘制文字时的这个x表示开始位置的x坐标(文字最左端),这个y却不是文字最顶端的y坐标,而应传入文字的baseline。因此若想要将文字绘制在某个区域居中部分,需要经过一番计算。本项目将其封装在了RenderUtil类中。实现如下。

/**
 * get the baseline to draw between top and bottom in the middle
 */
public static float getBaseline(float top, float bottom, Paint paint){
 Paint.FontMetrics fontMetrics = paint.getFontMetrics();
 return (top + bottom - fontMetrics.bottom - fontMetrics.top) / 2;
}
/**
 * get the x position to draw around the middle
 */
public static float getStartX(float middle, Paint paint, String text){
 return middle - paint.measureText(text) * 0.5f;
}

自定义监听器

控件需要自定义一些监听器,以在控件发生了某种行为或交互时提供一个外部接口来处理一些事情。本项目的CalendarView提供了两个接口,OnRefreshListener和OnItemClickListener,均为自定义的接口。onItemClick只传了day一个参数,年和月可通过CalendarView对象的getYear和getMonth方法获取。

interface OnItemClickListener{
 void onItemClick(int day);
}
interface OnRefreshListener{
 void onRefresh();
}

先介绍一下两种mode,CalendarView提供了两种模式,第一种普通日历模式,日历每个位置简单显示了day这个数字,第二种本月计划完成情况模式,绘制了一些图形来表示本月的某一天是否完成了计划(模仿自悦跑圈,用一个圈表示本日跑了步)。

OnRefreshListener用于刷新日历数据后进行回调。两种模式定义了不同的刷新方法,都对OnRefreshListener进行了回调。refresh0用于第一种模式,refresh1用于第二种模式。

/**
 * used for MODE_CALENDAR
 * legal values of month: 1-12
 */
@Override
public void refresh0(int year, int month) {
 if(mode == Constant.MODE_CALENDAR){
 selectedYear = year;
 selectedMonth = month;
 calendar.set(Calendar.YEAR, selectedYear);
 calendar.set(Calendar.MONTH, selectedMonth - 1);
 calendar.set(Calendar.DAY_OF_MONTH, 1);
 initial();
 invalidate();
 if(onRefreshListener != null){
  onRefreshListener.onRefresh();
 }
 }
}

/**
 * used for MODE_SHOW_DATA_OF_THIS_MONTH
 * the index 1 to 31(big month), 1 to 30(small month), 1 - 28(Feb of normal year), 1 - 29(Feb of leap year)
 * is better to be accessible in the parameter data, illegal indexes will be ignored with default false value
 */
@Override
public void refresh1(boolean[] data) {
 /**
 * the month and year may change (eg. Jan 31st becomes Feb 1st after refreshing)
 */
 if(mode == Constant.MODE_SHOW_DATA_OF_THIS_MONTH){
 calendar = Calendar.getInstance();
 selectedYear = calendar.get(Calendar.YEAR);
 selectedMonth = calendar.get(Calendar.MONTH) + 1;
 calendar.set(Calendar.DAY_OF_MONTH, 1);
 for(int i = 1; i <= daysOfCurrentMonth(); i++){
  if(i < data.length){
  this.data[i] = data[i];
  }else{
  this.data[i] = false;
  }
 }
 initial();
 invalidate();
 if(onRefreshListener != null){
  onRefreshListener.onRefresh();
 }
 }
}

OnItemClickListener用于响应点击了日历上的某一天这个事件。点击的判断在onTouch方法中实现。实现如下。在同一位置依次接收到ACTION_DOWN和ACTION_UP两个事件才认为完成了点击。

@Override
public boolean onTouch(View v, MotionEvent event) {
 float x = event.getX();
 float y = event.getY();
 switch (event.getAction()) {
 case MotionEvent.ACTION_DOWN:
  if(coordIsCalendarCell(y)){
  int index = getIndexByCoordinate(x, y);
  if(isLegalIndex(index)) {
   actionDownIndex = index;
  }
  }
  break;
 case MotionEvent.ACTION_UP:
  if(coordIsCalendarCell(y)){
  int actionUpIndex = getIndexByCoordinate(x, y);
  if(isLegalIndex(actionUpIndex)){
   if(actionDownIndex == actionUpIndex){
   actionDownIndex = -1;
   int day = date[actionUpIndex];
   if(onItemClickListener != null){
    onItemClickListener.onItemClick(day);
   }
   }
  }
  }
  break;
 }
 return true;
}

关于该日历控件

日历控件demo效果图如下,分别为普通日历模式和本月计划完成情况模式。

需要说明的是CalendarView控件部分只包括日历头与下面的日历,该控件上方的是其他控件,这里仅用作展示一种使用方法,你完全可以自定义这部分的样式。

此外,日历头的文字支持多种选择,比如周一有四种表示:一、周一、星期一、Mon。此外还有其他一些控制样式的接口,详情见源码:Android-CalendarView。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Android自定义日历Calender代码实现

    产品要做签到功能,签到功能要基于一个日历来进行,所以就根据 要求自定义了一个日历 自定义控件相信做android都知道: (1)首先创建一个类,继承一个容器类或者是一个控件 (2)然后就是你需要设置的属性等的,在attrs文件夹中 (3)然后就是在类里边进行属性的设置以及布局等等功能的添加 其实自定义一个日历问题都不多,很多人都会想到通过一个gridView然后填充就可以,确实是这样,主要是在显示每个月的第一天的位置以及每个月显示多少天有点绕. 思路:通过判断当前星期几然后进行日历的填充,但是填

  • android 开发教程之日历项目实践(一)

    前言:决定开始学习 Android 平台下的软件开发,以日历作为实践项目,进行一周后,基本完成. 为了总结及笔记,并给有需要的朋友借鉴,开始整理本教程. 开始之前: 在编写程序之前,需要进行项目设计,因为是练习项目,主要是确定软件 UI 界面,这是已经完成的屏幕截图: 对这个画面,进一步作分解: 这里总共分解为三个 View 文件: 1:activity_main.xml 作为启动的主画面,新建项目时,首先生成. 2:view_calendar_table.xml 定义月历视图,头部固定,其它行

  • Android自定义可标记日历效果

    先直接看看效果吧 初始情况 点击一个作为标记 再次点击后删除 3.这里还要感谢前辈的代码作为参考,毕竟以前也没有写过关于日历方面的东西,别人确实写得不错,我在原基础上加入了数据库操作等补充,以完成自己实际需求,作为尊重首先给出原作者的连接 就是这里–>Android自定义控件实现可多选课程日历CalendarView 4.然后贴出来关于数据库操作的代码,给大家作为参考 DatabaseHelper .java 这是关于简单数据库操作的部分 package com.xugongming38.edi

  • java制作android 日历代码分享

    代码很简单,就不多废话了 复制代码 代码如下: //读取日历事件     public static void getCalendarInfo(Activity activity,String tag){         String[] projection = new String[]{CalendarContract.Events._ID,CalendarContract.Events.TITLE};         ContentResolver cr = activity.getCon

  • Android实现自定义日历

    自定义日历控件,支持旧历.节气.日期标注.点击操作 (参考网络上的日历控件改写) 注:将下面的四张资源图片拷贝到所建包的下一个image目录中,如Calendar.java 所在包为 cc.util.android.view,则需要再创建一个包cc.util.android.view.image 然后将图片拷贝进去 /****************从此出开始将代码拷贝到一个文件中*******************/ package cc.util.android.view; import

  • Android实现日历控件示例代码

    做的是一个酒店的项目,可以选择入住和离开的日期.声明为了省事在网上找的资料,自己修改的逻辑,希望对需要的朋友有帮助.喜欢的给个好评.谢谢啦!祝生活愉快! 先上图 第一步,搭建布局xml <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_w

  • Android使用GridLayout绘制自定义日历控件

    效果图 思路:就是先设置Gridlayout的行列数,然后往里面放置一定数目的自定义日历按钮控件,最后实现日历逻辑就可以了. 步骤: 第一步:自定义日历控件(初步) 第二步:实现自定义单个日期按钮控件 第三步:将第二步得到的控件动态添加到第一步的布局中,并实现日期逻辑 第四步:编写单个日期点击监听器接口 第一步:自定义日历控件(初步) <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmln

  • Android使用GridView实现日历的简单功能

    简单的日历实现,只是显示了每一个月,没有显示当天和记事这些功能 主要是计算月初是周几,月末是周几,然后相应的显示上一月多少天和下一月多少天. 先看一下关于日期的用到的几个工具类 /** * 获取该月天数 */ public static int getCurrentMonthDay(long millSec) { Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(millSec); calendar.set(

  • Android自定义控件实现可多选课程日历CalendarView

    可多选课程日历CalendarView的效果图 开发环境 IDE版本:AndroidStudio2.0 物理机版本:Win7旗舰版(64位) 前言 最近的项目中用到了一个课程选择的日历View,于是在网上搜了搜自定义日历View,发现基本上都是单选的,不能够满足项目中的需求.于是自己重新造了个轮子,写了个可以被多选的自定义日历View.最后面会给出GitHub地址. 代码实现 package widget; import android.content.Context; import andro

  • android 开发教程之日历项目实践(二)

    一.创建 Android Project 在新建对话框中输入 App 属性,SDK版本全部选最新的,不作版本兼容.主题选择 Holo Dark. 下一步,使用默认设置 下一步,使用默认设置 下一步,使用默认配置 下一步,使用默认设置 创建完成后的初始画面 在上面的步骤中,我们选择了创建 MainActivity,ADT 帮我们在 src 目录下生成了 MainActivity.java 文件,在 res/layout/ 目录下生成了 activity_main.xml 文件,并在编辑窗口打开,如

随机推荐