为什么要自定义控件
有时,原生控件不能满足我们对于外观和功能的需求,这时候可以自定义控件来定制外观或功能;有时,原生控件可以通过复杂的编码实现想要的功能,这时候可以自定义控件来提高代码的可复用性。
如何自定义控件
下面我通过我在github上开源的Android-CalendarView项目为例,来介绍一下自定义控件的方法。该项目中自定义的控件类名是CalendarView。这个自定义控件覆盖了一些自定义控件时常需要重写的一些方法。
构造函数
为了支持本控件既能使用xml布局文件声明,也可在java文件中动态创建,实现了三个构造函数。
1 2 3 |
public CalendarView(Context context, AttributeSet attrs, int defStyle); public CalendarView(Context context, AttributeSet attrs); public CalendarView(Context context); |
可以在参数列表最长的第一个方法中写上你的初始化代码,下面两个构造函数调用第一个即可。
1 2 3 4 5 6 |
public CalendarView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public CalendarView(Context context) { this(context, null); } |
那么在构造函数中做了哪些事情呢?
1 读取自定义参数
读取布局文件中可能设置的自定义属性(该日历控件仅自定义了一个mode参数来表示日历的模式)。代码如下。只要在attrs.xml中自定义了属性,就会自动创建一些R.styleable下的变量。
1 2 |
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文件,需要在此文件中声明你自定义控件的自定义参数。
1 2 3 4 5 6 |
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="CalendarView"> <attr name="mode" format="integer" /> </declare-styleable> </resources> |
2 初始化关于绘制控件的相关参数
如字体的颜色、尺寸,控件各个部分尺寸。
3 初始化关于逻辑的相关参数
对于日历来说,需要能够判断对应于当前的年月,日历中的每个单元格是否合法,以及若合法,其表示的day的值是多少。未设定年月之前先用当前时间来初始化。实现如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
/** * 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()方法,因为某个月的第一天可能是星期一到星期日的任何一个,而且每个月的天数不尽相同,因此日历控件的行数会有多变化,也导致控件的高度会有变化。因此需要根据当前的年月计算控件显示的高度(宽度设为屏幕宽度即可)。实现如下。
1 2 3 4 5 6 7 |
@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()则是根据年月计算控件所需要的高度。实现如下,已经写了非常详细的注释。
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 |
/** * 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绘制文字。
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 |
/** * 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++) { |