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

2017-01-14 10:46:55来源:http://www.jianshu.com/p/b8795bd82beb作者:xiasuhuei321人点击

第七城市
写在前面

终于周末了,当我想要松懈一会去浪的时候,脑海中突然闪过了这个东西……



学习.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文件,在里面定义我们的属性和声明。


<?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对吧?让我们来看下运行的结果



全屏.png

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


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" />


灰色.png

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


参考资料:


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


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




第七城市

最新文章

123

最新摄影

微信扫一扫

第七城市微信公众平台