Search code examples
androidlistviewhorizontalscrollview

Stick a horizontalScrollView and capture touch events


I'm trying to reproduce the profile screen of the G+ Android app. In this screen, you can notice a horizontalScrollView containing tabs, which sticks at the top the screen.

The screen is made of a list, containing a HeaderView (ImageBackground, avatar, informations and tabs), and the list (cards).

I am able to make the tabs stick to the top and capture touch events, but the fling effect doesn't work anymore when the header fully disappear from the top (<=> if the firstVisible item of the listview is not the header). I am able to touch and move the HorizontalScrollView, it works until I try to fling.

Fling OK:

FLING OK

Fling KO:

FLING KO

Here is what I did:

1st step: subclass HorizontalScrollView to add a ScrollChangedListener:

@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
    super.onScrollChanged(l, t, oldl, oldt);
    if(mOnScrollChangedListener != null) mOnScrollChangedListener.onScrollChanged(l, t, oldl, oldt);
}

2nd step:

mViewToDraw.setOnScrollChangedListener(new MyHorizontalScrollView.OnScrollChangedListener() {
    @Override
    public void onScrollChanged(int l, int t, int oldl, int oldt) {
        mCurrentScrollX = l;
        invalidate(0, 0, getWidth(), mViewToDraw.getMeasuredHeight());
    }
});

Step 3: The draw

@Override
protected void dispatchDraw(Canvas canvas) {
    super.dispatchDraw(canvas);
    if (mViewToDraw == null) {
      return;
    }

    canvas.translate(-mCurrentScrollX, 0);
    mViewToDraw.draw(canvas);
}

Step 4: capturing the touch event

private enum TouchState {NONE, TOUCHING_HEADER}

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
  if (mViewToDraw == null) {
    return super.dispatchTouchEvent(ev);
  }

  int bottom = mViewToDraw.getMeasuredHeight();
  boolean captured = false;
  boolean invalidate = false;
  switch (ev.getAction()) {
    case MotionEvent.ACTION_DOWN:
      if (ev.getY() <= bottom) {
        mTouchState = TouchState.TOUCHING_HEADER;
        invalidate = true;
        captured = mViewToDraw.dispatchTouchEvent(ev);
      }
      break;
    case MotionEvent.ACTION_CANCEL:
    case MotionEvent.ACTION_UP:
      if (mTouchState == TouchState.TOUCHING_HEADER) {
        mTouchState = TouchState.NONE;
        invalidate = true;
        captured = mViewToDraw.dispatchTouchEvent(ev);
      }
      break;
    default:
      if (mTouchState == TouchState.TOUCHING_HEADER) {
        invalidate = true;
        captured = mViewToDraw.dispatchTouchEvent(ev);
      }
  }

  if (invalidate) {
    mViewToDraw.invalidate();
    invalidate(0, 0, getWidth(), bottom);
  }

  if (captured) {
    return true;
  }

  return super.dispatchTouchEvent(ev);
}

I'm trying not to duplicate the tabs view because I'm pretty sure I can do this way. Hope I was clear enough.

EDIT: I made a small video

As you can see, it works perfectly until the header view fully disappear.


Solution

  • I made a blog post about that HERE