适配器模式的定义
适配器模式把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。
适配器模式的使用场景
1.系统需要使用现有的类,而此类的接口不符合系统的需要,即接口不兼容。
2.想要建立一个可以重复使用的类,用于一些彼此之间没有太大关联的一些类,包括可能在将来引进的类一起工作。
3.需要一个统一的输出接口,而输入端的类型不可预知。
适配器模式的UML图
适配器模式分为两种,即类适配器模式 和 对象适配器模式。
1.类适配器
类适配器是通过实现Target接口以及继承Adaptee类来实现接口的转换,例如,目标接口需要的是operation2,但是Adaptee对象只有一个operation3,因此就出现了不兼容的情况,此时通过Adapter实现一个operation2函数,将Adaptee的operation3转换为Target需要的operation2,以此实现兼容。
角色介绍
1.Target:目标角色,也就是所期待得到的接口。注意,由于这里讨论的是类适配器模式,因此目标不可以是类。
2.Adaptee:现在需要适配的接口
3.Adapter:适配器角色,也是本模式的核心。适配器把源接口转换成目标接口。显然,这一角色不可以是接口,而必须是具体类。
2.对象适配器
与类适配器模式一样,对象的适配器模式把被适配的类的API转换成目标类的API,与类的适配器模式不同的是,对象适配器模式不是通过继承的关系连接到Adaptee类,UML图如下所示。
从下述代码可以看出,Adaptee类(Volt220)并没有getVolt5()方法,而客户端则期待这个方法。为了使客户端能够使用Adaptee类,需要提供一个包装类Adapter。这个包装类包装了一个Adaptee的实例,从而此包装类能够把Adaptee的API与Target的API衔接起来。Adapter与Adaptee是委派关系,这决定了适配器模式是对象的。
//对象适配器
public class VoltAdapter2 implements FiveVolt {
Volt220 mVolt220;
public VoltAdapter2(Volt220 adaptee) {
mVolt220 = adaptee;
}
public int getVolt220() {
return mVolt220.getVolt220();
}
@Override
public int getVolt5() {
return 5;
}
}
这种实现方式是:
直接将要被适配的对象传递到Adapter中,使用组合的形式实现接口兼容的效果。这比类适配器的方式更为灵活,它的另一个好处是被适配对象中的方法不会暴露出来,而类适配器由于继承了被适配对象,因此,被适配对象的函数在Adapter类中也都含有,这使得Adapter类出现一些奇怪的接口,用户使用成本较高。因此,对象适配器模式则更加灵活、实用。
在实际开发中,Adapter通常用于进行不兼容的类型转换的场景,还有一种就是输入有无数种情况,但是输出类型是统一的,我们可以通过Adapter返回一个统一的输出,而具体的输入则留给用户处理,内部只需要知道输出的是符合要求的类型即可。例如ListView的Adapter,用户的Item View各式各样,但最终都是属于View的,ListView只需要知道getView返回的是一个View即可,具体是什么View类型并不需要ListView关心。而在使用Adapter模式过程中建议尽量使用对象适配器的实现方式,多用合成或者聚合,少用继承。
类适配器简单Demo
D:\Users\user\AndroidStudioProjects\AdapterPatternDemo\app\src\main\java\adapterpattern\gome\com\adapterpatterndemo\FiveVolt.java
package adapterpattern.gome.com.adapterpatterndemo;
/**
* Created by ying.zhang on 2018/8/27.
*/
//Target角色(Target角色给出了需要的目标接口,而Adapter类则是需要被转换的对象。)
public interface FiveVolt {
public int getVolt5();
}
D:\Users\user\AndroidStudioProjects\AdapterPatternDemo\app\src\main\java\adapterpattern\gome\com\adapterpatterndemo\Volt220.java
package adapterpattern.gome.com.adapterpatterndemo;
/**
* Created by ying.zhang on 2018/8/27.
*/
//Adapter角色,需要被转换的对象
public class Volt220 {
public int getVolt220() {
return 220;
}
}
D:\Users\user\AndroidStudioProjects\AdapterPatternDemo\app\src\main\java\adapterpattern\gome\com\adapterpatterndemo\VoltAdapter.java
package adapterpattern.gome.com.adapterpatterndemo;
/**
* Created by ying.zhang on 2018/8/27.
*/
//Adapter角色,将220v的电压转换成5v电压。Adapter则是将Volt220转换成Target的接口。
public class VoltAdapter extends Volt220 implements FiveVolt {
@Override
public int getVolt5() {
return 5;
}
}
Android源码中的适配器模式
在开发过程中,ListView的Adapter是我们最为常见的类型之一。我们需要使用Adapter加载每个Item View的布局,并且进行数据绑定等操作。
ListView为什么要使用Adapter模式?
ListView作为最重要的控件,它需要能够显示各式各样的视图(Item View),每个人需要的显示效果各不相同,显示的数据类型、数量等也千变万化。Android的做法是增加一个Adapter层来隔离变化,将ListView需要的关于Item View接口 抽象到Adapter对象中,并且在ListView内部调用Adapter这些接口完成布局等操作。这样只要用户实现了Adapter接口。
#Android8.1
public abstract class AbsListView extends AdapterView<ListAdapter> implements TextWatcher,
ViewTreeObserver.OnGlobalLayoutListener, Filter.FilterListener,
ViewTreeObserver.OnTouchModeChangeListener,
RemoteViewsAdapter.RemoteAdapterConnectionCallback {
... ...
/**
* The adapter containing the data to be displayed by this view
*/(适配器包含此视图要显示的数据)
ListAdapter mAdapter;
//关联到Window时调用,获取调用Adapter中的getCount方法等
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
final ViewTreeObserver treeObserver = getViewTreeObserver();
treeObserver.addOnTouchModeChangeListener(this);
if (mTextFilterEnabled && mPopup != null && !mGlobalLayoutListenerAddedFilter) {
treeObserver.addOnGlobalLayoutListener(this);
}
//给适配器注册一个观察者,该墨水第12章已介绍
if (mAdapter != null && mDataSetObserver == null) {
mDataSetObserver = new AdapterDataSetObserver();
mAdapter.registerDataSetObserver(mDataSetObserver);
// Data may have changed while we were detached. Refresh.
mDataChanged = true;
mOldItemCount = mItemCount;
// 获取Item的数量,调用的是mAdapter的getCount方法
mItemCount = mAdapter.getCount();
}
}
}
#AbsListView
/**
* Subclasses should NOT override this method but
* {@link #layoutChildren()} instead.
*/(子类不应该覆盖此方法,而是覆盖{@link #layoutChildren()}。)
//子类需要覆写layoutChildren()函数来布局child view,也就是Item View。
@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();
}
//布局childView
layoutChildren();
mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR;
// TODO: Move somewhere sane. This doesn't belong in onLayout().
if (mFastScroll != null) {
mFastScroll.onItemCountChanged(getChildCount(), mItemCount);
}
mInLayout = false;
}
#AbsListView
/**
* Subclasses must override this method to layout their children.
*/(//子类必须覆盖此方法以布置其子项。)
protected void layoutChildren() {
}
AbsListView定义了集合视图的逻辑框架,比如Adapter模式的应用、复用Item View的逻辑、布局子视图的逻辑等。子类只需要覆写特定的方法即可实现集合视图的功能。首先在AbsListView类型的VIew中添加窗口(onAttachedToWindow函数),时会调用Adapter中的getCount方法获取到元素的个数,然后在onLayout函数中调用layoutChildren函数 对所有的子元素进行布局。AbsListView中并没有实现layoutChildren函数,具体的实现在子类中,这里我们分析ListView中的实现。
#ListView
@RemoteView
public class ListView extends AbsListView {
... ...
@Override
protected void layoutChildren() {
... ...(代码省略)
try {
super.layoutChildren();
invalidate();
//代码省略,根据布局模式来布局Item View
// Remember stuff we will need down below
switch (mLayoutMode) {
case LAYOUT_SET_SELECTION:
index = mNextSelectedPosition - mFirstPosition;
if (index >= 0 && index < childCount) {
newSel = getChildAt(index);
}
break;
case LAYOUT_FORCE_TOP:
case LAYOUT_FORCE_BOTTOM:
case LAYOUT_SPECIFIC:
case LAYOUT_SYNC:
break;
case LAYOUT_MOVE_SELECTION:
default:
// Remember the previously selected view
index = mSelectedPosition - mFirstPosition;
if (index >= 0 && index < childCount) {
oldSel = getChildAt(index);
}
// Remember the previous first child
oldFirst = getChildAt(0);
if (mNextSelectedPosition >= 0) {
delta = mNextSelectedPosition - mSelectedPosition;
}
// Caution: newSel might be null
newSel = getChildAt(index + delta);
}
... ...(代码省略)
}
}
ListView覆写了AbsListView中的layoutChildren函数,在该函数中根据布局模式来布局Item View,例如,默认情况下是从上到下开始布局,但是,也有从下到上布局的,例如QQ聊天窗口的气泡布局,最新的消息会布局到窗口的最底部。
/**
* Fills the list from pos down to the end of the list view.
*
* @param pos The first position to put in the list
*
* @param nextTop The location where the top of the item associated with pos
* should be drawn
*
* @return The view that is currently selected, if it happens to be in the
* range that we draw.
*/
//从上到下填充Item 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;
//通过makeAndAddView获取 Item View
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;
}
/**
* Fills the list from pos up to the top of the list view.
*
* @param pos The first position to put in the list
*
* @param nextBottom The location where the bottom of the item associated
* with pos should be drawn
*
* @return The view that is currently selected
*/
//从下到上布局
private View fillUp(int pos, int nextBottom) {
View selectedView = null;
int end = 0;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
end = mListPadding.top;
}
while (nextBottom > end && pos >= 0) {
// is this the selected item?
boolean selected = pos == mSelectedPosition;
//通过makeAndAddView获取Item View
View child = makeAndAddView(pos, nextBottom, false, mListPadding.left, selected);
nextBottom = child.getTop() - mDividerHeight;
if (selected) {
selectedView = child;
}
pos--;
}
mFirstPosition = pos + 1;
setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
return selectedView;
}
/**
* Obtains the view and adds it to our list of children. The view can be
* made fresh, converted from an unused view, or used as is if it was in
* the recycle bin.
*
* @param position logical position in the list
* @param y top or bottom edge of the view to add
* @param flow {@code true} to align top edge to y, {@code false} to align
* bottom edge to y
* @param childrenLeft left edge where children should be positioned
* @param selected {@code true} if the position is selected, {@code false}
* otherwise
* @return the view that was added
*/
//添加Item 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.
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.
setupChild(activeView, position, y, flow, childrenLeft, selected, true);
return activeView;
}
}
// Make a new view for this position, or convert an unused view if
// possible.
//获取一个item 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;
}
在每一种布局的函数中都会从makeAndAddView函数获取一个View,这个View就是ListView的每一项的视图,这里有一个pos参数,也就是对应这个View是ListView中的第几项,我们看看makeAddAddView中的实现。
在makeAddAddView函数中主要分为两个步骤,第一是根据position获取一个item View,然后将这个View布局到特定的位置。获取一个item View调用obatinView函数,这个函数在AbsListView中。
/**
* Gets a view and have it show the data associated with the specified
* position. This is called when we have already discovered that the view
* is not available for reuse in the recycle bin. The only choices left are
* converting an old view or making a new one.
*
* @param position the position to display
* @param outMetadata an array of at least 1 boolean where the first entry
* will be set {@code true} if the view is currently
* attached to the window, {@code false} otherwise (e.g.
* newly-inflated or remained scrap for multiple layout
* passes)
*
* @return A view displaying the data associated with the specified position
*/
//获取一个Item View,该函数在AbsListView中
View obtainView(int position, boolean[] outMetadata) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView");
outMetadata[0] = false;
// Check whether we have a transient state view. Attempt to re-bind the
// data and discard the view if we fail.
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);
}
}
outMetadata[0] = true;
// Finish the temporary detach started in addScrapView().
transientView.dispatchFinishTemporaryDetach();
return transientView;
}
//1.从缓存的Item View中获取,ListView的复用机制就在这里。
final View scrapView = mRecycler.getScrapView(position);
//3.没有缓存View的情况下getView的第二个参数为null。
2.注意,这里是将scrapView设置给了Adapter的getView函数,没有缓存View的情况下getView的第二个参数为null。
final View child = mAdapter.getView(position, scrapView, this);
if (scrapView != null) {
if (child != scrapView) {
// Failed to re-bind the data, return scrap to the heap.
//2.注意,这里是将scrapView设置给了Adapter的getView函数
mRecycler.addScrapView(scrapView, position);
} else if (child.isTemporarilyDetached()) {
outMetadata[0] = true;
// Finish the temporary detach started in addScrapView().
child.dispatchFinishTemporaryDetach();
}
}
if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
}
if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
}
setItemViewLayoutParams(child, position);
if (AccessibilityManager.getInstance(mContext).isEnabled()) {
if (mAccessibilityDelegate == null) {
mAccessibilityDelegate = new ListItemAccessibilityDelegate();
}
if (child.getAccessibilityDelegate() == null) {
child.setAccessibilityDelegate(mAccessibilityDelegate);
}
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
return child;
}
obtainView函数定义了列表控件的Item View的复用逻辑,首先会从RecycleBin中获取一个缓存的View,如果有缓存则将这个缓存的View传递到Adapter的getView第二个参数中,这也就是我们对Adapter的最常见的优化方式,即判断getView的convertView是否为空,如果为空则从xml中创建视图,否则使用缓存的View。这样避免每次都从xml中加载布局的消耗,能够显著提升ListView等列表控件的效率。
通过这种缓存机制,即使有成千上万的数据项,ListView也能够流畅运行,因此,只有填满一屏所需的Item View存在内存中。ListView根据Adapter设置的数据项数量循环调用getView方法获取视图,第一次加载填满屏幕的数据项时getView的第二个参数convertView都为空,此时每次都需要从xml中加载布局文件,填充数据之后返回给ListView。当整屏的数据项加载完毕之后,用户向下滚动屏幕,此时Item1滚出屏幕,一个新的项目从屏幕低端上来时,ListView再次请求一个视图,此时item1被缓存起来了,在下一项数据加载时传递给getView的第二个参数convertView,因此,convertView此时不是空值,它的值是Item1。此时只需要设定新的数据然后返回convertView,这样就避免每次都从xml中加载、初始化视图,减少了时间、性能上的消耗。
根据其工作原理,可以重复利用ListView的Item View,只要convertView不为空就直接使用,改变它绑定的数据就行了。
当然,由于图片被缓存了,视图中的数据也会被缓存,因此,你需要在每次获取到了Item view时对每个数据项重新赋值,否则会出现数据错乱的现象。
ListView等集合控件通过Adapter来获取Item View的数量、布局、数据等,在这里最为重要的就是getView函数,这个函数返回一个View类型的对象,也就是Item View。由于其返回的是一个View的抽象,而千变万化的UI视图都是VIew的子类,通过依赖抽象这个简单的原则和Adapter模式将Item View的变化隔离了,保证了AbsListView类族的高度可定制化。在获取了View之后,将这些View通过特定的布局方式设置到对应的位置上,再加上Item View的复用机制,整个ListView就运转起来了。
当然,这里的Adapter并不是经典的适配器模式,却是对象适配器模式的优秀示例,也很好地体现了面向对象的一些基本原则。这里的Target角色就是View,Adapter角色就是将Item View的操作抽象起来,ListView等集合视图通过Adapter对象获得Item的个数、数据、Item View等,从而达到适配各种数据、各种Item视图的效果。因为Item View和数据类型千变万化,Android的架构师将这些变化的部分交给用户来处理,通过getCount、getItem、getView等几个方法抽象出来,也就是将Item View的构造过程交给用户来处理,灵活地运用了适配器模式。
仔细一想,这个Adapter似乎并不是适配器模式,因为其缺少了适配器模式中最重要的对象转换语义,你可能会发现它似乎更像是抽象工厂模式,getView、getItem等对应工厂中构建对象的函数,具体的抽象工厂来构建具体的View、Item对象。
参考《Android源码设计模式》