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的构造过程交给用户来处理,灵活地运用了适配器模式,达到了无限适配、拥抱变化的目的。
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继承结构关系
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中增加了两个方法,即通知数据改变和通知数据无效。定义了一个变量,通过改变量实现了注册、取消注册、通知等。
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个东西
- ListView(用来显示数据的列表)
- Data(需要显示的数据)
- 一个绑定data和ListView的适配器ListAdapter
ListView.JPGListView的每一项其实都是TextView。
通过setAdapter方法来调用一个ListAdapter来绑定数据
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机制
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.
- convertView
The old view to reuse, if possible. Note: You should check that this view is non-null and of an appropriate type before using. If it is not possible to convert this view to display the correct data, this method can create a new view. Heterogeneous lists can specify their number of view types, so that this View is always of the right type (see getViewTypeCount()
and getItemViewType(int)
). - parent
The parent that this view will eventually be attached to
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数组当中。
- getActiveView() 这个方法和fillActiveViews()是对应的,用于从mActiveViews数组当中获取数据。该方法接收一个position参数,表示元素在ListView当中的位置,方法内部会自动将position值转换成mActiveViews数组对应的下标值。需要注意的是,mActiveViews当中所存储的View,一旦被获取了之后就会从mActiveViews当中移除,下次获取同样位置的View将会返回null,也就是说mActiveViews不能被重复利用。
- addScrapView() 用于将一个废弃的View进行缓存,该方法接收一个View参数,当有某个View确定要废弃掉的时候(比如滚动出了屏幕),就应该调用这个方法来对View进行缓存,RecycleBin当中使用mScrapViews和mCurrentScrap这两个List来存储废弃View。
- getScrapView 用于从废弃缓存中取出一个View,这些废弃缓存中的View是没有顺序可言的,因此getScrapView()方法中的算法也非常简单,就是直接从mCurrentScrap当中获取尾部的一个scrap view进行返回。
- setViewTypeCount() 我们都知道Adapter当中可以重写一个getViewTypeCount()来表示ListView中有几种类型的数据项,而setViewTypeCount()方法的作用就是为每种类型的数据项都单独启用一个RecycleBin缓存机制。实际上,getViewTypeCount()方法通常情况下使用的并不是很多,所以我们只要知道RecycleBin当中有这样一个功能就行了
第一次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.onLayout过程与普通视图的layout过程完全不同,下面是一个流程的思维导图
ListView_init.JPG-
先看构造函数,上图中1.1就不分析了,主要是读取一些ListView参数,直接来看1.2 ViewGroup构造函数源码
private void initViewGroup() { ...... // 初始化保存当前ViewGroup中所有View的数组 mChildren = new View[ARRAY_INITIAL_CAPACITY]; // 初始时其Child个数为0 mChildrenCount = 0; ...... }
-
接着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; }
-
ListView的onLayout
-
ListView.layoutChildren
-
ListView.fillFromTop
-
ListView.fillDown
-
ListView.makeAndAddView
-
ListView.setupChild
Android ListView使用BaseAdapter与ListView的优化
当系统开始绘制ListView的时候,首先调用getCount()方法,得到它的返回值,即ListView的长度,然后系统调用getView()方法,根据这个长度逐一绘制ListView的每一行。也就是说,如果让getCount()返回1,那么只显示一行。而getItem()和getItemId()则在需要处理和取得Adapter中的数据时调用。
系统显示列表时,首先需要实例化一个适配器.当手动完成适配时,必须手动映射数据,这需要重写getView方法。系统在绘制列表的每一行的时候将调用此方法。
-
AdapterView
adapter的相关抽象函数getAdapter、setAdapter
mEmptyView
观察者模式
Accessibility -
AbsListView
AbsListView定义了集合视图的框架
比如Adapter模式的应用
复用Item View的逻辑
布局Item View的逻辑
滑动事件相关
定义了变量: ListAdapter mAdapter;
使用了对象适配器。
(1) 拥有RecycleBin类,负责处理view的生成和回收,没有子视图的空间定位信息
(2) 滑动事件相关--加载数据并显示更多数据等
onTouchEvent
- ListView
ListView覆写了AbsListView中的layoutChilden函数,在该函数中根据布局模式来布局Item View。Item View的个数、样式都通过Adapter对应的方法来获取,获取个数、Item View之后,将这些Item View布局到ListView对应的坐标上,再加上Item View的复用机制,整个ListView就基本运转起来了。
补充知识点:
无论抽象类还是接口,抽象方法都需要在子类中实现,而且在子类中实现这些方法一个都不能少。而抽象类里面的非抽象方法,则在子类可以不重写实现里面的行为。
参考
Android之Adapter用法总结
主要提供了ArrayAdapter,SimpleAdapter、simpleCursorAdapter实例
Android ListView工作原理完全解析,带你从源码的角度彻底理解,androidlistview
http://www.android100.org/html/201507/26/168809.html
http://android.jobbole.com/81834/
Android视图绘制流程完全解析,带你一步步深入了解View(二)