一直想学自定义view,《安卓开发艺术》上讲的非常清楚,但是对我来说显得有点过于深入,洪洋大神的博客则显得通俗易懂,步骤讲的非常清楚。
基本步骤:
1.创建自定义组件类继承view,然后在res/attrs.xml里定义组件需要的属性:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="titleText" format="string"/>
<attr name="titleTextColor" format="color"/>
<attr name="titleTextSize" format="dimension"/>
<attr name="image" format="reference"/>
<attr name="imageScaleType">
<enum name="fillXY" value="0"/>
<enum name="center" value="1"/>
</attr>
<declare-styleable name="MyView1">
<attr name="titleText"/>
<attr name="titleTextColor"/>
<attr name="titleTextSize"/>
</declare-styleable>
<declare-styleable name="MyView2">
<attr name="titleText"/>
<attr name="titleTextColor"/>
<attr name="titleTextSize"/>
<attr name="image"/>
<attr name="imageScaleType"/>
</declare-styleable>
</resources>
这里每一个declare...标签对应一个自定义组件,name对应自定义组件的类名,这里面我写了两个,表示declare上面的属性是可以复用的。一个attr标签就是一个属性,format标明属性的值的类型,其中reference表示资源ID,如drawabale的id。
2.在构造方法中获取并处理属性:
在xml中直接放置组件调用的是两个参数的方法,但是一般我们在三个参数的构造方法里进行操作,所以让一个参数和两个参数的构造方法都通过this调用三个参数的构造方法。
先用TypedArray类取出包含所有属性的数组,这个数组里的元素都是属性的资源id,int类型,需要拿到这个id进一步取出里面的值。然后洪洋大神进行了switch判断而不是直接取值,应该是避免有些没有指定值的属性报空指针。
//获取并处理自定义属性
TypedArray array=context.obtainStyledAttributes(attrs, R.styleable.MyView1,defStyleAttr,0);
int n=array.getIndexCount();//获取到的属性数目.
for (int i=0;i<n;i++){
int attr=array.getIndex(i);//取出每一个属性的ID
switch (attr){
case R.styleable.MyView1_titleText:
text=array.getString(attr);//通过ID取属性的值
break;
case R.styleable.MyView1_titleTextColor:
mTextColor=array.getColor(attr,defStyleAttr);//def是默认值
break;
case R.styleable.MyView1_titleTextSize:
//设置默认值16sp
int def= (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,16,getResources().getDisplayMetrics());
mTextSize=array.getDimensionPixelSize(attr,def);
break;
}
}
array.recycle();//资源回收
把这些值赋给开始private的变量,(注意这里的mTextSize和mTextColor都是int类型,参数里的def是默认值也就是找不到时候的值。对于一些设置值的方法大多是设置的像素px,比如画笔的setTextSize,应该把sp或者dp先转为px的值传入),然后就可以在measure,draw里进一步使用。调用顺序是构造方法---onMeasure----onDraw。还要及时做资源回收
补充:这些变量应该有初始值,避免空指针或无效。
3.重写onMeasure:
MeasureSpec是measure的关键,每一个MeasureSpec里面存着两个Int数,分别是Mode和Size,也就是测量模式和测量的值。关于Mode的确定,《安卓开发艺术》里做了详细的解释,这里可以简单分为两种:如果Mode是EXACTLY,表示组件指定了精确的值或者match_parent,测量的值是我们希望的值,不用动。否则的话,也就是wrap_content,如果我们不做处理,组件会铺满剩余空间,也就是测量的大小效果等同于match_parent,这和我们平时的印象不同。我们直接使用Button,TextView的时候,这些组件的大小确实是内容自适应的,其实是因为这些组件的wrap_content都经过了判断处理,而对于我们自定义组件,显然需要我们自己处理,手动计算出组件需要的宽高再赋给他作为测量值。比如下面这段代码:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width,height;
//上面一张照片下面一段文字的布局
int widthMode=MeasureSpec.getMode(widthMeasureSpec);
int wigthValue=MeasureSpec.getSize(widthMeasureSpec);
if (widthMode==MeasureSpec.EXACTLY){
width=wigthValue;
} else {
// 由图片决定的宽
int desireByImg = getPaddingLeft() + getPaddingRight() + image.getWidth();
// 由字体决定的宽
int desireByTitle = getPaddingLeft() + getPaddingRight() + textRect.width();
int width2=Math.max(desireByImg,desireByTitle);//取较大的一个
width=Math.min(wigthValue,width2);//不能超过剩余空间
}
int heightMode=MeasureSpec.getMode(heightMeasureSpec);
int heightValue=MeasureSpec.getSize(heightMeasureSpec);
if (widthMode==MeasureSpec.EXACTLY){
height=heightValue;
} else {
int height2=getPaddingTop()+getPaddingBottom()+image.getHeight()+textRect.height();
height=Math.min(heightValue,height2);
}
setMeasuredDimension(width,height);
}
要注意padding的考虑,在内容区域外包一层padding才是组件的区域。接下来的onDraw在画的位置也要考虑padding。
另外,只有测量完才有getHeight,getWidth,getMeasuredHeight,getMeasuredWidth,测量前都是0.
4.重写onDraw:
画笔可以画很多东西,这一块的内容主要是canvas的各种方法
paint经常设置抗锯齿:
paint.setAntiAlias(true);//抗锯齿
rect的left,top,bottom,right可以看作左上和右下两个点坐标。
比如用画笔的STROKE模式画边框的时候,边框是从形状的边缘向内外各延伸50%的边框宽度。
5.使用:
在布局文件中放入这个组件,不能忘了声明一个命名空间来使用自定义属性,名称任意:
xmlns:cunstom="http://schemas.android.com/apk/res-auto"
5.其他
还可以直接在构造器内用this.来添加触摸事件点击事件等,实现了与活动的解耦。