当前位置: 代码迷 >> 综合 >> Android_ListView (基本使用 / RecycleBin机制 / 源码解析 / 异步图片错位解决方案)
  详细解决方案

Android_ListView (基本使用 / RecycleBin机制 / 源码解析 / 异步图片错位解决方案)

热度:63   发布时间:2023-12-20 21:40:18.0

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;@Overrideprotected 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;}@Overridepublic int getCount() {return listImage == null ? 0 : listImage.length;}@Overridepublic Object getItem(int position) {return listImage[position];}@Overridepublic long getItemId(int position) {return position;}@Overridepublic 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 {
          @Overridepublic int getCount() {return 0;}@Overridepublic Object getItem(int position) {return null;}@Overridepublic long getItemId(int position) {return 0;}@Overridepublic 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 uncheckedArrayList<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 MismatchedReadAndWriteOfArrayfinal 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 heapif (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里的View
      • View 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, 得到结果为0final 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 belowswitch (mLayoutMode) {<
  相关解决方案