轉載請做明出處:juejin.im/post/5b878e…android
前一段時間有一個需求,須要作一個左右雙向聯動的列表。相似於餓了麼點餐頁面,可是遠沒有那麼複雜,只是用於展現的一個頁面。不是很複雜,作完已經好久了,目前爲止沒有出現什麼問題,因此在此將它分享給你們。git
此功能依賴於BaseRecyclerViewAdapterHelper的adapter框架編寫,若是各位看官沒有接觸過這個庫,請先點擊連接,大體瞭解事後再回來看此文章,事半功倍(強烈推薦此庫,太好用了)。 github
<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,因此數據格式只是簡單的處理了一下,在正常開發的時候,這個格式須要同後臺同事協商好):bash
//獲取數據(若請求服務端數據,請求到的列表需有序排列)
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用來點擊左側列表而後定位右側列表位置的,當中妙處,沒必要多言,請往下看)。ide
可能你們都注意到了,右側有一個分類的title懸停在列表之上,那下一步就從這裏開始。佈局
首先是構建adapter以及給右側初始化一些數據:post
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的佈局(綠)。對應位置見下圖: ui
右側的初始化工做完成以後,就來到了本文的重點部分,也就是監聽右側列表的滑動。添加一個監聽:recRight.addOnScrollListener()。裏面重寫兩個方法:onScrollStateChanged()和onScrolled()。
首先來看onScrollStateChanged(),裏面只有一句:
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
//獲取右側title的高度
tHeight = rightTitle.getHeight();
}
複製代碼
而後咱們再看onScrolled(),幾乎全部的重點代碼都在這個方法裏面,因此我就拆開一步一步分析(代碼中基本每一句都有註釋,因此在文章中就不過多的解釋了):
//判斷若是是組名的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);
}
}
}
複製代碼
//由於每次滑動以後,右側列表中可見的第一個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());
}
}
複製代碼
//遍歷左邊列表,列表對應的內容等於右邊的title,則設置左側對應item高亮
for (int i = 0; i < left.size(); i++) {
if (left.get(i).equals(rightTitle.getText().toString())) {
leftAdapter.selectItem(i);
}
}
複製代碼
//若是右邊最後一個徹底顯示的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,但願能夠幫到你們。