Android 适配器模式(ListView与Adapter)

Android 23种设计模式

一、前言

适配器模式就是将两个不兼容的类融合在一起。通过转换使他们可以兼容的工作。Android代码中最常见的适配器就是Adapter了。ListView、GridView、RecyclerView都使用Adapter,Adapter的作用都一样,把高度定制化的item view和ListView分开。item view通过一个Adapter和ListView联系到一起。解耦而不失高度可定制。

二、适配器模式定义

将一个类的接口转换成客户希望的另一个接口。适配器模式让那些接口不兼容的类可以一起工作

三、例子

我们先来看下适配器模式的例子。学习到底什么是适配器模式。

3.1、我们举一个出水口出水量的例子。出水量有大有小,于是先定义两个接口

public interface BigOutlet {
    public void bigOutlet();
}

public interface SmallOutlet {
    public void smallOutlet();
}

3.2、然后有一个出水口water tap。出水量大。

public class BigWaterTap implements BigOutlet {
    private static final String TAG = WaterTap.class.getSimpleName();

    @Override
    public void bigOutlet() {
        Log.d(TAG,"bigOutlet");
    }
}

3.3、定义适配器

现在需求来了,我要出水口既能大量出水,也可以小量出水。而我们不能去更改BigWaterTap,因为通常很多时候一个类拟定好了过后,我们无法再去修改了。也没有源码。给它再继承SmallOutlet这个接口。我们需要的是另外的办法来添加出水量小的方法。这个时候适配器模式就派上用场了。
适配器模式写法有两种,这里先看第一种写法叫类适配器模式
类适配器模式

public class ClassWaterTapAdapter extends BigWaterTap implements SmallOutlet {
    private static final String TAG = ClassWaterTapAdapter.class.getSimpleName();

    @Override
    public void smallOutlet() {
        Log.d(TAG,"smallOutlet");
    }
}

调用

ClassWaterTapAdapter classWaterTapAdapter = new ClassWaterTapAdapter();
classWaterTapAdapter.bigOutlet();
classWaterTapAdapter.smallOutlet();

输出这里就省略了。我们可以看到适配器模式就是把两个不兼容的类结合到了一起,即可以出水量大,也可以出水量小了。达到了融合的作用。而不用去改变原来的类。然后看下另一种写法。对象适配器模式,其实就是代理模式的写法。
对象适配器模式

public class ProxyWaterTapAdapter implements SmallOutlet {
    private static final String TAG = ProxyWaterTapAdapter.class.getSimpleName();
    private BigWaterTap bigWaterTap;

    public ProxyWaterTapAdapter(BigWaterTap bigWaterTap) {
        this.bigWaterTap = bigWaterTap;
    }

    public void adapterBigOutlet() {
        bigWaterTap.bigOutlet();
    }

    @Override
    public void smallOutlet() {
        Log.d(TAG,"smallOutlet");
    }
}

调用

        ProxyWaterTapAdapter proxyWaterTapAdapter = new ProxyWaterTapAdapter(new BigWaterTap());
        proxyWaterTapAdapter.adapterBigOutlet();
        proxyWaterTapAdapter.smallOutlet();

一目了然,就是用代理的方式,拥有BigWaterTap来调用了bigOutlet。ProxyWaterTapAdapter就达到了兼容的目的。

4、小结

1、现在我们队适配器模式有个清晰的认识了。适配器就是不改变原有类的基础上,让它兼容别的接口方法,以实现新的功能,达到兼容的目的。
2、在我们开发过程中,笔者强烈建议最好还是使用对象适配器模式这种写法。对象适配器模式比类对象适配模式好处就是更加灵活,且不会暴露被适配者。因为继承了过后Adapter类中也有了一样的方法。

四、ListView与适配器模式

4.1、为了避免读者混淆,我先简单用上面的例子模拟一下Listview和适配器ListAdapter是如何工作的。

先写适配器ListWaterTapAdapter,等价于ListAdapter

public abstract class ListWaterTapAdapter implements SmallOutlet{
    public abstract void middleOutlet();
}

ListWaterTapAdapter抽象类,需要我们客户使用的时候具体去实现。然后重新写一下BigWaterTap,因为它本身有的功能就是bigOutlet()。等价于listview

public class ListViewWaterTap implements BigOutlet{
    private static final String TAG = ListViewWaterTap.class.getSimpleName();
    private ListWaterTapAdapter listWaterTapAdapter;

    @Override
    public void bigOutlet() {
        Log.d(TAG,"bigOutlet");
    }

    public void setAdapter(ListWaterTapAdapter listWaterTapAdapter) {
        this.listWaterTapAdapter = listWaterTapAdapter;
    }

    public void smallToBigOutlet() {
        listWaterTapAdapter.smallOutlet();
        int i = 100;
        while (i-- > 0);
        listWaterTapAdapter.middleOutlet();
    }
}

这里的ListViewWaterTap并非适配器。我们的适配器就是ListWaterTapAdapter。setAdapter来拥有适配器抽象类,调用抽象方法,让客户自己去实现smallOutlet和middleOutlet这两个方法。接着我们看下调用

        ListViewWaterTap listViewWaterTap = new ListViewWaterTap();
        ListWaterTapAdapter listWaterTapAdapter = new ListWaterTapAdapter() {
            @Override
            public void middleOutlet() {
                Log.d(TAG,"middleOutlet");
            }

            @Override
            public void smallOutlet() {
                Log.d(TAG,"smallOutlet");
            }
        };
        listViewWaterTap.setAdapter(listWaterTapAdapter);

        listViewWaterTap.bigOutlet();
        listViewWaterTap.smallToBigOutlet();

这里就是和listview与adapter一样的用法了。并不是标准的适配器模式写法。但确实最经典的适配器模式应用。下面我们来分析一下listview和adapter的关系。

4.2、ListView和ListAdapter

首先说下ListView用适配器模式的目的就是让listview的每个item可以客户自己高度定制化。你自己去实现就行了。无论你如何定制item使用就是一个view。listview用适配器模式就完美的达到了这个效果。
以下有射猎源码的地方,来自Android P。"..."代表省略代码

4.2.1、首先来一个普通示例

    listView = (ListView) findViewById(R.id.listView);
    MyAdapter myAdapter = new MyAdapter(this,mListTile);

public class MyAdapter extends BaseAdapter {
    private LayoutInflater layoutInflater;
    List<String> litTile;

    public MyAdapter(Context context, List<String> listTile) {
        layoutInflater = LayoutInflater.from(context);
        this.litTile = listTile;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder;
        if (convertView == null) {
            holder = new ViewHolder();
            convertView = layoutInflater.inflate(R.layout.layout_item,null);
            holder.title = (TextView) convertView.findViewById(R.id.title);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder)convertView.getTag();
        }
        holder.title.setText(litTile.get(position));
        return convertView;
    }

    class ViewHolder{
        public TextView title;
    }
// 篇幅原因,省略getCount、getItem和getItemId

这是我们最普通的运用listview的写法了。然后我们从适配器讲起,先看adapter是个啥
listview.java的setAdapter方法

@Override
    public void setAdapter(ListAdapter adapter) {
    ...
}

看看BaseAdapter的集成关系,然后开始讲适配器BaseAdapter

public abstract class BaseAdapter implements ListAdapter, SpinnerAdapter { ... }

public interface ListAdapter extends Adapter { ... }

public interface Adapter { ... }

4.2.2、适配器

为了节约篇幅我把注释删了。看Adapater代码如下:

public interface Adapter {
    void registerDataSetObserver(DataSetObserver observer);
    void unregisterDataSetObserver(DataSetObserver observer);
    int getCount();   
    Object getItem(int position);
    long getItemId(int position);
    boolean hasStableIds();
    View getView(int position, View convertView, ViewGroup parent);
    static final int IGNORE_ITEM_VIEW_TYPE = AdapterView.ITEM_VIEW_TYPE_IGNORE;
    int getItemViewType(int position);
    int getViewTypeCount();
    static final int NO_SELECTION = Integer.MIN_VALUE;
    boolean isEmpty();
    default @Nullable CharSequence[] getAutofillOptions() {
        return null;
    }
}

1>根据上面的继承关系,BaseAdapter跟开始举例一样,抽象类来定义适配器,要实现的接口是Adapter
2> 可以看到Adapter就是接口,接口的这些方法是要提供给ListView内部使用的。我们自己实现Adapter,完成这些接口,或者抽象方法。

4.2.3、listview如何使用adapter

首先看下listView的继承关系

public class ListView extends AbsListView { ... }

public abstract class AbsListView extends AdapterView<ListAdapter> implements TextWatcher,
        ViewTreeObserver.OnGlobalLayoutListener, Filter.FilterListener,
        ViewTreeObserver.OnTouchModeChangeListener,
        RemoteViewsAdapter.RemoteAdapterConnectionCallback { ... }

public abstract class AdapterView<T extends Adapter> extends ViewGroup { ... }

ListView就是一个ViewGroup,然后通过主要的逻辑实现代码就在ListView.java和AbsListView.java了

先讲一个getCount
在AbsListView.java的onAttachedToWindow方法

    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        ...
        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();
        }
    }

把view关联到window的时候,mAdapter.getCount(),这个getConut是我们继承时写的,就确定了我们这个listView有多少个item了。

再梳理一下getView是如何把item view加载出来的
大致流程如下,这里就只列出简化的代码了,本文主要理解适配器模式的思想
1>AbsListView:onlayout

 protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        ...
        layoutChildren();
        ....
    }

ViewGroup是组合模式,它在调用onlayout的时候调用layoutChildren来布局子控件,layoutChildren在AbsListView是一个空实现,实现代码在ListView
2>ListView:layoutChildren

protected void layoutChildren() {
    ...
   switch (mLayoutMode) {
            ...
            case LAYOUT_FORCE_BOTTOM:
                sel = fillUp(mItemCount - 1, childrenBottom);
                adjustViewsUpOrDown();
                break;
            case LAYOUT_FORCE_TOP:
                mFirstPosition = 0;
                sel = fillFromTop(childrenTop);
                adjustViewsUpOrDown();
                break;

调用到layoutChildren后来布局item view.
3>ListView:fillDown

private View fillDown(int pos, int nextTop) {
  ...
   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++;
        }

每个子view都是调用makeAndAddView然后调用AbsListView的obtainView方法

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
            boolean selected) {
    ...
     final View child = obtainView(position, mIsScrap);

4>AbsListView:obtainView

 View obtainView(int position, boolean[] outMetadata) {
     ...
       final View scrapView = mRecycler.getScrapView(position);
        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.
                mRecycler.addScrapView(scrapView, position);
            } else if (child.isTemporarilyDetached()) {
                outMetadata[0] = true;

                // Finish the temporary detach started in addScrapView().
                child.dispatchFinishTemporaryDetach();
            }
        }
    ...
}

这里用到了mAdapter.getView。从ListView布局每个item view的过程来看,最后布局使用view的时候就用到了我们去实现的getView方法返回的view。

我们对listview优化的时候,为啥写法是判断if (convertView == null)
接着分析上面第四步的代码
mRecycler.getScrapView是获得可复用的view,然后带入mAdapter.getView(position, scrapView, this);
如果还没有被加入到缓存list则

if (child != scrapView) {
    // Failed to re-bind the data, return scrap to the heap.
    mRecycler.addScrapView(scrapView, position);
} 

所以我们写代码的时候则这样来优化判断,当然获得缓存后数据也是原来的。所以我们要重新设置title

     if (convertView == null) {
            holder = new ViewHolder();
            convertView = layoutInflater.inflate(R.layout.layout_item,null);
            holder.title = (TextView) convertView.findViewById(R.id.title);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder)convertView.getTag();
        }
        holder.title.setText(litTile.get(position));

这里ListView和ListAdapter的关系就梳理到这里了。有兴趣的同学还可以去看看GridView,RecyleView喔。比如RecycleView就是ListView的一个升级版,RecycleView定义了ViewHolder的机制。更加巧妙。

五、总结

适配器模式就是将原本不兼容的接口融合在一起,以便更好的协同合作。当然设计模式不是一成不变的,litview的adapter就是很好的一个变化,让UI更加高度可定制化而不失自身实现。
优点:
1、把接口和类结合,通过适配器可以让接口定义的功能更好的复用。
2、扩展性好,不光可调用自己开发的功能,还自然的扩展了接口定义的其它功能。
缺点:
不易滥用,如果你的代码有n多个适配器,你想想那场面,调用十分凌乱,还不如直接修改源码设计更好的public api。
还是那句话,设计模式主要理解它的精髓。并不是一成不变。多思考是否应该使用这个设计模式才能事半功倍。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,907评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,987评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,298评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,586评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,633评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,488评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,275评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,176评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,619评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,819评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,932评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,655评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,265评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,871评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,994评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,095评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,884评论 2 354