Android 固定列頭列表的listview demo

公司的這個項目做了一年,感覺自己有了很大的提升。決定把這一年來做的比較好比較有用的一些東西抽出來記錄下來。既能整理自己的知識樹,又能給其他朋友一些參考。這篇講的是如何做一個可固定列頭列表滑動的listview。

剛開始做這個的時候,在網上查閱了大量資料,也下載了很多其他人提供的demo,參考了他們的思路。但是總是要不就是不符合我的需求,要不就是有些bug。最后,自己嘗試著編寫,經過不斷的更改和修復,終于完成了這個功能。
首先也是比較重要的一點是,listview的item布局,代碼如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:descendantFocusability="blocksDescendants"
android:orientation="horizontal">

<TextView
    android:id="@+id/tv_line"
    android:layout_width="80dp"
    android:layout_height="50dp"
    android:gravity="center"
    android:text="表頭"
    android:textColor="@android:color/black" />

<View
    android:layout_width="0.1dp"
    android:layout_height="50dp"
    android:background="@android:color/black" />
<!--攔截子控件的響應事件-->
<com.example.lanyee.demofixheadlist.InterceptRelayout
    android:layout_width="match_parent"
    android:layout_height="50dp"
    android:focusable="false">

    <com.example.lanyee.demofixheadlist.ChartHScrollView
        android:id="@+id/scroll_item"
        android:layout_width="wrap_content"
        android:layout_height="50dp"
        android:overScrollMode="never"
        android:scrollbars="none">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:focusable="false"
            android:gravity="center"
            android:orientation="horizontal">

            <TextView
                android:id="@+id/tv_1"
                android:layout_width="40dp"
                android:layout_height="match_parent"
                android:gravity="center"
                android:text="列1" />

            <View
                android:layout_width="0.1dp"
                android:layout_height="match_parent"
                android:background="@android:color/black" />

            <TextView
                android:id="@+id/tv_2"
                android:layout_width="60dp"
                android:layout_height="match_parent"
                android:gravity="center"
                android:singleLine="true"
                android:text="列2" />

            <View
                android:layout_width="0.1dp"
                android:layout_height="match_parent"
                android:background="@android:color/black" />

            <TextView
                android:id="@+id/tv_3"
                android:layout_width="80dp"
                android:layout_height="match_parent"
                android:gravity="center"
                android:singleLine="true"
                android:text="列3" />

            <View
                android:layout_width="0.1dp"
                android:layout_height="match_parent"
                android:background="@android:color/black" />

            <TextView
                android:id="@+id/tv_4"
                android:layout_width="60dp"
                android:layout_height="match_parent"
                android:gravity="center"
                android:singleLine="true"
                android:text="列4" />

            <View
                android:layout_width="0.1dp"
                android:layout_height="match_parent"
                android:background="@android:color/black" />

            <TextView
                android:id="@+id/tv_5"
                android:layout_width="60dp"
                android:layout_height="match_parent"
                android:gravity="center"
                android:singleLine="true"
                android:text="列5" />

            <View
                android:layout_width="0.1dp"
                android:layout_height="match_parent"
                android:background="@android:color/black" />

            <TextView
                android:id="@+id/tv_6"
                android:layout_width="80dp"
                android:layout_height="match_parent"
                android:gravity="center"
                android:singleLine="true"
                android:text="列6" />

            <View
                android:layout_width="0.1dp"
                android:layout_height="match_parent"
                android:background="@android:color/black" />

            <TextView
                android:id="@+id/tv_7"
                android:layout_width="80dp"
                android:layout_height="match_parent"
                android:gravity="center"
                android:singleLine="true"
                android:text="列7" />

            <View
                android:layout_width="0.1dp"
                android:layout_height="match_parent"
                android:background="@android:color/black" />

            <TextView
                android:id="@+id/tv_8"
                android:layout_width="80dp"
                android:layout_height="match_parent"
                android:gravity="center"
                android:singleLine="true"
                android:text="列8" />
        </LinearLayout>
    </com.example.lanyee.demofixheadlist.ChartHScrollView>
</com.example.lanyee.demofixheadlist.InterceptRelayout>

</LinearLayout>

    其中,InterceptRelayout是起攔截子控件的響應事件作用的。listview的item中的ChartHScrollView子控件是不響應觸摸事件的,觸摸事件統一交給列頭的ChartHScrollView來處理,然后遍歷通知item中的ChartHScrollView進行滑動。ChartHScrollView是自定義的view,繼承自HorizonScrollView,用觀察者模式。注意 android:descendantFocusability="blocksDescendants"

是覆蓋子類控件而直接獲得焦點,如果需要有item點擊響應,必須加這句代碼。這兩個類的代碼如下:
public class InterceptRelayout extends RelativeLayout{
public InterceptRelayout(Context context) {
super(context);
}

public InterceptRelayout(Context context, AttributeSet attrs) {
    super(context, attrs);
}

public InterceptRelayout(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    return true;
}

}

public class ChartHScrollView extends HorizontalScrollView {
//滑動事件的觀察者們,即listview的item中的ChartHScrollView
private ChartScrollViewObservable observable;
//滑動距離監聽
private ScrollViewMoveDistanceListener scrollViewMoveDistanceListener;

public ChartHScrollView(Context context) {
    super(context);
    observable = new ChartScrollViewObservable();
}

public ChartHScrollView(Context context, AttributeSet attrs) {
    super(context, attrs);
    observable = new ChartScrollViewObservable();
}

public ChartHScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    observable = new ChartScrollViewObservable();
}

public void addObserver(ChartHScrollView observer) {
    observable.addObserver(observer);
}

public void removeObserver(ChartHScrollView observer) {
    observable.addObserver(observer);
}


@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
    //通知觀察者們,當前滑動了多遠
    observable.notifyObservers(l, t);
    super.onScrollChanged(l, t, oldl, oldt);
    if (scrollViewMoveDistanceListener != null)
        scrollViewMoveDistanceListener.scrollviewMoveDistance(l);
}

public void setScrollViewMoveDistanceListener(ScrollViewMoveDistanceListener scrollViewMoveDistanceListener) {
    this.scrollViewMoveDistanceListener = scrollViewMoveDistanceListener;
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    super.onLayout(changed, l, t, r, b);
    //當scrollview的布局發生改變時,使其與列頭view滑動的距離保持一致
    if (scrollViewMoveDistanceListener != null)
        scrollTo(scrollViewMoveDistanceListener.getHeadScrollViewMoveDistance(), 0);
}

}

接下來就是Adapter了,Adapter要做的事情很簡單。在getview的回調中,當contentView為null的時候,用列頭的ChartHScrollView 對象,調用addObserver()方法,傳入item中的ChartHScrollView 對象參數。注意!只需要在當contentView為null的時候,添加觀察者就行了,因為當contentView!=null時,是復用的之前的item,所以觀察者對象集已經有此對象了。代碼如下:

public class Adapter extends BaseAdapter implements ScrollViewMoveDistanceListener, AdapterView.OnItemClickListener {
//列頭的scrollview
private ChartHScrollView hScrollView;
//當前滑動的距離,當item中的ChartHScrollView發生布局改變時,需要此參數使其滑動scrollDistance距離,與列頭保持一致。
private volatile int scrollDistance = 0;
private ArrayList<Integer> datas;

public Adapter(ChartHScrollView hScrollView, ArrayList<Integer> datas) {
    this.hScrollView = hScrollView;
    this.datas = datas;

    hScrollView.setScrollViewMoveDistanceListener(this);
}

@Override
public int getCount() {
    return datas.size();
}

@Override
public Object getItem(int position) {
    return datas.get(position);
}

@Override
public long getItemId(int position) {
    return 0;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    ViewHolder viewHolder;
    if (convertView == null) {
        convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_layout, null);
        viewHolder = new ViewHolder(convertView);
        //將觀察者對象添加進對象集
        hScrollView.addObserver(viewHolder.itemScroll);
        convertView.setTag(viewHolder);
    } else {
        viewHolder = (ViewHolder) convertView.getTag();
    }

    viewHolder.tvLine.setText("行" + datas.get(position));
    viewHolder.tv1.setText(String.valueOf(datas.get(position)));
    viewHolder.tv2.setText(String.valueOf(datas.get(position) + 1));
    viewHolder.tv3.setText(String.valueOf(datas.get(position) + 2));
    viewHolder.tv4.setText(String.valueOf(datas.get(position) + 3));
    viewHolder.tv5.setText(String.valueOf(datas.get(position) + 4));
    viewHolder.tv6.setText(String.valueOf(datas.get(position) + 5));
    viewHolder.tv7.setText(String.valueOf(datas.get(position) + 6));
    viewHolder.tv8.setText(String.valueOf(datas.get(position) + 7));

    return convertView;
}

@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    Toast.makeText(parent.getContext(), "點擊位置" + position, Toast.LENGTH_SHORT).show();
}

class ViewHolder {
    private TextView tvLine;
    private ChartHScrollView itemScroll;
    private TextView tv1;
    private TextView tv2;
    private TextView tv3;
    private TextView tv4;
    private TextView tv5;
    private TextView tv6;
    private TextView tv7;
    private TextView tv8;

    public ViewHolder(View view) {
        tvLine = (TextView) view.findViewById(R.id.tv_line);
        tv1 = (TextView) view.findViewById(R.id.tv_1);
        tv2 = (TextView) view.findViewById(R.id.tv_2);
        tv3 = (TextView) view.findViewById(R.id.tv_3);
        tv4 = (TextView) view.findViewById(R.id.tv_4);
        tv5 = (TextView) view.findViewById(R.id.tv_5);
        tv6 = (TextView) view.findViewById(R.id.tv_6);
        tv7 = (TextView) view.findViewById(R.id.tv_7);
        tv8 = (TextView) view.findViewById(R.id.tv_8);

        itemScroll = (ChartHScrollView) view.findViewById(R.id.scroll_item);
        itemScroll.setScrollViewMoveDistanceListener(Adapter.this);
    }

}

/**
 *  列頭的ChartHScrollView移動的距離
 * @param distance
 */
@Override
public void scrollviewMoveDistance(int distance) {
    scrollDistance = distance;
}

/**
 * 當item中的ChartHScrollView發生布局改變時,滑動scrollDistance使其保持與列頭一致
 * @return 列頭的ChartHScrollView移動的距離
 */
@Override
public int getHeadScrollViewMoveDistance() {
    return scrollDistance;
}

}

    接下來這個很重要,就是listview上的touch和列頭上的touch事件處理。代碼如下:

public class ListViewAndHeadViewTouchHandle implements View.OnTouchListener {
//列頭的scrollView
private ChartHScrollView scrollView;
private ListView listView;
//列頭
private LinearLayout headLine;

public ListViewAndHeadViewTouchHandle(LinearLayout headLine, ListView listView) {
    scrollView = (ChartHScrollView) headLine.findViewById(R.id.scroll_item);
    this.headLine = headLine;
    this.listView = listView;
    listView.setOnTouchListener(this);
    headLine.setOnTouchListener(this);
}

float x1 = 0, y1 = 0, x2 = 0, y2 = 0;
//區分當前的滑動狀態
boolean isClick = false;
boolean isHorizonMove = true;
boolean isVerticalMove = false;

@Override
public boolean onTouch(View arg0, MotionEvent arg1) {
    switch (arg1.getAction()) {
        case MotionEvent.ACTION_DOWN:
            x1 = arg1.getX();
            y1 = arg1.getY();

            //當在列頭 和 listView控件上touch時,將這個touch的事件分發給 ScrollView和listView處理。
            //一個view只有在接收到了down事件,才能繼續接收之后的觸摸事件。對這一塊不太熟悉的建議先去看看touch事件的分發機制。
            scrollView.onTouchEvent(arg1);
            listView.onTouchEvent(arg1);

            isClick = false;
            isHorizonMove = false;
            isVerticalMove = false;
            break;
        case MotionEvent.ACTION_MOVE:
            x2 = arg1.getX();
            y2 = arg1.getY();

            if (Math.abs(x2 - x1) < 10 && Math.abs(y2 - y1) < 10) {
                //判定當前動作是點擊
                isClick = true;
                isHorizonMove = false;
                isVerticalMove = false;
            } else {
                isClick = false;
                if (Math.abs(x2 - x1) > Math.abs(y2 - y1)) {
                    //水平
                    //如果之前有過垂直操作,則不再更改方向
                    if (!isVerticalMove) {
                        isHorizonMove = true;
                        isVerticalMove = false;
                    }
                } else {
                    //垂直
                    //如果之前有過水平操作,則不再更改方向
                    if (!isHorizonMove) {
                        isVerticalMove = true;
                        isHorizonMove = false;
                    }
                }
            }

            //垂直動作或點擊動作交給listView來處理
            if (isVerticalMove || isClick) {
                listView.onTouchEvent(arg1);
            } else {
                //水平動作交給列頭的scrollView來處理,列頭的scrollView接收后,會回調onScrollChanged(),重寫onScrollChanged()通知觀察者們滑動
                scrollView.onTouchEvent(arg1);
            }
            break;

        case MotionEvent.ACTION_UP:
            if (Math.abs(arg1.getX() - x1) < 10 && Math.abs(arg1.getY() - y1) < 10) {
                isClick = true;
            }

            //isClick && arg0 != headLine這個判斷是防止在列頭點擊時,listview會響應點擊事件
            if ((isClick && arg0 != headLine) || isVerticalMove) {
                listView.onTouchEvent(arg1);
            } else {
                scrollView.onTouchEvent(arg1);
            }

            isClick = false;
            isHorizonMove = false;
            isVerticalMove = false;
            break;
    }

    return true;
}

}

接下來就是mainActivity的代碼內容和布局內容了。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.example.lanyee.demofixheadlist.MainActivity">

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@android:color/darker_gray">

    <include
        android:id="@+id/headLine"
        layout="@layout/item_layout"/>
</RelativeLayout>

<ListView
    android:id="@+id/listview"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

</LinearLayout>

public class MainActivity extends AppCompatActivity {
private ListView listView;
private LinearLayout headLine;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    listView = (ListView) findViewById(R.id.listview);
    headLine = (LinearLayout) findViewById(R.id.headLine);

    ArrayList<Integer> datas = new ArrayList<>();
    for (int i = 0; i < 50; i++) {
        datas.add(i);
    }

    Adapter adapter = new Adapter((ChartHScrollView) headLine.findViewById(R.id.scroll_item), datas);
    listView.setAdapter(adapter);

    //統一處理列頭和listview的touch事件
    new ListViewAndHeadViewTouchHandle(headLine, listView);

    listView.setOnItemClickListener(adapter);
}

}

監聽文件代碼:

public interface ScrollViewMoveDistanceListener {
    void scrollviewMoveDistance(int distance);

    int getHeadScrollViewMoveDistance();
}
   所有的代碼都已經貼出來啦,代碼中也加了比較詳細的注釋。平時很少編輯文章,所以表述可能不是很清楚。另外這個文本編輯器貼代碼塊好像不是很好用。如果有疑問或更好的建議,歡迎留言評論,我們共同探討。
                                                               最后謝謝你的觀看。
                                                               轉載請注明出處。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容