安卓开发androidandroid

ListView详解--绘图、优化、适配器、观察者

2016-04-18  本文已影响2284人  cxm11

Android源码之ListView的适配器模式

Adapter Pattern
适配器模式分为两种,即类适配器,对象适配器模式。

类适配器是通过实现Target接口以及继承Adaptee类来实现接口转换;而对象适配器模式是通过实现Target接口和代理Adaptee的某个方法来实现(即在类内部定义Adaptee的变量)。

角色介绍:
目标(Target)角色:这就是所期待得到的接口。
源(Adaptee)角色:需要适配的接口。
适配器(Adapter)角色:适配器类是本模式的核心。适配器把源接口转换成目标接口。


ListView中的Adapter模式

在开发过程中,ListView的Adapter是我们最为常见的类型之一。一般的用法大致如下:

ListView myListView = (ListView)findViewById(listview_id);
//设置适配器
myListView.setAdapter(new MyAdapter(context, myDatas));
//适配器
public class MyAdapter extends BaseAdapter{

private LayoutInflater mInflater;
List<string> mDatas;

public MyAdatper(Context context, List<string> datas){
this.mInflater = LayoutInflater.from(context);
mDatas = datas;
}
@Override
public int getCount() {
return mDatas.size();
}
@Override
public String getItem(int pos){
return mDatas.get(pos);
}
@Override
public long getItemId(int pos){
return pos;
}

//解析、设置、缓存convertView以及相关内容
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
//Item View的复用
if(converView == null) {
convertView = mInflater.inflate(R.layout.my_listview_item, null);
//获取title
holder.title = (TextView)convertView.findViewById(R.id.title);
convertView.setTag(holder);
}else {
holder = (ViewHolder)convertView.getTag();
}
holder.title.setText(mDatas.get(position));
return convertView;
}
} 

通过代理数据集来告知ListView数据的个数(getCount函数)以及每个数据的类型(getItem函数),最重要的是要解决Item View的输出。Item View千变万化,但终究它都是View类型,Adapter统一将Item View输出为View(getView函数),这样就很好的应对了Item View的可变性。

那么ListView是如何通过Adapter模式(不止Adapter模式)来运作的呢?ListView继承自AbsListView,Adapter定义在AbsListView中,我们看一下这个类

public abstract class AbsListView extends AdapterView<listadapter> implements TextWatcher, ViewTreeObserver.OnGlobalLayoutListener, Filter.FilterListener,ViewTreeOberserver.OnTouchModeChangeListener,RemoteViewAdapter.RemoteAdapterConnectionCallback{

/** 
* The adapter containing the data to be displayed by this view 
*/    
ListAdapter mAdapter;

//关联到Window时调用的函数
@Override
protected void 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
        // 获取Item的数量,调用的是mAdapter的getCount方法
        mItemCount = mAdapter.getCount();
}
mIsAttached = true;
}
  /**
 * 子类需要覆写layoutChildren()函数来布局child view,也就是Item View
 */
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    super.onLayout(changed, l, t, r, b);
    mInLayout = true;
    if (changed) {
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            getChildAt(i).forceLayout();
        }
        mRecycler.markChildrenDirty();
    }

    if (mFastScroller != null && mItemCount != mOldItemCount) {
        mFastScroller.onItemCountChanged(mOldItemCount, mItemCount);
    }
    // 布局Child View
    layoutChildren();
    mInLayout = false;

    mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR;
}

// 获取一个Item View
View obtainView(int position, boolean[] isScrap) {
    isScrap[0] = false;
    View scrapView;
    // 从缓存的Item View中获取,ListView的复用机制就在这里
    scrapView = mRecycler.getScrapView(position);

    View child;
    if (scrapView != null) {
        // 代码省略
        child = mAdapter.getView(position, scrapView, this);
        // 代码省略
    } else {
        child = mAdapter.getView(position, null, this);
        // 代码省略
    }

    return child;
}

AbsListView定义了集合视图的框架,比如Adapter模式的应用、复用Item View的逻辑、布局Item View的逻辑等。子类只需要覆写特定的方法即可实现集合视图的功能,例如ListView。

ListView中的相关方法。

@Override
protected void layoutChildren() {
    // 代码省略

    try {
        super.layoutChildren();
        invalidate();
        // 代码省略
        // 根据布局模式来布局Item View
        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;
        }

}

// 从上到下填充Item View  [ 只是其中一种填充方式 ]
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++;
    }

    return selectedView;
}

// 添加Item View
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
        boolean selected) {
    View 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;
}

ListView覆写了AbsListView中的layoutChilden函数,在该函数中根据布局模式来布局Item View。Item View的个数、样式都通过Adapter对应的方法来获取,获取个数、Item View之后,将这些Item View布局到ListView对应的坐标上,再加上Item View的复用机制,整个ListView就基本运转起来了。

当然这里的Adapter并不是经典的适配器模式,但是却是对象适配器模式的优秀示例,也很好的体现了面向对象的一些基本原则。这里的Target角色和Adapter角色融合在一起,Adapter中的方法就是目标方法;而Adaptee角色就是ListView的数据集与Item View,Adapter代理数据集,从而获取到数据集的个数、元素。

通过增加Adapter一层来将Item View的操作抽象起来,ListView等集合视图通过Adapter对象获得Item的个数、数据元素、Item View等,从而达到适配各种数据、各种Item视图的效果。因为Item View和数据类型千变万化,Android的架构师们将这些变化的部分交给用户来处理,通过getCount、getItem、getView等几个方法抽象出来,也就是将Item View的构造过程交给用户来处理,灵活地运用了适配器模式,达到了无限适配、拥抱变化的目的。


Android设计模式系列(9)--SDK源码之适配器模式

Adapter.jpg

适配器原理解析

为了简明直接,省略了相关的其他适配器,只以此两个适配器为例。

ListView中有一个变量ListAdapter mAdapter(在AbsListView中定义);是显示在view视图上的数据;

/** 
* The adapter containing the data to be displayed by this view 
*/ 
ListAdapter mAdapter;

ListView作为Client,他所需要的目标接口(target interface)就是ListAdapter,包含getCount(),getItem(),getView()等几个基本方法,为了兼容List<T>,Cursor等数据源,我们专门定义两个适配器来适配它们:ArrayAdapter和CursorAdapter。这两个适配器,说白了就是针对目标接口对数据源进行兼容修饰。这就是适配器模式。
其中BaseAdapter实现了isEmpty()方法,使子类在继承BaseAdapter后不需要再实现此方法,这就是缺省适配器,这也是缺省适配器的一个最明显的好处。


ListView适配器Adapter介绍与优化
Adapter继承结构关系

Adapter2.JPG

Android学习四、Android中的Adapter
在实际应用中,adapter的继承体系应用广泛,所以,要对Adapter的方法有所了解:

public interface Adapter {  
// 为了避免产生大量的View浪费内存,在Android中,AdapterView中的View是可回收的使用的。比如你有100项数据要显示,而你的屏幕一次只能显示10条数据,则   
// 只产生10个View,当往下拖动要显示第11个View时,会把第1个View的引用传递过去,更新里面的数据再显示,也就是说View可重用,只是更新视图中的数据用于显示新 
// 的一项,如果一个视图的视图类型是IGNORE_ITEM_VIEW_TYPE的话,则此视图不会被重用    
static final int IGNORE_ITEM_VIEW_TYPE = AdapterView.ITEM_VIEW_TYPE_IGNORE; 
static final int NO_SELECTION = Integer.MIN_VALUE;

// 注册一个Observer,当Adapter所表示的数据改变时会通知它,DataSetObserver是一个抽象类,定义了两个方法:onChanged与onInvalidated 
void registerDataSetObserver(DataSetObserver observer); 

// 取消注册一个Observer   
void unregisterDataSetObserver(DataSetObserver observer);   

// 所表示的数据的项数    
int getCount(); 
// 返回指定位置的数据项   
Object getItem(int position);   
// 返回指定位置的数据项的ID    
long getItemId(int position);   
// 表示所有数据项的ID是否是稳定的,在BaseAdapter中默认返回了false,假设是不稳定的,在CursorAdapter中返回了true,Cursor中的_ID是不变的  
boolean hasStableIds(); 
// 为每一个数据项产生相应的视图   
View getView(int position, View convertView, ViewGroup parent); 
// 获得相应位置的这图类型  
int getItemViewType(int position);  
// getView可以返回的View的类型数量。(在HeaderViewListAdapter中可以包含Header和Footer,getView可以返回Header、Footer及Adapter 
// 中的视图,但其getViewTypeCount的实现只是调用了内部Adapter的getViewTypeCount,忽略了Header、Footer中的View 
// Type,不懂。 
int getViewTypeCount(); 
//是否为空  
boolean isEmpty();
}

ListAdapter中定义了所需要的接口函数:

public interface ListAdapter extends Adapter {
public boolean areAllItemsEnabled();
boolean isEnabled(int position);
}

抽象类BaseAdapter,我省略其他代码,只列出两个方法,以作示意:

public abstract class BaseAdapter implements ListAdapter, SpinnerAdapter {
//...
public View getDropDownView(int position, View convertView, ViewGroup parent) {
return getView(position, convertView, parent);
}
public boolean isEmpty() {
return getCount()==0;
}
}

ArrayAdapter对List<T>进行封装成ListAdapter的实现,满足ListView的调用:

public class ArrayAdapter<T> extends BaseAdapter implements Filterable {
private List<T> mObjects;
//我只列出这一个构造函数,大家懂这个意思就行
public ArrayAdapter(Context context, int textViewResourceId, T[] objects){
init(context, textViewResourceId, 0,     Arrays.asList(objects));
}
private void init(Context context, int resource, int textViewResourceId, List<T> objects) {
mContext = context;
mInflater = (LayoutInFlater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mResource = mDropDownResource = resource;
mObjects = objects; //引用对象,也是表达了组合优于继承的意思
mFieldId = textViewResourceId;
}
public int getCount() {
return mObjects.size();
}
public T getItem(int position){
return mObjects.get(position);
}
public View getView(int position, View convertView,     ViewGroup parent) {
return createViewFromResource(position,convertView,parent,mResource);
}
//...
}

我们就如此成功的把List<T>作为数据源以ListView想要的目标接口的样子传给了ListView,同理CursorAdapter也是一模一样的道理,就不写具体代码了。


BaseAdapter

类BaseAdapter(省略了部分继承和要实现的方法)

BaseAdapter中增加了两个方法,即通知数据改变和通知数据无效。定义了一个变量,通过改变量实现了注册、取消注册、通知等。

public abstract class BaseAdapter 
extends Object
implements ListAdapter, SpinnerAdapter{

private final DataSetObservable mDataSetObservable = new DataSetObservable();
//...省略不必要的代码
public BaseAdapter();
public void registerDataSetObserver(DataSetObserver observer){
mDataSetObservable.registerObserver(observer);
}
public void unregisterDataSetObserver(DataSetObserver observer) {
mDataSetObservable.unregisterObserver(observer);
}
public void notifyDataChanged(){
mDataSetObservable.notifychanged();
}
//Notifies the attached View that the underlying data has been changed and it should refresh itself
public void notifyDataSetInvalidated(){
mDataSetObservable.notifyInvalidated();
}
//...省略不必要的代码
}

BaseAdapter是一个抽象类,实现了ListAdapter和SpinnerAdapter两个接口,这两个接口都继承自Adapter接口。在这个接口中申明了我们需要实现的四个重要方法。

接下来,我们进入ListView中查看setAdapter方法,ListView就是通过调用这个方法与适配器联系起来的。该方法的传入参数为ListAdapter类型,ListAdapter同样继承了adapter,也就是说,我们只重写adapter中的一些回调方法才会起效。该方法中,首先会调用它的父类ABSListView中的requestLayout方法,该方法调用后会回调其中的onLayout方法,该方法会调用ListView中的layoutChildren方法,该方法会调用fillSpecific方法,fillSpecific方法通过调用makeAndAddView方法得到需要的view,然后将view放入list。查看makeAndAddView方法,它会调用父类的obtainView方法,而该方法会调用适配器中重写的getView方法。

AbsListView中定义了变量 ListAdapter mAdapter;


![ListView和Adapter之间的关系.JPG]](http:https://img.haomeiwen.com/i1563413/dc5bcf46fbbfcb65.JPG?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

其实说到底,控件就是为了交互和展示数据用的,只不过ListView更加特殊,它是为了展示很多很多数据用的,但是ListView只承担交互和展示工作而已,至于这些数据来自哪里,ListView并不关心。

ListView显示出来需要3个东西

  1. ListView(用来显示数据的列表)
  2. Data(需要显示的数据)
  3. 一个绑定data和ListView的适配器ListAdapter

ListView的每一项其实都是TextView。
通过setAdapter方法来调用一个ListAdapter来绑定数据

ListView.JPG

Android ViewGroup系列控件的使用

public abstract class AdapterView<T extends Adapter>extends ViewGroup{
//AdapterView继承ViewGroup,但AdapterView的child view由Adapter决定,不能通过addView()来添加。 
//setAdapter()来设置Adapter,getAdapter()获取。

//部分方法
abstract T getAdapter() 
abstract void setAdapter(T adapter) 
}

Adapter的getView方法详解
getView详解 Recycler机制

ListView_Recycler.JPG

getView的API:

public abstract View getView(int position, View convertView, ViewGroup parent)

Get a View that displays the data at the specified position in the data set. You can either create a View manually or inflate it from an XML layout file. When the View is inflated, the parent View (GridView, ListView...) will apply default layout parameters unless you use [inflate(int, android.view.ViewGroup, boolean)](http://localhost:8080/android/docs/reference/android/view/LayoutInflater.html#inflate(int, android.view.ViewGroup, boolean))
to specify a root view and to prevent attachment to the root.

Parameters

  • position
    The position of the item within the adapter's data set of the item whose view we want.

Returns
A View corresponding to the data at the specified position.

position是指当前dataset的位置,通过getCount和getItem来使用。如果list向下滑动的话那么就是最低端的item的位置,如果是向上滑动的话那就是最上端的item的位置。convert是指可以重用的视图,即刚刚出队的视图。parent应该就是list。


Android ListView工作原理解析

RecycleBin机制

这个机制是ListView能够实现成百上千条数据都不会OOM最重要的原因。RecycleBin是写在ABSListView中的一个内部类。所以所有继承自AbsListView的子类,也就是ListView和GridView,都可以使用这个机制。

方法简介:

  • fillActiveViews() 这个方法接收两个参数,第一个参数表示要存储的view的数量,第二个参数表示ListView中第一个可见元素的position值。RecycleBin当中使用mActiveViews这个数组来存储View,调用这个方法后就会根据传入的参数来将ListView中的指定元素存储到mActiveViews数组当中。
第一次Layout

不管怎么说,ListView是继承自View的,因此它的执行流程还将会按照View的规则来执行。可参考Android视图绘制流程完全解析,带你一步步深入了解View(二)

View的执行流程无非分为三步,onMeasure()用于测量View的大小,onLayout()用于确定View的布局,onDraw()用于将View绘制到界面上。ListView当中,onMeasure()并没有什么特殊的地方,因为它终归是一个View。onDraw()在ListView当中也没有什么意义,因为ListView本身也不负责绘制,而是由ListView当中的子元素来进行绘制的。那么ListView大部分的神奇功能其实都是在onLayout()方法中进行的了。

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);
mInLayout = true;
if (changed) {
    int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        getChildAt(i).forceLayout();
    }
    mRecycler.markChildrenDirty();
}
layoutChildren();
mInLayout = false;
}

可以看到,onLayout()方法中并没有做什么复杂的逻辑操作,主要就是一个判断,如果ListView的大小或者位置发生了变化,那么changed变量就会变成true,此时会要求所有的子布局都强制进行重绘。除此之外倒没有什么难理解的地方了,不过我们注意到,在第16行调用了layoutChildren()这个方法,从方法名上我们就可以猜出这个方法是用来进行子元素布局的,不过进入到这个方法当中你会发现这是个空方法,没有一行代码。这当然是可以理解的了,因为子元素的布局应该是由具体的实现类来负责完成的,而不是由父类完成。那么进入ListView的layoutChildren()方法

然后调用fillFromTop()方法

然后调用fillDown()方法

调用makeAndAddView()方法

调用obtainView()
在obtainView中调用了mAdapter.getView(position,null,this);

mAdapter就是当前ListView关联的适配器
mAdapter是ListAdapter类型,在AbsListView中定义。
在setAdapter函数中,传入我们定义的适配器adapter。有adapter构造mAdapter.从而在调用mAdapter.getView函数时调用是我们覆写的函数。从而完成了适配器模式

在setAdapter函数的最后调用了 requestLayout();请求重新布局,重新调用:onMeasure,onLayout,onDraw;从而开始了ListView的绘制过程。

Android中View的生命周期,调用invalidate()和requestLayout()会触发哪些方法,一图道破天机。

requestLayout.JPG
第二次Layout
滑动加载更多数据

ListView初始化简单分析

ListView.onLayout过程与普通视图的layout过程完全不同,下面是一个流程的思维导图

ListView_init.JPG
  1. 先看构造函数,上图中1.1就不分析了,主要是读取一些ListView参数,直接来看1.2 ViewGroup构造函数源码

     private void initViewGroup() { 
     ...... 
     // 初始化保存当前ViewGroup中所有View的数组 
     mChildren = new View[ARRAY_INITIAL_CAPACITY]; 
     // 初始时其Child个数为0 
     mChildrenCount = 0; 
     ...... 
     }
    
  2. 接着2即ListView.onMeasure方法,只是获取当前ListView的宽高

     @Override  
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
     // Sets up mListPadding  
     super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
    
     // 获取当前ListView总宽高  
     int widthSize = MeasureSpec.getSize(widthMeasureSpec);  
     int heightSize = MeasureSpec.getSize(heightMeasureSpec);  
    
     ......  
    
     setMeasuredDimension(widthSize , heightSize);  
     mWidthMeasureSpec = widthMeasureSpec;          
     }  
    
  3. ListView的onLayout

  4. ListView.layoutChildren

  5. ListView.fillFromTop

  6. ListView.fillDown

  7. ListView.makeAndAddView

  8. ListView.setupChild


Android ListView使用BaseAdapter与ListView的优化

当系统开始绘制ListView的时候,首先调用getCount()方法,得到它的返回值,即ListView的长度,然后系统调用getView()方法,根据这个长度逐一绘制ListView的每一行。也就是说,如果让getCount()返回1,那么只显示一行。而getItem()和getItemId()则在需要处理和取得Adapter中的数据时调用。

系统显示列表时,首先需要实例化一个适配器.当手动完成适配时,必须手动映射数据,这需要重写getView方法。系统在绘制列表的每一行的时候将调用此方法。


  1. AdapterView
    adapter的相关抽象函数getAdapter、setAdapter
    mEmptyView
    观察者模式
    Accessibility

  2. AbsListView
    AbsListView定义了集合视图的框架
    比如Adapter模式的应用
    复用Item View的逻辑
    布局Item View的逻辑
    滑动事件相关
    定义了变量: ListAdapter mAdapter;
    使用了对象适配器。
    (1) 拥有RecycleBin类,负责处理view的生成和回收,没有子视图的空间定位信息
    (2) 滑动事件相关--加载数据并显示更多数据等
    onTouchEvent

  1. ListView
    ListView覆写了AbsListView中的layoutChilden函数,在该函数中根据布局模式来布局Item View。Item View的个数、样式都通过Adapter对应的方法来获取,获取个数、Item View之后,将这些Item View布局到ListView对应的坐标上,再加上Item View的复用机制,整个ListView就基本运转起来了。

补充知识点:

无论抽象类还是接口,抽象方法都需要在子类中实现,而且在子类中实现这些方法一个都不能少。而抽象类里面的非抽象方法,则在子类可以不重写实现里面的行为。


参考

Android ViewGroup系列控件的使用

Android源码学习之适配器模式应用


Android开发优化-Adapter优化

Android高手进阶:Adapter深入理解与优化

AndroidAdapter用法总结
主要提供了ArrayAdapter,SimpleAdapter、simpleCursorAdapter实例

Android ListView工作原理完全解析,带你从源码的角度彻底理解,androidlistview
http://www.android100.org/html/201507/26/168809.html
http://android.jobbole.com/81834/

ListView源代码分析

Android视图绘制流程完全解析,带你一步步深入了解View(二)

Android设计模式源码解析之ListView观察者模式

Android AdapterView 源码分析以及其相关回收机制的分析

Android LayoutInflater原理分析,带你一步步深入了解View(一)

上一篇下一篇

猜你喜欢

热点阅读