Doing it right: vertical ScrollView with ViewPager and ListView

Yes, we cannot add one scroll item into another one. Yes, they waste your memory and bad for performance. But what if you must do this?

Let’s start with ScrollView. StackOverFlow offers us quite well method. It works, but not for using in nested fragments. Touch events in this case work awfully. The solution is here:

public class VerticalScrollView extends ScrollView
{
   private GestureDetector mGestureDetector;
    View.OnTouchListener mGestureListener;

    public VerticalScrollView(Context context) 
    {
        super(context);
        init(context);
    }

    public VerticalScrollView(Context context, AttributeSet attrs) 
    {
       super(context, attrs);
       init(context);
    }

    public VerticalScrollView(Context context, AttributeSet attrs, int defStyle) 
    {
       super(context, attrs, defStyle);
       init(context);
    }
    
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) 
    {
       return super.onInterceptTouchEvent(ev) && mGestureDetector.onTouchEvent(ev);
    }

    class YScrollDetector extends SimpleOnGestureListener
    {
       @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) 
       {             
          return Math.abs(distanceY) > Math.abs(distanceX);
        }
    }

    private void init(Context context)
    {
       mGestureDetector = new GestureDetector(context, new YScrollDetector());
        setFadingEdgeLength(0);
   }
}

We just rewrite onInterceptTouchEvent() and create own OnGestureDetector for vertical fling.

The next part is NestedListView. Original source is perfect, but not for nested fragments again. It consumes swipe events as such and all ScrollView will not move if this ListView occupies whole view rect.

Here is patched NestedListView:

public class NestedListView extends ListView implements OnTouchListener, OnScrollListener 
{
    private int listViewTouchAction;
    private static final int MAXIMUM_LIST_ITEMS_VIEWABLE = 99;

   public NestedListView(Context context, AttributeSet attrs) 
    {
        super(context, attrs);
        listViewTouchAction = -1;
        setOnScrollListener(this);
        setOnTouchListener(this);
    }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem,
            int visibleItemCount, int totalItemCount) 
    {
        if (getAdapter() != null && getAdapter().getCount() > MAXIMUM_LIST_ITEMS_VIEWABLE) 
        {
            if (listViewTouchAction == MotionEvent.ACTION_MOVE) 
            {
                scrollBy(0, -1);
            }
        }
    }

    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) 
    {
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 
    {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int newHeight = 0;
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        if (heightMode != MeasureSpec.EXACTLY) 
        {
            ListAdapter listAdapter = getAdapter();
            if (listAdapter != null && !listAdapter.isEmpty()) 
            {
                int listPosition = 0;
                for (listPosition = 0; listPosition < listAdapter.getCount()
                        && listPosition < MAXIMUM_LIST_ITEMS_VIEWABLE; listPosition++) 
                {
                    View listItem = listAdapter.getView(listPosition, null, this);
                    //now it will not throw a NPE if listItem is a ViewGroup instance
                    if (listItem instanceof ViewGroup) 
                    {
                        listItem.setLayoutParams(new LayoutParams(
                                LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
                    }
                    listItem.measure(widthMeasureSpec, heightMeasureSpec);
                    newHeight += listItem.getMeasuredHeight();
                }
                newHeight += getDividerHeight() * listPosition;
            }
            if ((heightMode == MeasureSpec.AT_MOST) && (newHeight > heightSize)) 
            {
                if (newHeight > heightSize) 
                {
                    newHeight = heightSize;
                }
            }
        } 
        else 
        {
            newHeight = getMeasuredHeight();
        }
        setMeasuredDimension(getMeasuredWidth(), newHeight);
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) 
    {
        if (getAdapter() != null && getAdapter().getCount() > MAXIMUM_LIST_ITEMS_VIEWABLE) 
        {
            if (listViewTouchAction == MotionEvent.ACTION_MOVE) 
            {
                scrollBy(0, 1);
            }
        }
        return false;
    }
}

Unfortunately, ViewHolder or another ListView optimizations will not work. But it scrolls perfectly.

And a couple words about ViewPager in ScrollView. We’ve already create vertical gesture detector for ScrollView that will redirect horizontal move to ViewPager. Here is it’s code:

public class WrapContentHeightViewPager extends ViewPager 
{
    public WrapContentHeightViewPager(Context context) 
    {
        super(context);
    }

    public WrapContentHeightViewPager(Context context, AttributeSet attrs)
    {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 
    {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

       int height = 0;
        for(int i = 0; i < getChildCount(); i++) 
        {
            View child = getChildAt(i);
            child.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
            
            int h = child.getMeasuredHeight();
            if(h > height) height = h;
        }

        heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);

       super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
}

This approach gives you blank pages in unpredictable places, this will get you crazy image carousel, if you plan to load images in it’s items. Only double super.onMeasure() call will solve the problem.

4 thoughts on “Doing it right: vertical ScrollView with ViewPager and ListView

  1. Hi there!
    Thank you for this snippet. It was very useful but i am facing a problem.

    The snippet doesn’t measure the height correctly. I am getting a lot of padding below the view. Do you happen to know why this might be?

    Like

    1. Good day! You can get a lot of padding if you use one or few very large images with some small thumbnails, for example. onMeasure method sets ViewPager’s height to max children height, so you should prefer images (or make other views) with the same size. Alternatively, set scaleType of ImageView to CENTER, CENTER_CROP or CENTER_INSIDE.

      Like

Leave a comment