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();
}
   所有的代码都已经贴出来啦,代码中也加了比较详细的注释。平时很少编辑文章,所以表述可能不是很清楚。另外这个文本编辑器贴代码块好像不是很好用。如果有疑问或更好的建议,欢迎留言评论,我们共同探讨。
                                                               最后谢谢你的观看。
                                                               转载请注明出处。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容