您的位置:首页 > 其它

ListView适配器模式的应用

2017-12-24 22:00 295 查看

ListView适配器模式的应用

文章开头先贴下另外几篇设计模式的分析,感兴趣的可以看下:

设计模式-简单说明

ListView观察者模式的应用

AsyncTask模板模式的应用

ListView桥接模式的应用

我们都知道ListView是用来展示列表的,然而列表在不同的使用场景中需要展示的样式,以及所提供的数据源都是不确定的,因为需求千变万化,Android团队在设计之初就考虑到了这一方面,所以适配器模式就理所当然的得到应用。我们简单说一下适配器模式的定义:源接口和目标接口不一致(这里的接口并不一定就是指interface),为了使两个不兼容的接口能够得到很好的对接,我们需要提供一个中间类,来将源接口进行转化成目标接口中需要的数据。



上面是一张简单的UML类图,说明了ListView和BaseAdapter的关系,而适配器模式的应用就在于Adapter的getView,将数据转化成View,然后在ListView中会将其添加到列表中。下面我们进入到源码分析一下适配器模式的具体实现,首先从Adapter的源码开始分析(作了适当的删减):

/**
* An Adapter object acts as a bridge between an {@link AdapterView} and the
* underlying data for that view. The Adapter provides access to the data items.
*
* Adapter的类声明,简单明了--> Adapter对象,充当AdapterView(ListView的间接父类)和数据视图之间的桥梁。提供了访问数据项的功能
*/
public interface Adapter {
int getCount();

Object getItem(int position);

long getItemId(int position);

View getView(int position, View convertView, ViewGroup parent);

int getItemViewType(int position);

int getViewTypeCount();

boolean isEmpty();
}


可以发现都是大家很熟悉的函数,而且Adapter的官方声明也说了,Adapter是AdapterView(ListView的父类)和数据视图之间的桥梁,接下来看下它的直接子类,我们以我们常用的Adapter->ListAdapter->BaseAdapter来分析,所以接下来就是ListAdapter的源码:

/**
* ListView和数据之间的桥梁,任何需要显示的数据都将会在ListAdapter中进行转化。实际上是在我们实现的Adapter 的子类中进行转化的
*/
public interface ListAdapter extends Adapter {

/**
* 是否所有的Item都是可点击和可选中的(分隔符是不满足条件的),只有都满足条件才返回true
*/
public boolean areAllItemsEnabled();

/**
* 判断一个Item是否可点击、可选中,满足条件返回true
*/
boolean isEnabled(int position);
}


发现ListAdapter也是接口,我们继续看它的直接子类BaseAdapter:

public abstract class BaseAdapter implements ListAdapter, SpinnerAdapter {

public boolean areAllItemsEnabled() {
return true;
}

public boolean isEnabled(int position) {
return true;
}

public View getDropDownView(int position, View convertView, ViewGroup parent) {
return getView(position, convertView, parent);
}

public int getItemViewType(int position) {
return 0;
}

public int getViewTypeCount() {
return 1;
}

public boolean isEmpty() {
return getCount() == 0;
}
}


我们发现这是一个抽象类,它是一个公共基础类,实现了一些常用的公共逻辑,可以简单的理解作了一些常用的默认实现,如果你感兴趣你也可以自定义ListView适配器,直接实现ListAdapter。那么剩下没有实现的就是我们天天用的了,这里自定义一个MyAdapter,看看它有哪些逻辑需要实现:

class MyAdapter extends BaseAdapter{
@Override
public int getCount() {
return 0;
}

@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) {
return null;
}
}


现在是不是倍感亲切,看着心里格外的舒坦呢。不要着急,这才刚刚开始呢,下面才是重点,Adapter我们已经看完了,大家现在肯定有一个疑问,Adapter返回的这些itemCount、View是怎么添加到ListView中的。我们从ListView的setAdapter为入口来分析:

@Override
public void setAdapter(ListAdapter adapter) {
...一些初始化操作

if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {
mAdapter = wrapHeaderListAdapterInternal(mHeaderViewInfos, mFooterViewInfos, adapter);
} else {
//为成员变量mAdapter赋值
mAdapter = adapter;
}

...

// AbsListView#setAdapter will update choice mode states.
super.setAdapter(adapter);

if (mAdapter != null) {
...
//调用mAdapter的getCount方法保存ListView的条目数
mItemCount = mAdapter.getCount();
...
} else {
...
}

//调用重新布局
requestLayout();
}


我们在以上的源码中注意到了我们想要的信息,首先它保存了我们设置的新的ListAdapter实例,然后调用其getCount()方法保存ListView的条目个数,最后调用了requestLayout来实现重新布局,我们看下具体的onLayout方法吧,这时候发现ListView并没有onLayout方法,那我们就去它的父类AbsListView中取寻找一下:

/**
* Subclasses should NOT override this method but
*  {@link #layoutChildren()} instead.
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);

...
//布局子View
layoutChildren();

...
}


我们看见在AbsListView的onLayout的函数声明上表明,我们不该去重写onLayout,如果需要布局子View应该重写layoutChildren函数,在AbsListView的onLayout内部调用了layoutChildren,所以子类调用layoutChildren可以就可以实现布局子View,那我们就去看下ListView的layoutChildren吧,layoutChildren是很重要的,在ListView的条目复用机制时也会涉及到,不过我们现在并不是分析条目复用,所以只保留我们关心由BaseAdapter的getView返回的View是怎么添加到ListView中的:

@Override
protected void layoutChildren() {
...

//这里会根据不同的布局模式来添加子View到ListView中,默认走default
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:
final int selectedPosition = reconcileSelectedPosition();
sel = fillSpecific(selectedPosition, mSpecificTop);
/**
* When ListView is resized, FocusSelector requests an async selection for the
* previously focused item to make sure it is still visible. If the item is not
* selectable, it won't regain focus so instead we call FocusSelector
* to directly request focus on the view after it is visible.
*/
if (sel == null && mFocusSelector != null) {
final Runnable focusRunnable = mFocusSelector
.setupFocusIfValid(selectedPosition);
if (focusRunnable != null) {
post(focusRunnable);
}
}
break;
case LAYOUT_MOVE_SELECTION:
sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
break;
default:
//我们刚设置Adapter,ItemView都还没添加到ListView中,所以走的是上面的分支。并且没有设置填充模式的情况下,默认是从上往下填充,即fillFromTop
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;
}

...
}


在layoutChildren中,会根据不同的模式来填充子View,默认走的default,我们刚设置Adapter,ItemView都还没添加到ListView中,所以走的是上面的分支。并且没有设置填充模式的情况下,默认是从上往下填充,即fillFromTop,我们看下它的源码:

/**
* Fills the list from top to bottom, starting with mFirstPosition
*
* @param nextTop The location where the top of the first item should be
*        drawn
*
* @return The view that is currently selected
*/
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,继续看看fillDown的源码:

private View fillDown(int pos, int nextTop) {
View selectedView = null;

//获取ListView的高度
int end = (mBottom - mTop);
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
end -= mListPadding.bottom;
}

//此处是关键,做了死循环,直到下一个要添加的ItemView的top>ListView高度,或者条目已经填充完成(简单的说就是要吗填充满一屏就退出,要吗就是全部填充完,但是没填充满也退出)
while (nextTop < end && pos < mItemCount) {
// is this the selected item?
boolean selected = pos == mSelectedPosition;
//这里获取一个View
View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
//将获取到的View的高度累加,用于判断是否添加满
nextTop = child.getBottom() + mDividerHeight;
if (selected) {
selectedView = child;
}
pos++;
}

setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
return selectedView;
}


在fillDown中我们发现了一个View对象,我们就进入到makeAndAddView中看一下:

/**
* Obtains the view and adds it to our list of children.
* 获取View并添加到列表中
*/
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
if (!mDataChanged) {
// Try to use an existing view for this position.
//从缓存中获取View,有则直接使用,没有下面的obtainView会重新创建.
final View activeView = mRecycler.getActiveView(position);
if (activeView != null) {
// Found it. We're reusing an existing child, so it just needs
// to be positioned like a scrap view.
//复用ItemView,所以放到特定位置即可
setupChild(activeView, position, y, flow, childrenLeft, selected, true);
return activeView;
}
}

// Make a new view for this position, or convert an unused view if
// possible.
//获取一个新的View对象
final View child = obtainView(position, mIsScrap);

// This needs to be positioned and measured.
//因为是新建ItemView,所以需要测量
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

return child;
}


因为我们刚设置Adapter,所以是没得复用的,所以就走到了obtainView的逻辑,我们看下它的实现,该函数在AbsListView中:

/**
* Gets a view and have it show the data associated with the specified
* position.
* 获取View,并绑定特定位置的数据
*/
View obtainView(int position, boolean[] outMetadata) {
...
final View child = mAdapter.getView(position, scrapView, this);
...
return child;
}


这里面代码不少,我们只关注核心,mAdapter.getView(),可以发现它调用了我们传递进来Adapter的实例的getView方法,并且将该View返回,所以说我们在ListView中的makeAndAddView函数使用的就是我们的Adapter实例的getView返回的View对象,既然已经返回,那么它是什么时候添加进去的呢,看下setupChild函数:

/**
* Adds a view as a child and make sure it is measured (if necessary) and
* positioned properly.
*  添加子View
*/
private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
boolean selected, boolean isAttachedToWindow) {
...
//获取View的类型,以及是否可选中可点击
p.viewType = mAdapter.getItemViewType(position);
p.isEnabled = mAdapter.isEnabled(position);

...

if ((isAttachedToWindow && !p.forceAdd) || (p.recycledHeaderFooter
&& p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
//当后面复用回收的Item时会走这里
attachViewToParent(child, flowDown ? -1 : 0, p);

// If the view was previously attached for a different position,
// then manually jump the drawables.
if (isAttachedToWindow
&& (((AbsListView.LayoutParams) child.getLayoutParams()).scrappedFromPosition)
!= position) {
child.jumpDrawablesToCurrentState();
}
} else {
p.forceAdd = false;
if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
p.recycledHeaderFooter = true;
}

//将View添加到父容器中
addViewInLayout(child, flowDown ? -1 : 0, p, true);
// add view in layout will reset the RTL properties. We have to re-resolve them
child.resolveRtlPropertiesIfNeeded();
}

...
}


我们发现setupChild函数的官方注释那里已经表明这是添加子View,具体的话这里有两个函数都可以添加子View,分别是

attachViewToParent和addViewInLayout,不过attachViewToParent是条目复用的时候使用的,我们第一次添加的时候调用的是addViewInLayout。

本人是新手,如果有什么错误或者意见请指出,谢谢大家抽出宝贵的时间阅读!!!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: