2

I want to implement 'Quick return header' with ScrollView. My header is not toolbar, it's FrameLayout..

OneCricketeer
  • 179,855
  • 19
  • 132
  • 245
Na Pro
  • 715
  • 1
  • 8
  • 23

1 Answers1

4

Basically you need to use the Nested Scrolling (added in API 21). This feature allows a child (the scrolling one) to communicate with its parent (typically the CoordinatorLayout). However, in order to have the Nested Scrolling, you need that:

  1. Parent view must implement NestedScrollingParent interface
  2. Child view must implement NestedScrollingChild interface

CoordinatorLayout implements NestedScrollingParent, but unfortunally, ScrollView does not implement NestedScrollingChild interface.

So, YOU CAN'T HAVE A NESTED SCROLLING USING SCROLLVIEW (before API 21: look the EDIT section on the bottom). However you can use a NestedScrollView that basically is a ScrollView that implements the NestedScrollingChild interface.

After that, you need to create a custom CoordinatorLayout.Behavior and override the method onStartNestedScroll, onNestedPreScroll and onNestedScroll. Below you can see the code that match your need (I took it from this example with some changes).

Supposing that you view is a FramLayout you can do something like this in your layout:

<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <android.support.v4.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">
        </LinearLayout>
    </android.support.v4.widget.NestedScrollView>
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="84dp"
        android:background="#FF0000"
        app:layout_behavior=".QuickHideBehavior">
    </FrameLayout>
</android.support.design.widget.CoordinatorLayout>

As you can see a custom behavior is attached to the FrameLayout. Then you need to modify the above QuickHideBehavior example in this way:

public class QuickHideBehavior extends CoordinatorLayout.Behavior<View> {

    private static final int DIRECTION_UP = 1;
    private static final int DIRECTION_DOWN = -1;

    /* Tracking direction of user motion */
    private int mScrollingDirection;
    /* Tracking last threshold crossed */
    private int mScrollTrigger;

    /* Accumulated scroll distance */
    private int mScrollDistance;
    /* Distance threshold to trigger animation */
    private int mScrollThreshold;


    private ObjectAnimator mAnimator;

    //Required to instantiate as a default behavior
    public QuickHideBehavior() {
    }

    //Required to attach behavior via XML
    public QuickHideBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);

        TypedArray a = context.getTheme()
                .obtainStyledAttributes(new int[] {R.attr.actionBarSize});
        //Use half the standard action bar height
        mScrollThreshold = a.getDimensionPixelSize(0, 0) / 2;
        a.recycle();
    }

    //Called before a nested scroll event. Return true to declare interest
    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
                                       View child, View directTargetChild, View target,
                                       int nestedScrollAxes) {
        //We have to declare interest in the scroll to receive further events
        return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

    //Called before the scrolling child consumes the event
    // We can steal all/part of the event by filling in the consumed array
    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout,
                                  View child, View target,
                                  int dx, int dy,
                                  int[] consumed) {
        //Determine direction changes here
        if (dy > 0 && mScrollingDirection != DIRECTION_UP) {
            mScrollingDirection = DIRECTION_UP;
            mScrollDistance = 0;
        } else if (dy < 0 && mScrollingDirection != DIRECTION_DOWN) {
            mScrollingDirection = DIRECTION_DOWN;
            mScrollDistance = 0;
        }
    }

    //Called after the scrolling child consumes the event, with amount consumed
    @Override
    public void onNestedScroll(CoordinatorLayout coordinatorLayout,
                               View child, View target,
                               int dxConsumed, int dyConsumed,
                               int dxUnconsumed, int dyUnconsumed) {
        //Consumed distance is the actual distance traveled by the scrolling view
        mScrollDistance += dyConsumed;
        if (mScrollDistance > mScrollThreshold
                && mScrollTrigger != DIRECTION_UP) {
            //Hide the target view
            mScrollTrigger = DIRECTION_UP;
            restartAnimator(child, getTargetHideValue(coordinatorLayout, child));
        } else if (mScrollDistance < -mScrollThreshold
                && mScrollTrigger != DIRECTION_DOWN) {
            //Return the target view
            mScrollTrigger = DIRECTION_DOWN;
            restartAnimator(child, 0f);
        }
    }

    //Called after the scrolling child handles the fling
    @Override
    public boolean onNestedFling(CoordinatorLayout coordinatorLayout,
                                 View child, View target,
                                 float velocityX, float velocityY,
                                 boolean consumed) {
        //We only care when the target view is already handling the fling
        if (consumed) {
            if (velocityY > 0 && mScrollTrigger != DIRECTION_UP) {
                mScrollTrigger = DIRECTION_UP;
                restartAnimator(child, getTargetHideValue(coordinatorLayout, child));
            } else if (velocityY < 0 && mScrollTrigger != DIRECTION_DOWN) {
                mScrollTrigger = DIRECTION_DOWN;
                restartAnimator(child, 0f);
            }
        }

        return false;
    }

    /* Helper Methods */

    //Helper to trigger hide/show animation
    private void restartAnimator(View target, float value) {
        if (mAnimator != null) {
            mAnimator.cancel();
            mAnimator = null;
        }

        mAnimator = ObjectAnimator
                .ofFloat(target, View.TRANSLATION_Y, value)
                .setDuration(250);

        /*mAnimator = ObjectAnimator.ofFloat(target, "alpha", 0f, 1f).setDuration(250);*/
        mAnimator.start();
    }

    private float getTargetHideValue(ViewGroup parent, View target) {
        if(target instanceof FrameLayout)
            return -target.getHeight();

        return 0f;
    }
}

And you are done.

I recommend you to take a look here for more examples or this video both by Dave Smith

EDIT: Actually, since API 21, ScrollView (or ListView) supports Nested Scrolling, but you need to enable it:

scrollView.setNestedScrollingEnabled(true);

and you need, of course, to set

minSdkVersion > 20

Saket
  • 2,945
  • 1
  • 29
  • 31
GVillani82
  • 17,196
  • 30
  • 105
  • 172
  • Many thanks Joseph, could you please explain further how to apply your solution in my layout. Here is my layout: /* My content */ – Na Pro Mar 16 '16 at 01:29
  • I want to apply quick return for layout_header_search that's FrameLayout sir. – Na Pro Mar 16 '16 at 01:34
  • @NaPro I have edited my answer. I have also tested it on my device: it works. Try it. – GVillani82 Mar 16 '16 at 13:17
  • Joseph, you mean my application has to set minSdkVersion = 21? – Na Pro Mar 17 '16 at 03:43
  • Hi Joseph, i just tried your solution but not sure why it's not working. I have tried this solution: https://android-arsenal.com/details/3/698 and it's working but not well as what i want, when header is hidden there is still 'white section' with height = header's height :-s – Na Pro Mar 17 '16 at 04:15
  • @NaPro no, you need to set minSdkVersion = 21, only if you use the `ScollView`. I suggest you, instead, to use the `NestedScrollView` as done in my example, so that you don't require 21 as minSdkVersion. I have tested my example, and it works. Which problems are you having? – GVillani82 Mar 17 '16 at 08:20
  • Hi Joseph, i set margin_top for NestedScrollView by header height, could you please tell me how to remove white section when header hidden? If i don't set margin_top so header will cover NestedScrollView. – Na Pro Mar 17 '16 at 12:21
  • @NaPro I think that you should focus on what behavior you really need. An idea can be to move up the ScrollView while the `FrameLayout` is going up. But in this case I'm not sure that the user will be happy seeing the `ScrollView` that moves faster that its finger. However, if you like this option, you should consume the event inside the prescroll (managing the consumed[] array) and don't start scrolling until the `FrameLayout` disappears and the `ScrollView` is pushed up. You can get a hint looking at the source code of `AppBarLayout`. – GVillani82 Mar 17 '16 at 16:19
  • Any way, many thanks Joseph! – Na Pro Mar 18 '16 at 03:25
  • You are welcome @NaPro ;) – GVillani82 Mar 18 '16 at 07:47