为什么要自定义控件

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

如何自定义控件

下面我通过我在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文件,需要在此文件中声明你自定义控件的自定义参数。

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。

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

android自定义日历插件,Android自定义日历控件实例详解相关推荐

  1. android自定义圆角进度条,Android自定义进度条的圆角横向进度条实例详解

    1.本文将向你介绍自定义进度条的写法,比较简单,但还是有些知识点是需要注意的: invalidate()方法 RectF方法的应用 onMeasure方法的应用 2.原理 画3层圆角矩形,底层为黑色, ...

  2. android控件使用大全,Android常见控件使用详解

    本文实例为大家分享了六种Android常见控件的使用方法,供大家参考,具体内容如下 1.TextView 主要用于界面上显示一段文本信息 2.Button 用于和用户交互的一个按钮控件 //为Butt ...

  3. android相册和拍照并裁剪图片大小,Android 拍照并对照片进行裁剪和压缩实例详解...

    Android 拍照并对照片进行裁剪和压缩实例详解 本文主要介绍 Android 调用摄像头拍照并对照片进行裁剪和压缩,文中给出了主要步骤和关键代码. 调用摄像头拍照,对拍摄照片进行裁剪,代码如下. ...

  4. Flash播放控件属性详解

    Flash 播放控件属性详解 一.属性篇 1.AlignMode(读写)  语法:AlignMode As Long  说明:对齐方式(与SAlign 属性联动).当控件的长宽比例与影片不一致且WMo ...

  5. C#Winform的DataGridView控件使用详解2—DataGridView表格样式设置及表格操作

    C#Winform的DataGridView控件使用详解2-DataGridView表格样式设置及表格操作 DataGridView表格样式设置 DataGridView行序号设置 右键弹出控件表格操 ...

  6. QT QLabel控件(使用详解)

    本文详细的介绍了TextLabel控件的各种操作,例如:显示边框.设置文字.设置字体.设置信息提示框.状态提示.居中对齐.加载图片.自适应图片大小.设置位置大小.样式表等操作. 本文作者原创,转载请附 ...

  7. QT QTabWidget 控件 使用详解

    本文详细的介绍了QTabWidget控件的各种操作,例如:新建界面.设置页面名字.设置提示信息.设置页面激活.设置标题栏位置.设置页面关闭按钮.设置页面关闭按钮.获取页面下标.获取页面总数.清空所有页 ...

  8. VB6.0 ActiveX 控件开发详解 [第一章:创建工程]

    前言 在CSDN的VB论坛上,我总是能够看见有人这样问"有没有这样的控件,一个列表框,每一个项前面有一个按钮"(这是例子),又或者见到这样:"怎么样做一个ActiveX控 ...

  9. C#Winform的DataGridView控件使用详解1—七种DataGridViewColumn类型使用方法

    C#Winform的DataGridView控件使用详解1-七种DataGridViewColumn类型使用方法 DataGirdView控件Column类型 DataGridViewButtonCo ...

  10. QT QSpinBox 整数计数器控件 使用详解

    本文详细的介绍了QSpinBox控件的各种操作,例如:获取数值.设置前后缀.设置最大/小值.进制转换.关联信号槽.优化信号.QSS优化.文件源码.样式表 .效果:可以设置背景.边框.向上按钮.向下按钮 ...

最新文章

  1. 2021年大数据Spark(五十二):Structured Streaming 事件时间窗口分析
  2. mysql 合并分钟_mysql合并和时间函数
  3. 使用python-nmap模块扫描端口脚本
  4. 数据分析之全国热门景点分析
  5. [TJOI2013]循环格
  6. Java9发布回顾Java 8的十大新特性
  7. Entity Framework 5.0系列之约定配置
  8. 简明Git与GitHub命令使用教程
  9. bean json转kotlin_Android kotlin插件神器Json直接生成javaBean
  10. windows下, nginx 提示错误 No input file specified
  11. 深入解析物联网操作系统(架构/功能/实例分析)
  12. 操作系统_图解deepin操作系统安装,体验定制版的国产操作系统
  13. CSS中伪类选择器及伪元素
  14. 全新外卖侠CPS全套源码,2021外卖CPS分销小程序源码
  15. 最好用的切图工具——firework
  16. csf格式手机播放器(安卓csf格式播放器)
  17. Shapefile属性操作之改
  18. 【JS】阮一峰js教程总结
  19. “墙裂”推荐!PDF文档办公必备的四大功能
  20. 如何做好自己的职业规划?

热门文章

  1. 基于multisim的电子秒表
  2. python超市管理系统实训报告_需求分析实验报告(小型超市管理系统)
  3. 计算机电源高频干扰,开关电源中电磁干扰的透彻分析及其解决办法
  4. 计算机的硬件软件组成
  5. “工资3000,一年存20万”:掌握理财技能,让我少奋斗10年
  6. 这些免费版音视频格式转换器哪个最好用
  7. php 图片木马,php图片木马怎么运行
  8. 系统分析师考试大纲(2009版)
  9. Android相同包名不同签名的apk安装失败问题分析
  10. 二手笔记本测试软件,想买二手笔记本?先学会这些检测方法才最大限度不被坑...