Wednesday 21 November 2012

Pinterest styled list for Android

Pinterest is more and more popular and many apps use its "masonry" pattern. I used the same pattern in my last project and must say it wasn't as easy job as it looked at first. Here's what I'd been doing...

At first logical solution was to use vertical LinearLayouts inside a ScrollView - easy and simple.
All I had to add is code which adds elements proportionately in each LinearLayout.
<ScrollView>
<LinearLayout
   orientation="horizontal">

   <LinearLayout
     android:layout_weight="0.5"
     orientation="vertical">

   <LinearLayout
     android:layout_weight="0.5"
     orientation="vertical">

</LinearLayout>
</ScrollView>
This worked fine at the beginning but when I added more than 100 items, as expected, app started to crash. Well if you think about it it was obvious, with this approach we don't use recycling so layout constantly had 100 items filled with layouts, pictures, formatted text..

Second solution I thought of was two synchronized ListViews. They have internal caching so all I had to do is synchronize them.
<LinearLayout 
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="horizontal"
    android:paddingLeft="10dp"
    android:paddingRight="10dp">

    <ListView
        android:id="@+id/list_view_left"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_weight="1"
        android:paddingRight="5dp"
        android:scrollbars="none" >
    </ListView>

    <ListView
        android:id="@+id/list_view_right"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_weight="1"
        android:paddingLeft="5dp"
        android:scrollbars="none" >
    </ListView>

</LinearLayout> 
My first attempt was to add OnTouchListener which will pass the touch event to the opposite list and OnScrollListener which will update first opposite child.
listOne.setOnTouchListener(new OnTouchListener() {
    @Override
       public boolean onTouch(View arg0, MotionEvent arg1) {
       listTwo.dispatchTouchEvent(arg1);
       return false;
    }
});
listOne.setOnScrollListener(new OnScrollListener() {
   @Override
   public void onScrollStateChanged(AbsListView arg0, int arg1) {
   }
   @Override
   public void onScroll(AbsListView arg0, int arg1, int arg2, int arg3) {
          if (l1.getChildAt(0) != null) {
              Rect r = new Rect();
              l1.getChildVisibleRect(l1.getChildAt(0), r, null);
              l2.setSelectionFromTop(l1.getFirstVisiblePosition(), r.top);
          }
    }
});  
Well this seemed as good solution. It was working with single list, scrolling was smooth so adding the same code to the opposite list should work fine but unfortunately it didn't. They ware both synced on first visible child so when one would disappear the other would automatically be positioned to match the new one. This made scrolling feel quite unnatural so other solution had to be found.

It occurred to me that I can just calculate distances from top of both elements, subtract one from another and add offset of the current element. Hmmm, well let's be more precise it took me a while get to this calculation but if you draw an example yourself, you'll see its just logical.
Source can be found on Github  
This solution works fine. I have few bugs to fix, simplify the code - make it more simple to use and that should be it. 
Still it bothers me if this can be implemented by using BaseAdapter and AdapterView. I'll  have an option to use adapters caching and what remains is child positioning. Well, I'll give it a try as soon as I find spare time...