After repeated testing and searching, I implemented it myself by combining the ListView with the internal HorizontalScrollView s.
First, I extended HorizontalScrollView to inform me of a scroll event by adding a listener:
public class MyHorizontalScrollView extends HorizontalScrollView { private OnScrollListener listener; @Override protected void onScrollChanged(int l, int t, int oldl, int oldt) { super.onScrollChanged(l, t, oldl, oldt); if (listener != null) listener.onScroll(this, l, t); } public void setOnScrollListener(OnScrollListener listener) { this.listener = listener; } public interface OnScrollListener { void onScroll(HorizontalScrollView view, int x, int y); } }
Then I created my layout with LinearLayout containing my title and ListView for my Activity (or Fragment , if you need it).
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <include android:id="@+id/header" layout="@layout/header" /> <ListView android:id="@+id/list" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>
Each item on my list is a LinearLayout , with a TextView (fixed column) and a HorizontalScrollView . The layout of both the headers and the lines is as follows:
<?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:orientation="horizontal"> <TextView style="?android:attr/textAppearanceMedium" android:background="#f00" android:minWidth="40dp" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <View android:layout_width="1px" android:layout_height="match_parent" android:background="@android:color/black" /> <net.rafaeltoledo.example.MyHorizontalScrollView android:id="@+id/scroll" android:scrollbars="none" android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:layout_width="wrap_content" android:layout_height="match_parent" android:orientation="horizontal"> </LinearLayout> </net.rafaeltoledo.example.MyHorizontalScrollView> </LinearLayout>
The trick is to scroll through everything. Using the EventBus (I used GreenRobot ) to fire the horizontal scroll event and move all the scrollers as one, My event object contains the same data from the listener class (maybe I can use the listener object itself?)
public static class Event { private final int x; private final int y; private final HorizontalScrollView view; public Event(HorizontalScrollView view, int x, int y) { this.x = x; this.y = y; this.view = view; } public int getX() { return x; } public int getY() { return y; } public HorizontalScrollView getView() { return view; } }
The list adapter class accepts a listener to set each item to the HorizontalScrollView .
public static class Adapter extends BaseAdapter { private final Context context; private MyHorizontalScrollView.OnScrollListener listener; public Adapter(Context context, MyHorizontalScrollView.OnScrollListener listener) { this.context = context; this.listener = listener; } @Override public int getCount() { return 30; } @Override public Object getItem(int position) { return new Object(); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = LayoutInflater.from(getContext()).inflate(R.layout.header, parent, false); MyHorizontalScrollView scroll = (MyHorizontalScrollView) convertView.findViewById(R.id.scroll); scroll.setOnScrollListener(listener); } return convertView; } public Context getContext() { return context; } }
Before continuing, I registered MyHorizontalScrollView in EventBus, adding EventBus.getDefault().register(this) to each version of the constructor and adding a receiver method to it:
public void onEventMainThread(MainActivity.Event event) { if (!event.getView().equals(this)) scrollTo(event.getX(), event.getY()); }
which will scroll to the accepted position if the scroll event were not triggered.
And finally, I set everything in the onCreate() method of my Activity :
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.list); MyHorizontalScrollView.OnScrollListener listener = new MyHorizontalScrollView.OnScrollListener() { @Override public void onScroll(HorizontalScrollView view, int x, int y) { Log.d("Scroll Event", String.format("Fired! %d %d", x, y)); EventBus.getDefault().post(new Event(view, x, y)); } }; ListView listView = (ListView) findViewById(R.id.list); ViewGroup header = (ViewGroup) findViewById(R.id.header); header.getChildAt(0).setBackgroundColor(Color.WHITE); header.setBackgroundColor(Color.BLUE); ((MyHorizontalScrollView) header.findViewById(R.id.scroll)).setOnScrollListener(listener); listView.setAdapter(new Adapter(this, listener)); listView.addFooterView(getLayoutInflater().inflate(R.layout.footer, listView, false)); }
(Please ignore some weird colors to better see what happens).
And ta-daa, here is the desired result!
