LJ的Blog

学海无涯苦做舟

0%

Android自定义View你需要了解的一些东西(巨图预警)

###写在前面###
终于周末了,当我想要松懈一会去浪的时候,脑海中突然闪过了这个东西……
学习.jpg
一图胜千言,日常唠嗑(1/1)。

###1 进入正题###
Android中自定义控件一直是一个比较难但又不得不面对的东西,虽然github+google能解决你的大部分需求,但是说实话,当一些bug发生在第三方控件上时,你仍然需要花费大量的时间去搞定。所以先了解一些和自定义相关的东西绝对是不亏的,话不多说,进入正题。

Android中自定义控件一般分以下三种:

  • 继承已有控件实现,可以理解为对原有控件功能的加强
  • 组合控件,将多个控件结合在一起实现一些功能
  • 完全自定义控件,一般继承于View或者ViewGroup

这三类控件在实现方式上有什么异同呢?一般来说第一种控件是对于原有控件功能的增强,比如给ListView增加下拉刷新,上拉加载更多的功能,我们不需要考虑ListView中每个item如何测量如何绘制,我们需要考虑的是如何实现需要增添的功能。第二种组合几种控件,比如轮播图的实现,你可以组合Viewpager+ImageView,这东西说实话也就是功能的实现,但是如果你没有封装好则会让你的代码显得杂乱无章。第三种则是比较难以上手的,因为他需要你了解一些View相关的知识。

View相关的东西很多,多到可以另开一篇文章写了,所以我尽量摘取重点,咳咳,大伙注意听了啊,小本本都可以拿出来了啊,xiasuhuei老师开始划重点了啊。

###2 xiasuhuei321的重点###
一个展示在屏幕上的View需要经历measure(测量),layout(布局),和draw(绘制)三个过程,其中measure确定View的宽高,layout确定View的最终宽高和四个顶点的位置,而draw则将View绘制到屏幕上。

为了更好的了解这个过程,我们首先需要了解的一个东西就是MeasureSpec
MeasureSpec是一个32位的int值,高2位代表SpecMode,低30位代表SpecSize。SpecMode代表测量模式,SpecSize代表的是在前一种测量模式下的测量值。

了解了MeasureSpec后,我们需要了解SpecMode
SpecMode有三种,表示三种测量模式:

1)UNSPECIFIED:
要多大给多大,父容器不对View有任何限制,这种情况一般不需要我们考虑。

2)EXACTLY
从字面上就能看出来,精确模式,包含了你声明控件宽高的数值和match_parent这两种情况。

3)AT_MOST
对应于wrap_content,这里需要注意,AT_MOST是父容器制定了一个SpecSize,View的大小不能大于这个值。如果你继承于View的代码没有处理wrap_content的话,那么wrap_content和match_parent的效果是一样的。

以上大概讲了一点View相关的知识,View相关的东西远远不及这些,有兴趣可以查阅其他的资料或者阅读源码了解,我这里便不再赘述了。

###3 自定义控件小案例——验证码###
最近在看hongyang大神的博客,刚好翻到了这个小案例,让我通过这个小案例一步一步的为你解析完全自定义控件(继承于View)的神秘面纱。

在上手做之前先分析一下这个验证码需要我们实现的功能:
1.生成随机数字或者字符串
2.点击要能够更换字符串

一个自定View要能做到以下几点:
1)自定义View的属性,要能在xml文件里直接用,方便使用
2)重写omMeasure
3)重写onDraw
第二步并不是必须的,但如果你的东西需要能处理wrap_content的话,那你还是乖乖的重写onMeasure去处理吧。

让我们跟着以上的步骤过一遍:
####3.1 自定义View属性
在res/values下新建一个attrs.xml文件,在里面定义我们的属性和声明。

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="titleText" format="string" />
<attr name="titleTextColor" format="color" />
<attr name="titleTextSize" format="dimension" />

<declare-styleable name="CustomTitleView">
<attr name="titleText" />
<attr name="titleTextColor" />
<attr name="titleTextSize" />
</declare-styleable>
</resources>

如果你用的是eclipse的话,需要你在xml文件里添加

xmlns:custom=”http://schemas.android.com/apk/res/+包名

而如果你是Android Studio的话则添加以下:

xmlns:custom=”http://schemas.android.com/apk/res-auto"

自定义属性有以下几种值:

  • color:颜色值
  • boolean:布尔值
  • dimesion:尺寸值
  • float:浮点值
  • integer:整型值
  • string:字符串
  • fraction:百分数
  • enum:枚举值
  • reference:引用

以上仅仅是说明一下,如果以后有用到碰到不明白的可以google或者百度。

这样就能够在xml文件里使用我们自定义的属性了,之后我们在代码中定义相应的字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 文本
*/
private String mTitleText;
/**
* 文本的颜色
*/
private int mTitleTextColor;
/**
* 文本的大小
*/
private int mTitleTextSize;

/**
* 绘制时控制文本绘制的范围
*/
private Rect mBound;
private Paint mPaint;

接下来需要我们做的便是获取这些属性,并且在代码中作出相应的处理。

在代码中获取属性值:

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
45
46
47
48
49
50
51
52
public CustomTitleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
/**
* 获取我们所定义的自定义样式属性
*/
TypedArray a = context.getTheme()
.obtainStyledAttributes(attrs, R.styleable.CustomTitleView, defStyleAttr, 0);

int n = a.getIndexCount();
for (int i = 0; i < n; i++) {
int attr = a.getIndex(i);
switch (attr) {
case R.styleable.CustomTitleView_titleText:
mTitleText = a.getString(attr);
break;

case R.styleable.CustomTitleView_titleTextColor:
//默认颜色为黑色
mTitleTextColor = a.getColor(attr, Color.BLACK);
break;

case R.styleable.CustomTitleView_titleTextSize:
//默认设置为16sp,TypeValue也可以把sp转化为px
mTitleTextSize = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(

TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
break;
}
}

a.recycle();

/**
* 获取绘制文本的宽和高
*/
mPaint = new Paint();
mPaint.setTextSize(mTitleTextSize);
mBound = new Rect();
mPaint.getTextBounds(mTitleText, 0, mTitleText.length(), mBound);

this.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
//获取随机字符串
mTitleText = randomText();
postInvalidate();
}
});
}

a.recycle();

前面我说如果继承于View的控件在代码中不对wrap_content作出处理,那么这个控件的wrap_content和match_parent的效果将会是一样的,那么就让我们试一试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

@Override
protected void onDraw(Canvas canvas) {
Log.i(TAG,"onDraw");
mPaint.setColor(Color.YELLOW);
canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);

mPaint.setColor(mTitleTextColor);
canvas.drawText(mTitleText, getWidth() / 2f - mBound.width() / 2f, getHeight() / 2f + mBound.height() / 2f, mPaint);
}

以上的onMeasure()方法直接继承于View,没有做任何的修改,在xml文件中声明如下:

1
2
3
4
5
6
<com.example.luo_pc.view.CustomView.CustomTitleView
custom:titleText="1234"
custom:titleTextColor="#ff0000"
custom:titleTextSize="40sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

看好咯,我声明的是wrap_content对吧?让我们来看下运行的结果
全屏.png

黄色并非我设置的背景,而是想要包裹验证码的背景。正如我所说的,如果不处理的话,就是这种效果,很明显这不是我们想要的,那么该如何处理呢?

View的measure()方法是final的,所以这个方法是无法被重写的,但是View提供了onMeasure()方法让我们来处理这些事。onMeasure()方法中带了两个int类型的参数

onMeasure(int widthMeasureSpec, int heightMeasureSpec)

看着这两个东西有没有回想起什么,前面我们了解过MeasureSpec。而这两个正是系统测量出的View的宽和高的MeasureSpec,所以我们便可以在onMeasure()中处理wrap_content的问题。

首先处理宽度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int width = 0;

Log.i(TAG,"onMeasure");
//设置宽度
int specMode = MeasureSpec.getMode(widthMeasureSpec);
int specSize = MeasureSpec.getSize(widthMeasureSpec);

switch (specMode) {
case MeasureSpec.EXACTLY: //精准模式,包含指定大小和match_parent
width = getPaddingLeft() + getPaddingRight() + specSize;
break;
case MeasureSpec.AT_MOST: //一般为wrap_content
width = getPaddingLeft() + getPaddingRight() + mBound.width();
break;
}

前面说了MeasureSpec是SpecMode和SpecSize的打包,我们首先要做的就是拆包。然后根据specMode来确定宽度。如果是EXACTLY自不必多说,直接左右padding加上指定的宽度(或match_parent宽度)就是我们所需的width。而如果是AT_MOST,在本案例中则是我们绘制的矩形背景的宽度。在处理高度的时候也是同样的道理。最终完整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
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = 0;
int height = 0;
Log.i(TAG,"onMeasure");
//设置宽度
int specMode = MeasureSpec.getMode(widthMeasureSpec);
int specSize = MeasureSpec.getSize(widthMeasureSpec);

switch (specMode) {
case MeasureSpec.EXACTLY: //精准模式,包含指定大小和match_parent
width = getPaddingLeft() + getPaddingRight() + specSize;
break;
case MeasureSpec.AT_MOST: //一般为wrap_content
width = getPaddingLeft() + getPaddingRight() + mBound.width();
break;
}

//设置高度
specMode = MeasureSpec.getMode(heightMeasureSpec);
specSize = MeasureSpec.getSize(heightMeasureSpec);
switch (specMode) {
case MeasureSpec.EXACTLY:
height = getPaddingTop() + getPaddingBottom() + specSize;
break;
case MeasureSpec.AT_MOST:
height = getPaddingTop() + getPaddingBottom() + mBound.height();
break;
}
setMeasuredDimension(width, height);
}

最后记得setMeasuredDimension(width, height);
如果不调用这个方法来存储width和height将会在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
/**
*生成随机数字字符串
**/
private String randomText() {
Random random = new Random();
Set<Integer> set = new HashSet<>();
while (set.size() < 4) {
int randomInt = random.nextInt(10);
set.add(randomInt);
}

StringBuilder sb = new StringBuilder();
for (Integer i : set) {
sb.append("" + i);
}
//赋值
text = sb.toString();

return sb.toString();
}

private String text;

public String getText() {
return text;
}

设置背景色,在attr的xml文件里加上两句:

1
2
3
<attr name="titleBackGroudColor" format="color" />
<!--在<declare-styleable name="CustomTitleView">中加入-->
<attr name="titleBackGroudColor" />

在自定义View中加入获取此属性的case:

1
2
case R.styleable.CustomTitleView_titleBackGroudColor:  
mTitleBackColor = a.getColor(attr,Color.YELLOW);

在绘制时加入获取到的颜色

1
mPaint.setColor(mTitleBackColor);

上面获取text的效果就不查看了,看代码就够一目了然了,下面我们将背景设置为灰色查看一下效果:

1
2
3
4
5
6
7
<com.example.luo_pc.view.CustomView.CustomTitleView
custom:titleText="1234"
custom:titleTextColor="#ff0000"
custom:titleTextSize="40sp"
custom:titleBackGroudColor="#bcbcbc"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

灰色.png

再次重申一下,以上这个小案例是从hongyang大神那看到的,各位如果想要深入学习自定义View,hongyang大神那的系列文章绝对是极好的。

参考资料:

Android 自定义View (一)——by hongyang
《开发艺术探索》

源码地址:
hongyang的源码
我整理的Android Studio版源码