Android开发的小伙伴都知道自定义view是一个让人又爱又恨的东西,爱的原因是因为自定义view可以实现项目的千变万化的需求,恨是因为自定义view有难么一点难以理解,一副雾里看花的感觉,当然这是我自己的个人感受,所以也是为了让自己对自定义view能够有更深入的理解和体会,写下该博客。
View正如它的字义理解,就是视图的意思,android中所有的控件都是继承于View这个类的。无论是viewgroup还是简单的widget控件,比如button,所以如果要真正搞明白自定义view,一定要知道view内部涉及到的主要方法和如何加载的过程。
一个Activity将view加载到屏幕上,代码实现很简单,只需要调用
setContentView(R.layout.activity_main);这个方法是android的一个封装,里面的流程大致为: 第一步:
LayoutInflater layoutInflater = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE);获取一个LayoutInflater 对象,然后调用inflate方法:
public View inflate(xmlPullParser parser, ViewGroup root, boolean attachToRoot) { synchronized (mConstructorArgs) { final AttributeSet attrs = Xml.asAttributeSet(parser); mConstructorArgs[0] = mContext; View result = root; try { int type; while ((type = parser.next()) != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT) { } if (type != XmlPullParser.START_TAG) { throw new InflateException(parser.getPositionDescription() + ": No start tag found!"); } final String name = parser.getName(); if (TAG_MERGE.equals(name)) { if (root == null || !attachToRoot) { throw new InflateException("merge can be used only with a valid " + "ViewGroup root and attachToRoot=true"); } rInflate(parser, root, attrs); } else { View temp = createViewFromTag(name, attrs); ViewGroup.LayoutParams params = null; if (root != null) { params = root.generateLayoutParams(attrs); if (!attachToRoot) { temp.setLayoutParams(params); } } rInflate(parser, temp, attrs); if (root != null && attachToRoot) { root.addView(temp, params); } if (root == null || !attachToRoot) { result = temp; } } } catch (XmlPullParserException e) { InflateException ex = new InflateException(e.getMessage()); ex.initCause(e); throw ex; } catch (IOException e) { InflateException ex = new InflateException( parser.getPositionDescription() + ": " + e.getMessage()); ex.initCause(e); throw ex; } return result; } }其中的rInflate的方法为:
PRivate void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs) throws XmlPullParserException, IOException { final int depth = parser.getDepth(); int type; while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { if (type != XmlPullParser.START_TAG) { continue; } final String name = parser.getName(); if (TAG_REQUEST_FOCUS.equals(name)) { parseRequestFocus(parser, parent); } else if (TAG_INCLUDE.equals(name)) { if (parser.getDepth() == 0) { throw new InflateException("<include /> cannot be the root element"); } parseInclude(parser, parent, attrs); } else if (TAG_MERGE.equals(name)) { throw new InflateException("<merge /> must be the root element"); } else { final View view = createViewFromTag(name, attrs); final ViewGroup viewGroup = (ViewGroup) parent; final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs); rInflate(parser, view, attrs); viewGroup.addView(view, params); } } parent.onFinishInflate(); }以上代码的主要作用是将xml布局文件转化为View类,然后加载在页面显示出来。 同时,我们在获取到LayoutInflate对象后有个重载方法:
inflate(int resource, ViewGroup root, boolean attachToRoot)有些人对这些参数不同值时代表的意义不是很理解,我觉得其实从参数类的名字的意义更好记忆。 resource:表示加载布局的资源id。 root:表示给该resource添加一个父布局。 attachToRoot:true表示root为该resource的父布局,false表示root不为该resource的父布局。 所以:root为null时,resource不主动添加一层父布局,attachToRoot设置的值也就不起作用。 root不为null时,resource主动添加一层父布局,attachToRoot为true时表示添加成功,false表示添加失败。
加载的过程中会涉及到很多重要的方法:
onMeasure(int widthMeasureSpec, int heightMeasureSpec); 参数是一个MeasureSpec类型,这个类是由一个低2位表示测量模式,低30位表示测量大小组成的。其中,测量类型mode的类型有三种:
EXACTLY 表示父视图希望子视图的大小应该是由specSize的值来决定的,一般对应的是具体的数值或者match_parent。
AT_MOST 表示子视图最多只能是specSize中指定的大小,一般对应的是warp_content。
UNSPECIFIED 表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制。这种情况比较少见,不太会用到。
在onMeasure方法中,最后调用setMeasuredDimension(widthSize, heightSize); 这个方法也是super中调用的方法,是真正起作用的方法。调用完该方法后,我们使用getMeasuredWidth()和getMeasuredHeight()来获取视图测量出的宽高才是有值得。对于继承ViewGroup的控件,onMeasure要用一个循环来测量子view的大小,比如像我们熟知的RelativeLayout和LinearLayout都对该方法进行了重写。
一般继承ViewGroup的控件需要对onLayout方法进行重写来显示子view的位置逻辑控件。调用完该方法后我们就可以调用getWidth()方法和getHeight()方法来获取视图的宽高了,该宽高是坐标相减计算得到的。
对于继承自view的控件,onDraw方法可以用来绘制我们想要的效果的东西。
onFinishInflate();从XML加载完毕后回调 onSizeChange();控件大小发生改变时,这两个方法多在组合控件中使用。
了解完了上面的简单知识点后,下篇文章会以三个具体的例子来说明自定义view的三种方式。 最近搞了个Android技术分享的公众号,欢迎关注投稿。
新闻热点
疑难解答