为什么要自定义控件呢?Android提供了一套复杂而强大的组件模板,让开发者基于基本的布局类View类和ViewGroup类来创建自定义控件。Android框架已经包含了各种各样的预设的View和VIewGroup子类,既基本控件和布局,开发者可以根据这些控件和布局开发独立的UI控件。部分基本控件包括Button,TextView,EditText, ListVIew, CheckBox,RadioButton,Gallery,Spinner等,以及具有特殊用途的AutoCompleteTextView,ImageSwitcher,和TextSwitcher等。基本布局类包括 LinearLayout, FrameLayout, RelativeLayout等
如果所有的这些预设控件都无法满足需求,那么开发者可以开发自己的View子类。
如果你只需要在现有的基本控件上做一些较小的调整,你可以简单的继承现有的控件,并重写其中一些方法即可。
一般实现自定义控件会有三种方式:1继承已有的控件实现:相当于扩展已有控件的功能、2组合已有的控件实现:利用系统已提供的控件,组合成一个新的控件也就是通过这种方式自定义出来的控件具有多种基本空间的功能,更加强大,较第一种实现方式复杂、3完全自定义控件:该自定义控件继承自View或者ViewGroup,自己绘制。(关于这三种自定义控件,后面慢慢会有专门的博客介绍)
创建自定义控件的一些基本步骤:
1、让自定义控件类(可以是内部类)继承View,或者View的子类。
2、写上构造方法。
3、重写父类的一些方法,通常都是一些以on开头的方法,比如:onMeasure(),onLayout(),onDraw(),onKeyDown()等。
4、应用自定义控件。
当我们继承自View时,系统会要求我们写上构造方法。我们一般重载两个或三个构造方法就可以。
package com.dy.switchdemo;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
/**
* Created by dy on 2016/5/17.
*/
public class CustomSwitchView extends View {
/**
*当在Java代码中创建视图的时候会调用此方法,例如:
* CustomSwitchView customSwitchView=new CustomSwitchView(this);
* @param context
*/
public CustomSwitchView(Context context) {
super(context);
}
/**
*xml创建视图并没有指定defStyleAttr时调用,是默认指定了的
* @param context
* @param attrs
*/
public CustomSwitchView(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
*xml创建视图并指定defStyleAttr时调用,一般不需要
* @param context
* @param attrs
* @param defStyleAttr
*/
public CustomSwitchView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
//带四个参数的构造方法是当API大于21时才有,一般不需要
}
写自定义控件涉及到的几个重要的方法:
1、onMeasure():回调该方法对控件进行测量,确定View的宽高。默认行为通常是设置一个100*100的视图尺寸。
2、onLayout():回调该方法对控件摆放位置进行确定,确定View四个顶点的位置。
3、onDraw():回调该方法对控件进行绘制,将View绘制在屏幕上。默认行为是什么都不做。
常用方法详解:
其中,measure确定View的宽高,layout确定View的四个顶点的位置,而draw将View绘制在屏幕上。
measure()方法是final类型的方法,子类无法复写,而layout()和draw()子类就可以复写
measure和onMeasure()
关于measure(),需要分两种情况讨论,
情况1,只是View,那么通过View的measure() 可以完成对View的测量,而measure() 会去调用 onMeasure方法。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
情况2,是ViewGroup,measure()除了完成对容器自身的测量之外,接着还会遍历去调用子元素(子元素可以使View或者ViewGroup)的measure(),各个子元素再去递归调用自身子元素的measure。注意是递归,递归,递归,重要的说三遍。
在ViewGroup的measure()会去调用onMeasure(),然后我们需要在onMeasure()里面对容器里的孩子进行 孩子.measure() 对孩子进行测量。
layout()和onLayout()
对于layout
如果是View,View调用layout可以指定View的位置
如果是ViewGroup,那么onLayout() 里面进行 孩子.layout 可以精确摆放孩子的位置
draw()和onDraw()
对于draw
这个就 比较简单了,他是按照如下几个流程走的
1、绘制背景 backgroud.draw(canvas)
2、绘制本身( onDraw )
3、绘制孩子( dispatchDraw )
4、绘制装饰 ( onDrawScrollBars )
其实实际开发中我们常用的,关心的也就是onDraw方法。
其实说了这么多,大概就是需要明白的是:
一个View的从无到有需要经过measure(),layout(),draw()三个步骤,measure()会辗转调用measure(),layout()会辗转调用onLayout,draw会转转调用onDraw().
如果是View,那么就写写onDraw,onLayout,至于onMeasure一般不需要。
如果是ViewGroup,那么些在onMeasure里面测量自身后接着进行对孩子的测量,在onLayout里面进行孩子的位置摆放。
.
.
我们知道,我们在Xml写的布局文件最终会在通过Pull解析的方式转成代码的。
onFinishInflate的作用,就是在xml加载组件完成后调用的。这个方法一般在自制ViewGroup的时候调用。
/**
* Finalize inflating a view from XML. This is called as the last phase
* of inflation, after all child views have been added.
*
* Even if the subclass overrides onFinishInflate, they should always be
* sure to call the super method, so that we get called.
*/
@CallSuper
protected void onFinishInflate() {
}
.
.
Returns the view at the specified position in the group.
返回该组中指定位置的视图。
这个位置按照我们include的顺序排列,索引从0开始。
/**
* Returns the view at the specified position in the group.
*
* @param index the position at which to get the view from
* @return the view at the specified position or null if the position
* does not exist within the group
*/
public View getChildAt(int index) {
if (index < 0 || index >= mChildrenCount) {
return null;
}
return mChildren[index];
}
.
.
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
孩子.measure(32位的MeasureSpec宽,32位的MeasureSpec高);
}
但看起来他们是一个int值,这两个参数可以是宽高的意思,但是不完全是,这宽和高都是32由 32位的二进制码组成的,并不是随随便便穿进去一个int类型的值就完事了的。
既然不能随随便便,那我们应该传什么?——我们应该传一个32位的二进制数。
那么我们计算出或者拿到这个数?—— 使用View类里面的MeasureSpec这个静态内部类。
我们首先需要看一个类,MeasureSpec
从上图我们可以看到,MeasureSpec这个类给我们提供了4个方法和3个常量
先看看几个方法
其中,size占30位,mode占2位
这个方法的2个参数
size(大小)怎么指定?
mode怎么确定?
利用LayoutParams得到size
我们的View有多大谁知道?——我们在写xml的布局文件的时候就知道了有多大了。
但是怎么获取文件的在xml布局时的大小呢?
利用View的getLayoutParams()方法可以得到一个LayoutParams类型的值,利用
layoutParams.width 和
layoutParams.height
就可以获得View在布局时的宽高。
怎么确定mode
mode有3种模式,分别是EXACTLY:精确模式;AT_MOST最大值模式,UNSPECIFIED : 未指定的模式,
MeasureSpec.EXACTLY,精确模式
当我们的 layout_width 和 layout_height 设定为
填充父窗体 match_parent 或者
指定具体数值 比如 android:layout_width="130dp" 时
就可以采用EXACTLY这种模式
.
MeasureSpec.AT_MOST,最大值模式
当我们的 layout_width 和 layout_height 设定为 warp_content 时,控件大小随着内容大小的变化而变化,就是采用AT_MOST这种模式。
.
MeasureSpec.UNSPECIFIED,未指定模式
父容器没有给子布局任何限制,子布局可以任意大小。比较奇怪,少用。
getMeasuredWidth 获取测量后的宽度
getMeasuredHeight 获取测量后的高度
这两个如果孩子没有被measure有效地测量过,那么返回的值是0。
getMeasuredWidth
/**
* Like {@link #getMeasuredWidthAndState()}, but only returns the
* raw width component (that is the result is masked by
* {@link #MEASURED_SIZE_MASK}).
*
* @return The raw measured width of this view.
*/
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
}
.
.
getMeasuredHeight
/**
* Like {@link #getMeasuredHeightAndState()}, but only returns the
* raw width component (that is the result is masked by
* {@link #MEASURED_SIZE_MASK}).
*
* @return The raw measured height of this view.
*/
public final int getMeasuredHeight() {
return mMeasuredHeight & MEASURED_SIZE_MASK;
}