最近在学习自定义view,想着既然学了那就记下来,以后忘了可以看看,顺便分享给大家,一起学习。
好,那我们就进入正题吧,我们知道,一般自定义view时,最简单的我们会继承view或viewgroup,首先呢便是复写三个构造方法
我们来看下这三个构造方法
public class SetItemView extends RelativeLayout {
public SetItemView(Context context) {
this(context, null);
}
public SetItemView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SetItemView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}
第一个构造方法就是我们普通在代码中新建一个view用到的方法,例如
SetItemView setItemView = new SetItemView(this);
一个自定义的view就被新建出来了,然后可以根据需求添加到布局里第二个构造方法就是我们一般在xml文件里添加一个view
<com.example.leac.mysixstudy.com.SetItemView
android:id="@+id/rl_clear"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="14dp" />
我们就把一个SetItemView添加到布局文件里,并且加了一些布局属性,宽高属性以及margin属性等系统自带默认的,这些属性会存放在第二个构造函数的AttributeSet参数里
第三个构造函数比第二个构造函数多了一个int型的值,名字叫defStyleAttr,这是一个关于自定义属性的参数,第三个构造函数不会被系统默认调用,而是需要我们自己去显式调用,比如在第二个构造函数里调用调用第三个函数,并将第三个参数设为0。
再来看看自定义属性
要给view支持自定义属性,需要在values/attrs.xml 文件里定义一个name为自己定义view名字的declare-styleable
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="SetItemView">
<attr name="leftText" format="string"/>
<attr name="leftIcon" format="integer"/>
<attr name="rightIcon" format="integer"/>
<attr name="textSize" format="float"/>
<attr name="textColor" format="color"/>
<attr name="isShowUnderLine" format="boolean"/>
</declare-styleable>
</resources>
这样就可以在xml文件里使用自己定义的属性了
<com.example.leac.mysixstudy.com.SetItemView
android:id="@+id/rl_clear"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
app:leftIcon="@drawable/clearcache"
app:leftText="@string/fragment_me_tv_clear"
app:rightIcon="@drawable/task_arrow"
app:textColor="@color/text_color_6"
app:textSize="16"/>
别忘了在前面加上自定义的命名空间,但要自定义属性生效还是要耗费一些功夫的,这时候前面讲的第三个构造方法的defStyleAttr参数就派上用场了。
/**
* 初始化控件自定义属性信息
*
* @param context
* @param attrs
*/
private void getCustomStyle(Context context, AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.SetItemView);
int num = typedArray.getIndexCount();
for (int i = 0; i < num; i++) {
int arr = typedArray.getIndex(i);
if (arr == R.styleable.SetItemView_leftText) {
mText = typedArray.getString(arr);
mTvLeftText.setText(mText);
} else if (arr == R.styleable.SetItemView_leftIcon) {
mLeftIcon = typedArray.getDrawable(arr);
mIvLeftIcon.setImageDrawable(mLeftIcon);
} else if (arr == R.styleable.SetItemView_rightIcon) {
mRightIcon = typedArray.getDrawable(arr);
mIvRightIcon.setImageDrawable(mRightIcon);
} else if (arr == R.styleable.SetItemView_textSize) {
// 默认设置为16sp
float textSize = typedArray.getFloat(arr, 16);
mTvLeftText.setTextSize(textSize);
} else if (arr == R.styleable.SetItemView_textColor) {
//文字默认灰色
mTextColor = typedArray.getColor(arr, Color.GRAY);
mTvLeftText.setTextColor(mTextColor);
} else if (arr == R.styleable.SetItemView_isShowUnderLine) {
boolean flag = typedArray.getBoolean(arr, true);
if (!flag) {
mUnderLine.setVisibility(View.GONE);
}
}
}
typedArray.recycle();
}
接下来我们看看控件的绘制流程
在Android里,一个view的绘制流程包括:Measure,Layout和Draw,通过onMeasure知道一个view要占界面的大小,然后通过onLayout知道这个控件应该放在哪个位置,最后通过onDraw方法将这个控件绘制出来,然后才能展现在用户面前,下面我将挨个分析一下这三个方法的作用。
onMeasure 测量,通过测量知道一个一个view要占的大小,measure()方法接收两个参数,widthMeasureSpec和heightMeasureSpec,这两个值分别用于确定视图的宽度和高度的规格和大小。MeasureSpec的值由specSize和specMode共同组成的,其中specSize记录的是大小,specMode记录的是规格。我们都知道,在java中,int型由4个字节(32bit)组成,在MeasureSpce中,用前两位表示mode,用后30位表示size
MeasureSpce的mode有三种:EXACTLY, AT_MOST,UNSPECIFIED,除却UNSPECIFIED不谈,其他两种mode:当父布局是EXACTLY时,子控件确定大小或者match_parent,mode都是EXACTLY,子控件是wrap_content时,mode为AT_MOST;当父布局是AT_MOST时,子控件确定大小,mode为EXACTLY,子控件wrap_content或者match_parent时,mode为AT_MOST。所以在确定控件大小时,需要判断MeasureSpec的mode,不能直接用MeasureSpec的size。在进行一些逻辑处理以后,调用setMeasureDimension()方法,将测量得到的宽高传进去供layout使用。需注意是 ,测量所得的宽高不一定是最后展示的宽高,最后宽高确定是在onLayout方法里,layou(left,top,right,bottom),不过一般都是一样的。
host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);
onLayout 实际上,方法里主要是具体摆放子view的位置,水平摆放或者垂直摆放,所以在单纯的自定义view(即继承的是view)是不需要重写onLayout方法,不过需要注意的一点是,子view的margin属性是否生效就要看parent是否在自身的onLayout方法进行处理,而view得padding属性是在onDraw方法中生效的。但是如果继承自ViewGroup,则所有ViewGroup的子类都必须重写onLayout这个方法,子视图的layout()方法接收四个参数,分别代表着左、上、右、下的坐标,当然这个坐标是相对于当前视图的父视图而言的,
onDraw ,一般自定义控件耗费心思最多的就是这个方法了,需要在这个方法里,用Paint在Canvas上画出你想要的图案,这样一个自定义view才算结束。