前段时间做个项目,要用到一个横向滑动的表格,顺手百度了一下,看到一位哥们的思路挺不错(http://www.javaapk.com/source/4374.html),参考了一下其中的思路,进行了一定的封装和优化。
封装后还没进行过较为详细的测试,如果遇见有bug或者有优化的建议的话,可随时联系我。
其实整个横竖表格的原理实现,竖向的滑动的话,就是listview的滑动,横向的话,则在listview的item里面利用HorizontalScrollView实现横向滑动,然后加以控制,当某一个item里的HorizontalScrollView发生横向滑动的时候,就把所有的item里的HorizontalScrollView也滑动到对应的位置。
我把这个listview和标题行封装到了一个LinearLayout中,封装成了VHTableView。
先看一下怎么用
布局xml如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
>
<com.demo.zj.vhtableview.VHTableView
android:id="@+id/vht_table"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
</com.demo.zj.vhtableview.VHTableView>
</RelativeLayout>
布局文件很简单,就这个自定义的view。
接着在activity里的调用,也很简单
<span style="white-space:pre"> </span>VHTableView vht_table=(VHTableView)findViewById(R.id.vht_table);
<span style="white-space:pre"> </span>VHTableAdapter tableAdapter=new VHTableAdapter(this,titleData,contentData);//titleData,contentData这两个数据源自己设置..
//vht_table.setFirstColumnIsMove(true);//设置第一列是否可移动,默认不可移动
//vht_table.setShowTitle(false);//设置是否显示标题行,默认显示
vht_table.setAdapter(tableAdapter);
里面的VHtableAdapter是实现了VHBaseAdapter接口的类,接着来看VHBaseAdapter接口
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
public interface VHBaseAdapter {
//表格内容的行数,不包括标题行
public int getContentRows() ;
//列数
public int getContentColumn();
//标题的view,这里从0开始,这里要注意,一定要有view返回去,不能为null,每一行
// 各列的宽度就等于标题行的列的宽度,且边框的话,自己在这里和下文的表格单元格view里面设置
public View getTitleView(int columnPosition, ViewGroup parent);
//表格正文的view,行和列都从0开始,宽度的话在载入的时候,默认会是以标题行各列的宽度,高度的话自适应
public View getTableCellView(int contentRow, int contentColum, View view, ViewGroup parent);
public Object getItem(int contentRow) ;
//每一行被点击的时候的回调
public void OnClickContentRowItem(int row, View convertView);
}
接下来看
VHTableView的具体实现,具体代码如下:
import android.content.Context;
import android.graphics.drawable.ColorDrawable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;
import com.demo.zj.R;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
public class VHTableView extends LinearLayout implements HListViewScrollView.ScrollChangedListener{
private Context context;
private LayoutInflater inflater;
//是否显示标题行
private boolean showTitle;
//第一列是否可移动
private boolean firstColumnIsMove;
//用于显示表格正文内容
private ListView listView;
//存放标题行中的每一列的宽度,所有的行里的每一列都是基于标题行的每一列的宽度,都跟标题行的每一列的宽度相等
private HashMap<String ,Integer> widthMap=new HashMap<>();
//存放所有的HScrollView
protected List<HListViewScrollView> mHScrollViews =new ArrayList<HListViewScrollView>();
public VHTableView(Context context) {
this(context,null);
}
public VHTableView(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
init();
}
private void init() {
this.inflater = LayoutInflater.from(context);
//默认显示标题行
showTitle=true;
//默认第一列不可滑动
firstColumnIsMove=false;
}
//设置是否显示标题
public void setShowTitle(boolean showTitle) {
this.showTitle = showTitle;
}
//设置第一列是否可以滑动
public void setFirstColumnIsMove(boolean firstColumnIsMove) {
this.firstColumnIsMove = firstColumnIsMove;
}
//设置adapter
public void setAdapter(final VHBaseAdapter conentAdapter) {
//清除各原有数据
cleanup();
//载入标题行
initTitles(conentAdapter);
//载入表格正文
initContentList(conentAdapter);
if(!showTitle){
//假如设置了不显示标题行,在这里隐藏掉
getChildAt(0).setVisibility(View.GONE);
}
}
public void cleanup(){
removeAllViews();
widthMap.clear();
mHScrollViews.clear();
}
private void initTitles(VHBaseAdapter conentAdapter) {
View titleView=inflater.inflate(R.layout.vhtable_item_listview,this,false);
LinearLayout ll_firstcolumn=(LinearLayout)titleView.findViewById(R.id.ll_firstcolumn);
int i=0;
if(firstColumnIsMove){
//假如设置是可移动的,则把不可移动的部分ll_firstcolumn隐藏掉,把所有数据都加在HListViewScrollView中
ll_firstcolumn.setVisibility(View.GONE);
}else {
//假如是设置了第一列不可移动的,则把第一列的数据加到ll_firstcolumn中,其余的都加到HListViewScrollView中
ll_firstcolumn.removeAllViews();
View view =conentAdapter.getTitleView(0,ll_firstcolumn);
//测量view的高度,都采用自适应的模式测量
view.measure(0,0);
ll_firstcolumn.addView(view, LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT);
//存起来以便设置表格正文的时候进行宽度设置
widthMap.put("0",view.getMeasuredWidth());
//之后的titleview就都放在CHListViewScrollView中,因为0 title已经设置了,所以从1开始
i=1;
}
HListViewScrollView chs_datagroup=(HListViewScrollView)titleView.findViewById(R.id.chs_datagroup);
//把CHListViewScrollView加入管理
addHViews(chs_datagroup);
LinearLayout ll_datagroup=(LinearLayout)titleView.findViewById(R.id.ll_datagroup);
ll_datagroup.removeAllViews();
for(;i<conentAdapter.getContentColumn();i++){
View view=conentAdapter.getTitleView(i,ll_datagroup);
view.measure(0,0);
ll_datagroup.addView(view,LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT);
widthMap.put(i+"", view.getMeasuredWidth());
}
addView(titleView);
}
private void initContentList(VHBaseAdapter conentAdapter) {
listView =new ListView(context);
//设置间隔线为0,以便在view中自己定义间隔线
listView.setDividerHeight(0);
ContentAdapter adapter=new ContentAdapter(conentAdapter);
listView.setAdapter(adapter);
addView(listView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}
public void addHViews(final HListViewScrollView hScrollView) {
if(!mHScrollViews.isEmpty()) {
int size = mHScrollViews.size();
HListViewScrollView scrollView = mHScrollViews.get(size - 1);
final int scrollX = scrollView.getScrollX();
//这是给第一次满屏,或者快速下滑等情况时,新创建的会再创建一个convertView的时候,把这个新进入的convertView里的HListViewScrollView移到对应的位置
if(scrollX != 0) {
listView.post(new Runnable() {
@Override
public void run() {
//在主线程中去移动到对应的位置
hScrollView.scrollTo(scrollX, 0);
}
});
}
}
hScrollView.setScrollChangedListener(this);
mHScrollViews.add(hScrollView);
}
private HListViewScrollView currentTouchView;
@Override
public void setCurrentTouchView(HListViewScrollView currentTouchView) {
this.currentTouchView=currentTouchView;
}
@Override
public HListViewScrollView getCurrentTouchView() {
return currentTouchView;
}
@Override
public void onUIScrollChanged(int l, int t, int oldl, int oldt) {
for(HListViewScrollView scrollView : mHScrollViews) {
//防止重复滑动
if(currentTouchView != scrollView)
scrollView.smoothScrollTo(l, t);
}
}
public class ContentAdapter extends BaseAdapter {
private VHBaseAdapter conentAdapter;
public ContentAdapter(VHBaseAdapter conentAdapter) {
this.conentAdapter=conentAdapter;
}
@Override
public int getCount() {
return conentAdapter.getContentRows();
}
@Override
public Object getItem(int position) {
return conentAdapter.getItem(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(final int position, View convertView, final ViewGroup parent) {
ViewHolder viewHolder;
int maxHeight=0;
if(null==convertView){
convertView=inflater.inflate(R.layout.vhtable_item_listview,parent,false);
HListViewScrollView chs_datagroup=(HListViewScrollView)convertView.findViewById(R.id.chs_datagroup);
//把CHListViewScrollView加入管理
addHViews(chs_datagroup);
viewHolder=new ViewHolder();
viewHolder.views = new View[conentAdapter.getContentColumn()];
viewHolder.ll_firstcolumn=(LinearLayout)convertView.findViewById(R.id.ll_firstcolumn);
viewHolder.ll_datagroup=(LinearLayout)convertView.findViewById(R.id.ll_datagroup);
viewHolder.rowClickListener=new RowClickListener();
convertView.setTag(viewHolder);
}else {
viewHolder= (ViewHolder) convertView.getTag();
}
//更新每行的views的数据
updateViews(conentAdapter,viewHolder,viewHolder.ll_firstcolumn,viewHolder.ll_datagroup,position);
//更熟的views数据后重新测量高度,取那一行的最大高度作为整行的高度
maxHeight=getMaxHeight(conentAdapter,viewHolder.views);
//重新更新view到表格上
updateUI(conentAdapter,viewHolder.ll_firstcolumn,viewHolder.ll_datagroup,viewHolder.views,maxHeight);
//为了尽可能少的影响ScrollView的触摸事件,所以点击事件这里取个巧,直接设置在这两个Linearlayout上
viewHolder.rowClickListener.setData(conentAdapter,position,convertView);
viewHolder.ll_firstcolumn.setOnClickListener(viewHolder.rowClickListener);
viewHolder.ll_datagroup.setOnClickListener(viewHolder.rowClickListener);
return convertView;
}
}
private ViewHolder updateViews(VHBaseAdapter conentAdapter, ViewHolder viewHolder,LinearLayout ll_firstcolumn, LinearLayout ll_datagroup,int row) {
for(int i=0;i<conentAdapter.getContentColumn();i++) {
if(!firstColumnIsMove&&i==0){
View view= conentAdapter.getTableCellView(row, 0, viewHolder.views[0], ll_firstcolumn);
viewHolder.views[0]=view;
}else {
View view=conentAdapter.getTableCellView(row, i, viewHolder.views[i], ll_datagroup);
viewHolder.views[i]=view;
}
}
return viewHolder;
}
private int getMaxHeight(VHBaseAdapter conentAdapter,View[] views) {
int maxHeight=0;
for(int i=0;i<conentAdapter.getContentColumn();i++) {
//测量模式:宽度以标题行各列的宽度为准,高度为自适应
int w = View.MeasureSpec.makeMeasureSpec(widthMap.get(""+i), MeasureSpec.EXACTLY);
int h = View.MeasureSpec.makeMeasureSpec(0,View.MeasureSpec.UNSPECIFIED);
views[i].measure(w,h);
maxHeight = Math.max(maxHeight, views[i].getMeasuredHeight());
}
return maxHeight;
}
private void updateUI(VHBaseAdapter conentAdapter, LinearLayout ll_firstcolumn, LinearLayout ll_datagroup,View[] views,int maxHeight) {
//其实这里可以优化一下,不用remove掉全部又加一次,以后再优化一下。。。。
ll_firstcolumn.removeAllViews();
ll_datagroup.removeAllViews();
for(int i=0;i<conentAdapter.getContentColumn();i++) {
if(!firstColumnIsMove&&i==0){
ll_firstcolumn.addView(views[0],widthMap.get("0"),maxHeight);
}else {
ll_datagroup.addView(views[i],widthMap.get(""+i),maxHeight);
}
}
}
public class RowClickListener implements OnClickListener {
private VHBaseAdapter conentAdapter;
private int row;
private View convertView;
public void setData(VHBaseAdapter conentAdapter, int row, View convertView){
this.conentAdapter=conentAdapter;
this.row=row;
this.convertView=convertView;
}
@Override
public void onClick(View v) {
if(null!=conentAdapter&&null!=convertView){
conentAdapter.OnClickContentRowItem(row,convertView);
}
}
}
public class ViewHolder {
LinearLayout ll_firstcolumn;
LinearLayout ll_datagroup;
View[] views;
RowClickListener rowClickListener;
}
}
这里用到的布局文件vhtable_item_listview如下
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/row_hlistview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/ll_firstcolumn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal"
>
</LinearLayout>
<com.demo.zj.vhtableview.HListViewScrollView
android:id="@+id/chs_datagroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="false"
android:scrollbars="none"
>
<LinearLayout
android:id="@+id/ll_datagroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
</LinearLayout>
</com.demo.zj.vhtableview.HListViewScrollView>
</LinearLayout>
稍作修改的
HorizontalScrollView如下
import android.content.Context;
import android.support.v4.view.ViewConfigurationCompat;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import android.widget.HorizontalScrollView;
public class HListViewScrollView extends HorizontalScrollView {
public HListViewScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public HListViewScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public HListViewScrollView(Context context) {
super(context);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
//记录当前触摸的HListViewScrollView
if(null!=this.listener){
listener.setCurrentTouchView(this);
}
return super.onTouchEvent(ev);
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
//当当前的HListViewScrollView被触摸时,滑动其它
if(null!=this.listener&&null!=listener.getCurrentTouchView()&&listener.getCurrentTouchView() == this) {
listener.onUIScrollChanged(l, t, oldl, oldt);
}else{
super.onScrollChanged(l, t, oldl, oldt);
}
}
private ScrollChangedListener listener;
public void setScrollChangedListener(ScrollChangedListener listener){
this.listener=listener;
}
public interface ScrollChangedListener{
public void setCurrentTouchView(HListViewScrollView currentTouchView);
public HListViewScrollView getCurrentTouchView();
public void onUIScrollChanged(int l, int t, int oldl, int oldt);
}
}
里面的注释其实说的挺清楚了,这里说一下大概的思路,说白了就是一个 LinearLayout,里面添加了两个子view,一个标题行,一个listview,标题行和listview的item用的都是同一个布局,这个布局里面也是一个 LinearLayout横向包含两个子view,一个普通的 LinearLayout和一个 HListViewScrollView,当设置第一列不可移动的时候,就把第一列放到那个普通的 LinearLayout里,不然就全扔到HListViewScrollView里。
然后就把标题行里跟listview里面所有创建了的HListViewScrollView都放一个List里面,然后监控各个HListViewScrollView的横向滑动事件,某一个滑动的时候,其他的也跟着滑动。
在加入标题行的view的时候,就进行控件的测量,并把宽度记下来,然后下面listview里面的每一列就都跟标题行一致,高度则全都是自适应。
其实东西不多,就这些东西,稍微封装优化了一下。。。。。
源码Demo:
https://git.oschina.net/jiancode/VHTable.git