设计模式之适配器模式

作为 Android 开发者,适配器模式的使用率非常高,无论是 ListView 还是 RecyclerView 都需要 Adapter ,上一篇博客设计模式之代理模式 开篇有一句话:软件开发中遇到的所有问题,都可以通过增加一层抽象而得以解决,如果不行就再加一层(开个玩笑)。这句话也适用于我们今天要学习的适配器模式,其中对象的适配器模式就是代理模式的应用。这篇文章将介绍适配器模式的概念、类的适配器、对象的适配器、适配器的简单实现以及 Android 源码中的适配器模式。

适配器模式的定义和使用场景

适配器模式把一个类的接口转换成客户端所期待的另一个接口,从而使原本因接口不匹配而不能在一起工作的两个类能够在一起工作。
适配器在生活中最经典的应用就属我们日常生活中不可或缺的电脑和手机的适配器了。我们都知道一般情况下,我们家里的插座的输出电压都是 220V ,但是手机和笔记本电脑的输入电压不可能有这么高,所以在给笔记本电脑和手机充电时都需要相应的适配器,这就是适配器模式在生活中的应用。

类的适配器和对象的适配器

其实类的适配器和对象的适配器的原理和本质是一样的,它们都是适配器模式(废话),只是它们的实现方式不同,其中类的适配器模式是用继承实现的,而对象的适配器模式是用组合来实现的,在 Java 中因为不支持多继承,因此我们建议使用对象的适配器实现,这也是 Java 编程中的一个原则,即多用组合少用继承。

类的适配器
class_adapter_uml.png

从上图可以看到,类的适配器通过实现 Target 接口并且继承 Adaptee 类来实现接口的转换。下面是类的适配器的简单示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public interface Target {
/**
* 这是源类Adaptee也有的方法
*/

public void sampleOperation1();
/**
* 这是源类Adapteee没有的方法
*/

public void sampleOperation2();
}

public class Adaptee {

public void sampleOperation1(){}

}

public class Adapter extends Adaptee implements Target {
/**
* 由于源类Adaptee没有方法sampleOperation2()
* 因此适配器补充上这个方法
*/

@Override
public void sampleOperation2() {
//写相关的代码
}
}

对象的适配器
object_adapter_uml.png

可以看到对象的适配器模式与类的适配器模式的最大不同就是类的适配器中 Adapter 是继承 Adaptee 的,而对象的适配器则有所不同,在对象的适配器模式中 Adapter 没有继承 Adaptee 而有 Adaptee 的引用,这就是对象的适配器和类的适配器的区别。可以看到对象的适配器和我们上篇博客介绍的代理模式有着异曲同工之处。下面是对象的适配器的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public interface Target {
/**
* 这是源类Adaptee也有的方法
*/

public void sampleOperation1();
/**
* 这是源类Adapteee没有的方法
*/

public void sampleOperation2();
}

public class Adaptee {

public void sampleOperation1(){}

}

public class Adapter implements Target{
private Adaptee adaptee;

public Adapter(Adaptee adaptee){
this.adaptee = adaptee;
}
/**
* 源类Adaptee有方法sampleOperation1
* 因此适配器类直接委派即可
*/

public void sampleOperation1(){
this.adaptee.sampleOperation1();
}
/**
* 源类Adaptee没有方法sampleOperation2
* 因此由适配器类需要补充此方法
*/

public void sampleOperation2(){
//写相关的代码
}
}

Android 源码中的适配器

Android 源码中对于适配器模式的应用当属 ListView 了,但是 ListView 中对于适配器模式的使用和经典的适配器模式是不同的。我们知道 ListView 的 Item 布局的样式和数据集都是千变万化的,怎么将这些千变万化的样式和数据集进行统一这就是 ListView 的 Adapter 要做的事了。 ListView 将 Adapter 暴露给用户进行实现,用户在 Adapter 的 getView 方法中返回各式各样的 View ,最后用户只要将自定义的 Adapter 设置给 ListView , ListView 就可以将用户设置的 View 进行绘制出来。
我们发现 Adapter 的成员变量定义在 ListView 的父类 AbsListView 中,下面我们看一下 AbsListView 的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public abstract class AbsListView extends AdapterView<ListAdapter> implements TextWatcher,
ViewTreeObserver.OnGlobalLayoutListener, Filter.FilterListener,
ViewTreeObserver.OnTouchModeChangeListener,
RemoteViewsAdapter.RemoteAdapterConnectionCallback {


//.....

@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();
}
}

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

public int getChildCount() {
return mChildrenCount;
}

//...
}

可以看到在 AbsListView 的 onAttachedToWindow 方法中会调用 Adapter 的 getCount 方法获取元素的个数,在 onLayout 方法中会调用 layoutChildren 方法进行子元素的布局, layoutChildren 方法 AbsListView 并没有进行实现,而是由具体的子类实现的,在 ListView 的 layoutChildren 方法中会根据 mLayoutMode 的不同分为由上到下布局和由下到上布局,我们看一下由上到下布局 fillDown :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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;
}

可以看到 fillDown 中又调用了 makeAndAddView 方法,我们再看看 makeAndAddView 方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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;
}

class RecycleBin {
private RecyclerListener mRecyclerListener;

/**
* The position of the first view stored in mActiveViews.
*/

private int mFirstActivePosition;

/**
* Views that were on screen at the start of layout. This array is populated at the start of
* layout, and at the end of layout all view in mActiveViews are moved to mScrapViews.
* Views in mActiveViews represent a contiguous range of Views, with position of the first
* view store in mFirstActivePosition.
*/

private View[] mActiveViews = new View[0];

/**
* Unsorted views that can be used by the adapter as a convert view.
*/

private ArrayList<View>[] mScrapViews;

//....
}

可以看到在 makeAndAddView 方法中首先调用了 mRecycler.getActiveView 去获取 View 其中 mRecycler 的类型是 RecycleBin , 在 RecycleBin 中维护着好几个 View List ,其中 mActiveViews 就是其中一个,我们从上面的注释可以看到 mActiveViews 中存储的是当前屏幕可见的 View ,只要 View 一移出当前屏幕,那么这个 View 就会被移动到 mScrapViews 中,这就是我们熟悉的 ListView 中的 View 复用原理。如果在 mActiveViews 没有找到相应的 View 就会进一步调用 obtainView 方法,obtainView 定义在 AbsListView 中,下面我们看一下 obtainView 方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
View obtainView(int position, boolean[] isScrap) {
//....
final View transientView = mRecycler.getTransientStateView(position);
if (transientView != null) {
final LayoutParams params = (LayoutParams) transientView.getLayoutParams();

// If the view type hasn't changed, attempt to re-bind the data.
if (params.viewType == mAdapter.getItemViewType(position)) {
final View updatedView = mAdapter.getView(position, transientView, this);

// If we failed to re-bind the data, scrap the obtained view.
if (updatedView != transientView) {
setItemViewLayoutParams(updatedView, position);
mRecycler.addScrapView(updatedView, position);
}
}

// Scrap view implies temporary detachment.
isScrap[0] = true;
return transientView;
}

final View scrapView = mRecycler.getScrapView(position);
final View child = mAdapter.getView(position, scrapView, this);
//....
return child;
}

View getScrapView(int position) {
if (mViewTypeCount == 1) {
return retrieveFromScrap(mCurrentScrap, position);
} else {
final int whichScrap = mAdapter.getItemViewType(position);
if (whichScrap >= 0 && whichScrap < mScrapViews.length) {
return retrieveFromScrap(mScrapViews[whichScrap], position);
}
}
return null;
}

从上面的代码我们发现在获取 View 时首先是去 mTransientStateViews 中找, mTransientStateViews 中存储的是一些已移出屏幕但是 View 具有瞬时态(Transient),例如在播放动画的 View 。具有瞬时态的 View 移出屏幕后不会被移动到 mScrapViews 中,页就是说具有瞬时态的 View 在移除屏幕后会将其移动到 mTransientStateViews 中以备复用,而普通的 View 会被移动到 mScrapViews 以备复用,后面的逻辑就非常简单了,如果不是瞬时态的 View 就会去 mScrapViews 中找可以复用的 View ,如果在 mScrapViews 中存在可以被复用的 View 那么我们定义的 Adapter 的 getView 方法的 convertView 参数就不是 null 了… 后面的事情我们都知道了,这就是 ListView 中 View 复用的原理。在 makeAndAddView 中最后拿到 View 之后就会调用 setupChild 方法将这个 View 布局到特定的位置。这就是 ListView 中的适配器模式的应用。其中 ListView 还应用了观察者模式,详情可见我的文章设计模式之观察者模式

总结

适配器模式的好处我们就不说了,我们理一理 ListView 中的适配器模式。 ListView 等列表控件通过 Adapter 来获取 Item View 的数量、布局、数据等。在 Adapter 的 getView 方法返回一个 View 的抽象,而千变万化的视图都是 View 的子类,通过这种方式保证了 AbsListView 的高度可定制化。