Android源码中的适配器模式


在Android开发过程中,ListView的Adapter是我们最常见的类型之一,我们需要使用Adapter加载Item View的布局,并且进行数据绑定、缓存复用等操作。代码大致如下:

ListView myListView =  (ListView)view.findViewById(R.id.id_list);
MyAdapter adapter = new MyAdapter();
myListView.setAdapter(adapter);
class MyAdapter extends BaseAdapter {
        @Override
        public int getCount() {
            return 6;
        }
        @Override
        public Object getItem(int position) {
            return null;
        }
        @Override
        public long getItemId(int position) {
            return 0;
        }
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {

            View view;
            MerchantSuccessViewHolder holder = null;
            if(convertView == null) {
                holder = new MerchantSuccessViewHolder();
                view = View.inflate(this,R.layout.item_view,null);
               
                holder.title = (TextView) view.findViewById(R.id.title);
                view.setTag(holder);
            } else {
                view = convertView;
                holder = (ViewHolder) view.getTag();
            }
           
            holder.title.setText("hello world");
                
            return view;
        }
    }

另外GridView的使用几乎完全一样。

ListView需要显示各式各样的视图,每个人考虑的显示效果各不相同,显示的数据类型,数量也千变万化,那么如何应对这种变化,Android的架构师的做法就是采用适配器模式。

Android的做法是增加一个Adapter层来隔离变化,将ListView需要的关于Item View接口抽象到Adapter对象中,并在ListView内部调用Adapter这些接口完成布局等操作。这样只要用户实现了Adapter接口,并且该Adapter设置给ListView,ListView就可以按照用户设定的UI效果、数量、数据来显示每一项数据。ListView最重要的问题是要解决每一项Item视图的输出,ItemView千变万化,但它终究都是View类型,Adapter统一将Item View输出为view类型,这样很好的应对了Item View的可变性。

那么ListView是如何通过Adapter将千变万化U效果设置给ListView的呢?

下面来跟踪源码一探究竟。

我们在ListView类中并没有发生Adapter相关的成员变量,其实在ListView的父类AbsListView中,AbsListView是一个列表空间的抽象。源码如下:

    @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();//获得Item的数量  这个方法需要我们重写,交给程序猿
        }
    }

   @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);
        }
    }
当Activity被启动时,它的布局中的ListView的onAttachToWindow方法就会被先调用,然后调用其onLayout方法。我们看到onAttachToWindow调用mAdapterd.getCount()方法,这时获取到了Item View的数量。然后执行在onLayout方法时,会调用layoutChilren这个方法,具体的实现在子类中。在AbsListView是个空实现,ListView实现了这个方法,源码如下:
   @Override
    protected void layoutChildren() {
        //省略一部分代码
        try {
            super.layoutChildren();
            invalidate();
           
 //省略一部分代码
            switch (mLayoutMode) {
            case LAYOUT_SET_SELECTION:
                if (newSel != null) {
                    sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
                } else {
                    sel = fillFromMiddle(childrenTop, childrenBottom);
                }
                break;
            case LAYOUT_SYNC:
                sel = fillSpecific(mSyncPosition, mSpecificTop);
                break;
            case LAYOUT_FORCE_BOTTOM:
                sel = fillUp(mItemCount - 1, childrenBottom);
                adjustViewsUpOrDown();
                break;
            case LAYOUT_FORCE_TOP:
                mFirstPosition = 0;
                sel = fillFromTop(childrenTop);
                adjustViewsUpOrDown();
                break;
            case LAYOUT_SPECIFIC:
                sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop);
                break;
            case LAYOUT_MOVE_SELECTION:
                sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
                break;
            default:
               //省略一部分代码
                break;
            }
             //省略一部分代码
    }

ListView重写了父类的layoutChilden方法,该方法根据布局模式来布局模式来布局Item View,这里讨论最常用的从上到下布局模式。源码如下:
    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);
    }

fillFromTop又调用了fillDown方法
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;
    }
可以看到上面代码用到了mItemCount,没错这个变量已经在AbsListView的onAttachToWindow初始化过了。上面代码的大概意思就是通过makeAndAddView方法获取Item View,然后将mItemCount个Item View逐个往下布局,然后将高度累加。

然后继续跟踪,看makeAndAddView,其源码如下:

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

makeAndAddView方法中,主要做了两件事,

1)通过obtainView方法根据position获取一个item View

2)通过setupChild方法将这个View布局到特定的位置。

这里的setupChild方法主要是View的绘制相关操作,这里不再赘述,主要是obtainView方法。下面来看看它是如何根据position获得一个item View的。这个定义在AbsListView中。其主要源码如下

final RecycleBin mRecycler = new RecycleBin();

    View obtainView(int position, boolean[] isScrap) {
        //省略一部分代码
	//1、从缓存中的ItemView中获取View  ListView的复用机制便在这里了
        final View scrapView = mRecycler.getScrapView(position);
	
	//2、调用mAdapter.getView方法,注意将scrapView设置给getView方法的convertView参数
        final View child = mAdapter.getView(position, scrapView, this);
        //省略一部分代码
        return child;
    }

可以看出,主要逻辑也就两句代码obtainView方法定义了列表空间的Item View的复用逻辑,首先会从RecycleBin中获取一个缓存的View,如果有缓存则将这个缓存的View传递给Adapter的getView方法的第二个参数convertView参数,这也就是我们队Adapter的最常见的优化方式啊,即判断getView的convertView是否为空,如果为空则从xml中创建一个新的View,否则使用缓存的View。这样避免了每次都从xml加载布局的消耗,能显著提升ListView等列表控件的效率,通常的实现如下。

 @Override
        public View getView(int position, View convertView, ViewGroup parent) {

            View view;
            MerchantSuccessViewHolder holder = null;
            if(convertView == null) {
                holder = new MerchantSuccessViewHolder();
                view = View.inflate(this,R.layout.item_view,null);
               
                holder.title = (TextView) view.findViewById(R.id.title);
                view.setTag(holder);
            } else {
                view = convertView;
                holder = (ViewHolder) view.getTag();
            }
           
            holder.title.setText("hello world");
                
            return view;
        }

通过这种缓存机制,即使有成千上万的数据项,ListView也能够流畅运行,因此,只有填满一屏所需的Item View存在内存中。流程图如图所示:


最后,总结一下,不然前面一直看源码都快忘了适配器模式了,ListView通过Adapter来获取Item View的数量、布局、数量等,在这里最为重要的就是getView方法,这个方法返回的是一个View的的对象,也就是Item View,由于它返回的是View,而千变万化的UI视图都是View的子类,通过依赖抽象这个简单的原则和Adapter模式将Item View的变化隔离了,保证了ListView的高度定制化,在获取了View之后,将这个View显示在特定的position商,在家桑Item View的复用机制,整个ListView就运转起来了。

虽然这里的BaseAdapter不是经典的适配器模式,确实适配器模式很好的扩展。也很好的提现了面向对象的一些基本准则。用户只要处理getCount、getItem、getView方法就可以了,达到了无线适配拥抱变化的目的。

最后再谈一下SimpleAdapter,它们都是给ListView的设置Adapter,与BaseAdapter不同的是,SimpleAdapter是具体的Adapter,BaseAdapter是抽象类,这样我们可以高度定制化,扩展。我们再集成BaseAdapter的时候,可以在getView方法中进行各种优化,缓存机制还有ViewHolder的应用。

但是在SimpleAdapter中,虽然有缓存复用,但是并没有ViewHolder的概念,所以说优化效果不如我们自己定制的效果,下面贴上SimpleAdapter的getView方法。

    public View getView(int position, View convertView, ViewGroup parent) {
        return createViewFromResource(mInflater, position, convertView, parent, mResource);
    }
    private View createViewFromResource(LayoutInflater inflater, int position, View convertView,
            ViewGroup parent, int resource) {
        View v;
        if (convertView == null) {
            v = inflater.inflate(resource, parent, false);
        } else {
            v = convertView;
        }
        bindView(position, v);
        return v;
    }


getView方法又会调用createViewFromResource方法,createViewFromResource中只是判断了缓存,但是并没有ViewHolder的优化,只是通过bindView方法将数据和Item View绑定而已。bindView方法源码如下
private void bindView(int position, View view) {
        final Map dataSet = mData.get(position);
        if (dataSet == null) {
            return;
        }
        final ViewBinder binder = mViewBinder;
        final String[] from = mFrom;
        final int[] to = mTo;
        final int count = to.length;
        for (int i = 0; i < count; i++) {
            final View v = view.findViewById(to[i]);
            if (v != null) {
                final Object data = dataSet.get(from[i]);
                String text = data == null ? "" : data.toString();
                if (text == null) {
                    text = "";
                }
                boolean bound = false;
                if (binder != null) {
                    bound = binder.setViewValue(v, data, text);
                }
                if (!bound) {
                    if (v instanceof Checkable) {
                        if (data instanceof Boolean) {
                            ((Checkable) v).setChecked((Boolean) data);
                        } else if (v instanceof TextView) {
                            // Note: keep the instanceof TextView check at the bottom of these
                            // ifs since a lot of views are TextViews (e.g. CheckBoxes).
                            setViewText((TextView) v, text);
                        } else {
                            throw new IllegalStateException(v.getClass().getName() +
                                    " should be bound to a Boolean, not a " +
                                    (data == null ? "<unknown type>" : data.getClass()));
                        }
                    } else if (v instanceof TextView) {
                        // Note: keep the instanceof TextView check at the bottom of these
                        // ifs since a lot of views are TextViews (e.g. CheckBoxes).
                        setViewText((TextView) v, text);
                    } else if (v instanceof ImageView) {
                        if (data instanceof Integer) {
                            setViewImage((ImageView) v, (Integer) data);                            
                        } else {
                            setViewImage((ImageView) v, text);
                        }
                    } else {
                        throw new IllegalStateException(v.getClass().getName() + " is not a " +
                                " view that can be bounds by this SimpleAdapter");
                    }
                }
            }
        }
    }

可以看出,bindView方法不能处理ViewGroup,只能是一些简单的TextView,ImageView等View。

所以,当数据很少,不会超过一屏,而且只能一些简单的View的组合,可以考虑使用SimpleAdapter。



做人要地道,好人有好报;做事要踏实,步履才坚实!听从命运安排的是凡人;主宰自己命运的是强者;没有主见的是盲从,三思而行的是智者。财富是一时的朋友,而朋友才是永久的财富;荣誉是一时的荣耀,做人才是永久的根本;学历是一时的知识,学习才是永久的智慧!