Android_ListView (基本使用 / RecycleBin机制 / 源码解析 / 异步图片错位解决方案)
本文由 Luzhuo 编写,转发请保留该信息.
原文: http://blog.csdn.net/Rozol/article/details/78161840
- 把数据用列表的形式,动态滚动的方式,展示给用户.
ListView 作为界面展示的容器控件必然会直接或者间接的继承ViewGroup, 现在看看源代码的继承结构
public class ListView extends AbsListView { } public abstract class AbsListView extends AdapterView<ListAdapter> implements TextWatcher, ViewTreeObserver.OnGlobalLayoutListener, Filter.FilterListener, ViewTreeObserver.OnTouchModeChangeListener, RemoteViewsAdapter.RemoteAdapterConnectionCallback { } public abstract class AdapterView<T extends Adapter> extends ViewGroup { }
现在知道ListView确实是继承ViewGroup的,那么就会重写 onMeasure() onLayout() onDraw() 这三个基本的方法, 大家是否注意到继承ViewGroup的后,onMeasure() onLayout()会多次执行的问题(执行了4次onMeasure(), 2次onLayout()),以下是log.
基本使用
public class MainActivity extends AppCompatActivity {
private ListView listView;
private String[] listImage = Resource.grilImage;
private BitmapUtils bitmapUtils;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
initData();
}
private void initView() {
bitmapUtils = new BitmapUtils(this);
listView = (ListView) findViewById(R.id.listview);
}
private void initData() {
ImageAdapter IAdapter = new ImageAdapter(MainActivity.this, listImage, bitmapUtils);
listView.setAdapter(IAdapter);
listView.setOnScrollListener(new PauseOnScrollListener(bitmapUtils, false, true));
}
}
public class ImageAdapter extends BaseAdapter {
private Context context;
private String[] listImage;
private LayoutInflater inflater;
private BitmapUtils bitmapUtils;
public ImageAdapter(Context context, String[] listImage, BitmapUtils bitmapUtils) {
this.context = context;
this.listImage = listImage;
inflater = LayoutInflater.from(context);
this.bitmapUtils = bitmapUtils;
}
@Override
public int getCount() {
return listImage == null ? 0 : listImage.length;
}
@Override
public Object getItem(int position) {
return listImage[position];
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder;
if(convertView == null){
convertView = inflater.inflate(R.layout.item_list, null);
viewHolder = new ViewHolder();
viewHolder.imageview = (ImageView)convertView.findViewById(R.id.imageview);
viewHolder.textview = (TextView)convertView.findViewById(R.id.textview);
convertView.setTag(viewHolder);
}else{
viewHolder = (ViewHolder) convertView.getTag();
}
if(listImage.length != 0){
bitmapUtils.display(viewHolder.imageview, listImage[position]);
viewHolder.textview.setText("第"+position+"张图片");
}
return convertView;
}
class ViewHolder{
ImageView imageview;
TextView textview;
}
}
Adapter适配器模式
从上面的使用代码可以看出,要让ListView正常工作,就要设置Adapter,Adapter就是适配器
public 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会被要求必须重写上述4个方法. 数据是不尽相同的, ListView只关心交互和展示的工作,不关心你数据是什么样的,从哪来的. 而Adapter的统一接口就解决的数据适配的问题.
- 示范图:
recycleBin机制
为了简洁说明ListView是怎么工作的,先讲下 AbsListView 类里的内部类 RecycleBin
public abstract class AbsListView extends AdapterView<ListAdapter> implements TextWatcher, ViewTreeObserver.OnGlobalLayoutListener, Filter.FilterListener, ViewTreeObserver.OnTouchModeChangeListener, RemoteViewsAdapter.RemoteAdapterConnectionCallback { final RecycleBin mRecycler = new RecycleBin(); class RecycleBin { private RecyclerListener mRecyclerListener; /** * The position of the first view stored in mActiveViews. */ private int mFirstActivePosition; /** * Views that were on screen at the start of layout. This array is populated at the start of * layout, and at the end of layout all view in mActiveViews are moved to mScrapViews. * Views in mActiveViews represent a contiguous range of Views, with position of the first * view store in mFirstActivePosition. */ // ↓↓↓ private View[] mActiveViews = new View[0]; /** * Unsorted views that can be used by the adapter as a convert view. */ // ↓↓↓ private ArrayList<View>[] mScrapViews; private int mViewTypeCount; // ↓↓↓ private ArrayList<View> mCurrentScrap; private ArrayList<View> mSkippedScrap; private SparseArray<View> mTransientStateViews; private LongSparseArray<View> mTransientStateViewsById; // ↓↓↓ public void setViewTypeCount(int viewTypeCount) { if (viewTypeCount < 1) { throw new IllegalArgumentException("Can't have a viewTypeCount < 1"); } //noinspection unchecked ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount]; for (int i = 0; i < viewTypeCount; i++) { scrapViews[i] = new ArrayList<View>(); } mViewTypeCount = viewTypeCount; mCurrentScrap = scrapViews[0]; mScrapViews = scrapViews; } /** * Fill ActiveViews with all of the children of the AbsListView. * * @param childCount The minimum number of views mActiveViews should hold * @param firstActivePosition The position of the first view that will be stored in * mActiveViews */ // ↓↓↓ void fillActiveViews(int childCount, int firstActivePosition) { if (mActiveViews.length < childCount) { mActiveViews = new View[childCount]; } mFirstActivePosition = firstActivePosition; //noinspection MismatchedReadAndWriteOfArray final View[] activeViews = mActiveViews; for (int i = 0; i < childCount; i++) { View child = getChildAt(i); AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams(); // Don't put header or footer views into the scrap heap if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { // Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in active views. // However, we will NOT place them into scrap views. activeViews[i] = child; // Remember the position so that setupChild() doesn't reset state. lp.scrappedFromPosition = firstActivePosition + i; } } } /** * Get the view corresponding to the specified position. The view will be removed from * mActiveViews if it is found. * * @param position The position to look up in mActiveViews * @return The view if it is found, null otherwise */ // ↓↓↓ View getActiveView(int position) { int index = position - mFirstActivePosition; final View[] activeViews = mActiveViews; if (index >=0 && index < activeViews.length) { final View match = activeViews[index]; activeViews[index] = null; return match; } return null; } /** * @return A view from the ScrapViews collection. These are unordered. */ // ↓↓↓ View getScrapView(int position) { final int whichScrap = mAdapter.getItemViewType(position); if (whichScrap < 0) { return null; } if (mViewTypeCount == 1) { return retrieveFromScrap(mCurrentScrap, position); } else if (whichScrap < mScrapViews.length) { return retrieveFromScrap(mScrapViews[whichScrap], position); } return null; } /** * Puts a view into the list of scrap views. * <p> * If the list data hasn't changed or the adapter has stable IDs, views * with transient state will be preserved for later retrieval. * * @param scrap The view to add * @param position The view's position within its parent */ // ↓↓↓ void addScrapView(View scrap, int position) { final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams(); if (lp == null) { // Can't recycle, but we don't know anything about the view. // Ignore it completely. return; } lp.scrappedFromPosition = position; // Remove but don't scrap header or footer views, or views that // should otherwise not be recycled. final int viewType = lp.viewType; if (!shouldRecycleViewType(viewType)) { // Can't recycle. If it's not a header or footer, which have // special handling and should be ignored, then skip the scrap // heap and we'll fully detach the view later. if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { getSkippedScrap().add(scrap); } return; } scrap.dispatchStartTemporaryDetach(); // The the accessibility state of the view may change while temporary // detached and we do not allow detached views to fire accessibility // events. So we are announcing that the subtree changed giving a chance // to clients holding on to a view in this subtree to refresh it. notifyViewAccessibilityStateChangedIfNeeded( AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE); // Don't scrap views that have transient state. final boolean scrapHasTransientState = scrap.hasTransientState(); if (scrapHasTransientState) { if (mAdapter != null && mAdapterHasStableIds) { // If the adapter has stable IDs, we can reuse the view for // the same data. if (mTransientStateViewsById == null) { mTransientStateViewsById = new LongSparseArray<>(); } mTransientStateViewsById.put(lp.itemId, scrap); } else if (!mDataChanged) { // If the data hasn't changed, we can reuse the views at // their old positions. if (mTransientStateViews == null) { mTransientStateViews = new SparseArray<>(); } mTransientStateViews.put(position, scrap); } else { // Otherwise, we'll have to remove the view and start over. getSkippedScrap().add(scrap); } } else { if (mViewTypeCount == 1) { mCurrentScrap.add(scrap); } else { mScrapViews[viewType].add(scrap); } if (mRecyclerListener != null) { mRecyclerListener.onMovedToScrapHeap(scrap); } } } } }
- 以上代码是从源码中拷贝出来,并删掉了一些不重要的方法
成员变量:
View[] mActiveViews
: 用于存放活动View(也就是在屏幕上展示的View)int mFirstActivePosition
: 存放mActiveViews中第一个View的Position(也就是第几个Item)ArrayList<View>[] mScrapViews
: 废弃的View,可通过Adapter转为convertView继续使用(看看基本使用Adapter代码,我们就是将这些废弃的view重复使用的)ArrayList<View> mCurrentScrap
: ViewTypeCount == 1 的废弃View会被存在这个集合里- 方法:
public void setViewTypeCount(int viewTypeCount) {}
: 该方法会根据传入的类型数量初始化 mScrapViews 和 mCurrentScrap; mScrapViews 存了不同类型View的集合, mCurrentScrap是mScrapViews的第一个集合; 看来ListView是可以传入多个类型的View的void fillActiveViews(int childCount, int firstActivePosition) {}
: 主要是将mActiveViews填满子View (保存屏幕上展示的View)View getActiveView(int position) {}
: 根据position获取mActiveViews里的ViewView getScrapView(int position) {}
: 源码主要调用了retrieveFromScrap(mCurrentScrap, position)
方法,现在看看这个方法是干吗的?private View retrieveFromScrap(ArrayList<View> scrapViews, int position) { final int size = scrapViews.size(); if (size > 0) { // See if we still have a view for this position or ID. for (int i = 0; i < size; i++) { final View view = scrapViews.get(i); final AbsListView.LayoutParams params = (AbsListView.LayoutParams) view.getLayoutParams(); if (mAdapterHasStableIds) { final long id = mAdapter.getItemId(position); if (id == params.itemId) { return scrapViews.remove(i); } } else if (params.scrappedFromPosition == position) { final View scrap = scrapViews.remove(i); clearAccessibilityFromScrap(scrap); return scrap; } } final View scrap = scrapViews.remove(size - 1); clearAccessibilityFromScrap(scrap); return scrap; } else { return null; } }
- 看来getScrapView(int position)是获取废弃的View, 如果能获取到就返回View,获取不到就返回null
void addScrapView(View scrap, int position) {}
: 把废弃的View添加到mCurrentScrap里, 把具有过渡效果的废弃View添加到mTransientStateViews里(带有过渡效果的View这里不做讲解)- 可见recycleBin主要工作就是填满和获取展示View,添加和获取缓存View.
ListView的执行逻辑源码
ListView的初始化逻辑
- ListView作为View容器控件,那么我们就从 onMeasure() onLayout() 这2个基本的被重写方法开始研究
onMeasure() 主要是测量ListView的大小; onLayout()用于确定子View的布局, 这才是核心, 该方法并没有在ListView中实现, 而是在抽象父类AbsListView中实现.
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(); // ↓↓↓ 1. 如果change == true, ListView的大小和位置发生变化 if (changed) { for (int i = 0; i < childCount; i++) { // ↓↓↓ 2. 那么就把所有子布局强制重绘 getChildAt(i).forceLayout(); } mRecycler.markChildrenDirty(); } // ↓↓↓ 3. 调用子类ListView的layoutChildren()方法 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); } }
接着看看
layoutChildren()
做了什么protected void layoutChildren() { final boolean blockLayoutRequests = mBlockLayoutRequests; if (blockLayoutRequests) { return; } mBlockLayoutRequests = true; try { super.layoutChildren(); invalidate(); if (mAdapter == null) { resetList(); invokeOnItemScrollListener(); return; } final int childrenTop = mListPadding.top; final int childrenBottom = mBottom - mTop - mListPadding.bottom; // ↓↓↓ 1. ListView中还未填充任务子View, 得到结果为0 final int childCount = getChildCount(); int index = 0; int delta = 0; View sel; View oldSel = null; View oldFirst = null; View newSel = null; // Remember stuff we will need down below switch (mLayoutMode) { <