Android 固定列头列表的listview demo

作者: 陈小狮_Teresa | 来源:发表于2017-08-10 14:58 被阅读367次

    公司的这个项目做了一年,感觉自己有了很大的提升。决定把这一年来做的比较好比较有用的一些东西抽出来记录下来。既能整理自己的知识树,又能给其他朋友一些参考。这篇讲的是如何做一个可固定列头列表滑动的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();
    }
    
       所有的代码都已经贴出来啦,代码中也加了比较详细的注释。平时很少编辑文章,所以表述可能不是很清楚。另外这个文本编辑器贴代码块好像不是很好用。如果有疑问或更好的建议,欢迎留言评论,我们共同探讨。
                                                                   最后谢谢你的观看。
                                                                   转载请注明出处。

    相关文章

      网友评论

        本文标题:Android 固定列头列表的listview demo

        本文链接:https://www.haomeiwen.com/subject/moyhrxtx.html