###写在前面### 终于周末了,当我想要松懈一会去浪的时候,脑海中突然闪过了这个东西……
###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文件,在里面定义我们的属性和声明。
<?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文件里使用我们自定义的属性了,之后我们在代码中定义相应的字段:
/**
* 文本
*/
private String mTitleText;
/**
* 文本的颜色
*/
private int mTitleTextColor;
/**
* 文本的大小
*/
private int mTitleTextSize;
/**
* 绘制时控制文本绘制的范围
*/
private Rect mBound;
private Paint mPaint;
复制代码
接下来需要我们做的便是获取这些属性,并且在代码中作出相应的处理。
在代码中获取属性值:
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的效果将会是一样的,那么就让我们试一试。
@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文件中声明如下:
<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对吧?让我们来看下运行的结果
黄色并非我设置的背景,而是想要包裹验证码的背景。正如我所说的,如果不处理的话,就是这种效果,很明显这不是我们想要的,那么该如何处理呢?
View的measure()方法是final的,所以这个方法是无法被重写的,但是View提供了onMeasure()方法让我们来处理这些事。onMeasure()方法中带了两个int类型的参数
onMeasure(int widthMeasureSpec, int heightMeasureSpec)
看着这两个东西有没有回想起什么,前面我们了解过MeasureSpec。而这两个正是系统测量出的View的宽和高的MeasureSpec,所以我们便可以在onMeasure()中处理wrap_content的问题。
首先处理宽度:
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()代码如下:
@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测量的过程中引发异常。其他的代码并没有变化,再跑一遍看看咋样了。
恩,包住了,点击也能换数字了,不过如果是验证码的话,还需要一个获取验证码内容的方法,这个不难,直接在生成的时候设置一个就成了。还有一个是背景色,现在是写死的,如果我想换个颜色呢,我自己可以改源码,但是要给别人用的话可不能让人这么用。不过实现起来都很简单,直接上代码。
获取文字内容:
/**
*生成随机数字字符串
**/
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文件里加上两句:
<attr name="titleBackGroudColor" format="color" />
<!--在<declare-styleable name="CustomTitleView">中加入-->
<attr name="titleBackGroudColor" />
复制代码
在自定义View中加入获取此属性的case:
case R.styleable.CustomTitleView_titleBackGroudColor:
mTitleBackColor = a.getColor(attr,Color.YELLOW);
复制代码
在绘制时加入获取到的颜色
mPaint.setColor(mTitleBackColor);
复制代码
上面获取text的效果就不查看了,看代码就够一目了然了,下面我们将背景设置为灰色查看一下效果:
<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" />
复制代码
再次重申一下,以上这个小案例是从hongyang大神那看到的,各位如果想要深入学习自定义View,hongyang大神那的系列文章绝对是极好的。
参考资料:
Android 自定义View (一)——by hongyang 《开发艺术探索》