1. 程式人生 > >Android雙向列表左右聯動效果

Android雙向列表左右聯動效果

轉載請作明出處:

開篇

前一段時間有一個需求,需要做一個左右雙向聯動的列表。類似於餓了麼點餐頁面,但是遠沒有那麼複雜,只是用於展示的一個頁面。不是很複雜,做完已經很久了,目前為止沒有出現什麼問題,所以在此將它分享給大家。

番外

此功能依賴於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,希望可以幫到大家。