Android源码中的适配器模式

来源:互联网 发布:火炬之光2mod汉化 mac 编辑:程序博客网 时间:2024/05/22 08:16

在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。




















4 0
原创粉丝点击