Android:高仿百度外卖、美团、淘点点二级联动效果!

美团,百度外卖的左右二级联动效果如下:


Android:高仿百度外卖、美团、淘点点二级联动效果!_第1张图片

具体的效果建议打开手机软件玩玩。

分析

首先我们一起分析一下这个界面给我们要怎么去实现。

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展示即可。

下载效果如下:


Android:高仿百度外卖、美团、淘点点二级联动效果!_第2张图片

如果你喜欢透明状态栏,可以配置下,那样顶部融为一体比较好看,这里我们要着重处理“商品”这个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。

现在左右两侧的数据已经能够展示出来了:


Android:高仿百度外卖、美团、淘点点二级联动效果!_第3张图片

怎么样。效果不错吧,不要介意界面美观的问题,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,怎么样,这个处理是不是很细节。

以上,就实现了这种二级联动的效果了!并且对性能进行了很大的优化处理!

你可能感兴趣的:(二级联动)