(一)LayoutInflater原理分析
LayoutInflater主要用于加载布局。通常情况下,加载布局的任务都是在Activity中调用setContentView()
方法来完成的,该方法内部使用LayoutInflater来加载布局。
想要使用LayoutInflater,首先需要获取到LayoutInflater的实例,有两种方法可以获取到:
LayoutInflater layoutInflater = LayoutInflater.from(context);
LayoutInflater layoutInflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
其实第一种就是第二种的简单写法,只是Android给我们做了一下封装而已。得到了LayoutInflater的实例之后就可以调用它的inflate()
方法来加载布局了。
layoutInflater.inflate(resourceId, root); //inflate()方法一般接收两个参数,第一个参数就是要加载的布局id,第二个参数是指给该布局的外部再嵌套一层父布局,如果不需要就直接传null。
这样就成功成功创建了一个布局的实例,之后再将它添加到指定的位置就可以显示出来了。
LayoutInflater广泛应用于需要动态添加View的时候,比如用在在ScrollView和ListView中。接下来从源码的角度看一下它是如何工作的:
不管使用哪个inflate()
方法的重载,最终都会辗转调用到LayoutInflater的如下代码中:
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
/**
* 获取一个实现此AttributeSet的实例。因为此XmlPullParser是继承自AttributeSet
* 的,所以parser对象可以直接作为一个AttributeSet对象。
**/
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();
//如果它的根节点是一个merge对象,则必须手动设置此view的父节点,否则抛出异常
//因为由merge创建的xml文件,常常被其他layout所包含
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;
}
}
LayoutInflater使用Android提供的pull解析方式来解析布局文件。第29行,调用了createViewFromTag()
方法,并把节点名和参数传了进去,它是用于根据节点名来创建View对象的,在该方法的内部又会去调用createView()
方法,然后使用反射的方式创建出View的实例并返回。
至此,只是创建出了一个根布局的实例,接下来在第38行调用的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 {
//创建View实例
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();
}
在第22行同样是createViewFromTag()
方法来创建View的实例,然后还会在第25行递归调用rInflate()
方法来查找这个View下的子元素,每次递归完成后则将这个View添加到父布局当中。把整个布局文件都解析完成后就形成了一个完整的DOM结构,最终会把最顶层的根布局返回,至此inflate()
过程全部结束。
(二)视图绘制流程
每一个视图的绘制过程都必须经历三个最主要的阶段,即onMeasure()
、onLayout()
和onDraw()
。
onMeasure()
onMeasure()
方法用于测量视图的大小。View系统的绘制流程会从ViewRoot的performTraversals()
方法中开始,在其内部调用View的measure()
方法,该方法接收两个参数,widthMeasureSpec和heightMeasureSpec,这两个值分别用于确定视图的宽度和高度的规格和大小。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) {
mPrivateFlags &= ~MEASURED_DIMENSION_SET;
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_MEASURE);
}
onMeasure(widthMeasureSpec, heightMeasureSpec);
if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) {
throw new IllegalStateException("onMeasure() did not set the"
+ " measured dimension by calling"
+ " setMeasuredDimension()");
}
mPrivateFlags |= LAYOUT_REQUIRED;
}
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
}
该方法是final的,因此我们无法在子类中去重写这个方法,说明Android是不允许我们改变View的measure框架的。然后在第9行调用了onMeasure()
方法,这里是真正去测量并设置View大小的地方,默认会调用getDefaultSize()
方法来获取视图的大小,如下所示:
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
这里传入的measureSpec是一直从measure()
方法中传递过来的。然后调用MeasureSpec.getMode()
方法解析出specMode,调用MeasureSpec.getSize()
方法解析出specSize。接下来进行判断,如果specMode等于AT_MOST或EXACTLY就返回specSize,这也是系统默认的行为。之后会在onMeasure()
方法中调用setMeasuredDimension()
方法来设定测量出的大小,这样一次measure过程就结束了。
onLayout()
measure结束后是layout的过程,onLayout()
方法是用于给视图进行布局的,也就是确定视图的位置。ViewRoot的performTraversals()
方法会在measure结束后继续执行,并调用View的layout()
方法来执行此过程,如下所示:
host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);
该方法接收四个参数,分别代表着左、上、右、下的坐标,这个坐标是相对于当前视图的父视图而言的,这里把刚才测量出的宽度和高度传到了该方法中。
public void layout(int l, int t, int r, int b) {
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = setFrame(l, t, r, b);
if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);
}
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~LAYOUT_REQUIRED;
if (mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>) mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~FORCE_LAYOUT;
}
在该方法中,首先会调用setFrame()
方法来判断视图的大小是否发生过变化,以确定有没有必要对当前的视图进行重绘,同时还会在这里把传递过来的四个参数分别赋值给mLeft、mTop、mRight和mBottom这几个变量。接下来会在第11行调用onLayout()
方法。View中的onLayout()
方法是一个空方法,因为onLayout()
过程是为了确定视图在布局中所在的位置,而这个操作应该是由布局来完成的,即父视图决定子视图的显示位置。既然如此,我们来看下ViewGroup中的onLayout()
方法是怎么写的吧,代码如下:
@Override
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
可以看到,ViewGroup中的onLayout()
方法是一个抽象方法,这就意味着所有ViewGroup的子类都必须重写这个方法,然后在内部按照各自的规则对子视图进行布局的。
onDraw()
layout过程结束后,接下来就进入到draw的过程了,onDraw()
方法对视图进行绘制。ViewRoot中的代码会继续执行并创建出一个Canvas对象,然后调用View的draw()
方法来执行具体的绘制工作。代码如下所示:
public void draw(Canvas canvas) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);
}
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN;
//draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
final Drawable background = mBGDrawable;
if (background != null) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if (mBackgroundSizeChanged) {
background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
}
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
}
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// draw the content
if (!dirtyOpaque) onDraw(canvas);
// draw the children
dispatchDraw(canvas);
// draw decorations (scrollbars)
onDrawScrollBars(canvas);
// we're done...
return;
}
}
从第9行代码开始,先是对视图的背景进行绘制,首先需要得到一个mBGDrawable对象,可以在XML中通过android:background属性设置图片或颜色,也可以在代码中通过setBackgroundColor()
、setBackgroundResource()
等方法进行赋值。然后根据layout过程确定的视图位置来设置背景的绘制区域,之后再调用Drawable的draw()
方法来完成背景的绘制工作。
接下来在第34行对视图的内容进行绘制,这里调用了onDraw()
方法,该方法是一个空方法,因为每个视图的内容部分各不相同,这部分的功能交给子类实现。
然后对当前视图的所有子视图进行绘制。但如果当前的视图没有子视图,那么也就不需要进行绘制了(View中的dispatchDraw()
方法是一个空方法,而ViewGroup的dispatchDraw()
方法中就会有具体的绘制代码)。
最后是对视图的滚动条进行绘制。
由此可以发现,View是不会帮我们绘制内容部分的,因此需要每个视图根据想要展示的内容来自行绘制。TextView、ImageView等类的源码都有重写onDraw()
这个方法,并且在里面执行了相当不少的绘制逻辑。绘制的方式主要是借助Canvas这个类,它会作为参数传入到onDraw()
方法中,供给每个视图使用。
(三)自定义控件
自定义View的实现方式大概可以分为三种,自绘控件、组合控件、以及继承控件。
自绘控件所展现的内容全部都是我们自己绘制出来的,绘制的代码是写在onDraw()
方法中的。主要步骤如下:
Step 1 写一个类继承View类或者View的子类。
Step 2 写一个构造函数,用于获取属性和参数。
Step 3 重写onMeasure(int,int)方法。(该方法可重写可不重写,具体看需求)
Step 4 重写onDraw(Canvas canvas)方法。
组合控件的意思就是,我们并不需要自己去绘制视图上显示的内容,而只是用系统原生的控件就好了,但我们可以将几个系统原生的控件组合到一起。
继承控件的意思就是,我们并不需要自己重头去实现一个控件,只需要去继承一个现有的控件,然后在这个控件上增加一些新的功能,就可以形成一个自定义的控件了。这种自定义控件的特点就是不仅能够按照我们的需求加入相应的功能,还可以保留原生控件的所有功能。
(四)ListView
ListVeiw功能的实现,需要以下三个元素:
(1)ListView中的每一列的View。
(2)填入View的数据或者图片等。
(3)连接数据与ListView的适配器。
ListView的实现是典型的适配器模式。适配器是一个连接数据和AdapterView(ListView等)的桥梁,通过它能有效地实现数据与AdapterView的分离设置,使AdapterView与数据的绑定更加简便,修改更加方便。
ArrayAdapter
ArrayAdapter用来绑定一个数组,支持泛型操作,可以实现简单的ListView的数据绑定。默认情况下,ArrayAdapter绑定每个对象的toString值到layout中预先定义的TextView控件上。通常我们使用android.R.layout.simple_list_item_1
,实现带有一个TextView的ListView,使用android.R.layout.simple_list_item_checked
实现带选择框的ListView,使用android.R.layout.simple_list_item_multiple_choice
实现带CheckBox的ListView,使用android.R.layout.simple_list_item_single_choice
实现带RadioButton的ListView,注意使用后三个资源时,是多选还是单选要通过setChoiceMode()
方法来指定。使用步骤如下:
(1)定义一个数组来存放ListView中item的内容。
(2)通过实现ArrayAdapter的构造函数来创建一个ArrayAdapter的对象。
(3)通过ListView的setAdapter()
方法绑定ArrayAdapter。
SimpleAdapter
可以通过SimpleAdapter自定义ListView中的item的内容,比如图片、多选框等。 使用simpleAdapter的数据一般都是用HashMap构成的列表,列表的每一个节点对应ListView的每一行。通过SimpleAdapter的构造函数,将HashMap的每个键的数据映射到布局文件中对应控件上。使用步骤如下:
(1)根据需要定义ListView每行所实现的布局。
(2)定义一个HashMap构成的列表,将数据以键值对的方式存放在里面。
(3)构造SimpleAdapter对象。
(4)将LsitView绑定到SimpleAdapter上。
BaseAdapter
在ListView中使用SimpleAdapter添加一个按钮到item上,会发现按钮无法获得焦点,点击操作被ListView的item所覆盖,为了使按钮实现单独的操作,可以使用BaseAdapter。BaseAdapter是一个抽象类,它的灵活性就在于使用时需要重写很多方法。当系统开始绘制ListView的时候,首先调用getCount()
方法,返回ListView的长度。然后调用getView()
方法根据长度逐一绘制ListView的每一行,在这个函数里面首先获得一个View(或ViewGroup),然后再实例化并设置各个组件及其数据内容并显示它。而getItem()
和getItemId()
则在需要处理和取得Adapter中的数据时调用。
ListView优化
使用convertView和viewHolder
/*
* 新建一个类继承BaseAdapter,实现视图与数据的绑定
*/
private class MyAdapter extends BaseAdapter {
private LayoutInflater mInflater;// 得到一个LayoutInfalter对象用来导入布局
/* 构造函数 */
public MyAdapter(Context context) {
this.mInflater = LayoutInflater.from(context);
}
@Override
public int getCount() {
return getDate().size();// 返回数组的长度
}
@Override
public Object getItem(int position) {
return null;
}
@Override
public long getItemId(int position) {
return 0;
}
@Override
public View getView(final int position, View convertView,
ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
convertView = mInflater.inflate(R.layout.item, null);
holder = new ViewHolder();
holder.title = (TextView) convertView
.findViewById(R.id.ItemTitle);
holder.text = (TextView) convertView
.findViewById(R.id.ItemText);
holder.bt = (Button) convertView.findViewById(R.id.Button);
convertView.setTag(holder);// 绑定ViewHolder对象
} else {
holder = (ViewHolder) convertView.getTag();// 取出ViewHolder对象
}
holder.title.setText(getDate().get(position).get("ItemTitle")
.toString());
holder.text.setText(getDate().get(position).get("ItemText")
.toString());
holder.bt.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Log.v("MyListViewBase", "你点击了按钮" + position);
}
});
return convertView;
}
}
当启动Activity呈现第一屏ListView的时候,convertView为null。当用户向下滚动ListView时,上面的条目变为不可见,下面出现新的条目。这时候convertView不再为null,而是创建了一系列的convertView的值。当又往下滚一屏的时候会复用第一屏的已绘制的view。也就是说convertView相当于一个缓存,开始为null,当有条目变为不可见,它缓存了它的实例,后面再出来的条目只需要更新数据就可以了,这样大大节省系统资料的开销。
public final class ViewHolder {
public TextView title;
public TextView text;
public Button bt;
}
当convertView为空时,用setTag()
方法为每个View绑定一个存放控件的ViewHolder对象。当convertView不为空,重复利用已经创建的view,使用getTag()
方法获取绑定的ViewHolder对象,这样就避免了重复使用findViewById所造成的开销。