在Android开发过程中,ListView的Adapter是我们最常见的类型之一,我们需要使用Adapter加载Item View的布局,并且进行数据绑定、缓存复用等操作。代码大致如下:
ListView myListView = (ListView)view.findViewById(R.id.id_list);
MyAdapter adapter = new MyAdapter();
myListView.setAdapter(adapter);
class MyAdapter extends BaseAdapter {
@Override
public int getCount() {
return 6;
}
@Override
public Object getItem(int position) {
return null;
}
@Override
public long getItemId(int position) {
return 0;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view;
MerchantSuccessViewHolder holder = null;
if(convertView == null) {
holder = new MerchantSuccessViewHolder();
view = View.inflate(this,R.layout.item_view,null);
holder.title = (TextView) view.findViewById(R.id.title);
view.setTag(holder);
} else {
view = convertView;
holder = (ViewHolder) view.getTag();
}
holder.title.setText("hello world");
return view;
}
}
另外GridView的使用几乎完全一样。
ListView需要显示各式各样的视图,每个人考虑的显示效果各不相同,显示的数据类型,数量也千变万化,那么如何应对这种变化,Android的架构师的做法就是采用适配器模式。
Android的做法是增加一个Adapter层来隔离变化,将ListView需要的关于Item View接口抽象到Adapter对象中,并在ListView内部调用Adapter这些接口完成布局等操作。这样只要用户实现了Adapter接口,并且该Adapter设置给ListView,ListView就可以按照用户设定的UI效果、数量、数据来显示每一项数据。ListView最重要的问题是要解决每一项Item视图的输出,ItemView千变万化,但它终究都是View类型,Adapter统一将Item View输出为view类型,这样很好的应对了Item View的可变性。
那么ListView是如何通过Adapter将千变万化U效果设置给ListView的呢?
下面来跟踪源码一探究竟。
我们在ListView类中并没有发生Adapter相关的成员变量,其实在ListView的父类AbsListView中,AbsListView是一个列表空间的抽象。源码如下:
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
final ViewTreeObserver treeObserver = getViewTreeObserver();
treeObserver.addOnTouchModeChangeListener(this);
if (mTextFilterEnabled && mPopup != null && !mGlobalLayoutListenerAddedFilter) {
treeObserver.addOnGlobalLayoutListener(this);
}
if (mAdapter != null && mDataSetObserver == null) {
mDataSetObserver = new AdapterDataSetObserver();
mAdapter.registerDataSetObserver(mDataSetObserver);
// Data may have changed while we were detached. Refresh.
mDataChanged = true;
mOldItemCount = mItemCount;
mItemCount = mAdapter.getCount();//获得Item的数量 这个方法需要我们重写,交给程序猿
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
mInLayout = true;
final int childCount = getChildCount();
if (changed) {
for (int i = 0; i < childCount; i++) {
getChildAt(i).forceLayout();
}
mRecycler.markChildrenDirty();
}
layoutChildren();
mInLayout = false;
mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR;
// TODO: Move somewhere sane. This doesn't belong in onLayout().
if (mFastScroll != null) {
mFastScroll.onItemCountChanged(getChildCount(), mItemCount);
}
}
当Activity被启动时,它的布局中的ListView的onAttachToWindow方法就会被先调用,然后调用其onLayout方法。我们看到
onAttachToWindow调用mAdapterd.getCount()方法,这时获取到了Item View的数量。然后执行在onLayout方法时,会调用layoutChilren这个方法,具体的实现在子类中。在AbsListView是个空实现,ListView实现了这个方法,源码如下:
@Override
protected void layoutChildren() {
//省略一部分代码
try {
super.layoutChildren();
invalidate();
//省略一部分代码
switch (mLayoutMode) {
case LAYOUT_SET_SELECTION:
if (newSel != null) {
sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
} else {
sel = fillFromMiddle(childrenTop, childrenBottom);
}
break;
case LAYOUT_SYNC:
sel = fillSpecific(mSyncPosition, mSpecificTop);
break;
case LAYOUT_FORCE_BOTTOM:
sel = fillUp(mItemCount - 1, childrenBottom);
adjustViewsUpOrDown();
break;
case LAYOUT_FORCE_TOP:
mFirstPosition = 0;
sel = fillFromTop(childrenTop);
adjustViewsUpOrDown();
break;
case LAYOUT_SPECIFIC:
sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop);
break;
case LAYOUT_MOVE_SELECTION:
sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
break;
default:
//省略一部分代码
break;
}
//省略一部分代码
}
ListView重写了父类的layoutChilden方法,该方法根据布局模式来布局模式来布局Item View,这里讨论最常用的从上到下布局模式。源码如下:
private View fillFromTop(int nextTop) {
mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);
mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);
if (mFirstPosition < 0) {
mFirstPosition = 0;
}
return fillDown(mFirstPosition, nextTop);
}
fillFromTop又调用了fillDown方法
private View fillDown(int pos, int nextTop) {
View selectedView = null;
int end = (mBottom - mTop);
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
end -= mListPadding.bottom;
}
while (nextTop < end && pos < mItemCount) {
// is this the selected item?
boolean selected = pos == mSelectedPosition;
View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
nextTop = child.getBottom() + mDividerHeight;
if (selected) {
selectedView = child;
}
pos++;
}
setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
return selectedView;
}
可以看到上面代码用到了mItemCount,没错这个变量已经在AbsListView的
onAttachToWindow初始化过了。上面代码的大概意思就是通过makeAndAddView方法获取Item View,然后将mItemCount个Item View逐个往下布局,然后将高度累加。
然后继续跟踪,看makeAndAddView,其源码如下:
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;
if (!mDataChanged) {
// Try to use an existing view for this position
child = mRecycler.getActiveView(position);
if (child != null) {
// Found it -- we're using an existing child
// This just needs to be positioned
setupChild(child, position, y, flow, childrenLeft, selected, true);
return child;
}
}
// Make a new view for this position, or convert an unused view if possible
child = obtainView(position, mIsScrap);
// This needs to be positioned and measured
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
在 makeAndAddView方法中,主要做了两件事,
1)通过obtainView方法根据position获取一个item View
2)通过setupChild方法将这个View布局到特定的位置。
这里的setupChild方法主要是View的绘制相关操作,这里不再赘述,主要是obtainView方法。下面来看看它是如何根据position获得一个item View的。这个定义在AbsListView中。其主要源码如下
final RecycleBin mRecycler = new RecycleBin();
View obtainView(int position, boolean[] isScrap) {
//省略一部分代码
//1、从缓存中的ItemView中获取View ListView的复用机制便在这里了
final View scrapView = mRecycler.getScrapView(position);
//2、调用mAdapter.getView方法,注意将scrapView设置给getView方法的convertView参数
final View child = mAdapter.getView(position, scrapView, this);
//省略一部分代码
return child;
}
可以看出,主要逻辑也就两句代码,obtainView方法定义了列表空间的Item View的复用逻辑,首先会从RecycleBin中获取一个缓存的View,如果有缓存则将这个缓存的View传递给Adapter的getView方法的第二个参数convertView参数,这也就是我们队Adapter的最常见的优化方式啊,即判断getView的convertView是否为空,如果为空则从xml中创建一个新的View,否则使用缓存的View。这样避免了每次都从xml加载布局的消耗,能显著提升ListView等列表控件的效率,通常的实现如下。
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view;
MerchantSuccessViewHolder holder = null;
if(convertView == null) {
holder = new MerchantSuccessViewHolder();
view = View.inflate(this,R.layout.item_view,null);
holder.title = (TextView) view.findViewById(R.id.title);
view.setTag(holder);
} else {
view = convertView;
holder = (ViewHolder) view.getTag();
}
holder.title.setText("hello world");
return view;
}
通过这种缓存机制,即使有成千上万的数据项,ListView也能够流畅运行,因此,只有填满一屏所需的Item View存在内存中。流程图如图所示:
最后,总结一下,不然前面一直看源码都快忘了适配器模式了,ListView通过Adapter来获取Item View的数量、布局、数量等,在这里最为重要的就是getView方法,这个方法返回的是一个View的的对象,也就是Item View,由于它返回的是View,而千变万化的UI视图都是View的子类,通过依赖抽象这个简单的原则和Adapter模式将Item View的变化隔离了,保证了ListView的高度定制化,在获取了View之后,将这个View显示在特定的position商,在家桑Item View的复用机制,整个ListView就运转起来了。
虽然这里的BaseAdapter不是经典的适配器模式,确实适配器模式很好的扩展。也很好的提现了面向对象的一些基本准则。用户只要处理getCount、getItem、getView方法就可以了,达到了无线适配拥抱变化的目的。
最后再谈一下SimpleAdapter,它们都是给ListView的设置Adapter,与BaseAdapter不同的是,SimpleAdapter是具体的Adapter,BaseAdapter是抽象类,这样我们可以高度定制化,扩展。我们再集成BaseAdapter的时候,可以在getView方法中进行各种优化,缓存机制还有ViewHolder的应用。
但是在SimpleAdapter中,虽然有缓存复用,但是并没有ViewHolder的概念,所以说优化效果不如我们自己定制的效果,下面贴上SimpleAdapter的getView方法。
public View getView(int position, View convertView, ViewGroup parent) {
return createViewFromResource(mInflater, position, convertView, parent, mResource);
}
private View createViewFromResource(LayoutInflater inflater, int position, View convertView,
ViewGroup parent, int resource) {
View v;
if (convertView == null) {
v = inflater.inflate(resource, parent, false);
} else {
v = convertView;
}
bindView(position, v);
return v;
}
getView方法又会调用createViewFromResource方法, createViewFromResource中只是判断了缓存,但是并没有ViewHolder的优化,只是通过bindView方法将数据和Item View绑定而已。bindView方法源码如下
private void bindView(int position, View view) {
final Map dataSet = mData.get(position);
if (dataSet == null) {
return;
}
final ViewBinder binder = mViewBinder;
final String[] from = mFrom;
final int[] to = mTo;
final int count = to.length;
for (int i = 0; i < count; i++) {
final View v = view.findViewById(to[i]);
if (v != null) {
final Object data = dataSet.get(from[i]);
String text = data == null ? "" : data.toString();
if (text == null) {
text = "";
}
boolean bound = false;
if (binder != null) {
bound = binder.setViewValue(v, data, text);
}
if (!bound) {
if (v instanceof Checkable) {
if (data instanceof Boolean) {
((Checkable) v).setChecked((Boolean) data);
} else if (v instanceof TextView) {
// Note: keep the instanceof TextView check at the bottom of these
// ifs since a lot of views are TextViews (e.g. CheckBoxes).
setViewText((TextView) v, text);
} else {
throw new IllegalStateException(v.getClass().getName() +
" should be bound to a Boolean, not a " +
(data == null ? "<unknown type>" : data.getClass()));
}
} else if (v instanceof TextView) {
// Note: keep the instanceof TextView check at the bottom of these
// ifs since a lot of views are TextViews (e.g. CheckBoxes).
setViewText((TextView) v, text);
} else if (v instanceof ImageView) {
if (data instanceof Integer) {
setViewImage((ImageView) v, (Integer) data);
} else {
setViewImage((ImageView) v, text);
}
} else {
throw new IllegalStateException(v.getClass().getName() + " is not a " +
" view that can be bounds by this SimpleAdapter");
}
}
}
}
}
可以看出,bindView方法不能处理ViewGroup,只能是一些简单的TextView,ImageView等View。
所以,当数据很少,不会超过一屏,而且只能一些简单的View的组合,可以考虑使用SimpleAdapter。