ListView

  • ListView
  • 简单示例
  • AbsListView
  • WrapperListAdapter
  • ListAdapter
  • Adapter
  • RecyclerView
  • RecyclerView.Adapter
  • 对数据集的增删改
  • ListView 复用的机制
  • RecycleBin
  • ScrapViews
  • Layout 阶段
  • RecyclerView 的复用机制
  • 复用的问题
  • View.setTag
  • 实例:音乐播放列表


实现方式:

  1. 界面添加 ListView
  2. 使用数据集创建适配器,返回项视图
  3. ListView 应用适配器
  4. 数据集增删改的通知

ListView

https://developer.android.com/reference/android/widget/ListView

显示可垂直滚动的视图集合,其中每个视图都位于列表中上一个视图的紧下方。对于一种更现代,灵活和高效的列表显示方法,请使用 RecyclerView。

java.lang.Object
   ↳	android.view.View
 	   ↳	android.view.ViewGroup
 	 	   ↳	android.widget.AdapterView<android.widget.ListAdapter>
 	 	 	   ↳	android.widget.AbsListView
 	 	 	 	   ↳	android.widget.ListView

列表视图并不知道视图的细节,只根据需要从 ListAdapter 中请求视图,例如在用户向上或向下滚动时请求新视图。

为了显示列表中的项目,请调用 setAdapter(android.widget.ListAdapter) 将适配器与列表关联。

简单示例

要为数据集中的每个项目显示自定义的视图,请实现 ListAdapter。例如,扩展 BaseAdapter 并在 getView(…) 中创建并配置每个数据项的视图:

private class MyAdapter extends BaseAdapter {

    // override other abstract methods here

    @Override
    public View getView(int position, View convertView, ViewGroup container) {
        if (convertView == null) {
            convertView = getLayoutInflater().inflate(R.layout.list_item, container, false);
        }

        ((TextView) convertView.findViewById(android.R.id.text1))
                .setText(getItem(position));
        return convertView;
    }
}

ListView 尝试重用视图对象,以提高性能并避免响应用户滚动的滞后。要利用此功能,请在创建或扩展新视图对象之前检查提供给 getView(…) 的 convertView 是否为 null。

常用方法
setAdapter(ListAdapter adapter):在此 ListView 后面配置数据。传递给此方法的适配器可以再由 WrapperListAdapter 包装,增加添加页眉 and/or 页脚的功能。
ListAdapter负责维护支持该列表的数据,并产生一个视图以表示该数据集中的项目。

addHeaderView (View v):添加固定视图以显示在列表顶部。如果多次调用此方法,则视图将按添加顺序出现。如果需要,使用此调用添加的视图可以成为焦点。
注意:首次引入此方法时,只能在使用setAdapter(android.widget.ListAdapter)设置适配器之前调用此方法。从Build.VERSION_CODES.KITKAT开始,可以随时调用此方法。如果ListView的适配器未扩展HeaderViewListAdapter,则它将与WrapperListAdapter的支持实例一起包装。

常用属性
android:divider:可绘制或可在列表项之间绘制的颜色,@null 为不绘制。
android:dividerHeight:分隔线的高度。如果未指定,将使用分隔线的固有高度。
android:scrollbars:定义在滚动时是否显示滚动条,none 为不显示。

AbsListView

https://developer.android.com/reference/android/widget/AbsListView#setChoiceMode(int)

setChoiceMode (int choiceMode):定义列表的选择行为。默认情况下,列表没有任何选择行为(CHOICE_MODE_NONE)。通过将choiceMode设置为CHOICE_MODE_SINGLE,列表可以允许最多一项处于选定状态。通过将choiceMode设置为 CHOICE_MODE_MULTIPLE,列表允许选择任意数量的项目。

WrapperListAdapter

包装另一个列表适配器的列表适配器。可以通过调用 getWrappedAdapter() 来取回包装的适配器。

public interface WrapperListAdapter implements ListAdapter

ListAdapter

实现 Adapter 接口,它是 ListView 和支持列表的数据之间的桥梁。通常,数据来自 Cursor,但这不是必需的。ListView 可以显示任何包装在 ListAdapter 中的数据。

Adapter

Adapter 对象充当 AdapterView 和该视图的基础数据之间的桥梁。适配器提供对数据项的访问。适配器还负责为数据集中的每个项目创建一个 View。

android.widget.Adapter

public interface Adapter

已知子类
ArrayAdapter, BaseAdapter, CursorAdapter, HeaderViewListAdapter, ListAdapter, ResourceCursorAdapter, SimpleAdapter, SimpleCursorAdapter, SpinnerAdapter, ThemedSpinnerAdapter, WrapperListAdapter

常见方法
getView(int position, View convertView, ViewGroup parent):获取一个显示数据集中指定位置数据的视图。
getItemViewType(int position):获取 getView(int, View, ViewGroup) 将要创建的指定项 View 的类型。使用该方法可以为不同类型的项创建不同样式的 View。当然使用的 Adapter 的逻辑要自己实现。一种实现是在 getView 中通过 getItemViewType 返回的类型码使用不同的 ViewHold 创建不同样式的 View。如果适配器始终为所有项目返回相同类型的 View,则此方法应返回 1。
getViewTypeCount():返回将由 getView(int, View, ViewGroup) 创建的 View 的类型数。可以在 getView 中调用 getItemViewType 根据返回的不同类型创建不同的 ViewHolder。

getCount():此适配器表示的数据集中有多少个项目。
isEmpty():如果此适配器不包含任何数据,则为true。这用于确定是否应显示空白视图。典型的实现将返回 getCount() == 0,但是自从 getCount() 包含页眉和页脚,专用适配器可能不这样返回。

getItem(int position):获取与指定位置关联的数据集中的数据项。
getItemId(int position):获取与指定位置关联的数据集中数据项的行 ID。

registerDataSetObserver(DataSetObserver observer):注册一个观察者,该适配器使用的数据发生更改时将通知该观察者。
unregisterDataSetObserver(DataSetObserver observer):注消观察者。

getAutofillOptions():获取适配器数据的字符串表示形式,它可以帮助 AutofillService 自动填充适配器支持的视图。

RecyclerView

https://developer.android.com/reference/androidx/recyclerview/widget/RecyclerView

灵活的视图,可以使用有限窗口展示大型数据集。AndroidX 中可以直接使用,Support 中需要单独引入,见 Android - 构建编译 & Java 库插件。

java.lang.Object
   ↳	android.view.View
 	   ↳	android.view.ViewGroup
 	 	   ↳	android.support.v7.widget.RecyclerView

RecyclerView.Adapter

RecyclerView的子类,负责为 RecyclerView 提供表示数据集中项目的视图。

java.lang.Object
   ↳	android.support.v7.widget.RecyclerView.Adapter<VH extends android.support.v7.widget.RecyclerView.ViewHolder>

ViewHolder:内部类,定义 View 元素,构造时绑定 View 组件

getItemViewType(int position):返回该项目的视图类型,以用于视图循环使用。与 ListView 适配器不同的是类型不必是连续的。可以考虑使用 ID 资源来唯一标识项目视图类型。
onCreateViewHolder:当 RecyclerView 需要新的给定类型的 RecyclerView.ViewHolder 来创建 View 时,通过 createViewHolder(ViewGroup parent, int viewType) 回掉。具体实现是新建 ViewHolder,对 ViewHolder 元素进行初始化
onBindViewHolder:由 RecyclerView 调用 bindViewHolder(VH holder, int position) 时回调在指定位置显示数据。与 ListView 不同,如果只是结构更改事件,则 RecyclerView 将不会再次回调此方法,除非该项目本身无效或无法确定新位置。具体实现是对 ViewHolder 子项进行赋值。
getItemCount:返回适配器持有的数据集中的项目总数。

hasStableIds():如果此适配器已经为数据集中指定的项生成唯一的 long 型键值,也就是说适配器内的键值列表没有新的 long 值加入时,则返回 true。若该项内容不作改变,单纯重新放置入数据集中时,它们的键值依然相同。

onDetachedFromRecyclerView(RecyclerView recyclerView):在 RecyclerView 停止观察此适配器时调用。

onViewAttachedToWindow(VH holder):当此适配器创建的视图已附加到窗口时调用。这可以用作用户即将看到该视图的合理信号。如果适配器先前 onViewDetachedFromWindow 释放了任何资源,则应在此处还原这些资源。
onViewDetachedFromWindow(VH holder):当此适配器创建的视图从其窗口分离时,调用此方法。从窗户上移开不一定是永久的情况。适配器视图的使用者可以选择在不可见的情况下在屏幕外缓存视图,并根据需要进行 Attach 和 Detach。

对数据集的增删改

notifyDataSetChanged():通知任何注册观察员数据集已更改。RecycleView 在设置适配器的同时会注册成为观察者,获得通知会更改相应的 View。

数据更改事件有两种不同的类别,即项目内容的更改或结构的更改。项目内容的更改是指单个项目的数据已更新但未发生位置更改的情况。结构更改是指数据集的插入,删除或移动。 notifyDataSetChanged 未指定是哪种类型,使得所有观察者需认定所有现有项目内容和结构不再有效。LayoutManager 将强制所有可见视图的重新绑定和重新布局。
如果 hasStableIds 返回 true 表示项目内容未发生变化,则 RecyclerView 将尝试使用结构更改事件。这可以保留动画和可见对象,但是仍然需要重新放置和重新布局个别项目视图。 如果您正在编写适配器,则编写更具体的更改事件方法总是更加高效。将 notifyDataSetChanged() 作为最后的选择。

notifyItemChanged(int position, Object payload):项目内容变更事件。它表示该位置数据的任何反映都已过时,应进行更新。该位置的项目保留原来的标识。

传递有效负载可以进行部分更改。如果项目已经由 ViewHolder 表示,则所有有效载荷将被合并成 List 并传递给 onBindViewHolder(ViewHolder, int, List),此间可以做部分修改(eg. 将 ViewHold 隐藏的控件赋值显示反馈等内容),或者调用 onBindViewHolder(ViewHolder, int) 做原本的修改。有效载荷为 null 的 notifyItemRangeChanged() 会清除所有现有的有效载荷。当 View 未 attachedToWindow 时,有效载荷将被简单地丢弃。

notifyItemChanged(int position):同上(无有效载荷,传递的是 onBindViewHolder(ViewHolder, int))
notifyItemInserted(int position):结构更改事件。数据集中其他现有项目的表示形式仍被认为是最新的,并且不会重新绑定,尽管它们的位置可能会发生变化。
notifyItemRemoved(int position):同上
notifyItemMoved(int fromPosition, int toPosition):结构更改事件。原先在 fromPosition 的项目已移至 toPosition。数据集中其他现有项目的表示形式仍被认为是最新的,并且不会重新绑定,尽管它们的位置可能会发生变化。
notifyItemRangeChanged(int positionStart, int itemCount, Object payload):内容更改事件。一个范围所有项目的更新。
notifyItemRangeChanged(int positionStart, int itemCount):同上(无有效载荷)
notifyItemRangeInserted(int positionStart, int itemCount):范围插入
notifyItemRangeRemoved(int positionStart, int itemCount):范围移除

ListView 复用的机制

layout 开始时会通过 getView 创建一屏幕的视图放在 ActiveViews 中,同时也会创建与视图对应的 ViewHolder;在 layout 结束时 ActiveViews 中的所有视图均降级为 ScrapViews,方便复用。(见 ListView.layoutChildren

当有个视图滑进屏幕,意味着另一边有个视图从窗口中 detach、添加到弃置列表中。

新的视图可以从弃置列表中获取同样类型的视图,对其进行重新初始化配置来复用(表现为往 getView 中传入 scrapView 作为 convertView,判断时不为 null,可以直接对它的控件进行初始化配置)。如果没有存储该种视图则创建新的视图。

android skia 图表 android列表视图_android skia 图表

具体操作见源码:

https://www.androidos.net.cn/android/9.0.0_r8/xref/frameworks/base/core/java/android/widget/ListView.java https://www.androidos.net.cn/android/9.0.0_r8/xref/frameworks/base/core/java/android/widget/AbsListView.java

RecycleBin

RecycleBin有助于跨布局重复使用视图。RecycleBin具有两个级别的存储:ActiveViews和ScrapViews。

AbsListView # RecycleBin 内部类的成员变量:

/**
 * 存储在 mActiveViews 中的第一个视图的位置。
 */
private int mFirstActivePosition;

/**
 * 布局开始时在屏幕上显示的视图。此数组在布局开始时填充,在布局结束时将mActiveViews中的所有视图移至mScrapViews。
 * mActiveViews中的视图表示视图的连续范围,其中第一个视图存储在mFirstActivePosition中。
 */
private View[] mActiveViews = new View[0];

/**
 * 适配器可以将其用作转换视图的弃置视图列表,其所存储的视图是无序的。
 */
private ArrayList<View>[] mScrapViews;

private int mViewTypeCount;	// 视图类型数,mScrapViews 的 length 根据此确定

ScrapViews

ScrapViews 保存临时 detach 的视图(按类型),可能被适配器用来避免不必要地分配视图。

AbsListView # RecylceBin.getScrapView会根据类型获取对应的视图。

/**
 * @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;
}

AbsListView # RecylceBin.addScrapView

/**
 * 将视图放入 ScrapViews 列表中。
 * <p>
 * 如果列表数据未更改或者适配器具有 stable IDs,处于过渡状态的视图将被保留以供以后的检索。
 *
 * @param scrap 要添加的视图
 * @param position 视图在其父级中的位置
 */
void addScrapView(View scrap, int position) {
    final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
	...
	
    lp.scrappedFromPosition = position;

    // 弃置列表中的视图不包括 header、footer 以及其他不进行重用的视图,所以在此将其移除。
    final int viewType = lp.viewType;
    if (!shouldRecycleViewType(viewType)) {
        // 除 header 和 footer 外,不进行重用的特殊视图应完全废弃。使用 getSkippedScrap 添加该视图,稍后将完全 detach。
        if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
            getSkippedScrap().add(scrap);
        }
        return;
    }

    scrap.dispatchStartTemporaryDetach();	// 视图临时从窗口 detach

    // 在临时分离时,视图的可访问性状态可能会更改,并且我们不允许分离的视图触发可访问性事件。因此,我们通告子树
    // 发生更改,使持有此子树视图的客户端有机会刷新它。
    notifyViewAccessibilityStateChangedIfNeeded(
            AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);

    // 处于过渡状态(具有动画效果)的视图不需要添加到 ScrapViews 列表中。这种状态的 View 只有跟 Adapter 绑定的数据源没有发生变化或者 View 有相同的 ID 的时候才能进行缓存复用,因为这两种情况下 Item 要么数据不变,不用重新绑定数据;要么 View 不变,不需要重新创建。复用视图也不加入 ScrapViews,而是 RecylceBin.mTransientStateViews 和 RecylceBin.mTransientStateViewsById,一个存数据没有发生变化的视图,一个存有稳定 ID 的视图。
    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.
            clearScrapForRebind(scrap);
            getSkippedScrap().add(scrap);
        }
    } else {
        clearScrapForRebind(scrap);
        if (mViewTypeCount == 1) {
            mCurrentScrap.add(scrap);
        } else {
            mScrapViews[viewType].add(scrap);	// 添加到相应的类型维度中
        }

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

Layout 阶段

ListView.layoutChildren

@Override
protected void layoutChildren() {
    ...

    try {
        super.layoutChildren();
		
		...
        
        final int childCount = getChildCount();
        
		...
		
        // Pull all children into the RecycleBin.
        // These views will be reused if possible
        final int firstPosition = mFirstPosition;
        final RecycleBin recycleBin = mRecycler;
        if (dataChanged) {
            for (int i = 0; i < childCount; i++) {
                recycleBin.addScrapView(getChildAt(i), firstPosition+i);
            }
        } else {
            recycleBin.fillActiveViews(childCount, firstPosition);	// 用 AbsListView 的所有子项填充 ActiveViews。
        }

		...
		
        // Flush any cached views that did not get reused above
        recycleBin.scrapActiveViews();	// 将 ActiveViews 中的所有视图移动到 ScrapViews。

		...
    } finally {
        ...
    }
}

AbsListView # RecycleBin

ActiveViews 中的视图表示视图的连续范围,第一个位置的视图存储在 FirstActivePosition 中。

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;	// 记住位置,以便在 setupChild 时不会重置状态
        }
    }
}

ListView.makeAndAddView

/**
 * 获取视图并将其添加到我们的子级列表中。该视图可以是新制作的,从不使用的视图转换的(mScrapView),或直接使用 RecycleBin 里的活跃视图(mActiveView)。
 *
 * @param position 列表中的逻辑位置
 * @param y 要添加的视图的顶部或底部边缘
 * @param flow 设置为 true 使上边缘与y对齐,为 false 使下边缘与y对齐
 * @param childrenLeft 放置在子级视图的左边缘
 * @param selected 如果选择了该位置,设置为 true,其他为 false
 * @return the 添加的视图
 */
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
        boolean selected) {
    if (!mDataChanged) {
        // 尝试对该位置使用现有视图。
        final View activeView = mRecycler.getActiveView(position);
        if (activeView != null) {
            // 找到了。我们正在重用现有的子级视图,所以只需要重新定位即可。
            setupChild(activeView, position, y, flow, childrenLeft, selected, true);
            return activeView;
        }
    }

    // 为此位置创建一个新视图,或在可能的情况下转换未使用的视图
    final View child = obtainView(position, mIsScrap);

    // 这需要进行定位和测量。
    setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

    return child;
}

AbsListView.obtainView

两层处理,首先就是尝试获取 transientView,也就是拥有瞬时态的 view,通过 transientView 以完成复用。如果这一步走的失败了,也就是 transientView 为 null 时,或者说这个 view 并不拥有瞬时态,那么就从 ScrapView 中获取一个s crapView。

/**
 * 获取指定位置的视图并显示其数据。当视图不在 recycle bin 中直接使用时,调用来复用一个旧视图或制作一个新视图。
 *
 * @param position 显示位置
 * @param outMetadata 至少包含 1 个布尔值的数组。如果视图当前已 attach 到窗口,则将第一项设为 true;否则(如
 * 新创建的视图,或在多次布局过程中始终维持在 ScrapViews 列表中的 scrapView)设为 false
 *
 * @return 指定位置显示其关联数据的视图
 */
View obtainView(int position, boolean[] outMetadata) {
	...
	
    outMetadata[0] = false;

	// Check whether we have a transient state view. Attempt to re-bind the
    // data and discard the view if we fail.
    final View transientView = mRecycler.getTransientStateView(position);
    if (transientView != null) {
        final LayoutParams params = (LayoutParams) transientView.getLayoutParams();

        // If the view type hasn't changed, attempt to re-bind the data.
        if (params.viewType == mAdapter.getItemViewType(position)) {
            final View updatedView = mAdapter.getView(position, transientView, this);

            // If we failed to re-bind the data, scrap the obtained view.
            if (updatedView != transientView) {
                setItemViewLayoutParams(updatedView, position);
                mRecycler.addScrapView(updatedView, position);
            }
        }

        outMetadata[0] = true;

        // Finish the temporary detach started in addScrapView().
        transientView.dispatchFinishTemporaryDetach();
        return transientView;
    }

    final View scrapView = mRecycler.getScrapView(position);
    final View child = mAdapter.getView(position, scrapView, this);
    if (scrapView != null) {
        if (child != scrapView) {
            // 重新绑定数据失败,将 scrapView 放回 ScrapViews 列表维持在堆栈中。
            mRecycler.addScrapView(scrapView, position);
        } else if (child.isTemporarilyDetached()) {
            outMetadata[0] = true;

            // 结束在 addScrapView 时启动的临时 detach,重新 attach 到窗口。
            child.dispatchFinishTemporaryDetach();
        }
    }
	...
    setItemViewLayoutParams(child, position);
    ...
    return child;
}

你真的了解ListView的缓存吗Android开发——ListView的复用机制源码解析

RecyclerView 的复用机制

RecyclerView 的复用机制与 ListView 基本一致,不同的是 RecyclerView 将 ListView 使用的 getView 拆分成 onCreateViewHolder(创建复用视图)和 onBindViewHolder(重新初始化配置)两个过程。

其调用顺序是:先通过 getItemViewType 判断类型,onCreateViewHolder 创建复用视图,onBindViewHolder 对其进行初始化设置,然后重复上述过程。当创建一定数目的复用视图后,则不再 onCreateViewHolder,仅 getItemViewType 判断类型;类型已存在列表中则复用,不存在时再 onCreateViewHolder 创建新的视图替换,然后继续 onBindViewHolder 重新初始化设置。

探究RecyclerView的ViewHolder复用

复用的问题

由于存在复用,对某个 View 设置某一状态,当其变为复用视图时(添加到弃置列表),使用它的视图也会出现相同的状态(实际没有设置该状态)。

几种解决方法:

  1. 使用条件,通过 if…else 在满足条件时设置某种状态,相反条件下的设为默认状态。一种实现是在设置状态时使用三目运算符赋值。
  2. 使用标签,标记某控件状态设置的情况(eg. viewHolder.textView.setTag(isSet)),使用标签为条件,通过方法 1 设置状态。
  3. 对 ListView 的所有子项都创建复用视图,使用 Map 用 position 作为键进行保存,复用时获取保存视图进行复用。这种方式造成的空间代价大。
if (mHashMap.get(position) == null) {
    holder = new ViewHolder();
    convertView = inflater.inflate(R.layout.softmanager_localapp_item,
            null);

    mHashMap.put(position, convertView);
    convertView.setTag(holder);
} else {
    convertView = mHashMap.get(position);
    holder = (ViewHolder) convertView.getTag();
}


View.setTag

https://developer.android.com/reference/android/view/View#setTag(int,%20java.lang.Object)

Tags 在 SDK 中解释为:

Unlike IDs, tags are not used to identify views. Tags are essentially an extra piece of information that can be associated with a view. They are most often used as a convenience to store data related to views in the views themselves rather than by putting them in a separate structure.

与ID不同,标签不用于标识视图。标签本质上是可以与视图关联的额外信息。它们最常用于将视图相关的数据存储在视图本身中的便利,而不是将它们放在单独的结构中。

setTag (Object tag):设置与此视图关联的标签。标签可用于在其层次结构中标记视图,并且在层次结构中不必唯一。标签还可以用于在视图中存储数据,而无需求助于其他数据结构。
setTag (int key, Object tag):设置与此视图关联的标签和键。标签可用于在其层次结构中标记视图,并且在层次结构中不必唯一。标签还可以用于在视图中存储数据,而无需诉诸其他数据结构。指定的密钥应该是在应用程序资源中声明的ID,以确保其唯一(请参阅ID资源类型)。标识为属于Android框架或未与任何程序包相关联的键会导致引发IllegalArgumentException。

getTag ():返回此视图的标签。
getTag (int key):返回与此视图关联的标签和指定的键。

定义 key
在 res/values/strings.xml 中添加:

<resources>
	<string name="type_normal">Normal</string>
	<!-- setTag 的 key -->
	<item type="id" name="tag_first">Tag1</item>
	<item type="id" name="tag_second">Tag2</item>
</resources>

实例:音乐播放列表

public class MusicListAdapter extends RecyclerView.Adapter<MusicListAdapter.ViewHolder> {

    private List<Music> mMusicList;

    public MusicListAdapter(List<Music> songs) {
        mMusicList = songs;
    }

    @Override
    public ViewHolder onCreateViewHolder(final ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_music_item, parent, false);
        final ViewHolder holder = new ViewHolder(view);
        // 点击曲名跳转到相应播放界面,传递曲子在 ListView 位置上的 id
        holder.musicName.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                MainActivity.actionStartWithResult(parent.getContext(), mMusicList.get(holder.position).getMusicId());
            }
        });
        return holder;
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        holder.position = position;
        holder.musicName.setText(mMusicList.get(position).getMusicName());
    }

    @Override
    public int getItemCount() {
        return mMusicList.size();
    }

    public class ViewHolder extends RecyclerView.ViewHolder {
        int position;
        TextView musicName;
        public ViewHolder (View itemView) {
        	musicName = (TextView) itemView.findViewById(R.id.music_name);
        }
    }
}

绑定的控件能否在 ViewHolder 构造函数中初始化?

能。可以将固定的控件初始化配置放到 ViewHolder 中,变动的初始化配置放在 onBindViewHolder 中以作区分。