Android 源码中的适配器模式

2,371

从装饰者模式到Context类族

当观察者模式和回调机制遇上Android源码

Android源码中的静态工厂方法

Android中的工厂方法模式

Android源码中的命令模式

前段时间写了当观察者模式和回调机制遇上Android源码,分析了下ListView中的观察者模式,今天我们来分析下ListView部分的另一个模式,适配器模式。

定义

适配器模式把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。

使用场景

  • 当你想使用一个已经存在的类,而它的接口不符合你的需求;

  • 你想创建一个可以复用的类,该类可以与其他不相关的类或不可预见的类协同工作;

  • 你想使用一些已经存在的子类,但是不可能对每一个都进行子类化以匹配它们的接口,对象适配器可以适配它的父亲接口。

结构

适配器模式有类的适配器模式对象的适配器模式两种不同的形式。

类适配器

对象适配器

可以看到,对象的适配器模式不是使用继承关系连接到Adaptee类,而是使用委派关系连接的。

模式所涉及的角色有:

  • 目标(Target)角色:这就是所期待得到的接口。注意:由于这里讨论的是类适配器模式,因此目标不可以是类。

  • 源(Adapee)角色:现在需要适配的接口。

  • 适配器(Adaper)角色:适配器类是本模式的核心。适配器把源接口转换成目标接口。显然,这一角色不可以是接口,而必须是具体类。

实现

这里我们通过一个实例来模拟一下适配器模式。需求是这样的:IPhone7的耳机口被取消,我们怎么保证之前的耳机还能用呢?当然是需要一个转接头了,这个转接头呢,其实就类似我们的适配器。耳机需要的接口就是我们的目标角色,手机提供的接口就是我们的源角色,转接头当然就是适配器角色了。

类适配器

目标角色

public interface ITarget {
    //获取需要的接口
    String getRightInterface();
}

源角色

public class IPhoneSeven {

    //获取iphone7提供的接口
    public String getInterface(){
        return "iphone7 interface";
    }
}

适配器

public class CAdapter extends IPhoneSeven implements ITarget{

    @Override
    public String getRightInterface() {
        String newInterface = getInterface();
        return suit(newInterface);
    }

    /**
     * 转换操作
     * @param newInterface
     * @return
     */
    private String suit(String newInterface) {
        return "3.5mm interface";
    }
}

对象适配器

对象适配器的目标角色和源角色是一样的,我们就不再写了。

适配器

public class Adapter implements ITarget {

    private IPhoneSeven mIPhoneSeven;

    public Adapter(IPhoneSeven IPhoneSeven) {
        mIPhoneSeven = IPhoneSeven;
    }

    @Override
    public String getRightInterface() {
        String newInterface = mIPhoneSeven.getInterface();
        return suit(newInterface);
    }

    /**
     * 转换操作
     * @param newInterface
     * @return
     */
    private String suit(String newInterface) {
        return "3.5mm interface";
    }
}

Android源码中的适配器模式,最典型的无非就是ListView、GridVIew、RecyclerView等等。我们来简单分析一下ListView,其他的大同小异。

我们前边在使用场景中有这么一点:你想使用一些已经存在的子类,但是不可能对每一个都进行子类化以匹配它们的接口,对象适配器可以适配它的父亲接口。

这句话怎么理解呢?其实放到ListView源码中理解起来更容易一些。首先我们的Item View可以是有各种不同布局的,我们不可能对每一个布局都去进行匹配,但是我们可以去适配它们的父类,父类是什么,就是View类。

ListView做为client,他所需要的目标接口(target interface)就是ListAdapter,包含getCount(),getItem(),getView()等几个基本的方法,为了兼容List,Cursor等数据类型作为数据源,还专门定义两个适配器来适配他们:ArrayAdapter和CursorAdapter。这两个适配器,说白了,就是针对目标接口对数据源进行兼容修饰。

我们来看下结构,以下UML图省略了部分不重要的类。

接下来我们看看源码中的具体实现。我们Adapter的使用时在ListView的父类,AbsListView中,我们看下注释

/**
* Base class that can be used to implement virtualized lists of items. A list does
* not have a spatial definition here. For instance, subclases of this class can
* display the content of the list in a grid, in a carousel, as stack, etc.
*/

AbsListView定义了集合视图的逻辑框架,比如Adapter的使用、复用Item View的逻辑、布局子视图的逻辑等,子类只需要重写特定的方法即可实现集合视图的功能。我们看下代码

public abstract class AbsListView extends AdapterView 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();
        }
    }

    /**
     * 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);

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

    /**
     * Subclasses must override this method to layout their children.
     */
    protected void layoutChildren() {
    }
}

首先在onAttachedToWindow()方法中调用mAdapter.getCount(),获取Item View的数量,然后调用onLayout(boolean changed, int l, int t, int r, int b)方法进行布局,从注释里也可以看到,子类对于Item View的布局需要重写layoutChildren()方法。我们看下ListView中的layoutChildren()方法。

@Override
protected void layoutChildren() {
    try {
        //根据不同的布局模式来布局Item View
        switch (mLayoutMode) {
            //~省略部分无关代码~
            case LAYOUT_FORCE_BOTTOM:
                sel = fillUp(mItemCount - 1, childrenBottom);
                adjustViewsUpOrDown();
                break;
            case LAYOUT_FORCE_TOP:
                mFirstPosition = 0;
                sel = fillFromTop(childrenTop);
                adjustViewsUpOrDown();
                break;
            //~省略部分无关代码~
            defaule:
                break;
        }
    }
}

/**
 * Fills the list from pos up to the top of the list view.
 *
 */
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;
        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;
}

/**
 * Fills the list from top to bottom, starting with mFirstPosition
 */
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);
}

/**
 * Fills the list from pos down to the end of the list 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;
}

从layoutChildren()方法这一串调用来看,无非就是对应ListView的不同布局模式,一种是新数据显示在最上方,一种是把新数据显示在最下方。但是无论是哪一种方式,在循环中获取View都是通过makeAndAddView(pos, nextTop, true, mListPadding.left, selected)方法来进行的。

/**
 * Obtain the view and add 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.
 */
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;
}

从注释了解到,这个方法是用来对我们的Item View进行缓存的。这里边又调用了obtainView(position, mIsScrap)来获取Item View,因为我们的Adapter变量是在AbsListView中,通过mAdapter来调用getView肯定也在AbsListView中。

因为缓存机制不是我们今天分析的重点,我们看下方法注释好了

/**
* Get 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.
* /

注释说的很明确:获得一个View,并且使它显示特定位置的数据。同时这里会处理View的复用或者重新生成。

总结一下就是,我们在负责展示数据的集合视图(AbsListView类族)和用户提供的布局、数据之间隔了一个类,即Adapter。Adapter会返回给集合视图用户数据的数量、布局,但是它吧所有的布局都抽象成了View。这样保证了AbsListView类族的高度可定制化。

测试代码已上传到github

参考链接

blog.csdn.net/bboyfeiyu/a…

www.codexiu.cn/android/blo…