選單ListView聯動內容RecyclerView(帶吸頂效果)
這兩天測試發的bug修得差不多了,有點屬於自己的時間,寫了個仿美團/京東的ListView聯動Demo,現供大家參考參考,如有bug或更好的實現方式,望大家多多指出。
先來看看效果
一、首先我們來看看MainActivity.java 這裡有data和view的處理。data為測試用的,view為左邊一個ListView以及介面卡AdapterLeft和右邊RecyclerView和介面卡AdapterRight,提醒下RecyclerView別忘了setLayoutManager()。
package com.zyf.linkagelistview;
import android.app.Activity;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import com.zyf.linkagelistview.adapter.AdapterLeft;
import com.zyf.linkagelistview.adapter.AdapterRight;
import com.zyf.linkagelistview.bean.Bean;
import java.util.ArrayList;
public class MainActivity extends Activity {
private static final String TAG = MainActivity.class.getSimpleName();
private ListView mListViewLeft;
private AdapterLeft mAdapterLeft;
private RecyclerView mListViewRight;
private AdapterRight mAdapterRight;
private ArrayList<Bean> dataList = new ArrayList<>();
private ArrayList<String> titleList = new ArrayList<>();
private ArrayList<Integer> titlePosList = new ArrayList<>();
private String mCurTitle = "";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
initData();
}
private void initView(){
mListViewLeft = (ListView) findViewById(R.id.listview_left);
mAdapterLeft = new AdapterLeft(this, titleList);
mListViewLeft.setAdapter(mAdapterLeft);
mListViewLeft.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int pos, long l) {
mAdapterLeft.setSelection(pos);
if (null != titleList && titleList.size()>pos)
mAdapterRight.setSelection(pos);
}
});
mListViewRight = (RecyclerView) findViewById(R.id.listview_right);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
mListViewRight.setLayoutManager(linearLayoutManager);
mAdapterRight = new AdapterRight(this, dataList, titlePosList, mListViewRight);
mListViewRight.addItemDecoration(new ItemDecoration(this, dataList, new ItemDecoration.OnDecorationCallback() {
@Override
public String onGroupId(int pos) {
if (dataList.get(pos).getTitle() != null)
return dataList.get(pos).getTitle();
return "-1";
}
@Override
public String onGroupFirstStr(int pos) {
if (dataList.get(pos).getTitle() != null)
return dataList.get(pos).getTitle();
return "";
}
@Override
public void onGroupFirstStr(String title) {
for (int i=0; i<titleList.size(); i++){
if (!mCurTitle.equals(title) && title.equals(titleList.get(i))){
mCurTitle = title;
mAdapterLeft.setSelection(i); // 設定左邊ListView選中item
Log.i(TAG, "onGroupFirstStr: i = "+i);
}
}
}
}));
mListViewRight.setAdapter(mAdapterRight);
}
/**
* 資料
*/
private void initData(){
titlePosList.add(0);
for (int i=0; i<5; i++){
Bean bean = new Bean();
bean.setTitle("0");
bean.setText("zzzz");
dataList.add(bean);
}
titleList.add(dataList.get(dataList.size()-1).getTitle());
titlePosList.add(dataList.size());
for (int i=0; i<15; i++){
Bean bean = new Bean();
bean.setTitle("1");
bean.setText("xxxx");
dataList.add(bean);
}
titleList.add(dataList.get(dataList.size()-1).getTitle());
titlePosList.add(dataList.size());
for (int i=0; i<20; i++){
Bean bean = new Bean();
bean.setTitle("2");
bean.setText("cccc");
dataList.add(bean);
}
titleList.add(dataList.get(dataList.size()-1).getTitle());
titlePosList.add(dataList.size());
for (int i=0; i<10; i++){
Bean bean = new Bean();
bean.setTitle("3");
bean.setText("dddd");
dataList.add(bean);
}
titleList.add(dataList.get(dataList.size()-1).getTitle());
mAdapterLeft.notifyDataSetChanged();
mAdapterRight.notifyDataSetChanged();
}
}
其佈局也就一個ListView和一個RecyclerView
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<ListView
android:id="@+id/listview_left"
android:layout_width="100dp"
android:layout_height="match_parent"
android:scrollbars="none"/>
<android.support.v7.widget.RecyclerView
android:id="@+id/listview_right"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="none"/>
</LinearLayout>
二、資料需要的Bean
package com.zyf.linkagelistview.bean;
/**
* Created by zyf on 2017/5/8.
*/
public class Bean {
private String title;
private String text;
public Bean() {
}
public Bean(String title, String text) {
this.title = title;
this.text = text;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
@Override
public String toString() {
return "Bean{" +
"title='" + title + '\'' +
", text='" + text + '\'' +
'}';
}
}
三、兩個介面卡
1、左邊ListView介面卡AdapterLeft.java,方法setSelection(int selection)設定選中其中的item。此處佈局就一個TextView就不貼布局程式碼了。
package com.zyf.linkagelistview.adapter;
import android.content.Context;
import android.graphics.Color;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
import com.zyf.linkagelistview.R;
import java.util.ArrayList;
/**
* Created by zyf on 2017/5/8.
* 左邊ListView介面卡
*/
public class AdapterLeft extends BaseAdapter {
private static final String TAG = AdapterLeft.class.getSimpleName();
private Context mContext;
private ArrayList<String> mDataList = new ArrayList<>();
private int mSelection = 0;
public AdapterLeft(Context mContext, ArrayList<String> mDataList) {
this.mContext = mContext;
this.mDataList = mDataList;
}
@Override
public int getCount() {
if (null != mDataList)
return mDataList.size();
return 0;
}
@Override
public Object getItem(int i) {
if (null != mDataList)
return mDataList.get(i);
return null;
}
@Override
public long getItemId(int i) {
return 0;
}
@Override
public View getView(int position, View view, ViewGroup viewGroup) {
ViewHolder viewHolder = null;
if (null == view){
viewHolder = new ViewHolder();
view = LayoutInflater.from(mContext).inflate(R.layout.item_left, null);
viewHolder.textContent = (TextView) view.findViewById(R.id.text_content);
view.setTag(viewHolder);
}else {
viewHolder = (ViewHolder) view.getTag();
}
// 設定被選中的item的字型顏色
if (null != viewHolder.textContent && mSelection == position){
viewHolder.textContent.setTextColor(Color.RED);
}else {
viewHolder.textContent.setTextColor(Color.BLACK);
}
if (null != viewHolder.textContent && null != mDataList && mDataList.size()>0){
viewHolder.textContent.setText(mDataList.get(position));
}else {
Log.i(TAG, "getView: null == mDataList");
}
return view;
}
public int getSelection() {
return mSelection;
}
public void setSelection(int selection) {
mSelection = selection;
notifyDataSetChanged();
}
class ViewHolder{
TextView textHead;
TextView textContent;
}
}
2、右邊RecyclerView介面卡AdapterRight.java,重點是RecyclerView移動到指定的位置moveToPosition(int n)方法。佈局同選單ListView中的item佈局,只有一個TextView就不貼出來了。
package com.zyf.linkagelistview.adapter;
import android.content.Context;
import android.os.SystemClock;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
import com.zyf.linkagelistview.MainActivity;
import com.zyf.linkagelistview.R;
import com.zyf.linkagelistview.bean.Bean;
import java.util.ArrayList;
/**
* Created by zyf on 2017/5/8.
* 右邊RecyclerView介面卡
*/
public class AdapterRight extends RecyclerView.Adapter {
private static final String TAG = AdapterRight.class.getSimpleName();
private Context mContext;
private ArrayList<Bean> mDataList = new ArrayList<>();
private ArrayList<Integer> mTitleIntList = new ArrayList<>();
private RecyclerView mRecyclerView;
private boolean mShouldScroll = false;
public AdapterRight(Context context, ArrayList<Bean> dataList, ArrayList<Integer> titleIntList, RecyclerView recyclerView) {
mContext = context;
mDataList = dataList;
mTitleIntList = titleIntList;
mRecyclerView = recyclerView;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new ViewHolder(LayoutInflater.from(mContext).inflate(R.layout.item_right, null));
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
ViewHolder viewHolder = (ViewHolder) holder;
if (null != mDataList && mDataList.size() > 0) {
viewHolder.textContent.setText(mDataList.get(position).getText());
} else {
Log.i(TAG, "getView: null == mDataList");
}
}
@Override
public int getItemCount() {
return mDataList.size();
}
public void setSelection(int pos) {
if (pos < mDataList.size()) {
moveToPosition(pos);
}
}
/**
* 使RecyclerView移動到指定的位置
*/
private void moveToPosition(final int n) {
//先從RecyclerView的LayoutManager中獲取第一項和最後一項的Position
int firstItem = ((LinearLayoutManager) mRecyclerView.getLayoutManager()).findFirstVisibleItemPosition();
int lastItem = ((LinearLayoutManager) mRecyclerView.getLayoutManager()).findLastVisibleItemPosition();
int pos = mTitleIntList.get(n);
mShouldScroll = false;
mRecyclerView.setOnScrollListener(new RecyclerViewListener(n));
//然後區分情況
if (pos <= firstItem) {
//當要置頂的項在當前顯示的第一個項的前面時
mRecyclerView.scrollToPosition(pos);
} else if (pos <= lastItem) {
//當要置頂的項已經在螢幕上顯示時
int top = mRecyclerView.getChildAt(pos - firstItem).getTop() - 50;
mRecyclerView.scrollBy(0, top);
} else {
//當要置頂的項在當前顯示的最後一項的後面時,呼叫scrollToPosition只會將該項滑動到螢幕上。需要再次滑動到頂部
mRecyclerView.scrollToPosition(pos);
//這裡這個變數是用在RecyclerView滾動監聽裡面的
mShouldScroll = true;
}
}
/**
* 滾動監聽
*/
class RecyclerViewListener extends RecyclerView.OnScrollListener{
private int n = 0;
RecyclerViewListener(int n) {
this.n = n;
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
//在這裡進行第二次滾動
if (mShouldScroll ){
mShouldScroll = false;
moveToPosition(n);
}
}
}
class ViewHolder extends RecyclerView.ViewHolder {
TextView textContent;
ViewHolder(View itemView) {
super(itemView);
textContent = (TextView) itemView.findViewById(R.id.text_content);
}
}
}
四、RecyclerView吸頂效果的實現所必須要的自定義ItemDecoration 類 這個類需要認真看一下,實現了吸頂效果。通過getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)方法設定預留吸頂的高度,在onDraw(Canvas c, RecyclerView parent, RecyclerView.State state)方法以及onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state)方法中進行了座標的計算,然後使用Canvas以及Paint、TextPaint繪製出吸頂
package com.zyf.linkagelistview;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.support.v7.widget.RecyclerView;
import android.text.TextPaint;
import android.text.TextUtils;
import android.view.View;
import com.zyf.linkagelistview.bean.Bean;
import java.util.ArrayList;
/**
* Created by zyf on 2017/5/8.
* 實現吸頂功能的RecyclerView
*/
public class ItemDecoration extends RecyclerView.ItemDecoration {
private static final String TAG = ItemDecoration.class.getSimpleName();
private Context mContext;
private ArrayList<Bean> mDataList = new ArrayList<>();
private OnDecorationCallback mOnDecorationCallback;
private Paint mPaint;
private TextPaint mTextPaint;
private int mTopGap = 50; // 吸頂高(可隨意改變)
private int mAlignBottom = 10;
public ItemDecoration(Context context, ArrayList<Bean> dataList, OnDecorationCallback onDecorationCallback) {
this.mContext = context;
this.mDataList = dataList;
this.mOnDecorationCallback = onDecorationCallback;
Resources resources = mContext.getResources();
mPaint = new Paint();
mPaint.setColor(resources.getColor(R.color.black));
mTextPaint = new TextPaint();
mTextPaint.setColor(Color.WHITE);
mTextPaint.setAntiAlias(true); // 去鋸齒
mTextPaint.setTextSize(25);
mTextPaint.setTextAlign(Paint.Align.LEFT);
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
int pos = parent.getChildAdapterPosition(view);
String groupId = mOnDecorationCallback.onGroupId(pos);
if (groupId.equals("-1"))
return;
if (pos == 0 || isGroupFirstItem(pos)){
outRect.top = mTopGap; // 每組的頭部都預留出位置
if (mDataList.get(pos).getTitle().equals(""))
outRect.top = 0;
}else {
outRect.top = 0;
}
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
int left = parent.getPaddingLeft();
int right = parent.getWidth() - parent.getPaddingRight();
int childCount = parent.getChildCount();
for (int i=0; i<childCount; i++){
View view = parent.getChildAt(i);
int pos = parent.getChildAdapterPosition(view);
String groupId = mOnDecorationCallback.onGroupId(pos);
if (groupId.equals("-1"))
return;
String textLine = mOnDecorationCallback.onGroupFirstStr(pos).toUpperCase();
if (textLine.equals("")){
float top = view.getTop();
float bottom = view.getTop();
c.drawRect(left, top, right, bottom, mPaint);
return;
}else {
if (pos == 0 || isGroupFirstItem(pos)){ // 當前位置為0或為一組中的第一個時,顯示頂部
float top = view.getTop() - mTopGap;
float bottom = view.getTop();
c.drawRect(left, top, right, bottom, mPaint);
c.drawText(textLine, left, bottom, mTextPaint);
}
}
}
}
// 在onDraw之後呼叫,此處做吸頂一直存在的功能
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
int itemCount = state.getItemCount();
int childCount = parent.getChildCount();
int left = parent.getPaddingLeft();
int right = parent.getWidth() - parent.getPaddingRight();
String preGroupId = "";
String groupId = "-1";
for (int i=0; i<childCount; i++){
View view = parent.getChildAt(i);
int position = parent.getChildAdapterPosition(view);
preGroupId = groupId;
groupId = mOnDecorationCallback.onGroupId(position);
if (groupId.equals("-1") || groupId.equals(preGroupId))
continue;
String textLine = mOnDecorationCallback.onGroupFirstStr(position).toUpperCase();
if (TextUtils.isEmpty(textLine))
continue;
int viewBottom = view.getBottom();
// 當view.getTop()<mTopGap的時候,一直顯示在頂部mTopGap的位置
float textY = Math.max(mTopGap, view.getTop());
// 此處實現被後一個title頂出螢幕的效果
if (position + 1 < itemCount){
String nextGroupId = mOnDecorationCallback.onGroupId(position + 1);
// 當後面一組的頂部位置到達當前組吸頂的底部時,將當前組吸頂往上移動(被頂出螢幕)
if (!nextGroupId.equals(groupId) && viewBottom < textY){
textY = viewBottom;
}
}
if (view.getTop() < textY){
mOnDecorationCallback.onGroupFirstStr(textLine);
}
c.drawRect(left, textY - mTopGap, right, textY, mPaint);
c.drawText(textLine, left + 2 * mAlignBottom, textY - mAlignBottom, mTextPaint);
}
}
/**
* 判斷是否為組內的第一個item
* @param pos
* @return
*/
private boolean isGroupFirstItem(int pos){
if (pos == 0){
return true;
}else{
String preGroupId = mOnDecorationCallback.onGroupId(pos - 1);
String groupId = mOnDecorationCallback.onGroupId(pos);
if (groupId.equals(preGroupId)){
return false;
}else {
return true;
}
}
}
/**
* 外部介面
*/
interface OnDecorationCallback{
String onGroupId(int pos); // 返回pos位置對應的title
String onGroupFirstStr(int pos); // 返回pos位置對應的title(用於對比title)
void onGroupFirstStr(String title); // 傳入的是title
}
}
這樣就可以實現簡單的ListView聯動效果。
五、總結
該聯動Demo主要思路是點選左邊選單ListView的item,呼叫RecyclerView介面卡中移動到指定的位置moveToPosition(int n)方法,實現右邊RecyclerView滾動到指定位置;滑動右邊RecyclerView,呼叫左邊選單ListView中介面卡的setSelection(int pos)方法實現左邊聯動的功能。 以及RecyclerView的吸頂效果的實現,也是本篇部落格的重點。
建議認真理解下RecyclerView的介面卡類AdapterRight中moveToPosition(int n)方法,以及實現吸頂功能的自定義ItemDecoration類中吸頂效果的實現方式。大神們如發現有bug或者更好的實現方式,歡迎私信交流!
—————by Jeff—————-
—————分享使我快樂,感謝您的閱讀—————