本文为学习完以下几位大神之后整理的学习笔记:
首先,在我的理解,自定义控件就是自己定义的控件,而相对的就是系统提供的控件,例如TextView,ImageView之类的,学习自定义View时,脑海中可对照着系统控件思考。
一、自定义控件的自定义属性
首先我们需要在
res/values/attrs.xml
文件(如果没有请自己新建)里面声明一个自定义属性:
1
2
3
4
5
6
7
8
|
<?
xml
version
=
"1.0"
encoding
=
"utf-8"
?>
<
resources
>
<!--此处为自定义属性集的名字,在代码实现中会用到,但xml布局中不会调用详情可看下文-->
<
declare-styleable
name
=
"CustomView"
>
<!--这些为自定义的属性-->
<!--format为属性类型,共有:string,color,demension,integer,enum,reference,float,boolean,fraction,flag-->
<
attr
name
=
"titleText"
format
=
"string"
/>
<
attr
name
=
"titleTextColor"
format
=
"color"
/>
<
attr
name
=
"titleTextSize"
format
=
"dimension"
/>
</
declare-styleable
>
</
resources
>
|
二、自定义控件的布局
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
<?
xml
version
=
"1.0"
encoding
=
"utf-8"
?>
<!--此句为添加命名空间,自定义属性调用时需引入,custom即为命名空间,可随心修改-->
xmlns:custom = "http://schemas.android.com/apk/res-auto"
android:layout_width
=
"match_parent"
android:layout_height
=
"match_parent"
tools:context
=
"com.eebbk.androidstudy.CustomView.CustomViewActivity"
>
<
com.eebbk.androidstudy.CustomView.CustomView
android:layout_width
=
"wrap_parent"
android:layout_height
=
"wrap_content"
<!--后面三句都是自定义属性的添加赋值,命名空间的作用在此体现了-->
custom:titleText
=
"4324"
custom:titleTextColor
=
"#ff0000"
custom:titleTextSize
=
"40sp"
/>
</
RelativeLayout
>
|
三、自定义控件的实现
/自定义View效果图
先上张效果图,后面读代码时方便理解。
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
|
public
class
CustomView
extends
View {
private
String mTitleText; //效果图中对应的文本,需在xml中自定义属性里取值
private
int
mTitleTextColor; //效果图中文本对应的颜色,需在xml中自定义属性里取值
private
int
mTitleTextSize; //效果图中文本对应的尺寸,需在xml中自定义属性里取值
private
Rect mBound; //效果图中文本对应的边界,需根据文本大小进行计算,后有细说
private
Paint mPaint; //画笔
/*
* 构造函数有三,他们分别对应不同的构造场景:
* 1.对应View view = new View(context);
* 2.对应在xml布局文件中使用View时,在inflate时调用
* 3.与二类似,不同之处在于,三中添加了style属性设置
* 本例中所有构造方法最后调用的都是3,避免代码冗余
*/
public
CustomView(Context context) {
this
(context,
null
);
}
public
CustomView(Context context, AttributeSet attrs) {
this
(context,
attrs
,
0
);
}
public
CustomView(Context context, AttributeSet attrs,
int
defStyle) {
super
(context, attrs);
//此处是从xml中获取自定义属性的值,参数二就是自定义属性集的名字,在步骤一中定义
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomView, defStyle,
0
);
//获取自定义属性时,键值应为R.styleable.自定义属性集_自定义属性
mTitleText = a.getString(R.styleable.CustomView_titleText);
mTitleTextColor = a.getColor(R.styleable.CustomView_titleTextColor, Color.BLACK);
mTitleTextSize = a.getDimensionPixelSize(R.styleable.CustomView_titleTextSize,
100
);
//获取完毕后,需添加此句,对TypedArray对象进行回收
a.recycle();
mPaint =
new
Paint();
mPaint.setTextSize(mTitleTextSize);
mBound =
new
Rect();
mPaint.getTextBounds(mTitleText,
0
, mTitleText.length(), mBound);
}
//onMeasure顾名思义,就是对本View大小的测量,此处是坑,先有个印象吧
protected
void
onMeasure(
int
widthMeasureSpec,
int
heightMeasureSpec) {
super
.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
//onDraw顾名思义,就是对本View的绘制
@Override
protected
void
onDraw(Canvas canvas) {
mPaint.setColor(Color.YELLOW);
//此处的getMeasuredWidth/Height是在onMeasure后才可调用的,对应的是控件的宽度和高度
canvas.drawRect(
0
,
0
, getMeasuredWidth(), getMeasuredHeight(), mPaint);
mPaint.setColor(mTitleTextColor);
canvas.drawText(mTitleText, getWidth() /
2
- mBound.width() /
2
, getHeight() /
2
+ mBound.height() /
2
, mPaint);
}
}
|
android:layout_width
=
"wrap_content"
android:layout_height
=
"wrap_content"
四、onMeasure解析
首先了解下onMeasure的参数,
widthMeasureSpec和heightMeasureSpec,
两个整型值。整型32位 = 2位模式 + 30位值。
1、模式共有三种:
EXACTLY 模式: 准确的、精确的;这种模式,是最容易理解和处理的,可以理解为大小固定,比如在定义layout_width的时候,定义为固定大小 10dp,20dp,或者match_parent(此时父控件是固定的)这时候,获取出 来的mode就是EXACTLY
AT_MOST 模式: 最大的;这种模式稍微难处理些,不过也好理解,就是View的大小最大不能超过父控件,超过了,取父控件的大小,没有,则取自身大小,这种情况一般都是在layout_width设为warp_content时。
UNSPECIFIED 模式:不指定大小,这种情况,我们几乎用不上,它是什么意思呢,就是View的大小想要多大,就给多大,不受父View的限制,几个例子就好理解了,ScrollView控件就是。
当我们在xml中layout_width/layout_height设置成match_parent 或 wrap_content时,只都是match_parent的值,这也就是效果图产生的原因,至于为什么会如此,不明所以啊,所以这就是我们需要重新写onMeasure的原因了。
当我们在xml中对layout_width/layout_height设置具体值时,widthMeasureSpec和heightMeasureSpec就是设置的值。
所以只有当我们的控件设置的是wrap_content模式时,尺寸测量会出现问题
有了以上认知之后,我们就可以重写onMeasure了。
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
|
protected
void
onMeasure(
int
widthMeasureSpec,
int
heightMeasureSpec) {
mPaint.setTextSize(mTitleTextSize);
mPaint.getTextBounds(mTitleText,
0
, mTitleText.length(), mBound);
float
textWidth = mBound.width();
float
textHeight = mBound.height();
int
desiredWidth = (
int
) (getPaddingLeft() + textWidth + getPaddingRight());
int
desiredHeight = (
int
) (getPaddingTop() + textWidth + getPaddingBottom());
setMeasuredDimension(getViewSize(desiredWidth, widthMeasureSpec),
getViewSize(desiredHeight, heightMeasureSpec));
}
//参数一为wrap_content模式时控件的尺寸,参数二为父类传入的尺寸
public
static
int
getViewSize(
int
desiredSize,
int
measureSpec) {
int
specMode = MeasureSpec.getMode(measureSpec);
int
specSize = MeasureSpec.getSize(measureSpec);
int
result = desiredSize;
switch
(specMode) {
case
MeasureSpec.UNSPECIFIED:
result = desiredSize;
break
;
case
MeasureSpec.AT_MOST:
result = Math.min(specSize, desiredSize);
break
;
case
MeasureSpec.EXACTLY:
result = specSize;
break
;
}
return
result;
}
|