I want to implement 'Quick return header' with ScrollView. My header is not toolbar, it's FrameLayout..
1 Answers
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:
- Parent view must implement NestedScrollingParent interface
- 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

- 2,945
- 1
- 29
- 31

- 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 */ -
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