Wednesday 8 October 2014

Scroll Inside Scroll



Placing vertically scroll-able view into another vertically scroll-able view is a bit tedious in android. Here we show how to place ScrollView inside ScrollViewListView inside ScrollView, ListView inside GridView, etc without sacrificing the recycling feature of various AbsListViews. And even we can place ListView indide ScrollView which in turn inside another ScrollView, etc.

A project which shows how to add a vertically scroll-able view into another vertically scroll-able view up to any level of depth is published in http://durgadass.github.io/ScrollInsideScroll.

Note Even though you can nest scroll views to any level, please consider the user experience. The project contains a complicated xml layout file scroll_in_scroll.xml which has no good user experience.

This post explains a more general issue - "Placing vertically scroll-able view into another vertically scroll-able view". More common use cases of this are "Placing ListView inside ScrollView" and "Placing GridView inside ExpandableListView". The workaround given here is same for both these common use cases and others.

Step 1

Write a method which determines whether a View can be scrolled vertically and place the method inside a common utility class as follows. This method is taken from ViewPager.java and modified to find whether a View can vertically scroll-able.

public static boolean canScroll(View v,
    boolean checkV, int dy, int x, int y) {
     if (v instanceof ViewGroup) {
         final ViewGroup group = (ViewGroup) v;
         final int scrollX = v.getScrollX();
         final int scrollY = v.getScrollY();
         final int count = group.getChildCount();
         for (int i = count - 1; i >= 0; i--) {
             final View child = group.getChildAt(i);
             if (x + scrollX >= child.getLeft()
                 && x + scrollX < child.getRight()
                 && y + scrollY >= child.getTop()
                 && y + scrollY < child.getBottom()
                 && canScroll(child, true, dy,
                            x + scrollX - child.getLeft(), y + scrollY
                            - child.getTop())) {
                 return true;
             }
         }
     }
     return checkV && ViewCompat.canScrollVertically(v, -dy);
 }
 

Step 2

Subclass the enclosing vertically scroll-able view, it may be ScrollView or ListView, or the like and override the onInterceptTouchEvent() method as follows.

 public boolean onInterceptTouchEvent(MotionEvent event) {
      int action = event.getAction();
      float x = event.getX();
      float y = event.getY();
      float dy = y - mLastMotionY;
      switch (action) {
      case MotionEvent.ACTION_DOWN:
           mLastMotionY = y;
      break;
      case MotionEvent.ACTION_MOVE:
           if (Util.canScroll(this, false, (int) dy, (int) x, (int) y)) {
                mLastMotionY = y;
                return false;
           }
      break;
      }
      return super.onInterceptTouchEvent(event);
 }

Step 3

Subclass the enclosed vertically scroll-able view, it may be GridView or ListView or the like and override the onMeasure() method as follows. No need to override this method in ScrollView. Its default implementation behaves in the right way.

 protected void onMeasure(int widthMeasureSpec, 
    int heightMeasureSpec) {
      super.onMeasure(widthMeasureSpec, heightMeasureSpec);
      int mode = MeasureSpec.getMode(widthMeasureSpec);
      if (mode == MeasureSpec.UNSPECIFIED) {
           int height = getLayoutParams().height;
           if (height > 0)
           setMeasuredDimension(getMeasuredWidth(), height);
      }
 }

Step 4

Finally create an xml layout file as given below and see how it works. We have to hard code the layout_height of CustomListView. If you create the view hierarchy by java code, then set the height via LayoutParams.

<com.dass.scroll_inside_scroll.CustomScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent" >
            
            <!-- Other Children -->
            
            <com.dass.scroll_inside_scroll.CustomListView
                android:layout_width="match_parent"
                android:layout_height="300dp" >
            </com.dass.scroll_inside_scroll.CustomListView>
            
            <!-- Other Children -->
            
        </LinearLayout>
</com.dass.scroll_inside_scroll.CustomScrollView>

The project in Github has classes CustomListView, CustomExpandableListView, CustomGridView overrides the method onInterceptTouchEvent() in order to place other vertically scroll-able views inside them. If we don't add vertically scroll-able views inside any of these classes then no need to override onInterceptTouchEvent().

This hard coding is not necessary if you use your own measuring strategy rather than one specified in step 3. For example if your list contains 100 items and you decided to show half of your list size 50 then in onMeasure() of your ListView  can be changed as follows.

 protected void onMeasure(int widthMeasureSpec, 
    int heightMeasureSpec) {
      super.onMeasure(widthMeasureSpec, heightMeasureSpec);
      int mode = MeasureSpec.getMode(widthMeasureSpec);
      if (mode == MeasureSpec.UNSPECIFIED) {
           int height = getHeightBasedOnChildCount();//Note the change here
           if (height > 0)
           setMeasuredDimension(getMeasuredWidth(), height);
      }
 }

 private int getHeightBasedOnChildCount(){
      int height = //Write code to sum up heights of 50 children including the dividers
      return height;
 }

Please comment if you find this post useful or it does not works.