Android双向列表左右联动效果

转载请作明出处://www.greatytc.com/p/82ced8707f2b

开篇

前一段时间有一个需求,需要做一个左右双向联动的列表。类似于饿了么点餐页面,但是远没有那么复杂,只是用于展示的一个页面。不是很复杂,做完已经很久了,目前为止没有出现什么问题,所以在此将它分享给大家。

番外

此功能依赖于BaseRecyclerViewAdapterHelper的adapter框架编写,如果各位看官没有接触过这个库,请先点击链接,大致了解过后再回来看此文章,事半功倍(强烈推荐此库,太好用了)。


先看一下效果图吧,下图是做好的demo的效果图:
效果图

首先是布局,布局文件是两个RecyclerView,和一个TextView组成。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rec_left"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:background="@color/white" />

    <View
        android:layout_width="0.5dp"
        android:layout_height="match_parent"
        android:background="@color/divider" />

    <FrameLayout
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="3">

        <android.support.v7.widget.RecyclerView
            android:id="@+id/rec_right"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

        <TextView
            android:id="@+id/right_title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/divider"
            android:paddingBottom="@dimen/dp10"
            android:paddingLeft="@dimen/dp13"
            android:paddingRight="@dimen/dp10"
            android:paddingTop="@dimen/dp10"
            android:singleLine="true"
            android:textColor="@color/black"
            android:textSize="@dimen/sp16" />
    </FrameLayout>
</LinearLayout>

功能

然后再看一下数据格式吧(由于是自己写的demo,所以数据格式只是简单的处理了一下,在正常开发的时候,这个格式需要同后台同事协商好):

//获取数据(若请求服务端数据,请求到的列表需有序排列)
private void initData() {
    left = new ArrayList<>();
    left.add("第一组");
    left.add("第二组略略略略略略略");
    left.add("第三组哈哈哈哈哈哈哈哈哈哈hahahahahahaha");
    left.add("第四组哈哈哈哈哈嗝~");
    left.add("第五组");
    left.add("第六组哎呀我去");
    left.add("第七组");

    right = new ArrayList<>();

    right.add(new ScrollBean(true, left.get(0)));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("1111111", left.get(0))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("1111111", left.get(0))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("1111111", left.get(0))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("1111111", left.get(0))));

    right.add(new ScrollBean(true, left.get(1)));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("2222222", left.get(1))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("2222222", left.get(1))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("2222222", left.get(1))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("2222222", left.get(1))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("2222222", left.get(1))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("2222222", left.get(1))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("2222222", left.get(1))));

    right.add(new ScrollBean(true, left.get(2)));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("3333333", left.get(2))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("3333333", left.get(2))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("3333333", left.get(2))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("3333333", left.get(2))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("3333333", left.get(2))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("3333333", left.get(2))));

    right.add(new ScrollBean(true, left.get(3)));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("4444444", left.get(3))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("4444444", left.get(3))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("4444444", left.get(3))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("4444444", left.get(3))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("4444444", left.get(3))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("4444444", left.get(3))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("4444444", left.get(3))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("4444444", left.get(3))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("4444444", left.get(3))));

    right.add(new ScrollBean(true, left.get(4)));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("5555555", left.get(4))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("5555555", left.get(4))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("5555555", left.get(4))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("5555555", left.get(4))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("5555555", left.get(4))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("5555555", left.get(4))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("5555555", left.get(4))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("5555555", left.get(4))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("5555555", left.get(4))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("5555555", left.get(4))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("5555555", left.get(4))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("5555555", left.get(4))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("5555555", left.get(4))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("5555555", left.get(4))));

    right.add(new ScrollBean(true, left.get(5)));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("6666666", left.get(5))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("6666666", left.get(5))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("6666666", left.get(5))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("6666666", left.get(5))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("6666666", left.get(5))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("6666666", left.get(5))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("6666666", left.get(5))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("6666666", left.get(5))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("6666666", left.get(5))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("6666666", left.get(5))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("6666666", left.get(5))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("6666666", left.get(5))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("6666666", left.get(5))));

    right.add(new ScrollBean(true, left.get(6)));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("7777777", left.get(6))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("7777777", left.get(6))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("7777777", left.get(6))));
    right.add(new ScrollBean(new ScrollBean.ScrollItemBean("7777777", left.get(6))));

    for (int i = 0; i < right.size(); i++) {
        if (right.get(i).isHeader) {
            //遍历右侧列表,判断如果是header,则将此header在右侧列表中所在的position添加到集合中
            tPosition.add(i);
        }
    }
}

首先是左侧列表的数据集,很简单,只是一个String类型的list集合。而右侧列表有人看了可能会懵,这个是我为什么想让大家看了上面那个adapter库之后再看这篇文章的原因,右侧的数据格式是brvah专门应对这种情况而定义的一种数据格式。简单来说,每一组中间我都空着一行来区分,每一组的第一条数据(也就是那行短的)是右侧组名的数据,其余等长的则是每一组的item数据。组名的先不说,单说item的bean,里面有两个参数,一个是String类型的文本内容,一个是区分分组的String类型的type。代码如下:

public static class ScrollItemBean {
    private String text;
    private String type;

    public ScrollItemBean(String text, String type) {
        this.text = text;
        this.type = type;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }
}

然后在最下面有一个循环,遍历右侧数据集合,将右侧组名所在的position拿出来存在一个集合中(此position用来点击左侧列表然后定位右侧列表位置的,当中妙处,不必多言,请往下看)。
可能大家都注意到了,右侧有一个分类的title悬停在列表之上,那下一步就从这里开始。
首先是构建adapter以及给右侧初始化一些数据:

rightManager = new GridLayoutManager(mContext, 3);

if (rightAdapter == null) {
    rightAdapter = new ScrollRightAdapter(R.layout.scroll_right, R.layout.layout_right_title, null);
    recRight.setLayoutManager(rightManager);
    recRight.addItemDecoration(new RecyclerView.ItemDecoration() {
        @Override
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
            super.getItemOffsets(outRect, view, parent, state);
            outRect.set(dpToPx(mContext, getDimens(mContext, R.dimen.dp3))
                    , 0
                    , dpToPx(mContext, getDimens(mContext, R.dimen.dp3))
                    , dpToPx(mContext, getDimens(mContext, R.dimen.dp3)));
        }
    });
        recRight.setAdapter(rightAdapter);
} else {
    rightAdapter.notifyDataSetChanged();
}

rightAdapter.setNewData(right);

//设置右侧初始title
if (right.get(first).isHeader) {
    rightTitle.setText(right.get(first).header);
}

解释一波,setNewData()是adapter库中自带的一个方法,也就是给adapter添加数据,此处不提。下面一个方法,是给右侧列表上方悬停title设置初始数据的,其中有一个成员变量:first,这个参数是一个int类型的数值,记录右侧列表中可见的第一个item的position。first我抽取了一个成员变量,并且给了一个默认值0。

//记录右侧当前可见的第一个item的position
private int first = 0;

再看看new Adapter里面的两个布局,相信您看完上面的链接,就会知道:前一个布局文件是右侧列表item的布局(红),后面一个布局则是右侧列表分类title的布局(绿)。对应位置见下图:


重点

右侧的初始化工作完成之后,就来到了本文的重点部分,也就是监听右侧列表的滑动。添加一个监听:recRight.addOnScrollListener()。里面重写两个方法:onScrollStateChanged()和onScrolled()。

首先来看onScrollStateChanged(),里面只有一句:

@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
    super.onScrollStateChanged(recyclerView, newState);
    //获取右侧title的高度
    tHeight = rightTitle.getHeight();
}

然后我们再看onScrolled(),几乎所有的重点代码都在这个方法里面,所以我就拆开一步一步分析(代码中基本每一句都有注释,所以在文章中就不过多的解释了):

1.先是右侧顶部title设置偏移量的方法(右侧当前显示哪一组,顶部title就显示该组名):

//判断如果是组名的item
if (right.get(first).isHeader) {
    //获取此组名item的view
    View view = rightManager.findViewByPosition(first);
    if (view != null) {
        //如果此组名item顶部和父容器顶部距离大于等于title的高度,则设置偏移量
        if (view.getTop() >= tHeight) {
            rightTitle.setY(view.getTop() - tHeight);
        } else {
            //否则不设置
            rightTitle.setY(0);
        }
    }    
}

2.title的偏移量处理完了之后,就是title赋值的问题了:

//因为每次滑动之后,右侧列表中可见的第一个item的position肯定会改变,并且右侧列表中可见的第一个item的position变换了之后//,才有可能改变右侧title的值,所以这个方法内的逻辑在右侧可见的第一个item的position改变之后一定会执行
int firstPosition = rightManager.findFirstVisibleItemPosition();
if (first != firstPosition && firstPosition >= 0) {
    //给first赋值
    first = firstPosition;
    //不设置Y轴的偏移量
    rightTitle.setY(0);

    //判断如果右侧可见的第一个item是否是header,设置相应的值
    if (right.get(first).isHeader) {
        rightTitle.setText(right.get(first).header);
    } else {
        rightTitle.setText(right.get(first).t.getType());
    }
}

3.title的赋值问题之后,是与左侧关联的方法:

//遍历左边列表,列表对应的内容等于右边的title,则设置左侧对应item高亮
for (int i = 0; i < left.size(); i++) {
    if (left.get(i).equals(rightTitle.getText().toString())) {
        leftAdapter.selectItem(i);
    }
}

4.最后就是一些细节上面的处理了:

//如果右边最后一个完全显示的item的position,等于bean中最后一条数据的position(也就是右侧列表拉到底了),
//则设置左侧列表最后一条item高亮
if (rightManager.findLastCompletelyVisibleItemPosition() == right.size() - 1) {
    leftAdapter.selectItem(left.size() - 1);
}

右侧Adapter也没有什么可看的,就是brvah中封装的一个adapter,里面只是给右侧组名和item赋值而已,附上代码:

public class ScrollRightAdapter extends BaseSectionQuickAdapter<ScrollBean, BaseViewHolder> {

    public ScrollRightAdapter(int layoutResId, int sectionHeadResId, List<ScrollBean> data) {
        super(layoutResId, sectionHeadResId, data);
    }

    @Override
    protected void convertHead(BaseViewHolder helper, ScrollBean item) {
        helper.setText(R.id.right_title, item.header);
    }

    @Override
    protected void convert(BaseViewHolder helper, ScrollBean item) {
        ScrollBean.ScrollItemBean t = item.t;
        helper.setText(R.id.right_text, t.getText());
    }
}

至此为止,右侧列表的处理,也就是本文中最重要的一部分已经都结束了,那我们再来看看左侧列表吧。由于左侧的处理只是点击,所以除了初始化左侧RecyclerView之外,只有一个点击事件,代码如下:

private void initLeft() {
    if (leftAdapter == null) {
        leftAdapter = new ScrollLeftAdapter(R.layout.scroll_left, null);
        recLeft.setLayoutManager(new LinearLayoutManager(mContext, LinearLayoutManager.VERTICAL, false));
        recLeft.addItemDecoration(new DividerItemDecoration(mContext, DividerItemDecoration.VERTICAL));
        recLeft.setAdapter(leftAdapter);
    } else {
        leftAdapter.notifyDataSetChanged();
    }

    leftAdapter.setNewData(left);

    leftAdapter.setOnItemChildClickListener(new BaseQuickAdapter.OnItemChildClickListener() {
        @Override
        public void onItemChildClick(BaseQuickAdapter adapter, View view, int position) {
            switch (view.getId()) {
                //点击左侧列表的相应item,右侧列表相应的title置顶显示
                //(最后一组内容若不能填充右侧整个可见页面,则显示到右侧列表的最底端)
                case R.id.item:
                    leftAdapter.selectItem(position);
                    rightManager.scrollToPositionWithOffset(tPosition.get(position), 0);
                    break;
            }
        }
    });
}

这就是左侧RecyclerView的初始化以及联动的处理方法,最后看一看左侧adapter吧:

public class ScrollLeftAdapter extends BaseQuickAdapter<String, BaseViewHolder> {

    private List<TextView> tv = new ArrayList<>();

    public ScrollLeftAdapter(int layoutResId, @Nullable List<String> data) {
        super(layoutResId, data);
    }

    @Override
    protected void convert(BaseViewHolder helper, String item) {
        helper.setText(R.id.left_text, item)
                .addOnClickListener(R.id.item);
        //将左侧item中的TextView添加到集合中
        tv.add((TextView) helper.getView(R.id.left_text));
        //设置进入页面之后,左边列表的初始状态
        if (tv != null && getData() != null && tv.size() == getData().size()) {
            selectItem(0);
        }

        helper.getView(R.id.item).setSelected(true);
    }
}

这里面有一个selectItem(),这个也就是本文最后一个需要注意的地方了,此方法是右侧列表滑动,左侧跟着联动的一个方法。使用此方法需要传一个参数,这个position是右侧组名所对应的position(知道上面为何要循环取出右侧列表组名所对应的position了吧)。附上代码:

//传入position,设置左侧列表相应item高亮
public void selectItem(int position) {
    for (int i = 0; i < getData().size(); i++) {
        if (position == i) {
            tv.get(i).setBackgroundColor(0xff0068b7);
            tv.get(i).setTextColor(ContextCompat.getColor(mContext, R.color.white));

            //以下是指定某一个TextView滚动的效果
            tv.get(i).setEllipsize(TextUtils.TruncateAt.MARQUEE);
            tv.get(i).setFocusable(true);
            tv.get(i).setFocusableInTouchMode(true);
            tv.get(i).setMarqueeRepeatLimit(-1);
        } else {
            tv.get(i).setBackgroundColor(0xffffffff);
            tv.get(i).setTextColor(ContextCompat.getColor(mContext, R.color.black));

            //失去焦点则停止滚动
            tv.get(i).setEllipsize(TextUtils.TruncateAt.END);
            tv.get(i).setFocusable(false);
            tv.get(i).setFocusableInTouchMode(false);
            tv.get(i).setMarqueeRepeatLimit(0);
        }
    }
}

判断方法中,前两行是对应item的背景和字体颜色的改变,下面四行,是一个跑马灯的效果(左侧item中,我设置了只显示一行,对应的item中的内容如果显示不下,则无限滚动显示)。

结尾

好了,demo中比较重要的方法我都罗列出来了,也贴出了相应的代码,最后还是忍不住推荐一下这个adapter库:BaseRecyclerViewAdapterHelper,这个库真的是太好用了,我现在几乎所有的项目包括本篇文章涉及到的内容,都在用这个,强烈推荐!!!最后附上demo,希望可以帮到大家。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,585评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,283评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,760评论 0 324
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,461评论 1 266
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,280评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,268评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,656评论 3 385
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,322评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,629评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,691评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,445评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,299评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,694评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,982评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,244评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,642评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,829评论 2 335