您的位置:首页 > 其它

ListView之多种类型Item

2016-07-01 17:10 435 查看
一、概述

一般而言,listview每个item的样式是一样的,但也有很多应用场景下不同位置的item需要不同的样式。

拿微信举例,前者的代表作是消息列表,而后者的典型则是聊天会话界面。

本文重点介绍后者,也就是多类型item的listview的实现思路和方法,比如实现一个这样的聊天会话页面:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#EEEEEE"
android:orientation="vertical"
tools:context="${relativePackage}.${activityClass}" >

<TextView
android:id="@+id/listview_multi_type_item_txt_recv_txt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxWidth="250dp"
android:layout_gravity="left"
android:layout_margin="4dp"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:paddingRight="5dp"
android:paddingLeft="10dp"
android:background="@drawable/listview_multi_type_item_txt_recv_bg"
android:textColor="@android:color/black"
android:textSize="13sp"
android:text="接收的消息"
android:autoLink="web" />

</LinearLayout>


View Code

3.2 重头戏在于Adapter的处理

private class MultiTypeAdapter extends BaseAdapter {
private LayoutInflater mInflater;
private List<JsonListData.Message> mMessages;
private SimpleDateFormat mSdfDate = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss", Locale.getDefault());

public MultiTypeAdapter(Context context, List<JsonListData.Message> messages) {
mInflater = LayoutInflater.from(context);
mMessages = messages;
}

private class DateViewHolder {
public DateViewHolder(View viewRoot) {
date = (TextView)viewRoot.findViewById(R.id.listview_multi_type_item_date_txt);
}
public TextView date;
}

private class TxtSentViewHolder {
public TxtSentViewHolder(View viewRoot) {
txt = (TextView)viewRoot.findViewById(R.id.listview_multi_type_item_txt_sent_txt);
}
public TextView txt;
}

private class TxtRecvViewHolder {
public TxtRecvViewHolder(View viewRoot) {
txt = (TextView)viewRoot.findViewById(R.id.listview_multi_type_item_txt_recv_txt);
}
public TextView txt;
}

@Override
public int getViewTypeCount() {
return JsonListData.Message.TYPE_COUNT;
}

@Override
public int getItemViewType(int position) {
return getItem(position).type;
}

@Override
public int getCount() {
return mMessages.size();
}

@Override
public JsonListData.Message getItem(int position) {
return mMessages.get(position);
}

@Override
public long getItemId(int position) {
return position;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
switch (getItemViewType(position)) {
case JsonListData.Message.TYPE_DATE:
return handleGetDateView(position, convertView, parent);
case JsonListData.Message.TYPE_TXT_SENT:
return handleGetTxtSentView(position, convertView, parent);
case JsonListData.Message.TYPE_TXT_RECV:
return handleGetTxtRecvView(position, convertView, parent);
default:
return null;
}
}

private View handleGetDateView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = mInflater.inflate(R.layout.listview_multi_type_item_date, parent, false);
convertView.setTag(new DateViewHolder(convertView));
}
if (convertView != null && convertView.getTag() instanceof DateViewHolder) {
final DateViewHolder holder = (DateViewHolder)convertView.getTag();
holder.date.setText(formatTime(getItem(position).time));
}
return convertView;
}

private View handleGetTxtSentView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = mInflater.inflate(R.layout.listview_multi_type_item_txt_sent, parent, false);
convertView.setTag(new TxtSentViewHolder(convertView));
}
if (convertView != null && convertView.getTag() instanceof TxtSentViewHolder) {
final TxtSentViewHolder holder = (TxtSentViewHolder)convertView.getTag();
holder.txt.setText(getItem(position).txt);
}
return convertView;
}

private View handleGetTxtRecvView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = mInflater.inflate(R.layout.listview_multi_type_item_txt_recv, parent, false);
convertView.setTag(new TxtRecvViewHolder(convertView));
}
if (convertView != null && convertView.getTag() instanceof TxtRecvViewHolder) {
final TxtRecvViewHolder holder = (TxtRecvViewHolder)convertView.getTag();
holder.txt.setText(getItem(position).txt);
}
return convertView;
}

private String formatTime(long time) {
return mSdfDate.format(new Date(time * 1000));
}
}


可以看到, int getViewTypeCount(); 和 int getItemViewType(int position); 的处理是非常清晰的。

需要注意的在于,ViewType必须在 [0, getViewTypeCount() - 1] 范围内。

3.3 ViewHolder为何能正确的工作

回顾一下单一类型的listview,其ViewHolder的工作机制在于系统会将滑出屏幕的item的view回收起来,并作为getView的第二个参数 convertView 传入。

那么,在多种类型的listview中,滑出屏幕的view与即将滑入屏幕的view类型很可能是不同的,那这么直接用不就挂了吗?

其实不然,android针对多种类型item的情况已经做好处理了,如果getView传入的 convertView 不为null,那它一定与当前item的view类型是匹配的。

所以,在3.2节中对ViewHolder的处理方式与单类型的listview并没有本质区别,却也能正常的工作。

[转载请保留本文地址:/article/11872795.html]

四、demo工程

保存下面的图片,扩展名改成 .zip 即可



[转载请保留本文地址:/article/11872795.html]

五、番外篇 —— ListView回收机制简要剖析

在3.3节中简单介绍了android系统会处理好多类型item的回收和重用,那具体是怎么实现的呢?

下面简要剖析一下支持多种类型item的listview中,View回收的工作机制。

5.1 View回收站的初始化

ListView的父类AbsListView中定义了一个内部类RecycleBin,这个类维护了listview滑动过程中,view的回收和重用。

在ListView的 setAdapter 方法中,会通过调用 mRecycler.setViewTypeCount(mAdapter.getViewTypeCount()) 来初始化RecycleBin。

让我们看下RecycleBin中对应都做了什么:

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;
}


看源码,说白了就是创建了一个大小为 getViewTypeCount() 的数组 mScrapViews ,从而为每种类型的view维护了一个回收站,此外每种类型的回收站自身又是一个View数组。

这也就解释了为什么ViewType必须在 [0, getViewTypeCount() - 1] 范围内。

5.2 View回收站的构建和维护

AbsListView在滑动时,会调用 trackMotionScroll 方法:

boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
//...
final boolean down = incrementalDeltaY < 0;
//...
if (down) {
int top = -incrementalDeltaY;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
top += listPadding.top;
}
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
if (child.getBottom() >= top) {
break;
} else {
count++;
int position = firstPosition + i;
if (position >= headerViewsCount && position < footerViewsStart) {
// The view will be rebound to new data, clear any
// system-managed transient state.
if (child.isAccessibilityFocused()) {
child.clearAccessibilityFocus();
}
                        mRecycler.addScrapView(child, position);
                     }
}
}
} else {
int bottom = getHeight() - incrementalDeltaY;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
bottom -= listPadding.bottom;
}
for (int i = childCount - 1; i >= 0; i--) {
final View child = getChildAt(i);
if (child.getTop() <= bottom) {
break;
} else {
start = i;
count++;
int position = firstPosition + i;
if (position >= headerViewsCount && position < footerViewsStart) {
// The view will be rebound to new data, clear any
// system-managed transient state.
if (child.isAccessibilityFocused()) {
child.clearAccessibilityFocus();
}
                        mRecycler.addScrapView(child, position);
                     }
}
}
}
//...
}


在 trackMotionScroll 方法中,会根据不同的滑动方向,调用 addScrapView ,将滑出屏幕的view加到RecycleBin中:

void addScrapView(View scrap, int position) {
final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
if (lp == null) {
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)) {
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<View>();
}
                    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<View>();
}
                    mTransientStateViews.put(position, scrap);
                 } else {
// Otherwise, we'll have to remove the view and start over.
if (mSkippedScrap == null) {
mSkippedScrap = new ArrayList<View>();
}
mSkippedScrap.add(scrap);
}
} else {
if (mViewTypeCount == 1) {
mCurrentScrap.add(scrap);
} else {
                    mScrapViews[viewType].add(scrap);
                 }

// Clear any system-managed transient state.
if (scrap.isAccessibilityFocused()) {
scrap.clearAccessibilityFocus();
}

scrap.setAccessibilityDelegate(null);

if (mRecyclerListener != null) {
mRecyclerListener.onMovedToScrapHeap(scrap);
}
}
}


在 addScrapView 方法中,被回收的view会根据其类型加入 mScrapViews 中。

特别的,如果这个view处于TransientState(瞬态,view正在播放动画或其他情况),则会被存入 mTransientStateViewsById 、 mTransientStateViews 。

5.3 从View回收站获取View

Adapter的getView方法在AbsListView的 obtainView 中被调用:

View obtainView(int position, boolean[] isScrap) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView");
isScrap[0] = false;
View scrapView;
scrapView = mRecycler.getTransientStateView(position);
         if (scrapView == null) {
scrapView = mRecycler.getScrapView(position);
         }

View child;
if (scrapView != null) {
child = mAdapter.getView(position, scrapView, this);
             if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
}
if (child != scrapView) {
                mRecycler.addScrapView(scrapView, position);
                 if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
}
} else {
isScrap[0] = true;
// Clear any system-managed transient state so that we can
// recycle this view and bind it to different data.
if (child.isAccessibilityFocused()) {
child.clearAccessibilityFocus();
}
child.dispatchFinishTemporaryDetach();
}
} else {
child = mAdapter.getView(position, null, this);
             if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
}
if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
}
}

//...

return child;
}


可以看到,对于不处于TransientState的View,将会尝试通过 getScrapView 方法获取回收的View,如果有,就会作为参数传入Adatper的getView方法中。

而 getScrapView 方法,其实就是先调用Adapter的 getItemViewType 方法取position对应的view类型,然后从 mScrapViews 中根据类型取view。

View getScrapView(int position) {
if (mViewTypeCount == 1) {
return retrieveFromScrap(mCurrentScrap, position);
} else {
int whichScrap = mAdapter.getItemViewType(position);
if (whichScrap >= 0 && whichScrap < mScrapViews.length) {
return retrieveFromScrap(mScrapViews[whichScrap], position);
}
}
return null;
}


至此,我们简要了解了多类型的listview中,是如何在滑动屏幕时回收view并进行重用的。

而如何维护每个类型item对应的View数组,以及TransientState的维护,本篇文章就不做详细介绍了,有兴趣的读者可以着重研究一下AbsListView的源码。

[转载请保留本文地址:/article/11872795.html]
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: