目录
概述
HeaderRecycleViewAdapter
这是一个通用的RecycleView.Adapter
,可以不需要继承即可简单的使用.此Adapter
实现了带头部的处理显示,不需要使用头部显示时可以使用其简单版本SimpleRecycleViewAdapter
.
- 适用范围
任何类型的数据
支持
LinearLayoutManager
及GridLayoutManager
支持竖直方向及横向方向
暂时不支持
reverseLayout
,即反方向布局 - 其它
对于文中提到一些不太能马上理解的点,后面基本都会有更详细地说明.
关于头部的实现
recycleView
的视图view
是自己维护的,adapter
仅仅只是提供了数据和做数据绑定的一个操作.因此header
的实现也必须通过这种形式进行.
使用过RecycleView.Adapter
的应该都知道继承Adapter
需要实现两个抽象方法,实际可以实现的方法还有很多,有一些也是必须的.
//获取adapter中的item数量
public int getItemCount();
//根据位置获取item的视图类型
public int getItemViewType(int position);
通过方法getItemViewType(int position)
我们就可以实现在正常的数据中添加item
的头部了.根据位置确定需要显示为头部的位置,然后返回头部的viewType
类型,再通过“加载出头部的视图即可.
关于具体的实现方式
固定头部的实现参考了一部分为RecyclerView打造通用Adapter,链接中也提到如何去为RecycleView
添加头部,但是个人觉得那咱添加方式有点过于难理解,如果说到通用肯定是使用方式更简单,操作更容易会更加方便合理.
所以针对这个对实现header
的方式进行了修改,不再需要自己实现某些处理的接口,完全可以小白方式的操作使用.需要做的仅仅是要自己绑定所有的item
及header
显示的数据.
header
的实现方式
- 数据源形式为
List<List>
的形式,将需要显示不同的header
的分组以不同的列表分隔开,每一个列表即对应了一个header
header
数据通过Map<>
传入Adapter
,通过分组列表的索引自动匹配对应的header
数据,再回调绑定header
.
//数据源如下,若数据源中存在两个列表
List<List<String>> groupList;
//header数据创建
//key必须是0和1,因为列表只有两个,索引必定是0和1
//可以不放入对应key的数据或者放入其它key的数据,但是相应的该部分的header绑定数据将返回null
Map<Integer,String> headerMap;
headerMap.put(0,"header1");
headerMap.put(1,"header2");
由于header
是动态添加上去的,所以并不需要提前将header
的数据添加到数据源的列表中,这样可以很好地分离数据源和header
之间的关联,当需要从adapter
中获取数据源进行处理数据时,也不会受到添加的header
的影响.
动态添加header
的问题
header
是Adapter
通过计算添加入的List<List>
分组数据源列表自动添加到RecycleView
中显示出来的,所以存在的问题是,原数据源的位置position
将会被改变,由于header
的存在.这是个不可避免的一个问题.
由于position
往往是被用在处理该项或都获取数据源中某项数据来使用,所以解决方案从这两个使用方面入手.
- 在某项
itemView
的单击返回接口中,将完整地提供position/groudId/childId
(分别对应位置,分组索引与分组的列表内的索引)
/**
* 带header的item单击事件
*
* @param groupId 当前item所在分组,分组ID从0开始
* @param childId 当前item在所有分组中的ID,从0开始,当此值为-1时,当前为该分组的头部
* @param position 当前item所有分组的位置(header也会占用一个位置,请注意)
* @param viewId 当前响应的单击事件view的ID,若为rootView,则该值为{@link #ROOT_VIEW_ID}
* @param isHeader 当前item是否为header
* @param rootView 当前item的rootView
* @param holder
*/
public void onItemClick(int groupId, int childId, int position, int viewId, boolean isHeader, View rootView, HeaderRecycleViewHolder holder);
Adapter
提供了相应的与位置及实际数据源相关的方法
//根据位置获取非header位置的item数据
public T getItem(int position);
//根据数据源的分组索引及分组内列表的索引,获取非header位置的item数据,若该位置为header,返回null
public T getItem(int groupId, int childId)
//根据position计算分组的索引及分组内列表的数据项索引
public Point getGroupIdAndChildIdFromPosition(List<Integer> eachGroupCountList, int position, boolean isShowGroup);
//根据分组数据列表获取分组数据量的列表
public List<Integer> getEachGroupCountList(List<List<T>> groupList);
header
的添加与计算方式
header
的添加中需要处理的东西并不多,但可能会相对复杂一些,有几个很重要的点是必须解决的.
Adapter
的数据项总数- 计算位置对应的数据源的数据项索引
- 判断某个位置是否为
header
对于以上两个需求其实都依赖于数据源的数据量进行处理.首先需要处理数据源,得到数据源的每个分组列表中的数据量.
//若数据源为 List<List<String>> groupList;
//计算数据源的分组列表中数据量并缓存起来
List<Integer> eachGroupCountList = new ArrayList<Integer>(groupList.size());
for (List group : groupList) {
eachGroupCountList.add(group == null ? 0 : group.size());
}
得到数据源列表数据量之后,Adapter
的数据项总数就很容易计算出来了.并将计算的数据项总数记录下来,作为getItemCount()
的返回数据,这样就不需要每一次getItemCount()
的时候都要重新计算一次了.
if (eachGroupCountList != null) {
for (int groupCount : eachGroupCountList) {
//若显示Header将Header添加到总数据量中,每一个Header占用一行
mCount += 1 + groupEachLine;
}
}
最后是最重要的一部分了,通过某个位置计算数据源中数据项的索引.由于数据源是使用List<List>
二维的数据形式,所以一个数据项的索引必须有两个值(使用Point
类来表示),其中point.x
表示哪一个分组,point.y
表示分组内列表的索引.
if (eachGroupCountList != null) {
for (int groupCount : eachGroupCountList) {
//获取分组的数据量
childId = groupCount;
//将当前position计算与分组数据量进行计算
//1表示header
position = position - 1 - childId;
//当position小于0时,说明当前的位置在该分组列表中
if (position < 0) {
//回滚到该列表中的数据项索引
//此时groupId即为分组的索引
//childId即为分组内列表的索引
childId += position;
break;
}
//每计算完一组分组ID添加1
groupId++;
}
//new Point(groupId,childId)即为当前位置的数据项在数据源中的索引
}
以上为如何通过位置计算出数据源的方式;得到的结果已经包含了两种情况:
- 数据源数据项:
groupList.get(point.x).get(point.y)
header
:若为header
,则该索引值的groupId是指当前header
所在的分组,而childId则为-1.
上面的计算中需要注意的是:position
本身是正数,通过不断地按顺序排除分组而到所在的分组索引时,计算后得到的childId就是分组内列表的索引.(建议举个实际的例子和数据代入计算一下就非常清晰了)
总之,计算
header
的结果所得的Point
,groupId为分组的索引,而childId<0时表示当前位置为header
,否则childId为分组内列表的索引
关于数据相关的绑定接口
HeaderRecycleAdapter
本质还是扩展自RecycleView.Adapter
,所以必然是需要实现关于item界面的加载及数据绑定的功能.正常的情况下是通过继承实现此功能,但是在这里通过接口的方式.使用这种处理方式有两个好处.
- 将界面与数据绑定的功能独立出来
HeaderRecycleAdapter
仅仅是处理显示界面,并不处理数据的绑定功能,会更加清晰地分离逻辑与界面. HeaderRecycleAdapter
更加专注并且独立不需要任何继承或者修改就可以直接进行使用,对于不同的item数据,仅需要实现
IHeaderAdapterOption
接口即可处理不同的界面.切换显示不同的内容时甚至不需要重新创建一个
Adapter
,只需要把HeaderRecycleAdapter
中的IHeaderAdapterOption
替换即可.
/**
* 带头部的adapter配置接口
* 其中参数T为每个item对应的设置的数据类型
* 参数H为每个header对应设置的数据类型
*/
public interface IHeaderAdapterOption<T, H> {
/**
* 不存在headerView类型,{@value Integer#MIN_VALUE}
*/
public static final int NO_HEADER_TYPE = Integer.MIN_VALUE;
/**
* 获取headerView的类型,headerView类型专用
*
* @param groupId 分组类型
* @return
*/
public int getHeaderViewType(int groupId, int position);
/**
* 获取View的类型
*
* @param position 位置
* @param groupId
* @param childId
* @param isHeaderItem
* @param isShowHeader 是否显示header
* @return
*/
public int getItemViewType(int position, int groupId, int childId, boolean isHeaderItem, boolean isShowHeader);
/**
* 根据ViewType获取加载的当前项layoutID
*
* @param viewType
* @return
*/
public int getLayoutId(int viewType);
/**
* 设置Header显示绑定数据
*
* @param groupId 当前组ID
* @param header 当前Header数据,来自于Map
* @param holder
*/
public void setHeaderHolder(int groupId, H header, HeaderRecycleViewHolder holder);
/**
* 设置子项ViewHolder
*
* @param groupId 分组ID
* @param childId 当前组子项ID
* @param position 当前项位置(此位置为recycleView中的位置)
* @param itemData
* @param holder
*/
public void setViewHolder(int groupId, int childId, int position, T itemData, HeaderRecycleViewHolder holder);
}
查看以上绑定数据的接口IHeaderAdapterOption
可见,这里绑定数据还分为两个不同的内容,一个是header
的数据绑定,一个是普通的子view数据绑定,在绑定数据时也会更加清晰,降低出错的可能.
关于内置的OnItemClickListener
接口
关于item的单击监听事件,此处为内置的一个接口,返回的item信息更加具体;但是对一般的
RecycleView
添加item监听事件时,可以参考下面的博客:RecyclerView无法添加onItemClickListener最佳的高效解决方案,需要注意的是该方案暂时只能提供对整个item的单击监听事件
由于这是一个列表展示,往往是需要处理某一项单击处理的事件,所以HeaderReycleViewHolder
提供了内置的item处理接口.
接口中提供了position/groupId/childId/viewId/isHeader/viewHolder
(分别对应被单击item的位置/分组索引/分组内列表的索引/被单击view的ID/是否为header
/viewHolder).
/**
* 带header的item单击事件
*
* @param groupId 当前item所在分组,分组ID从0开始
* @param childId 当前item在所有分组中的ID,从0开始,当此值为-1时,当前为该分组的头部
* @param position 当前item所有分组的位置(header也会占用一个位置,请注意)
* @param viewId 当前响应的单击事件view的ID,若为rootView,则该值为{@link #ROOT_VIEW_ID}
* @param isHeader 当前item是否为header
* @param rootView 当前item的rootView
* @param holder
*/
public void onItemClick(int groupId, int childId, int position, int viewId, boolean isHeader, View rootView, HeaderRecycleViewHolder holder);
- 处理
header
和非header
数据项分别可以通以下方式:
if(isHeader){
//当前单击位置为header
//获取当前位置的header: holder.getAdapter().getHeader(groupId);
}else{
//当前单击位置为某个数据项
//获取当前位置的数据项: holder.getAdapter().getItem(groupId,childId);
}
- 通过
HeaderRecycleViewHolder
注册监听方法由于item中可能存在很多不同的控件,所以
HeaderRecycleViewHolder
中也提供了根据view的ID对指定view单击事件进行注册监听的功能.
//给指定ID的某个view注册单击监听事件
public boolean registerViewOnClickListener(int viewId, OnItemClickListener listener) {
if (mItemClickMap == null) {
mItemClickMap = new ArrayMap<Integer, OnItemClickListener>(10);
}
View view = this.getView(viewId);
if (view != null) {
//若该view有效,则缓存起其注册的监听事件
mItemClickMap.put(viewId, listener);
view.setOnClickListener(this);
return true;
} else {
return false;
}
}
从以上代码可以看出,注册的监听事件是被缓存起来的,当view被单击触发事件时若该view的ID与缓存的监听事件匹配则会回调事件.
这里也提供了对整个item(rootView)的注册监听.当rootView被注册了监听事件时,该item的任何控件被触发单击事件时,都只会触发rootView的监听事件.
rootView的监听事件不会清除其它控件的监听事件,但会优先于所有控件的监听事件,并且当rootView监听事件存在时,其它监听事件将被忽略.(当移除rootView的监听事件时,其它监听事件可以正常响应)
//注册rootView的单击监听事件
public void registerRootViewItemClickListener(OnItemClickListener listener) {
mRootViewClickListener = listener;
mRootView.setOnClickListener(this);
}
GridLayoutManager添加header
以上对头部的添加处理都是针对LinearLayoutManager类型的RecycleView
,除了线性布局之外,其实网格布局使用率也是不低的,所以要实现GridLayoutManager添加头部.
由于GridLayoutManager是将多个item置于同一行显示的,所以当需要添加头部时,就必须确定如何将多行显示为1行.
通过查阅资料可以知道,GridLayoutManager可以设置一个SpanSizeLookup
的类,该类只有一个抽象方法getSpanSize(int position)
,该方法决定了当前GridLayoutManager需要显示的某个位置的item占用了多少个网格的空间.
当我们确定GridLayoutManager每行的显示的网格数时,即可实现该类的抽象方法,返回header
需要占用的网格数.
HeaderSpanSizeLookup
由以上分析我们需要自己实现该抽象方法以正确返回header
占用的网格数.由于SpanSizeLookup
是通过item的位置确定该位置需要返回显示的网格数,所以我们需要知道当前position
位置的item是否为一个header
.
考虑到自定义SpanSizeLookup
的扩展性和尽可能与其它类解耦,原本是将HeaderRecycleAdapter
作为内部成员参数保留在lookup中从而进行检测当前位置的item是否为header
.但后来修改为使用一个接口来实现这些操作.这样在既使不是使用HeaderRecycleAdapter
的情况下也可以实现对应的接口从而活用此类.
- 以下为接口
ISpanSizeHandler
的方法
//是否特殊的item,如header或者某些特别的item
public boolean isSpecialItem(int position);
//获取特殊item占用的网格数,此方法仅会在是特殊item时才调用
public int getSpecialItemSpanSize(int spanCount, int position);
//获取正常item占用的网格数,默认值理论上应该为1,但可以由实现类决定,此方法仅会在非特殊item时调用
public int getNormalItemSpanSize(int spanCount,int position);
从以上方法可以得出其实这个接口和HeaderSpanSizeLookup
并不局限于对header
item的检测,只要是特殊的item需要改变在GridLayoutManager中占用的网格数都可以实现此接口和通过HeaderSpanSizeLookup
对界面显示进行布局的.
HeaderRecycleAdapter
默认实现了此接口,所以可以直接使用到HeaderSpanSizeLookup
中
HeaderGridLayoutManager
当需要使用GridLayoutManager显示header
时,只需要为GridLayoutManager通过setSpanSizeLookup(SpanSizeLookup)
设置HeaderSpanSizeLookup
即可.
//假设已经存在headerAdapter(HeaderRecycleAdapter)
//rv(RecyclerView)
//创建一个普通的GridLayoutManager
GridLayoutManager layoutManager=new GridLayoutManager(this,3);
//创建一个基于GridLayoutManager的HeaderSpanSizeLookup
//其中HeaderRecycleAdapter已经实现了ISpanSizeHandler
HeaderSpanSizeLookup lookup=new HeaderSpanSizeLookup(layoutManager.getSpanCount(),headerAdapter);
//将lookup设置为GridLayoutManager使用的SpanSizeLookup
layoutManager.setSpanSizeLookup(lookup);
rv.setLayoutManager(layoutManager);
以上为直接使用HeaderSpanSizeLookup
结合GridLayoutManager实现带header
的GridView.
但是有时可能会在运行中改动显示的GridLayoutManager展示的每行网格数(spanCount),在这种情况下,如果不去修改HeaderSpanSizeLookup
中对应的setSpanCount(int)
,则会导致最终显示出来的header
是不正常的.
所以增加了一个自定义的GridLayoutManager,用于处理HeaderSpanSizeLookup
与GridLayoutManager关联的数据,在需要对GridLayoutManager进行任何调整时,直接对其进行修改就可以将修正的数据反应到HeaderSpanSizeLookup
中.
//假设已经存在headerAdapter(HeaderRecycleAdapter)
//rv(RecyclerView)
//直接创建一个专用于header的GridLayoutManager
HeaderGridLayoutManager layoutManager=new HeaderGridLayoutManager(this,3,headerAdapter);
//对于需要修改GridLayoutManager的配置时可以直接设置
//如:layoutManager.setSpanCount(5)相关的对象都会自动更新
//需要注意的是setSpanSizeLookup()时只能设置为HeaderSpanSizeLookup的类型
rv.setLayoutManager(layoutManager);
对于HeaderGridLayoutManager
来说修改的内容并不多,主要是重写了setSpanSizeLookup()
和setSpanCount()
两个方法.
- 覆盖
setSpanSizeLookup(SpanSizeLookup)
//参数必须是 HeaderSpanSizeLookup 的类型,因为这是专用于处理不同item显示不同的网格空间的GridLayoutManager
if (spanSizeLookup instanceof HeaderSpanSizeLookup) {
mLookup = (HeaderSpanSizeLookup) spanSizeLookup;
super.setSpanSizeLookup(spanSizeLookup);
} else {
throw new IllegalArgumentException("spanSizeLookup must be HeaderSpanSizeLookup");
}
- 覆盖
setSpanCount(int)
//在通过GridLayoutManager更新spanCount时,也一并更新HeaderSpanSizeLookup中的spanCount,确保设置同步以正确显示界面布局
super.setSpanCount(spanCount);
if (mLookup != null) {
mLookup.setSpanCount(spanCount);
}
附加功能–是否显示header
对于HeaderRecycleAdapter
主要是为了解决显示header
的item,附加的功能也是跟header
相关的.有时候在某些情况下可能不需要暂时性不需要显示header
(虽然暂时还想不出有什么情况…),所以预留了这个功能.
//设置当前是否显示header
public void setIsShowHeader(boolean isShowHeader) {
//当isShowHeader的设置不一样时,进行显示的调整
if (mIsShowHeader != isShowHeader) {
//更新当前的item数量(少了header位置信息会变动)
updateCount(mEachGroupCountList, isShowHeader);
mIsShowHeader = isShowHeader;
}
}
以上的是对是否显示header
进行设置,设置中并没有设置完毕后直接notifyDataChanged
,所以当设置后数据没有改变时,需要手动调用adapter进行通知RecycleView
更新数据.
使用示例
HeaderRecycleAdapter
使用起来是比较简单的,只需要提供数据源,头部数据,还有自己实现数据绑定IHeaderAdapterOption
接口即可.
- 创建数据源及头部数据
mGroupList = new LinkedList<List<String>>();
mHeaderMap = new ArrayMap<Integer, String>();
int groupId = 0;
int count = 0;
count = groupId + 10;
//数据源使用 List<List> 的数据结构
for (; groupId < count; groupId++) {
int childCount = 8;
List<String> childList = new ArrayList<String>(childCount);
for (int j = 0; j < childCount; j++) {
childList.add("child - " + j);
}
mGroupList.add(childList);
//头部数据使用与分组索引对应的 Map<Integer,xxx>
mHeaderMap.put(groupId, "title - " + groupId);
}
- 实现
IHeaderAdapterOption
接口,自定义数据的绑定显示
//创建数据绑定option时可以设置泛型数据的类型,第一个为子item的数据类型,第二个为header的数据类型
private class HeaderAdapterOption implements HeaderRecycleAdapter.IHeaderAdapterOption<String, String> {
//获取头部layout的类型
@Override
public int getHeaderViewType(int groupId, int position) {
return -1;
}
//获取子item layout的类型
@Override
public int getItemViewType(int position, int groupId, int childId, boolean isHeaderItem, boolean isShowHeader) {
if (isHeaderItem) {
return getHeaderViewType(groupId, position);
} else {
return 0;
}
}
//根据view的类型返回对应的layoutId,用于adapter加载界面
@Override
public int getLayoutId(int viewType) {
switch (viewType) {
case 0:
case NO_HEADER_TYPE:
return R.layout.item_content_2;
case -1:
return R.layout.item_header;
default:
return R.layout.item_content;
}
}
//设置头部数据绑定
@Override
public void setHeaderHolder(int groupId, String header, HeaderRecycleViewHolder holder) {
//注册rootView的监听事件
holder.registerRootViewItemClickListener(MainActivity.this);
//获取holder的缓存的子view进行数据绑定
TextView tv_header = holder.getView(R.id.tv_header);
if (tv_header != null) {
tv_header.setText(header.toString());
}
}
//子item数据绑定,类似头部数据绑定
//参数提供了完整地子item的分组索引,分组内列表的索引,当前item所在的位置
@Override
public void setViewHolder(int groupId, int childId, int position, String itemData, HeaderRecycleViewHolder holder) {
holder.registerRootViewItemClickListener(MainActivity.this);
TextView tv_content = holder.getView(R.id.tv_content);
tv_content.setText(itemData.toString());
}
}
- 创建
HeaderRecycleAdapter
并绑定到RecycleView
//创建 adapter
//参数要求包括 context,option,数据源及头部数据
HeaderRecycleAdapter adapter=new HeaderRecycleAdapter<String, String>(this, new HeaderAdapterOption(), mGroupList, mHeaderMap);
//绑定到recycleView
rv.setAdapter(adapter);
简单版的SimpleRecycleAdapter
SimpleRecycleAdapter
是不带header
的Adapter
,其实跟普通的创建一个adapter的结果是没有什么区别的.这里只是为了某些时间方便使用所以添加的一个简单版不带header
的adapter.
同时相对于一般的adapter,SimpleRecycleAdapter
继承自HeaderRecycleAdapter
,所以保存了其大部分的特点.包括方便地注册监听事件,所以某些情况下使用简单版也可以节省很多不必要的时间和代码编写.
SimpleRecycleAdapter
的使用方式与HeaderRecycleAdapter
是一致的,创建数据源及数据绑定option即可.通过以上的示例我们知道只有分组的情况下会显示Header
,所以SimpleRecycleAdapter
内部其实只是覆盖和修改了部分设置.
- 修改数据源为一维数据
List<>
//本质还是创建了一个List<List>的数据源,只是这里作了另一层转换,调用者可以不需要自己创建二维数据源,直接使用一维数据源即可
//一维数据源永远只放在第一项.并且不存在其它的一维数据源(这样就不会有不同的分组了)
public void setItemList(List<T> itemList) {
List<List<T>> groupList = this.getGroupList();
if (groupList == null) {
groupList = new LinkedList<List<T>>();
}
groupList.clear();
groupList.add(itemList);
this.setGroupList(groupList);
mItemList = itemList;
}
- 修改头部显示功能
//覆盖头部显示的功能,永远不启用显示头部的功能
public void setIsShowHeader(boolean isShowHeader) {
super.setIsShowHeader(false);
}
- 简化了
IHeaderAdapterOption
IHeaderAdatperOption
接口是针对带header
的数据绑定,当然不显示header
时,仅会调用到子item的数据绑定部分的代码.对此继承自该接口实现了一个简化版的
SimpleAdapterOption
由于简单版adapter不需要显示
header
,因此位置position也是正常的数据位置,不再受header
影响.所以直接与普通的adapter一样根据位置返回item类型并绑定数据即可.不需要显示
header
的时候,使用此SimpleAdapterOption
更加简单方便,不建议直接实现IHeaderAdaperOption
接口,可能有部分方法会造成混乱
//继承自 IHeaderAdapterOption 接口的简单Adapter配置抽象类
public static abstract class SimpleAdapterOption<T> implements IHeaderAdapterOption<T, Object> {
@Override
public int getItemViewType(int position, int groupId, int childId, boolean isHeaderItem, boolean isShowHeader) {
return getViewType(position);
}
@Override
public int getHeaderViewType(int groupId, int position) {
return NO_HEADER_TYPE;
}
@Override
public void setHeaderHolder(int groupId, Object header, HeaderRecycleViewHolder holder) {
//简单Adapter不处理Header,所以此方法不需要使用到,空实现
}
@Override
public void setViewHolder(int groupId, int childId, int position, T itemData, HeaderRecycleViewHolder holder) {
setViewHolder(itemData, position, holder);
}
//获取ViewType
public abstract int getViewType(int position);
//设置view数据绑定
public abstract void setViewHolder(T itemData, int position, HeaderRecycleViewHolder holder);
}
事实上,完全可以不使用
SimpleRecycleAdapter
创建不带头部的adapter,直接用HeaderRecycleAdapter
并设置其setIsShowHeader(boolean)
为false即可,简单版的实现原理其实也是基于这个.
小结
通过以上说明的原理,可以在不需要自己计算header
的位置,只需要将需要分组的数据源正确地分为不同的列表并创建对应的List<List>
的分组数据源HedaerRecycleAdapter
即可自动计算并显示出header
.
相比需要自己通过重写Adapter
的方式去处理需要返回的header
的位置,会更加容易和方便.当然关于数据的绑定方面还是需要自己完成了.
示例图片
GitHub地址
https://github.com/CrazyTaro/RecycleViewAdatper
资源下载
不想下载github项目的,或者不使用AS只需要类文件的,可以到以下下载地址直接下载类文件:
http://download.csdn.net/detail/u011374875/9556686
回到目录