Recycleview双列表联动与悬停

3,775 阅读11分钟

前言

最近看了别人的一篇blog,也是实现recycleview的双列表联动,同时应用了MVP框架。于是就模仿写了一个类似的双列表联动与悬停。在MVP方面,我仿照的是官方的todo-mvp,感觉写得有点不伦不类了,这里就不详述,另外在实现需求方面,和那个大神相比,也做了许多改变,当然有些具体的难点我没想到,参照了他的思路,然后实现出来了。在开发中,也尝试了其他的方法:
1.在点击左边省份时,若右边的省份在下面不可见区域,会出现bug,后面会详述。
2.滑动右边城市区域是,想和左边的省份联动,当时也用了其他方法,但是失败了,后面也会具体讲到。
最后想说一点:看别人的代码要有耐心,刚开始看的时候也是一脸懵逼,后面静下心来慢慢看,看到后面就觉得很简单了。多看,多写,多尝试,多思考。

实现的功能

先直接上一个效果图:

show.gif
show.gif

然后再来分析实现的功能:
1.点击左边省份,省份背景改变,右边顶部显示省份悬停,下面显示省份的城市;
2.滑动右边的城市,顶部省份悬停,左边随着省份的改变而改变;
3.所有控件可点击;
4.仿官方mvpdemo的mvp框架;
5.snackbar的使用;
6.recycleview的花式使用。

下面在来分析下实现的思路:

思路

之前就说了思路是非常重要的,下面来详细说说如果有一个这种需求,改从何下手:
先看效果分析大概:
1:可以把左边和右边的布局各设置为一个Recycleview;
2:点击左边省份,右边需要跟着滑动,是不是可以计算需要滑动的距离,然后通过recycleview的方法进行指定滑动呢。只是一种猜想,实际比这复杂一点;
3:滑动右边的城市,然后左边省份需要跟着滑动,更改背景颜色,试想下,通过判断滑动的position,然后计算滑动到了哪个省份,进行改变呢。

接下来我再来详细说下实现的过程:

1.通过RecycleView加载左边的省份,省份是在一个String-array下定义的,然后获取资源,通过适配器加载出来;
2.通过RecycleView加载右边的数据,这个时候要注意了,因为左边数据和右边数据是对应的,所以我们通过遍历String-array下的省份,然后往不同的省份注入不同的城市,从效果可以看出来,我们右边不只是简单的布局,可以看出来上面有一个title(省份),下面是显示content(城市),我们通过在设置城市数据时候给一个isTitle来区分省份和城市,后面布局也通过isTitle来区分,我们可以看出右边省份是一行只有一个数据,而城市有三个数据。然后通过不同的布局显示出来。到这里,我们的将数据显示出来了。
3.点击省份,背景颜色改变,就是讲点击的item设置成你想要的颜色,没有点击的就是其他颜色了,通过position判断,右边的城市需要滑动,主要通过计算滑动的position。比较麻烦,后面具体讲。
4.滑动右边的数据,前面我们在加载城市数据的时候,我们将城市和省份通过一个tag进行了绑定,当我们滑动的时候,获取这个tag(position),让他与左边的position比较,不相同的话,就把tag赋值给position,有了这个position,我们就可以更改背景颜色了;
5.由于滑动的时候会出现bug,我们将左边选中的省份一直显示在屏幕中间。通过recycleView设置下就可以。
6.所有都可点击,recycleView的点击事件属于RecycleView的基础,不清楚的可以看我的一个demo:

github.com/Simon986793…

实现过程

实现过程也按照思路来

1.加载左边城市的数据:

通过RecycleView加载左边的省份,省份是在一个String-array下定义的,然后获取资源,通过适配器加载出来;

  String [] province=getResources().getStringArray(R.array.province);//获取省份
        final List<String> list= Arrays.asList(province);
        /*
        适配数据和设置监听事件
         */
        adapter=new ProvinceRvAdapter(this, list, new ItemClickListener() {
            @Override
            public void onItemClick(View view, int position) {
                Utils.showSnackBar(recycleview,list.get(position));
                mposition=position;
                startMove(position,true);
                Log.i(">>>>>>","position:"+position);
                moveToCenter(position);
            }
        });
        recycleview.setAdapter(adapter);

2.加载右边城市数据:

通过RecycleView加载右边的数据,这个时候要注意了,因为左边数据和右边数据是对应的,所以我们通过遍历String-array下的省份,然后往不同的省份注入不同的城市,从效果可以看出来,我们右边不只是简单的布局,可以看出来上面有一个title(省份),下面是显示content(城市),我们通过在设置城市数据时候给一个isTitle来区分省份和城市,后面布局也通过isTitle来区分,我们可以看出右边省份是一行只有一个数据,而城市有三个数据。然后通过不同的布局显示出来。到这里,我们的将数据显示出来了。

1.先看下bean对象:

public class CityBean {
    public String city;
    public boolean isTitle;//判断是否为省份,来进行加载数据
    public String province;
    public String tag;//一个position,同时将城市与省份绑定
    public void setTitle(boolean title)
    {
        isTitle=title;
    }
    public void setProvince (String province)
    {
        this.province=province;

    }
    public String getProvince()
    {
        return province;
    }
    public boolean isTitle()
    {
        return  isTitle;
    }
    public void setCity(String city)
    {
        this.city=city;
    }
    public String getCity()
    {
        return city;
    }
    public void setTag(String tag)
    {
        this.tag=tag;
    }
    public String getTag()
    {
        return tag;
    }
}

2.获取数据源: 通过遍历省份,给省份添加城市数据;

  for (int i=0;i<province.length;i++)
            {
                CityBean titleBean=new CityBean();
                titleBean.setProvince(province[i]);
                titleBean.setTitle(true);//设置为title
                titleBean.setTag(String.valueOf(i));//设置tag,方便获取position
                list.add(titleBean);

                for (int j=0;j<citylist.get(i).length;j++)
                {
                    CityBean cityBean=new CityBean();
                    cityBean.setCity(citylist.get(i)[j]);
                    cityBean.setTag(String.valueOf(i));//设置成和省份一样的tag,将省份与城市绑定。
                    list.add(cityBean);
                }


            }

3.通过设置的isTitle与否来加载数据:

 int itemViewTtpe=CityRvAdapter.this.getItemViewType(position);
            switch (itemViewTtpe)
            {
                case 0://省份
                    title.setText(list.get(position).getProvince());
                    break;
                case 1://城市
                    city.setText(list.get(position).getCity());
                    break;
                case 2:
                    break;
            }

具体就不详细讲,代码中也有。

左边联动右边(省份联动城市)

点击省份,背景颜色改变,就是讲点击的item设置成你想要的颜色,没有点击的就是其他颜色了,通过position判断,右边的城市需要滑动,主要通过计算滑动的position。

1.背景的改变:

获取点击的position,传到adapter中,然后进行判断,进行背景改变:

贴出关键代码:

 if (position==clickPositon)
            {
                view.setBackgroundColor(Color.parseColor("#9EABF4"));
                textView.setTextColor(Color.parseColor("#ffffff"));
            }
            else {
                view.setBackgroundColor(Color.parseColor("#00FFFFFF"));//设置为透明的,因为白色会覆盖分割线
                textView.setTextColor(Color.parseColor("#1e1d1d"));
            }
            textView.setText(s);
        }

2.左边联动右边:

建议先看这篇blog,滑动定位的解决方案:
blog.csdn.net/tyzlmjj/art…

通过计算需要滑动的距离来进行滑动

我们先计算需要滑动的position:

 for (int i=0;i<position;i++)//position 为点击的position
        {
            Log.i("<<<<<<",i+":"+cityFragment.citylist.get(i).length);
            counts+=cityFragment.citylist.get(i).length;//计算需要滑动的城市数目
        }
        if (isLeft)
        {
            cityFragment.setCounts(counts+position);//加上title(省份)数目
        }

官方提供了两种滑动方案:
1.scrollToPosition(int)
滑动到指定的item

2.scrollBy(int x,int y) 滑动到指定的距离

一开始用的scrollToPosition(int),但是在实际开发终于到了问题

scrollToposition 只能将item显示出来,至于显示在哪里他就不管了,不过有一点可以肯定的,若item从不可见滑动到可见,一般会出现在最底部,而我们需要的是在最顶部,显然是不行的。我们可以通过用scrollToPosition()和scrollBy 结合使用。

我们可以将滑动分为三种情况: 第一种:从上往下滑动(目标item不可见),这种最复杂,需要scrollToPosition()和scrollBy 结合使用,监听scroll接口;

第二种:从上往下滑动(目标item可见),scrollBy就可以解决;

第三种:从下往上滑动,目标可见不可见一样,调用scrollToPosition()都会显示在顶部;

贴出具体代码与我尝试的其他方法(在注释中)

 int firstItem=gridLayoutManager.findFirstVisibleItemPosition();//获取屏幕可见的第一个item的position
        int lastItem=gridLayoutManager.findLastVisibleItemPosition();//获取屏幕可见的最后一个item的position
        if (moveCounts<firstItem)
        {
            recyclerView.scrollToPosition(moveCounts);
        }
        else if (moveCounts<lastItem)
        {
            View aimsView=recyclerView.getChildAt(moveCounts-firstItem);
            int top =aimsView.getTop();
            recyclerView.scrollBy(0,top);
        }
        else {
            /*
            当往下滑动的position大于可见的最后一个item的时候,调用 recyclerView.scrollToPosition(moveCounts);
            只能讲item滑动到屏幕的底部。
             */
            /*
            第一种方案:先将item移动到底部,然后在调用scrollBy移动到顶部。不可行,不能讲item滑动到顶部,
            离上面还有一小段距离;
             recyclerView.scrollToPosition(moveCounts);
            int top=recyclerView.getHeight();
            recyclerView.scrollBy(0,top);

            第二种方案:直接计算要滑动的距离。程序崩溃,报空指针。看系统源码可知,当
            滑动的距离大于ChildCount(可见的item数目),将返回空。
            int top=recyclerView.getChildAt(moveCounts-firstItem).getTop();
            recyclerView.scrollBy(0,top);

            第三种解决方案:先将目标item滑动到底部,然后进行异步处理。调用滚动监听方法RecyclerViewListener,滑动到顶部。

             */

//            int top=recyclerView.getHeight();
//            recyclerView.scrollBy(0,top);
//            int childcount=recyclerView.getChildCount();
//            Log.i("<<<<<<<<<<","childcount"+childcount);
//            int top=recyclerView.getChildAt(moveCounts-firstItem).getTop();
//            recyclerView.scrollBy(0,top);

            recyclerView.scrollToPosition(moveCounts);
            move=true;
        }

监听回调,从底部滑动到顶部:

class RecyclerViewListener extends RecyclerView.OnScrollListener{
        /*
        监听回调,滑动结束回调。
         */
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);
            //在这里进行第二次滚动(最后的100米!)
            if (move ){
                move = false;
                //获取要置顶的项在当前屏幕的位置,moveCount是记录的要置顶项在RecyclerView中的位置
                int n = moveCounts - gridLayoutManager.findFirstVisibleItemPosition();
                if ( 0 <= n && n < recyclerView.getChildCount()){
                    //获取要置顶的项顶部离RecyclerView顶部的距离
                    int top = recyclerView.getChildAt(n).getTop();
                    //最后的移动
                    recyclerView.scrollBy(0, top);
                }
            }
        }
            /*
            监听回调,滑动状态改变回调
             */
        @Override
        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
            super.onScrollStateChanged(recyclerView, newState);

            if (move&&newState==RecyclerView.SCROLL_STATE_IDLE)
            {
                move=false;
                int n=moveCounts-gridLayoutManager.findFirstVisibleItemPosition();
                if (0<=n&&n<recyclerView.getChildCount())
                {
                    int top=recyclerView.getChildAt(n).getTop();
                    recyclerView.scrollBy(0,top);
                }
            }
        }

这样我们就讲左边联动右边完成了。

右边联动左边

滑动右边的数据,前面我们在加载城市数据的时候,我们将城市和省份通过一个tag进行了绑定,当我们滑动的时候,获取这个tag(position),让他与左边的position比较,不相同的话,就把tag赋值给position,有了这个position,我们就可以更改背景颜色了;

贴出关键代码:

 if (!TextUtils.equals(tag, currentTag)) {
            currentTag = tag;
            Log.i("zhangcong",currentTag);
            Integer integer = Integer.valueOf(currentTag);
            mCheckListener.check(integer, false);
        }

最后我们调用一个接口回调方法,让左边的省份背景改变。

小bug

由于右边滑动的时候左边省份背景改变了,但是不在可见view中,我们将左边选中的省份一直显示在屏幕中间。通过recycleView设置下就可以。

代码:

//将当前选中的item居中
    public   void moveToCenter(int position) {
        //将点击的position转换为当前屏幕上可见的item的位置以便于计算距离顶部的高度,从而进行移动居中
        Log.i(">>>>>>>>>",position - manager.findFirstVisibleItemPosition()+"eeeee");
        int itemPosition=position-manager.findFirstVisibleItemPosition();
        /*
        当往上滑动太快,会出现itemPosition为-1的情况。做下判断
         */
        if (0<itemPosition&&itemPosition<manager.getChildCount())
        {
            View childAt = recycleview.getChildAt(position - manager.findFirstVisibleItemPosition());
            Log.i("<<<<<<",position - manager.findFirstVisibleItemPosition()+"");

            int y = (childAt.getTop() - recycleview.getHeight() / 2);
            Log.i("<<<<<<",childAt.getTop()+"ssssss");
            Log.i("<<<<<<", y+"");
            recycleview.smoothScrollBy(0, y);
        }

    }

遇到的问题

在做右边联动左边的时候,当时想着获取title的position,然后将position传过去,进行背景的改变,这种方法是可行的,但是体验非常的不好,因为当你滑动很快的时候,是获取不到中间经过的省份的position,就感觉不连贯,直接跳到了最后你滑动的位置,所以换了一种解决思路(如果你以为是我想出来的你就太年轻了)。

总结

总结的话就没有,前面思路讲的已经够清楚了。最后给大家一点看代码的兴趣,这里面还缺少了许多的城市和省份,大家看懂了这些代码的话可以在里面加上自己的城市,或者加上图片展示功能;然后提交pull request,我这边会帮你们merge的(请在添加代码的地方加上你自己的注释 如: Simon add shanghai city on 20170727)。也非常欢迎大家在底下留言,探讨一些问题,当然有看不明白的也可以提出问题,我会尽力解答的。大家觉得有帮助的话麻烦给我的github来一个star吧。

最后是github地址

github.com/Simon986793…