首先我们一起分析一下这个界面给我们要怎么去实现。
1.最上面的ToolBar不用多解释,比较简单。
2.下面三个界面切换,可以使用TabLayout+ViewPager,切换3个Fragment。
3.我们要处理的主要是最左边(也就是“点菜”)这个Fragment中的内容,另外的两个Fragment不用管。
4.这个Fragment分为左右两侧,左边就是一个简单的ListView。
5.右侧的就稍微复杂一点。首先,右侧的列表需要分组,我们使用StickyListHeaders。
这是一个粘列表标题的三方控件。类似于Android联系人的列表效果。
配置它:
compile 'se.emilsjolander:stickylistheaders:2.7.0'
使用StickyListHeaders的主要代码如下:
public class GoodsFragmentGoodsAdapter extends BaseAdapter implements StickyListHeadersAdapter {
//处理分类条目头
public View getHeaderView(int position, View convertView, ViewGroup parent) {
return null;
}
//获取条目数据对应的分类
public long getHeaderId(int position) {
return 0;
}
}
首先,主页面的布局如下:
ToolBar+Tablayout+ViewPager,基本的组合,很简单 。
然后看它对应的Activity的代码:
public class SellerDetailActivity extends BaseActivity {
private Toolbar toolBar;
private String[] titles = {"商品", "评论", "商家"};
private TabLayout tabLayout;
private ViewPager viewPager;
private MyAdapter adapter;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_seller_detail);
toolBar = ((Toolbar) findViewById(R.id.toolbar));
toolBar.setTitle("南京大排档(德基店)");
setSupportActionBar(toolBar); //替换toolbar的相关配置需要在这个方法前完成
getSupportActionBar().setDisplayHomeAsUpEnabled(true); //显示返回键
tabLayout = ((TabLayout) findViewById(R.id.tabs));
// tabLayout.addTab();//添加
viewPager = ((ViewPager) findViewById(R.id.vp));
adapter = new MyAdapter(getSupportFragmentManager());
viewPager.setAdapter(adapter);
tabLayout.setupWithViewPager(viewPager);
}
//ViewPager的适配器
private class MyAdapter extends FragmentPagerAdapter {
public MyAdapter(FragmentManager fm) {
super(fm);
}
@Override
public Fragment getItem(int position) {
Fragment fragment = null;
switch (position) {
case 0:
fragment = new GoodsFragment();
break;
case 1:
fragment = new RecommendFragment();
break;
case 2:
fragment = new SellerFragment();
break;
}
return fragment;
}
@Override
public int getCount() {
return titles.length;
}
//ViewPager和Tablayout结合使用时候需要复写
@Override
public CharSequence getPageTitle(int position) {
return titles[position];
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
}
return super.onOptionsItemSelected(item);
}
}
这样就简单的实现了三个Fragment的切换,然后,我们着重要考虑的就是GoodsFragment的设计了。另外两个Fragment放个text展示即可。
下载效果如下:
如果你喜欢透明状态栏,可以配置下,那样顶部融为一体比较好看,这里我们要着重处理“商品”这个Fragment,就不再处理这些细节了。
GoodsFragment布局如下:
//这就是一个浮动的购物车,可以暂时不要
水平的LinearLayout,左侧放ListView,右侧放StickyListHeadersListView。
GoodsFragment代码如下:
public class GoodsFragment extends BaseFragment implements AdapterView.OnItemClickListener, AbsListView.OnScrollListener {
@InjectView(R.id.shl)
StickyListHeadersListView shl;
@InjectView(R.id.lv)
ListView lv;
private GroupAdapter groupAdapter;
private HeadAdapter headAdapter;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_goods, null);
ButterKnife.inject(this, view);
return view;
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
testData();
headAdapter = new HeadAdapter();
lv.setAdapter(headAdapter);
groupAdapter = new GroupAdapter();
shl.setAdapter(groupAdapter);
lv.setOnItemClickListener(this);
shl.setOnScrollListener(this);
}
/左侧--头信息的点击事件/
@Override
public void onItemClick(AdapterView> parent, View view, int position, long id) {
headAdapter.setSelectPosition(position);
Head head = heads.get(position);
shl.setSelection(head.groupFirstIndex);
isScroll = false;
}
/右侧--分组信息的滚动事件
boolean isScroll = false;
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
//这个方法触发才代表用户的滚动
isScroll = true;
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
//右侧分组信息滚动,左侧对应的头信息高亮处理
if (isScroll) {
Data data = datas.get(firstVisibleItem);
headAdapter.setSelectPosition(data.headIndex);
//判断头容器是否处于可见状态
//获取到第一个和最后一个可见的,比第一个小或比最后一个大的均为不可见
int firstVisiblePosition = lv.getFirstVisiblePosition();
int lastVisiblePosition = lv.getLastVisiblePosition();
if (data.headIndex >= lastVisiblePosition || data.headIndex <= firstVisiblePosition) {
lv.setSelection(data.headIndex);//可见处理
}
}
}
private ArrayList heads = new ArrayList<>();
class Head {
String info;
int groupFirstIndex;
}
private ArrayList datas = new ArrayList<>();
class Data {
public String info;
int headId;
int headIndex;
}
private void testData() {
for (int i = 0; i < 10; i++) {
Head head = new Head();
head.info = "头" + i;
heads.add(head);
for (int j = 0; j < 10; j++) {
Data data = new Data();
data.info = "普通条目" + j;
data.headId = i;
data.headIndex = i;
if (j == 0) {
head.groupFirstIndex = datas.size();
}
datas.add(data);
}
}
}
/右侧--分组信息的适配器/
private class GroupAdapter extends BaseAdapter implements StickyListHeadersAdapter {
@Override
public View getHeaderView(int position, View convertView, ViewGroup parent) {
int headIndex = datas.get(position).headIndex;
Head head = heads.get(headIndex);
TextView textView = new TextView(MyApplication.getContext());
textView.setText(head.info);
textView.setTextColor(Color.BLACK);
textView.setBackgroundColor(Color.parseColor("#BFBFBF"));
return textView;
}
@Override
public long getHeaderId(int position) {
return datas.get(position).headId;
}
@Override
public int getCount() {
return datas.size();
}
@Override
public Object getItem(int position) {
return datas.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
TextView textView = new TextView(MyApplication.getContext());
textView.setText(datas.get(position).info);
textView.setTextColor(Color.BLACK);
return textView;
}
}
//左侧头信息--ListView的adapter///
private class HeadAdapter extends BaseAdapter {
private int selectPosition;
@Override
public int getCount() {
return heads.size();
}
@Override
public Object getItem(int position) {
return heads.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
TextView textView = new TextView(MyApplication.getContext());
textView.setText(heads.get(position).info);
textView.setLayoutParams(new ListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 100));
textView.setGravity(Gravity.CENTER);
textView.setTextSize(16);
textView.setTextColor(Color.BLACK);
if (position == selectPosition) {
textView.setBackgroundColor(Color.WHITE);
} else {
textView.setBackgroundColor(Color.parseColor("#BFBFBF"));
}
return textView;
}
public void setSelectPosition(int selectPosition) {
if (this.selectPosition == selectPosition) {
return;
}
this.selectPosition = selectPosition;
notifyDataSetChanged();
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
ButterKnife.reset(this);
}
}
下面我会把上面的代码分开来按照具体的实现过程一一讲解,也是实现二级联动个的核心:
1.首先我们通过ButterKnife找到了控件。
2.我们需要为ListView和StickyListHeadersListView构建一些模拟数据,而这里有很明显的一点,就是ListView的
数据和StickyListHeadersListView的头信息的数据是完全一样的。
所以我们需要两组数据,一组是头数据(既用于ListView,又用于StickyListHeadersListView的分组头数据),一组是
普通条目的数据。这点理解了就可以分析下面的内容了。下面我们会把左侧的listView或右侧的分组头统称为头信
息,或头数据。因为他们本身也是一致的。
3.于是我们构建了两个实体类
/**
* 头信息---用于左侧的listView和右侧分组信息的头
*/
private ArrayList heads = new ArrayList<>();
class Head {
String info;
int groupFirstIndex; //点击头任意角标的时候,需要知道其对应组的第一条元素下标,用于点击头,将对应组信息置顶。
}
/**
* StickyListHeadersListView的普通条目信息
*/
private ArrayList datas = new ArrayList<>();
class Data {
public String info;
int headId; //进行分组操作,同组数据该字段值相同,可以是任意值
int headIndex; //当前普通条目对应的头数据所在集合的下标---也就是说,把头信息的position保存到分组数据中去。
}
刚开始的时候,我们不会想到要在Head里面添加groupFirstIndex字段,也不会想到在Data数据中添加headId和headIndex字段。只会都写上info字段,也就是头数据和普通条目最基本的数据。
然后我们模拟出测试数据:
private void testData() {
for (int i = 0; i < 10; i++) { //左侧--头信息
Head head = new Head();
head.info = "头" + i;
heads.add(head);
for (int j = 0; j < 10; j++) { //右侧--分组中的条目
Data data = new Data();
data.info = "普通条目" + j;
data.headId = i;
data.headIndex = i;
if (j == 0) { //在每个分组的第一条数据的时候,给头信息添加它的第一个元素的角标
head.groupFirstIndex = datas.size(); //0--10--20....
}
datas.add(data);
}
}
}
代码写到这里,刚才的几个字段理论上是还没有出现的理由的。
但是当我们给StickyListHeadersListView设置适配器的时候:
/右侧--分组信息的适配器/
private class GroupAdapter extends BaseAdapter implements StickyListHeadersAdapter {
//注意:这里面所有的position都是普通条目的position,这个position跟头数据无关
分组头信息的处理///
@Override
public View getHeaderView(int position, View convertView, ViewGroup parent) {
//获取头信息的position
int headIndex = datas.get(position).headIndex;
Head head = heads.get(headIndex);
TextView textView = new TextView(MyApplication.getContext());
textView.setText(head.info);
textView.setTextColor(Color.BLACK);
textView.setBackgroundColor(Color.parseColor("#BFBFBF"));
return textView;
}
@Override
public long getHeaderId(int position) {
//依据position获取普通条目,普通条目中存放了headId。这样就获取到了条目对应的分类
return datas.get(position).headId;
}
普通条目的处理
@Override
public int getCount() {
return datas.size();
}
@Override
public Object getItem(int position) {
return datas.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
TextView textView = new TextView(MyApplication.getContext());
textView.setText(datas.get(position).info);
textView.setTextColor(Color.BLACK);
return textView;
}
}
首先我们继承了BaseAdapter,因为这面有普通的条目信息,需要继承BaseAdapter ,然后因为我们使用的第三方的控件来处理分组的头信息,需要实现它的StickyListHeadersAdapter ,这样就需要复写2组方法,第一组就是对头信息的处理,第二组就是baseAdapter中的对普通条目进行处理的四个方法,如图。
在getView方法中处理普通的条目,构建一个TextView返回即可。
重点在于,在getHeaderId方法中我们要获取到头数据的id。看到这里,你应该清楚了,前面的headId 字段就是这个作用,我们在for循环的内层为每个Head数据设置了它的headId,那么在getHeaderId方法中就可以从普通条目中拿到它所对应的头数据的headId.
你可能会问,为什么不把headId储存在Head里面,而是储存在它的分组数据里面?
因为GroupAdapter 里面所有复写方法中的的position都是普通条目的position,这个position跟头数据无关。
所以我们把headId储存到普通条目中,通过datas.get(position).headId就可以拿到了。
然后在getHeaderView方法中要处理头信息。就需要拿到头数据的角标了。
//获取头信息的position
int headIndex = datas.get(position).headIndex;
Head head = heads.get(headIndex);
同样的,我们只能根据普通条目的position拿到普通条目,所以头数据的index还是要储存到普通条目中去。
再回头看看这两个实体类是不是豁然开朗了。实体来中还有一个字段groupFirstIndex后面解释。
然后我们处理了左侧的listView的适配器,这就很简单了。看代码HeadAdapter。
现在左右两侧的数据已经能够展示出来了:
怎么样。效果不错吧,不要介意界面美观的问题,UI的事情我们不管,主要是功能嘛。
别高兴得太早,下面的问题还多着呢?
左侧我们叫头容器,右侧叫条目容器。
1.在头容器中点击某个条目的时候,该条目背景高亮处理,同时让对应的该组信息在条目容器中置顶
2.条目容器滚动时,头容器跟着进行调整,包括背景高亮处理。
3.性能优化:避免频繁的刷新头信息。
首先我们给listView设置点击事件,看代码:
/左侧--头信息的点击事件/
@Override
public void onItemClick(AdapterView> parent, View view, int position, long id) {
// 1.高亮点击条目
headAdapter.setSelectPosition(position);
//2.点击头容器,分组容器对应分组信息进行置顶
Head head = heads.get(position);
shl.setSelection(head.groupFirstIndex);
}
public void setSelectPosition(int selectPosition) {
this.selectPosition = selectPosition;
notifyDataSetChanged();//刷新适配器
}
在listView中通过如下代码设置了高亮的判断
if (position == selectPosition) {
textView.setBackgroundColor(Color.WHITE);
} else {
textView.setBackgroundColor(Color.parseColor("#BFBFBF"));
}
然后条目容器中的置顶就要用到前面剩余没讲的那个字段了:groupFirstIndex。
shl.setSelection(head.groupFirstIndex);
class Head {
String info;
int groupFirstIndex; //点击头任意角标的时候,需要知道其对应组的第一条元素下标,用于点击头,将对应组信息置顶。
}
然后是StickyListHeadersListView的滚动事件的处理。
boolean isScroll = false;
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
//这个方法触发才代表用户的滚动
isScroll = true;
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
//右侧分组信息滚动,左侧对应的头信息高亮处理
if (isScroll) {
Data data = datas.get(firstVisibleItem);
headAdapter.setSelectPosition(data.headIndex);
//判断头容器是否处于可见状态
//获取到第一个和最后一个可见的,比第一个小或比最后一个大的均为不可见
int firstVisiblePosition = lv.getFirstVisiblePosition();
int lastVisiblePosition = lv.getLastVisiblePosition();
if (data.headIndex >= lastVisiblePosition || data.headIndex <= firstVisiblePosition) {
lv.setSelection(data.headIndex);//可见处理
}
}
}
其中:
Data data = datas.get(firstVisibleItem);
headAdapter.setSelectPosition(data.headIndex);
右侧滚动时,对应左侧头的高亮处理。
1.点击左侧头条目,会引起右侧StickyListHeadersListView的滚动,滚动就会触发onScroll方法。
又会重复触发headAdapter.setSelectPosition(data.headIndex)来设置头容器的定位(显然这里已经没必要了)
会重复调用setSelectPosition中的notifyDataSetChanged。也就会造成重复刷新了。
为了避免这个问题,我们可以通过打印日志来确定,但我们点击左侧的时候,会调用shl.setSelection(head.groupFirstIndex)来给条目容器分组进行置顶,而这个方法只会触发onScroll,不会触发onScrollStateChanged。也就是说,只有onScrollStateChanged方法触发才能代表用户的滚动。
所以我们添加了这个变量
boolean isScroll = false;
来记录是否是用户的滚动。然后在onScroll方法对这个变量进行判断,只有是用户滚动的时候才做相应处理。而单纯的点击左侧listview是不会触发滚动事件的回调的。
2.当右侧分组信息虽然在滑动,但仍然处在同一个分组时,没必要刷新界面。
这个我们在setSelectPosition的方法中添加了这个判断:
if (this.selectPosition == selectPosition) {
return;
}
如果是同一个分组,直接return,怎么样,这个处理是不是很细节。
以上,就实现了这种二级联动的效果了!并且对性能进行了很大的优化处理!