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。
还是那句话,设计模式主要理解它的精髓。并不是一成不变。多思考是否应该使用这个设计模式才能事半功倍。

    原文作者:Yink_Liu
    原文地址: https://www.jianshu.com/p/fb558642823e
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞