13.1 问题
希望以独特的方式展示大型数据集合,而不是以垂直滚动列表显示;或者,要以AdapterView小部件无法轻松支持的方式样式化此集合。
13.2 解决方案
(API Level 1)
以Android支持库中的RecyeclerView为基础构建解决方案。RecyclerView小部件利用与AdapterView组件相同的视图回收功能来提供大型数据集的高效内存使用显示方式。然而,与核心框架中的AdapterView不同的是,RecyclerView以更加灵活的模型为基础,在此模型中,子视图组件的放置委托给LayoutManager实例完成。Android库支持两个内置的布局管理器:
- LineLayoutManager :将子视图垂直(自上而下)或水平(自左而右)放置在列表中。垂直布局行为类似于ListView框架。
- GridLayoutManager : 将子视图垂直(自上而下)或水平(自左而右)放置在网格中。该管理器支持添加行/列跨度值以使网格中的子视图交错显示。对于单一跨度项的垂直布局行为类似于GridView框架。
RecyclerView.ItemDecoration实例使用应用程序支持在子视图的上方和下方进行自定义的绘制操作,此外还直接支持页边距以在子视图之间添加间距。该实例可用于绘制像网格线和连接线这样的简单对象,还可以用于在内容区域中绘制更多复杂的图案或图片。
RecyclerView.Adapter实例还包括用于通知数据集视图变化的新方法,使得该小部件可以更好地处理各种变化的动画,如添加或删除元素,而使用AdapterView较难处理这些动画:
- notifyItemInserted()、notifyItemRemoved()、notifyItemChanged() : 表明已添加、删除关联数据集中的单个项,或者该项已改变位置。
- notifyItemRangeInserted()、notifyItemRangeRemoved()、notifyItemRangeChanged() : 表明关联数据集中已修改的一定位置范围的项。
这些方法接受的参数是特定项的位置,因此RecyclerView可以智能地判断如何制作变化的动画。标准的notifyDataSetChanged()方法仍然得到支持,但它不会制作变化的动画。
要点:
RecyclerView仅在Android支持库中提供:它不是任意平台级别中的原生SDK的一部分。然而,目标平台为API Level 7或之后版本的任意应用程序都可以通过包含支持库使用此小部件。有关在项目中包含支持库的更多信息,请参考如下网址:http://developer.android.com/tools/support-library/index.html 。
13.3 实现机制
下面的示例使用4个不同的LayoutManager实例,通过RecycleView显示相同的项数据。以下两图显示了在垂直和水平列表中显示的数据:
垂直列表集合
水平列表集合
以下两图分别在交错的垂直网格和均匀的水平网格中显示相同的数据:
垂直网格集合
水平网格集合
首先,以下两段代码显示了Activity和用于选择布局的选项菜单。
使用了RecyclerView的Activity
public class SimpleRecyclerActivity extends ActionBarActivity implements
SimpleItemAdapter.OnItemClickListener {
private RecyclerView mRecyclerView;
private SimpleItemAdapter mAdapter;
/* 布局管理器 */
private LinearLayoutManager mHorizontalManager;
private LinearLayoutManager mVerticalManager;
private GridLayoutManager mVerticalGridManager;
private GridLayoutManager mHorizontalGridManager;
/* 修饰 */
private ConnectorDecoration mConnectors;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mRecyclerView = new RecyclerView(this);
mHorizontalManager = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false);
mVerticalManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
mVerticalGridManager = new GridLayoutManager(this,
2, /* 网格的列数 */
LinearLayoutManager.VERTICAL, /* 垂直定位网格 */
false);
mHorizontalGridManager = new GridLayoutManager(this,
3, /*网络的行数 */
LinearLayoutManager.HORIZONTAL, /* 水平定位网格*/
false);
//垂直网格的连接线修饰
mConnectors = new ConnectorDecoration(this);
//交错垂直网格
mVerticalGridManager.setSpanSizeLookup(new GridStaggerLookup());
mAdapter = new SimpleItemAdapter(this);
mAdapter.setOnItemClickListener(this);
mRecyclerView.setAdapter(mAdapter);
//对所有连接音乐应用边缘修饰
mRecyclerView.addItemDecoration(new InsetDecoration(this));
//默认为垂直布局
selectLayoutManager(R.id.action_vertical);
setContentView(mRecyclerView);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.layout_options, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
return selectLayoutManager(item.getItemId());
}
private boolean selectLayoutManager(int id) {
switch (id) {
case R.id.action_vertical:
mRecyclerView.setLayoutManager(mVerticalManager);
mRecyclerView.removeItemDecoration(mConnectors);
return true;
case R.id.action_horizontal:
mRecyclerView.setLayoutManager(mHorizontalManager);
mRecyclerView.removeItemDecoration(mConnectors);
return true;
case R.id.action_grid_vertical:
mRecyclerView.setLayoutManager(mVerticalGridManager);
mRecyclerView.addItemDecoration(mConnectors);
return true;
case R.id.action_grid_horizontal:
mRecyclerView.setLayoutManager(mHorizontalGridManager);
mRecyclerView.removeItemDecoration(mConnectors);
return true;
case R.id.action_add_item:
//插入新的项
mAdapter.insertItemAtIndex("Android Recipes", 1);
return true;
case R.id.action_remove_item:
//删除第一项
mAdapter.removeItemAtIndex(1);
return true;
default:
return false;
}
}
/** OnItemClickListener 方法 */
@Override
public void onItemClick(SimpleItemAdapter.ItemHolder item, int position) {
Toast.makeText(this, item.getSummary(), Toast.LENGTH_SHORT).show();
}
}
res/menu/layout_options.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_add_item"
android:title="Add Item"
android:icon="@android:drawable/ic_menu_add"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_remove_item"
android:title="Remove Item"
android:icon="@android:drawable/ic_menu_delete"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_vertical"
android:title="Vertical List"
app:showAsAction="never"/>
<item
android:id="@+id/action_horizontal"
android:title="Horizontal List"
app:showAsAction="never"/>
<item
android:id="@+id/action_grid_vertical"
android:title="Vertical Grid"
app:showAsAction="never"/>
<item
android:id="@+id/action_grid_horizontal"
android:title="Horizontal Grid"
app:showAsAction="never"/>
</menu>
此例使用选项菜单选择应该应用于RecyclerView的布局管理器。任何改动都会触发selectLayoutManager()辅助方法,该方法将请求的管理器传递给setLayoutManager()。这会从现有的适配器中重新加载目前的数据,因此我们不需要维护多个RecyclerView实例即可动态更改布局。
可以看到,利用内置的布局管理器不需要太多的代码。此例构造两个LinearlayoutManager实例,它们的构造函数以方向常量为参数(VERTICAL或HORIZONTAL)。该管理器还支持(通过最后的布尔参数)翻转布局,以便按照最后项最先显示的顺序布置适配器数据。
同样,我们构造两个GridLayoutManager实例,分别用于水平和垂直布局。此对象获取一个附加参数,即spanCount,表示布局应使用的行数(水平网格)或列数(垂直网格)。此参数与支持交错网格没有任何关系;稍后将对此进行讨论。
与所有集合视图一样,我们需要让适配器将数据项与视图关联。你可能已注意到,在Activity的代码清单中创建了SimpleItemAdapter类。该适配器的实现如以下两段代码所示:
res/layout/collection_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp"
android:background="#CCF">
<TextView
android:id="@+id/text_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:textAppearanceLarge"/>
<TextView
android:id="@+id/text_summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:textAppearanceMedium"/>
</LinearLayout>
RecyclerView的适配器实现
public class SimpleItemAdapter extends RecyclerView.Adapter<SimpleItemAdapter.ItemHolder> {
/*
* 单击处理程序接口。 与AdapterViews不同,RecyclerView 没有自己的内置接口
*/
public interface OnItemClickListener {
public void onItemClick(ItemHolder item, int position);
}
private static final String[] ITEMS = {
"Apples", "Oranges", "Bananas", "Mangos",
"Carrots", "Peas", "Broccoli",
"Pork", "Chicken", "Beef", "Lamb"
};
private List<String> mItems;
private OnItemClickListener mOnItemClickListener;
private LayoutInflater mLayoutInflater;
public SimpleItemAdapter(Context context) {
mLayoutInflater = LayoutInflater.from(context);
//创建虚拟项的静态列表
mItems = new ArrayList<String>();
mItems.addAll(Arrays.asList(ITEMS));
mItems.addAll(Arrays.asList(ITEMS));
}
@Override
public ItemHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = mLayoutInflater.inflate(R.layout.collection_item, parent, false);
return new ItemHolder(itemView, this);
}
@Override
public void onBindViewHolder(ItemHolder holder, int position) {
holder.setTitle("Item #"+(position+1));
holder.setSummary(mItems.get(position));
}
@Override
public int getItemCount() {
return mItems.size();
}
public OnItemClickListener getOnItemClickListener() {
return mOnItemClickListener;
}
public void setOnItemClickListener(OnItemClickListener listener) {
mOnItemClickListener = listener;
}
/* 管理数据集修改的方法 */
public void insertItemAtIndex(String item, int position) {
mItems.add(position, item);
//通知视图触发变化动画
notifyItemInserted(position);
}
public void removeItemAtIndex(int position) {
if (position >= mItems.size()) return;
mItems.remove(position);
//通知视图触发变化动画
notifyItemRemoved(position);
}
/* 封装项视图所需的ViewHolder实现*/
public static class ItemHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
private SimpleItemAdapter mParent;
private TextView mTitleView, mSummaryView;
public ItemHolder(View itemView, SimpleItemAdapter parent) {
super(itemView);
itemView.setOnClickListener(this);
mParent = parent;
mTitleView = (TextView) itemView.findViewById(R.id.text_title);
mSummaryView = (TextView) itemView.findViewById(R.id.text_summary);
}
public void setTitle(CharSequence title) {
mTitleView.setText(title);
}
public void setSummary(CharSequence summary) {
mSummaryView.setText(summary);
}
public CharSequence getSummary() {
return mSummaryView.getText();
}
@Override
public void onClick(View v) {
final OnItemClickListener listener = mParent.getOnItemClickListener();
if (listener != null) {
listener.onItemClick(this, getPosition());
}
}
}
}
RecyclerView.Adapter重点实施ViewHolder设计模式,在此模式中,它要求实现返回RecyclerView.ViewHolder类型的子集。该类在内部用作与子项关联的元数据(例如其当前位置和稳定的ID)的存储位置。具体的实现通常还提供对视图内部字段的直接访问,从而尽量减少对findViewById()的重复调用,该方法调用大量系统资源,因为它会遍历整个视图层次结构以查找请求的子项。
RecyclerView.Adapter实现与CursorAdapter类似的模式,其中通过onCreateViewHolder()和onBindViewHolder()分开执行创建和绑定步骤。如果必须重头创建新的视图,则调用前一个方法,因此我们在此构造一个新的ItemHolder返回。如果特定位置的数据(在此是简单的字符串)需要附加到新的视图,则调用最后一个方法;这可能是新创建的或回收的视图。这种模式与ArrayAdapter相反,后者将两个方法的功能结合到一个getView()方法中。
在我们的示例中,还利用此适配器提供来自AdapterView的一个附加功能,RecyclerView本质上不支持此功能 : 项单击侦听器。为了使用最少量的引用交换处理子视图的单击事件,我们将每个ViewHolder设置为根项视图的OnClickListener。然后,ViewHolder处理这些事件,并将其发送回在适配器上定义的公共侦听器接口。完成上述操作后,ViewHolder就可以在最后的侦听器回调中添加位置元数据,我们过去期望在诸如AdapterView.OnItemClickListener的类似侦听器中看到这样的结果。
1. 交错网格
在Activity示例中,垂直网格布局管理器还配备有SpanSizeLookup辅助类,该类用于生成上图(垂直与水平网格集合)所示的交错效果。以下代码显示了具体的实现:
交错网格SpanSizeLookup
public class GridStaggerLookup extends GridLayoutManager.SpanSizeLookup {
@Override
public int getSpanSize(int position) {
return (position % 3 == 0 ? 2 : 1);
}
}
getSpanSize()方法用于提供查询,告诉布局管理器给定位置应占据多少跨度(行数或列数,具体取决于布局方向)。该例表明每隔三个位置应战据两列,而所有其他位置应仅占据一列。
2. 修饰项
可以注意到,在Activity示例中还添加了两个ItemDecoration实例。第一个修饰实例Insetdecoration应用于所有示例布局管理器,为每个子项提供页边距。第二个修饰实例ConnectorDecoration仅应用于垂直交错网络,用于在主要和次要网格项之间绘制连接线。以下三段代码定义了这些修饰实例:
提供嵌入式页边距的ItemDecoration
public class InsetDecoration extends RecyclerView.ItemDecoration {
private int mInsetMargin;
public InsetDecoration(Context context) {
super();
mInsetMargin = context.getResources()
.getDimensionPixelOffset(R.dimen.inset_margin);
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
//对子视图的所有4个边界应用计算得出的页边距
outRect.set(mInsetMargin, mInsetMargin, mInsetMargin, mInsetMargin);
}
}
提供连接线的ItemDecoration
public class ConnectorDecoration extends RecyclerView.ItemDecoration {
private Paint mLinePaint;
private int mLineLength;
public ConnectorDecoration(Context context) {
super();
mLineLength = context.getResources()
.getDimensionPixelOffset(R.dimen.inset_margin);
int connectorStroke = context.getResources()
.getDimensionPixelSize(R.dimen.connector_stroke);
mLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mLinePaint.setColor(Color.BLACK);
mLinePaint.setStyle(Paint.Style.STROKE);
mLinePaint.setStrokeWidth(connectorStroke);
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
final RecyclerView.LayoutManager manager = parent.getLayoutManager();
for (int i=0; i < parent.getChildCount(); i++) {
final View child = parent.getChildAt(i);
boolean isLarge = parent.getChildViewHolder(child).getPosition() % 3 == 0;
if (!isLarge) {
final int childLeft = manager.getDecoratedLeft(child);
final int childRight = manager.getDecoratedRight(child);
final int childTop = manager.getDecoratedTop(child);
final int x = childLeft + ((childRight - childLeft) / 2);
c.drawLine(x, childTop - mLineLength, x, childTop + mLineLength, mLinePaint);
}
}
}
}
res/values/dimens.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="inset_margin">8dp</dimen>
<dimen name="connector_stroke">2dp</dimen>
</resources>
ItemDecoration可以实现3个主要的回调。第一个回调是getItemOffsets(),它提供修饰器可用于对给定子视图应用页边距的Rect实例。在此例中,我们希望所有子视图具有相同的页边距,因此在每次调用中设置相同的值。
提示:
即使没有在getItemOffsets()中采用位置参数,也仍然可以在需要此位置确定如何应用页边距时,通过getChildViewHolder(view).getPosition()从RecyclerView获得位置。
另外两个调用——onDraw()和onDrawOver()提供修饰器可用于绘制其他内容的Canvas。这些方法分别在子视图的下方和上方绘制内容。ConnectorDecoration()使用onDraw()渲染任何可见子项之间的连接线。为此,我们遍历子视图,并且在未占据两列的每个子项(根据前面描述的交错查询)上绘制中心线。
这些绘制回调方法将在RecyclerView需要重绘时被调用,例如当内容滚动时,因此我们必须经常了解视图当前所在位置,以便知道在何处绘制线。相比于直接询问子视图左/右/上/下坐标,我们更喜欢通过getDecoratedXxx()从布局管理器请求此信息。这是因为其他修饰实例(例如InsetDecoration)可能在事后修改视图的边界,我们的绘制方法需要考虑这些因素。
3. 项动画
支持适配器数据集的变化动画的逻辑内置在每个布局管理器中。为了使管理器适当地确定如何在数据集改变时制作动画,我们必须使用特定于RecyclerView的适配器更新方法,而不是传统的notifyDataSetChanged()。
修改适配器数据含两个步骤:必须首先添加或删除数据项,然后适配器必须通知视图变化发生的确切位置。在本例中,数据项添加选项触发适配器上的notifyItemInserted(),而数据项删除选项触发notifyItemRemoved()。
Demo下载地址:
1.13 建立可扩展的集合视图
网友评论