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