1, 概述
ListView继承关系如下,
ListView也是一个控件,但是不同于其他的简单的控件, 它处理大量的内容元素。那么,这些元素从哪里来呢, Adapter为ListView提供数据。
2 实现
这个例子主要是读取手机上的联系人并用listview显示。
1,listview
sortListView = (ListView) findViewById(R.id.country_lvcountry); // Listview
sortListView.setOnItemClickListener(new OnItemClickListener() { // item单击事件
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Toast.makeText(getApplication(), ((SortModel)adapter.getItem(position)).getName(),
Toast.LENGTH_SHORT).show();}
});
布局如下,
<ListView
android:id="@+id/country_lvcountry"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_gravity="center"
android:divider="@null" />
2,adapter 填充数据
SourceDateList = getContactsData(); // 获取phone中的联系人
Collections.sort(SourceDateList, pinyinComparator);
adapter = new SortAdapter(this, SourceDateList); // 新建SortAdapter
sortListView.setAdapter(adapter); // 将SortAdapter和ListView联系在一起
private List<SortModel> getContactsData() {
List<SortModel> mSortList = new ArrayList<SortModel>();
ContentResolver resolver = getContentResolver();
Cursor phoneCursor = resolver.query(Phone.CONTENT_URI, PHONES_PROJECTION,
ContactsContract.RawContacts.ACCOUNT_NAME + " = 'PHONE'", null, null);
if (phoneCursor.moveToFirst()) {
do{
String contactName = phoneCursor.getString(0);
String phoneNumber = phoneCursor.getString(1);
String lookfornumber = null;
if (TextUtils.isEmpty(phoneNumber)) {
lookfornumber = null;
} else {
if (phoneNumber.contains("+86")) {
lookfornumber = phoneNumber.replace("+86", "").replaceAll("[^\\d]", "");
} else {
lookfornumber = phoneNumber.replaceAll("[^\\d]", "");
}
}
SortModel sortModel = new SortModel();
sortModel.setName(contactName);
sortModel.setNumber(lookfornumber);
String pinyin = characterParser.getSelling(contactName);// change to
String sortString = pinyin.substring(0, 1).toUpperCase();
if (sortString.matches("[A-Z]")) {
sortModel.setSortLetters(sortString.toUpperCase());
} else {
sortModel.setSortLetters("#");
}
mSortList.add(sortModel);
}while (phoneCursor.moveToNext());
phoneCursor.close();
}
return mSortList;
}
SortAdapter主要部分如下,
public class SortAdapter extends BaseAdapter {
private List<SortModel> list = null;
private Context mContext;
public SortAdapter(Context mContext, List<SortModel> list) {
this.mContext = mContext;
this.list = list;
}
public void updateListView(List<SortModel> list){
this.list = list;
notifyDataSetChanged(); // 更新数据库
}
public View getView(final int position, View view, ViewGroup arg2) {
ViewHolder viewHolder = null;
final SortModel mContent = list.get(position);
if (view == null) {
viewHolder = new ViewHolder();
view = LayoutInflater.from(mContext).inflate(R.layout.item, null);
viewHolder.tvTitle = (TextView) view.findViewById(R.id.title);
viewHolder.tvLetter = (TextView) view.findViewById(R.id.catalog);
viewHolder.tvnumber = (TextView) view.findViewById(R.id.number);
view.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) view.getTag();
}
int section = getSectionForPosition(position);
if(position == getPositionForSection(section)){
viewHolder.tvLetter.setVisibility(View.VISIBLE);
viewHolder.tvLetter.setText(mContent.getSortLetters());
}else{
viewHolder.tvLetter.setVisibility(View.GONE);
}
viewHolder.tvTitle.setText(this.list.get(position).getName());
viewHolder.tvnumber.setText(this.list.get(position).getNumber());
return view;
}
final static class ViewHolder {
TextView tvLetter;
TextView tvTitle;
TextView tvnumber;
}
}
3 listview绘制解析
3.1 onMeasure
Listview中onMeasure部分方法如下,
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Sets up mListPadding
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
~~~
mItemCount = mAdapter == null ? 0 : mAdapter.getCount(); // item 数量
if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED
|| heightMode == MeasureSpec.UNSPECIFIED)) {
final View child = obtainView(0, mIsScrap); // 获取第一个子view
// Lay out child directly against the parent measure spec so that
// we can obtain exected minimum width and height.
// 仅测量在第一个位置的值,确定最小的高和宽。
measureScrapChild(child, 0, widthMeasureSpec, heightSize);
~~~
}
在measureScrapChild方法中会调用子View的measure方法。
为什么只获取第一个子View呢,如果一次性全部获取,在item数量巨大的情况下会导致内存泄漏,所以listview采取动态加载子view的机制,仅需要确定每个字view的宽和高就可以了。
3.2 第一次onLayout
ListView中并没有onLayout方法,而是在父类AbsListView中,
完整的流程图如下,
layoutChildren方法代码比较多,不要被吓到了,节选如下
switch (mLayoutMode) { // 在AbsListView中,int mLayoutMode = LAYOUT_NORMAL;
•••
default:
if (childCount == 0) { //ListView刚开始是没有子部局的,childCount =0
if (!mStackFromBottom) { // 默认布局从上往下
final int position = lookForSelectablePosition(0, true);
setSelectedPositionInt(position);
sel = fillFromTop(childrenTop);
} else {
final int position = lookForSelectablePosition(mItemCount - 1, false);
setSelectedPositionInt(position);
sel = fillUp(mItemCount - 1, childrenBottom);
}
}
fillDown方法如下,主要是有一个循环,循环加载子View
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;
}
这里使用了一个while循环来执行重复逻辑,一开始nextTop的值是第一个子元素顶部距离整个ListView顶部的像素值,pos则是刚刚传入的mFirstPosition的值,而end是ListView底部减去顶部所得的像素值,mItemCount则是Adapter中的元素数量。因此一开始的情况下nextTop必定是小于end值的,并且pos也是小于mItemCount值的。那么每执行一次while循环,pos的值都会加1,并且nextTop也会增加,当nextTop大于等于end时,也就是子元素已经超出当前屏幕了,或者pos大于等于mItemCount时,也就是所有Adapter中的元素都被遍历结束了,就会跳出while循环。
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); // 获取子View
// This needs to be positioned and measured
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
1 obtainView
父类的obtainView方法最后会调用Adapter的getView方法,自己实现的一个getView方法如下,
public View getView(final int position, View view, ViewGroup arg2) {
ViewHolder viewHolder = null;
final SortModel mContent = list.get(position);
if (view == null) {
viewHolder = new ViewHolder();
view = LayoutInflater.from(mContext).inflate(R.layout.item, null);
viewHolder.tvTitle = (TextView) view.findViewById(R.id.title);
viewHolder.tvLetter = (TextView) view.findViewById(R.id.catalog);
viewHolder.tvnumber = (TextView) view.findViewById(R.id.number);
view.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) view.getTag();
}
int section = getSectionForPosition(position);
if(position == getPositionForSection(section)){
viewHolder.tvLetter.setVisibility(View.VISIBLE);
viewHolder.tvLetter.setText(mContent.getSortLetters());
}else{
viewHolder.tvLetter.setVisibility(View.GONE);
}
viewHolder.tvTitle.setText(this.list.get(position).getName());
viewHolder.tvnumber.setText(this.list.get(position).getNumber());
return view;
}
由源码可知,第一次layout过程当中,所有的子View都是动态调用LayoutInflater的inflate()方法加载出来的,这样就虽然比较耗时,但是后面就不会再有这种情况了。
2 setupChild
setupChild方法如下,
private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
boolean selected, boolean recycled) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "setupListItem");
final boolean isSelected = selected && shouldShowSelector();
final boolean updateChildSelected = isSelected != child.isSelected();
final int mode = mTouchMode;
final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL &&
mMotionPosition == position;
final boolean updateChildPressed = isPressed != child.isPressed();
final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested();
// Respect layout params that are already in the view. Otherwise make some up...
// noinspection unchecked
AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
if (p == null) {
p = (AbsListView.LayoutParams) generateDefaultLayoutParams();
}
p.viewType = mAdapter.getItemViewType(position);
p.isEnabled = mAdapter.isEnabled(position);
if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter
&& p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
attachViewToParent(child, flowDown ? -1 : 0, p);
} else {
p.forceAdd = false;
if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
p.recycledHeaderFooter = true;
}
addViewInLayout(child, flowDown ? -1 : 0, p, true);
}
if (updateChildSelected) {
child.setSelected(isSelected);
}
if (updateChildPressed) {
child.setPressed(isPressed);
}
if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) {
if (child instanceof Checkable) {
((Checkable) child).setChecked(mCheckStates.get(position));
} else if (getContext().getApplicationInfo().targetSdkVersion
>= android.os.Build.VERSION_CODES.HONEYCOMB) {
child.setActivated(mCheckStates.get(position));
}
}
if (needToMeasure) {
final int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
mListPadding.left + mListPadding.right, p.width);
final int lpHeight = p.height;
final int childHeightSpec;
if (lpHeight > 0) {
childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
} else {
childHeightSpec = MeasureSpec.makeSafeMeasureSpec(getMeasuredHeight(),
MeasureSpec.UNSPECIFIED);
}
child.measure(childWidthSpec, childHeightSpec);
} else {
cleanupLayoutState(child);
}
final int w = child.getMeasuredWidth();
final int h = child.getMeasuredHeight();
final int childTop = flowDown ? y : y - h;
if (needToMeasure) {
final int childRight = childrenLeft + w;
final int childBottom = childTop + h;
child.layout(childrenLeft, childTop, childRight, childBottom);
} else {
child.offsetLeftAndRight(childrenLeft - child.getLeft());
child.offsetTopAndBottom(childTop - child.getTop());
}
if (mCachingStarted && !child.isDrawingCacheEnabled()) {
child.setDrawingCacheEnabled(true);
}
if (recycled && (((AbsListView.LayoutParams)child.getLayoutParams()).scrappedFromPosition)
!= position) {
child.jumpDrawablesToCurrentState();
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
刚才调用obtainView()方法获取到的子元素View,addViewInLayout()方法将它添加到了ListView当中。然后依次调用子View的measure和layout方法。根据fillDown()方法中的while循环,会让子元素View将整个ListView控件填满然后就跳出,也就是说即使我们的Adapter中有一千条数据,ListView也只会加载第一屏的数据,剩下的数据反正目前在屏幕上也看不到,所以不会去做多余的加载工作,这样就可以保证ListView中的内容能够迅速展示到屏幕上。
那么到此为止,第一次Layout过程结束。
3.3 第二次onLayout
第二次调用onLayout方法时,会涉及到子View的缓存
流程大体上和第一次一样,细节有些不同而已。
layoutChildren中关键方法,
final RecycleBin recycleBin = mRecycler;
if (dataChanged) {
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
} else {
recycleBin.fillActiveViews(childCount, firstPosition); // 保存所有子view
}
detachAllViewsFromParent();//因为已经保存在mActiveViews中,所以在这儿清理所有子view完全无影响
•••
default:
if (childCount == 0) {
if (!mStackFromBottom) {
final int position = lookForSelectablePosition(0, true);
setSelectedPositionInt(position);
sel = fillFromTop(childrenTop);
} else {
final int position = lookForSelectablePosition(mItemCount - 1, false);
setSelectedPositionInt(position);
sel = fillUp(mItemCount - 1, childrenBottom);
}
} else {
if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
sel = fillSpecific(mSelectedPosition,
oldSel == null ? childrenTop : oldSel.getTop());
} else if (mFirstPosition < mItemCount) {
sel = fillSpecific(mFirstPosition,
oldFirst == null ? childrenTop : oldFirst.getTop());
} else {
sel = fillSpecific(0, childrenTop);
}
}
break;
}
1,调用了RecycleBin的fillActiveViews方法保存所有子view
2,调用fillSpecific方法,源码如下,
private View fillSpecific(int position, int top) {
boolean tempIsSelected = position == mSelectedPosition;
View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
// Possibly changed again in fillUp if we add rows above this one.
mFirstPosition = position;
View above;
View below;
final int dividerHeight = mDividerHeight;
if (!mStackFromBottom) {
above = fillUp(position - 1, temp.getTop() - dividerHeight);
// This will correct for the top of the first view not touching the top of the list
adjustViewsUpOrDown();
below = fillDown(position + 1, temp.getBottom() + dividerHeight);
int childCount = getChildCount();
if (childCount > 0) {
correctTooHigh(childCount);
}
} else {
below = fillDown(position + 1, temp.getBottom() + dividerHeight);
// This will correct for the bottom of the last view not touching the bottom of the list
adjustViewsUpOrDown();
above = fillUp(position - 1, temp.getTop() - dividerHeight);
int childCount = getChildCount();
if (childCount > 0) {
correctTooLow(childCount);
}
}
if (tempIsSelected) {
return temp;
} else if (above != null) {
return above;
} else {
return below;
}
}
fillSpecific()这算是一个新方法了,不过其实它和fillUp()、fillDown()方法功能也是差不多的,主要的区别在于,fillSpecific()方法会优先将指定位置的子View先加载到屏幕上,然后再加载该子View往上以及往下的其它子View。那么由于这里我们传入的position就是第一个子View的位置,于是fillSpecific()方法的作用就基本上和fillDown()方法是差不多的了,这里我们就不去关注太多它的细节,而是将精力放在makeAndAddView()方法上面。再次回到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;
}
第一次layout时会调用obtainView来加载子view,第二次就可以直接调用getActiveView方法从mActiveViews中获取了。
3.4 dispatchDraw
View的ondraw方法会调用dispatchDraw方法来绘制子view,
Ondraw—>dispatchDraw--> drawChild--> draw
虽然listview 重写了dispatchDraw方法,但是逻辑上并没有大的改变。