能够使头部悬停的listview在项目中是经常用到的,例如qq的好友列表或者地区选择。
先不多说,看效果图:(懒得上gif图了)
这里借鉴了别人的核心代码,我做了一些分析。
主要是使用PinnedSectionListView来替换listview。
这里的PinnedSectionListView是别人的。我们主要看如何使用这个PinnedSectionListView以及如何适配数据进去。
博文的最后我会上传demo,里面有PinnedSectionListView和完整项目。可以根据这篇博客来看看是如何适配数据进去的。
先看项目结构图:
其中:PinnedSectionListView是原封不动借鉴的别人的
其他的则是适配listview和数据的类
1,activity
activity里面很简单了
public class IndexActivity extends AppCompatActivity {
private PinnedSectionListView pinned_section_list;
private IndexAdapter indexAdapter;
private List<CityBean> data;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//实例化拥有悬停头的控件
pinned_section_list = (PinnedSectionListView) findViewById(R.id.pinned_section_list);
//模拟数据
data = new TestData().initData();
//初始化适配器
indexAdapter = new IndexAdapter(this, data);
//添加适配器
pinned_section_list.setAdapter(indexAdapter);
}
}
2,xml布局
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.android.list.PinnedSectionListView
android:id="@+id/pinned_section_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="@null"
android:footerDividersEnabled="false"
android:headerDividersEnabled="false" />
</RelativeLayout>
3,城市实体类
public class CityBean {
private int cityId;//城市id
private String cityName;//城市名
private int superiorId;//上级城市id
private List<CityBean> subordinateList;//下级城市集合
//忽略get/set方法
}
4,测试数据,模拟城市数据
public class TestData {
private List<CityBean> data;
public List<CityBean> initData() {
data = new ArrayList<>();
for (int j = 1; j < 11; j++) {
List<CityBean> beijingList = new ArrayList<>();
for (int i = 1; i < 11; i++) {
CityBean cityBean = new CityBean();
cityBean.setCityId(i + 10);
cityBean.setCityName("北京" + i + "区");
beijingList.add(cityBean);
}
CityBean cityBean = new CityBean();
cityBean.setCityId(1);
cityBean.setCityName("北京" + j);
cityBean.setSubordinateList(beijingList);
data.add(cityBean);
List<CityBean> shanghaiList = new ArrayList<>();
for (int i = 1; i < 11; i++) {
CityBean cityBean1 = new CityBean();
cityBean1.setCityId(i + 20);
cityBean1.setCityName("上海" + i + "区");
shanghaiList.add(cityBean1);
}
CityBean cityBean1 = new CityBean();
cityBean1.setCityId(2);
cityBean1.setCityName("上海" + j);
cityBean1.setSubordinateList(shanghaiList);
data.add(cityBean1);
List<CityBean> shenzhenList = new ArrayList<>();
for (int i = 1; i < 11; i++) {
CityBean cityBean2 = new CityBean();
cityBean2.setCityId(i + 20);
cityBean2.setCityName("深圳" + i + "区");
shenzhenList.add(cityBean2);
}
CityBean cityBean2 = new CityBean();
cityBean2.setCityId(3);
cityBean2.setCityName("深圳" + j);
cityBean2.setSubordinateList(shenzhenList);
data.add(cityBean2);
}
return data;
}
}
5,Item类,真正在listview中显示的对象
public class Item {
public static final int ITEM = 0;//判断是否是普通item
public static final int SECTION = 1;//判断是否是需要置顶悬停的item
public final int type;//外部传入的类型,ITEM或者SECTION
public final CityBean cityBean;//外部传入的数据,这里我们将它写成城市实体类,可以任意更换
public int sectionPosition;//头标记,一般用父数据的id标记
public int listPosition;//集合标记,一般用自身的id标记
public Item(int type, CityBean cityBean) {
this.type = type;
this.cityBean = cityBean;
}
//获得其中保存的数据
public CityBean getCityBean() {
return cityBean;
}
}
Item类用来保存外部传进来的数据和设置该数据的类型是属于悬停头还是普通item。
6,最重要的,adapter
悬停listview的适配器必须实现PinnedSectionListView中的接口:PinnedSectionListAdapter和android原生接口:SectionIndexer
public class IndexAdapter extends BaseAdapter implements PinnedSectionListView.PinnedSectionListAdapter, SectionIndexer {
//为了区分头部的背景颜色,也可换成其他方式。例如:布局样式
private static final int[] COLORS = new int[]{
R.color.green_light, R.color.orange_light,
R.color.blue_light, R.color.red_light};
private List<CityBean> data;//外部传进来的原始数据
private List<Item> items;//这个才是真正显示的list
private Context context;
private Item[] sections;//头标记数组
public IndexAdapter(Context context, List<CityBean> data) {
this.data = data;
this.context = context;
initSection();
}
//初始化显示数据
private void initSection() {
items = new ArrayList<>();
sections = new Item[data.size()];
//数据准备
for (int i = 0; i < data.size(); i++) {
//添加头信息,将头标记和数据传入
Item section = new Item(Item.SECTION, data.get(i));
section.sectionPosition = data.get(i).getCityId();//将父类城市id作为头id传入,因为父类id没有上级城市了,所以传自身id
section.listPosition = data.get(i).getCityId();//传入自身id,将当前城市id作为普通id传入
//头标记组中城市id的标记相对应放入该城市的Item实例
sections[section.sectionPosition] = section;
items.add(section);
//当前城市的下级城市
for (int j = 0; j < data.get(i).getSubordinateList().size(); j++) {
//下级城市为普通item,所以传入Item.ITEM
Item item = new Item(Item.ITEM, data.get(i).getSubordinateList().get(j));
item.sectionPosition = data.get(i).getCityId();//将父类城市id作为头id传入,证明该普通id下的城市属于哪个父类城市
item.listPosition = data.get(i).getSubordinateList().get(j).getCityId();//传入自身id,将当前城市id作为普通id传入
items.add(item);
}
}
}
/*************************
* 以下是PinnedSectionListView的重写方法
*************************************************/
//当前view是否属于固定的item
@Override
public boolean isItemViewTypePinned(int viewType) {
return viewType == Item.SECTION;
}
/*************************
* 以下是adapter的重写方法
*************************************************/
//传入视图类型的个数,表示有几种视图类型
//PinnedSectionListView中如果发现adapter的getViewTypeCount<2会抛出异常
@Override
public int getViewTypeCount() {
return 2;
}
//返回每一个视图的类型
@Override
public int getItemViewType(int position) {
return getItem(position).type;
}
@Override
public int getCount() {
return items.size();
}
@Override
public Item getItem(int position) {
return items.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View view, ViewGroup parent) {
View v;
ViewHolder vh;
Item item = items.get(position);// 从集合中获取当前行的数据
if (view == null) {
// 说明当前这一行不是重用的
// 加载行布局文件,产生具体的一行
v = View.inflate(context, R.layout.item, null);
// 创建存储一行控件的对象
vh = new ViewHolder();
// 将该行的控件全部存储到vh中
vh.tvName = (TextView) v.findViewById(R.id.text_text);
v.setTag(vh);// 将vh存储到行的Tag中
} else {
v = view;
// 取出隐藏在行中的Tag--取出隐藏在这一行中的vh控件缓存对象
vh = (ViewHolder) view.getTag();
}
// 从ViewHolder缓存的控件中改变控件的值
// 这里主要是避免多次强制转化目标对象而造成的资源浪费
vh.tvName.setText(item.getCityBean().getCityName());
if (item.type == Item.SECTION) {
v.setBackgroundColor(parent.getResources().getColor(COLORS[item.sectionPosition % COLORS.length]));
}
return v;
}
// 存储一行中的控件(缓存作用)---避免多次强转每行的控件
class ViewHolder {
TextView tvName;
}
/************************
* 以下是SectionIndexer接口的重写方法
******************************************/
//返回一个对象数组代表列表的部分,用于来显示头
//这里返回的Item[]是装的头的部分
@Override
public Item[] getSections() {
return sections;
}
//给定的索引部分数组内的部分对象,返回在适配器部分的起始位置。
@Override
public int getPositionForSection(int sectionIndex) {
//以免抛出异常
if (sectionIndex >= sections.length) {
sectionIndex = sections.length - 1;
}
//返回当前头集合的头id
return sections[sectionIndex].listPosition;
}
//给定一个适配器内的位置,返回相应的索引部分数组内的部分对象。
@Override
public int getSectionForPosition(int position) {
if (position >= getCount()) {
position = getCount() - 1;
}
//返回当前item中保存的头id
return getItem(position).sectionPosition;
}
}
整个项目的代码就是这些,有兴趣的可以看看PinnedSectionListView中是如何实现的。我看了一部分,因为时间关系没有继续研究了。