ListView适配器模式的应用

ListView适配器模式的应用

文章开头先贴下另外几篇设计模式的分析,感兴趣的可以看下:
设计模式-简单说明
ListView观察者模式的应用
AsyncTask模板模式的应用
ListView桥接模式的应用

我们都知道ListView是用来展示列表的,然而列表在不同的使用场景中需要展示的样式,以及所提供的数据源都是不确定的,因为需求千变万化,Android团队在设计之初就考虑到了这一方面,所以适配器模式就理所当然的得到应用。我们简单说一下适配器模式的定义:源接口和目标接口不一致(这里的接口并不一定就是指interface),为了使两个不兼容的接口能够得到很好的对接,我们需要提供一个中间类,来将源接口进行转化成目标接口中需要的数据。

这里写图片描述

上面是一张简单的UML类图,说明了ListView和BaseAdapter的关系,而适配器模式的应用就在于Adapter的getView,将数据转化成View,然后在ListView中会将其添加到列表中。下面我们进入到源码分析一下适配器模式的具体实现,首先从Adapter的源码开始分析(作了适当的删减):

/**
 * An Adapter object acts as a bridge between an {@link AdapterView} and the
 * underlying data for that view. The Adapter provides access to the data items.
 *
 * Adapter的类声明,简单明了--> Adapter对象,充当AdapterView(ListView的间接父类)和数据视图之间的桥梁。提供了访问数据项的功能
 */
public interface Adapter {
    int getCount();   

    Object getItem(int position);

    long getItemId(int position);

    View getView(int position, View convertView, ViewGroup parent);

    int getItemViewType(int position);

    int getViewTypeCount();

    boolean isEmpty();
}

可以发现都是大家很熟悉的函数,而且Adapter的官方声明也说了,Adapter是AdapterView(ListView的父类)和数据视图之间的桥梁,接下来看下它的直接子类,我们以我们常用的Adapter->ListAdapter->BaseAdapter来分析,所以接下来就是ListAdapter的源码:

/**
 * ListView和数据之间的桥梁,任何需要显示的数据都将会在ListAdapter中进行转化。实际上是在我们实现的Adapter 的子类中进行转化的
 */
public interface ListAdapter extends Adapter {

    /**
     * 是否所有的Item都是可点击和可选中的(分隔符是不满足条件的),只有都满足条件才返回true
     */
    public boolean areAllItemsEnabled();

    /**
     * 判断一个Item是否可点击、可选中,满足条件返回true
     */
    boolean isEnabled(int position);
}

发现ListAdapter也是接口,我们继续看它的直接子类BaseAdapter:

public abstract class BaseAdapter implements ListAdapter, SpinnerAdapter {

    public boolean areAllItemsEnabled() {
        return true;
    }

    public boolean isEnabled(int position) {
        return true;
    }

    public View getDropDownView(int position, View convertView, ViewGroup parent) {
        return getView(position, convertView, parent);
    }

    public int getItemViewType(int position) {
        return 0;
    }

    public int getViewTypeCount() {
        return 1;
    }

    public boolean isEmpty() {
        return getCount() == 0;
    }
}

我们发现这是一个抽象类,它是一个公共基础类,实现了一些常用的公共逻辑,可以简单的理解作了一些常用的默认实现,如果你感兴趣你也可以自定义ListView适配器,直接实现ListAdapter。那么剩下没有实现的就是我们天天用的了,这里自定义一个MyAdapter,看看它有哪些逻辑需要实现:

class MyAdapter extends BaseAdapter{
        @Override
        public int getCount() {
            return 0;
        }

        @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) {
            return null;
        }
    }

现在是不是倍感亲切,看着心里格外的舒坦呢。不要着急,这才刚刚开始呢,下面才是重点,Adapter我们已经看完了,大家现在肯定有一个疑问,Adapter返回的这些itemCount、View是怎么添加到ListView中的。我们从ListView的setAdapter为入口来分析:

@Override
public void setAdapter(ListAdapter adapter) {
    ...一些初始化操作

    if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {
      mAdapter = wrapHeaderListAdapterInternal(mHeaderViewInfos, mFooterViewInfos, adapter);
    } else {
      //为成员变量mAdapter赋值
      mAdapter = adapter;
    }

    ...

    // AbsListView#setAdapter will update choice mode states.
    super.setAdapter(adapter);

    if (mAdapter != null) {
      ...
      //调用mAdapter的getCount方法保存ListView的条目数
      mItemCount = mAdapter.getCount();
      ...
    } else {
      ...
    }

    //调用重新布局
    requestLayout();
}

我们在以上的源码中注意到了我们想要的信息,首先它保存了我们设置的新的ListAdapter实例,然后调用其getCount()方法保存ListView的条目个数,最后调用了requestLayout来实现重新布局,我们看下具体的onLayout方法吧,这时候发现ListView并没有onLayout方法,那我们就去它的父类AbsListView中取寻找一下:

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

     ...
     //布局子View
     layoutChildren();

     ...
 }

我们看见在AbsListView的onLayout的函数声明上表明,我们不该去重写onLayout,如果需要布局子View应该重写layoutChildren函数,在AbsListView的onLayout内部调用了layoutChildren,所以子类调用layoutChildren可以就可以实现布局子View,那我们就去看下ListView的layoutChildren吧,layoutChildren是很重要的,在ListView的条目复用机制时也会涉及到,不过我们现在并不是分析条目复用,所以只保留我们关心由BaseAdapter的getView返回的View是怎么添加到ListView中的:

@Override
    protected void layoutChildren() {
        ...

            //这里会根据不同的布局模式来添加子View到ListView中,默认走default
            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:
                final int selectedPosition = reconcileSelectedPosition();
                sel = fillSpecific(selectedPosition, mSpecificTop);
                /**
                 * When ListView is resized, FocusSelector requests an async selection for the
                 * previously focused item to make sure it is still visible. If the item is not
                 * selectable, it won't regain focus so instead we call FocusSelector
                 * to directly request focus on the view after it is visible.
                 */
                if (sel == null && mFocusSelector != null) {
                    final Runnable focusRunnable = mFocusSelector
                            .setupFocusIfValid(selectedPosition);
                    if (focusRunnable != null) {
                        post(focusRunnable);
                    }
                }
                break;
            case LAYOUT_MOVE_SELECTION:
                sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
                break;
            default:
                //我们刚设置Adapter,ItemView都还没添加到ListView中,所以走的是上面的分支。并且没有设置填充模式的情况下,默认是从上往下填充,即fillFromTop
                if (childCount == 0) {
                    if (!mStackFromBottom) {
                        final int position = lookForSelectablePosition(0, true);
                        setSelectedPositionInt(position);
                        sel = fillFromTop(childrenTop);
                    } else {
                        final int position = lookForSelectablePosition(mItemCount - 1, false);
                        setSelectedPositionInt(position);
                        sel = fillUp(mItemCount - 1, childrenBottom);
                    }
                } else {
                    if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
                        sel = fillSpecific(mSelectedPosition,
                                oldSel == null ? childrenTop : oldSel.getTop());
                    } else if (mFirstPosition < mItemCount) {
                        sel = fillSpecific(mFirstPosition,
                                oldFirst == null ? childrenTop : oldFirst.getTop());
                    } else {
                        sel = fillSpecific(0, childrenTop);
                    }
                }
                break;
            }

            ...
    }

在layoutChildren中,会根据不同的模式来填充子View,默认走的default,我们刚设置Adapter,ItemView都还没添加到ListView中,所以走的是上面的分支。并且没有设置填充模式的情况下,默认是从上往下填充,即fillFromTop,我们看下它的源码:

/**
 * Fills the list from top to bottom, starting with mFirstPosition
 *
 * @param nextTop The location where the top of the first item should be
 *        drawn
 *
 * @return The view that is currently selected
 */
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,继续看看fillDown的源码:

private View fillDown(int pos, int nextTop) {
     View selectedView = null;

     //获取ListView的高度
     int end = (mBottom - mTop);
     if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
         end -= mListPadding.bottom;
     }

     //此处是关键,做了死循环,直到下一个要添加的ItemView的top>ListView高度,或者条目已经填充完成(简单的说就是要吗填充满一屏就退出,要吗就是全部填充完,但是没填充满也退出)
     while (nextTop < end && pos < mItemCount) {
         // is this the selected item?
         boolean selected = pos == mSelectedPosition;
         //这里获取一个View
         View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
        //将获取到的View的高度累加,用于判断是否添加满
         nextTop = child.getBottom() + mDividerHeight;
         if (selected) {
             selectedView = child;
         }
         pos++;
     }

     setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
     return selectedView;
 }

在fillDown中我们发现了一个View对象,我们就进入到makeAndAddView中看一下:

/**
  * Obtains the view and adds it to our list of children.
  * 获取View并添加到列表中
  */
 private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
         boolean selected) {
     if (!mDataChanged) {
         // Try to use an existing view for this position.
         //从缓存中获取View,有则直接使用,没有下面的obtainView会重新创建.
         final View activeView = mRecycler.getActiveView(position);
         if (activeView != null) {
             // Found it. We're reusing an existing child, so it just needs
             // to be positioned like a scrap view.
             //复用ItemView,所以放到特定位置即可
             setupChild(activeView, position, y, flow, childrenLeft, selected, true);
             return activeView;
         }
     }

     // Make a new view for this position, or convert an unused view if
     // possible.
     //获取一个新的View对象
     final View child = obtainView(position, mIsScrap);

     // This needs to be positioned and measured.
     //因为是新建ItemView,所以需要测量
     setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

     return child;
    }

因为我们刚设置Adapter,所以是没得复用的,所以就走到了obtainView的逻辑,我们看下它的实现,该函数在AbsListView中:

/**
   * Gets a view and have it show the data associated with the specified
   * position. 
   * 获取View,并绑定特定位置的数据
   */
  View obtainView(int position, boolean[] outMetadata) {
      ...
      final View child = mAdapter.getView(position, scrapView, this);
      ...
      return child;
  }

这里面代码不少,我们只关注核心,mAdapter.getView(),可以发现它调用了我们传递进来Adapter的实例的getView方法,并且将该View返回,所以说我们在ListView中的makeAndAddView函数使用的就是我们的Adapter实例的getView返回的View对象,既然已经返回,那么它是什么时候添加进去的呢,看下setupChild函数:

/**
 * Adds a view as a child and make sure it is measured (if necessary) and
 * positioned properly.
 *  添加子View
 */
private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
        boolean selected, boolean isAttachedToWindow) {
    ...
    //获取View的类型,以及是否可选中可点击
    p.viewType = mAdapter.getItemViewType(position);
    p.isEnabled = mAdapter.isEnabled(position);

    ...

    if ((isAttachedToWindow && !p.forceAdd) || (p.recycledHeaderFooter
            && p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
        //当后面复用回收的Item时会走这里
        attachViewToParent(child, flowDown ? -1 : 0, p);

        // If the view was previously attached for a different position,
        // then manually jump the drawables.
        if (isAttachedToWindow
                && (((AbsListView.LayoutParams) child.getLayoutParams()).scrappedFromPosition)
                        != position) {
            child.jumpDrawablesToCurrentState();
        }
    } else {
        p.forceAdd = false;
        if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
            p.recycledHeaderFooter = true;
        }

        //将View添加到父容器中
        addViewInLayout(child, flowDown ? -1 : 0, p, true);
        // add view in layout will reset the RTL properties. We have to re-resolve them
        child.resolveRtlPropertiesIfNeeded();
    }

    ...
    }

我们发现setupChild函数的官方注释那里已经表明这是添加子View,具体的话这里有两个函数都可以添加子View,分别是

attachViewToParent和addViewInLayout,不过attachViewToParent是条目复用的时候使用的,我们第一次添加的时候调用的是addViewInLayout。

本人是新手,如果有什么错误或者意见请指出,谢谢大家抽出宝贵的时间阅读!!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值