###写在前面### 终于周末了,当我想要松懈一会去浪的时候,脑海中突然闪过了这个东西…… 一图胜千言,日常唠嗑(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: 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对吧?让我们来看下运行的结果
黄色并非我设置的背景,而是想要包裹验证码的背景。正如我所说的,如果不处理的话,就是这种效果,很明显这不是我们想要的,那么该如何处理呢?
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: width = getPaddingLeft() + getPaddingRight() + specSize; break ; case MeasureSpec.AT_MOST: 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: width = getPaddingLeft() + getPaddingRight() + specSize; break ; case MeasureSpec.AT_MOST: 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" /> <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" />
再次重申一下,以上这个小案例是从hongyang 大神那看到的,各位如果想要深入学习自定义View,hongyang大神那的系列文章绝对是极好的。
参考资料:
Android 自定义View (一)——by hongyang 《开发艺术探索》
源码地址:hongyang的源码 我整理的Android Studio版源码