I want to get have over scroll listener in NestedScrollView in order to make my top ImageView get Zoomed when user over scrolls. Something like this
Above library uses ScrollView
which in my case I need NestedScrollView
. So i wanted to follow the same approach by the developer, but having some trouble solving few issues.
In View
there is a protected
method overScrollBy
that is used in ScrollView
which developer overrides in his CustomScrollView
. Unfortunately, instead of overScrollBy
NestedScrollView
uses it's own overScrollByCombat
which is private and I cannot override it. So, I am kind of stuck at how to get "overScrollListener"
in my CustomNestedScrollView
.
The only solution I could think of was actually making my PreCustomNestedScrollView
in which I just copy paste the source code of NestedScrollView
and setting the overScrollByCombat
as public. It works but I don't thinks it's an elegant way.
If there are already any such libraries that gives the same effect with NestedScrollView
, you are welcome to recommend.
Here are two ways to get this.
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.design.widget.CoordinatorLayout;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.Transformation;
public class OverScrollBounceBehavior extends CoordinatorLayout.Behavior<View> {
private static final String TAG = "Behavior";
private int mNormalHeight = 0;
private int mMaxHeight = 0;
private float mFactor = 1.8f;
private int mOverScrollY;
private View mTargetView;
private OnScrollChangeListener mListener;
public OverScrollBounceBehavior() {
}
public OverScrollBounceBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull View child,
@NonNull View directTargetChild,
@NonNull View target,
int nestedScrollAxes, int type) {
findTargetView();
Log.d(TAG, "onStartNestedScroll " + "type = " + type);
//TYPE_TOUCH handle over scroll
if (checkTouchType(type) && checkTargetView()) {
mOverScrollY = 0;
mNormalHeight = mTargetView.getHeight();
mMaxHeight = (int) (mNormalHeight * mFactor);
}
return true;
}
public void setFactor(float factor) {
this.mFactor = factor;
}
public void setOnScrollChangeListener(OnScrollChangeListener listener) {
this.mListener = listener;
}
public void setTargetView(View targetView) {
//set a target view from outside, target view should be NestedScrollView child
this.mTargetView = targetView;
}
private void findTargetView() {
//implement a fixed find target view as you wish
}
@Override
public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull View child,
@NonNull View target,
int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed,
int type) {
//unconsumed == 0 no overScroll
//unconsumed > 0 overScroll up
if (dyUnconsumed >= 0) {
return;
}
Log.d(TAG, "onNestedScroll : dyUnconsumed = " + dyUnconsumed);
mOverScrollY -= dyUnconsumed;
Log.d(TAG, "onNestedScroll : mOverScrollY = " + mOverScrollY + "type = " + type);
//TYPE_TOUCH handle over scroll
if (checkTouchType(type) && checkTargetView()) {
if (mOverScrollY > 0 && mTargetView.getLayoutParams().height + Math.abs(mOverScrollY) <= mMaxHeight) {
mTargetView.getLayoutParams().height += Math.abs(mOverScrollY);
mTargetView.requestLayout();
if (mListener != null) {
mListener.onScrollChanged(calculateRate(mTargetView, mMaxHeight, mNormalHeight));
}
}
}
}
@Override
public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull View child,
@NonNull View target,
int type) {
Log.d(TAG, "onStopNestedScroll" + "type = " + type);
//TYPE_TOUCH handle over scroll
if (checkTouchType(type)
&& checkTargetView()
&& mTargetView.getHeight() > mNormalHeight) {
ResetAnimation animation = new ResetAnimation(mTargetView, mNormalHeight, mListener);
animation.setDuration(300);
mTargetView.startAnimation(animation);
}
}
private boolean checkTouchType(int type) {
return type == ViewCompat.TYPE_TOUCH;
}
private boolean checkTargetView() {
return mTargetView != null;
}
public static class ResetAnimation extends Animation {
int targetHeight;
int originalHeight;
int extraHeight;
View view;
OnScrollChangeListener listener;
ResetAnimation(View view, int targetHeight, OnScrollChangeListener listener) {
this.view = view;
this.targetHeight = targetHeight;
this.originalHeight = view.getHeight();
this.extraHeight = this.targetHeight - originalHeight;
this.listener = listener;
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
int newHeight = (int) (targetHeight - extraHeight * (1 - interpolatedTime));
view.getLayoutParams().height = newHeight;
view.requestLayout();
if (listener != null) {
listener.onScrollChanged(calculateRate(view, originalHeight, targetHeight));
}
}
}
public interface OnScrollChangeListener {
void onScrollChanged(float rate);
}
private static float calculateRate(View targetView, int maxHeight, int targetHeight) {
float rate = 0;
if (targetView != null) {
rate = (maxHeight - (float) targetView.getLayoutParams().height) / (maxHeight - targetHeight);
}
return rate;
}
}
(1). Create a delegate subclass in package android.support.v4.widget
and override `overScrollByCompat()` to invoke customized `openedOverScrollByCompat()` method.
(2). Create your owner StretchTopNestedScrollView
override
openedOverScrollByCompat()
then you can do what you want.
Delegate view
package android.support.v4.widget;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
public class OpenedNestedScrollView extends NestedScrollView {
public OpenedNestedScrollView(@NonNull Context context) {
this(context, null);
}
public OpenedNestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, -1);
}
public OpenedNestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
boolean overScrollByCompat(int deltaX, int deltaY,
int scrollX, int scrollY,
int scrollRangeX, int scrollRangeY,
int maxOverScrollX, int maxOverScrollY,
boolean isTouchEvent) {
return openedOverScrollByCompat(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent);
}
protected boolean openedOverScrollByCompat(int deltaX, int deltaY,
int scrollX, int scrollY,
int scrollRangeX, int scrollRangeY,
int maxOverScrollX, int maxOverScrollY,
boolean isTouchEvent) {
return super.overScrollByCompat(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent);
}
}
Your owner view
ublic class StretchTopNestedScrollView extends OpenedNestedScrollView {
private View mTopView, mBottomView;
private int mNormalHeight, mMaxHeight;
private onOverScrollChanged mChangeListener;
private float mFactor = 1.6f;
private interface OnTouchEventListener {
void onTouchEvent(MotionEvent ev);
}
public StretchTopNestedScrollView(Context context) {
this(context, null);
}
public StretchTopNestedScrollView(Context context, AttributeSet attrs) {
this(context, attrs, -1);
}
public StretchTopNestedScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public void setFactor(float f) {
mFactor = f;
mTopView.postDelayed(new Runnable() {
@Override
public void run() {
mNormalHeight = mTopView.getHeight();
mMaxHeight = (int) (mNormalHeight * mFactor);
}
}, 50);
}
public View getTopView() {
return mTopView;
}
public View getBottomView() {
return mBottomView;
}
@Override
public void onFinishInflate() {
super.onFinishInflate();
if (getChildCount() > 1)
throw new IllegalArgumentException("Root layout must be a LinearLayout, and only one child on this view!");
if (getChildCount() == 0 || !(getChildAt(0) instanceof LinearLayout))
throw new IllegalArgumentException("Root layout is not a LinearLayout!");
if (getChildCount() == 1 && (getChildAt(0) instanceof LinearLayout)) {
LinearLayout parent = (LinearLayout) getChildAt(0);
if (parent.getChildCount() != 2) {
throw new IllegalArgumentException("Root LinearLayout's has not EXACTLY two Views!");
} else {
mTopView = parent.getChildAt(0);
mBottomView = parent.getChildAt(1);
mTopView.postDelayed(new Runnable() {
@Override
public void run() {
mNormalHeight = mTopView.getHeight();
mMaxHeight = (int) (mNormalHeight * mFactor);
}
}, 50);
}
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
}
@Override
protected boolean openedOverScrollByCompat(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {
if (scrollY == 0) {
//down, zoom in
if (deltaY < 0 && mTopView.getLayoutParams().height + Math.abs(deltaY) > mMaxHeight) {
mTopView.getLayoutParams().height = mMaxHeight;
} else if (deltaY < 0 && mTopView.getLayoutParams().height + Math.abs(deltaY) <= mMaxHeight) {
mTopView.getLayoutParams().height += Math.abs(deltaY);
}
//up, zoom out
else if (deltaY > 0 && mTopView.getLayoutParams().height - Math.abs(deltaY) < mNormalHeight) {
mTopView.getLayoutParams().height = mNormalHeight;
} else if (deltaY > 0 && mTopView.getLayoutParams().height - Math.abs(deltaY) >= mNormalHeight) {
mTopView.getLayoutParams().height -= Math.abs(deltaY);
}
}
if (mChangeListener != null) mChangeListener.onChanged(
(mMaxHeight - (float) mTopView.getLayoutParams().height) / (mMaxHeight - mNormalHeight)
);
if (deltaY != 0 && scrollY == 0) {
mTopView.requestLayout();
mBottomView.requestLayout();
}
if (mTopView.getLayoutParams().height == mNormalHeight) {
super.overScrollBy(deltaX, deltaY, scrollX,
scrollY, scrollRangeX, scrollRangeY,
maxOverScrollX, maxOverScrollY, isTouchEvent);
}
return true;
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
touchListener.onTouchEvent(ev);
return super.onTouchEvent(ev);
}
public interface onOverScrollChanged {
void onChanged(float v);
}
public void setChangeListener(onOverScrollChanged changeListener) {
mChangeListener = changeListener;
}
private OnTouchEventListener touchListener = new OnTouchEventListener() {
@Override
public void onTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_UP) {
if (mTopView != null && mTopView.getHeight() > mNormalHeight) {
ResetAnimation animation = new ResetAnimation(mTopView, mNormalHeight);
animation.setDuration(400);
mTopView.startAnimation(animation);
}
}
}
};
public class ResetAnimation extends Animation {
int targetHeight;
int originalHeight;
int extraHeight;
View mView;
ResetAnimation(View view, int targetHeight) {
this.mView = view;
this.targetHeight = targetHeight;
originalHeight = view.getHeight();
extraHeight = this.targetHeight - originalHeight;
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
int newHeight = (int) (targetHeight - extraHeight * (1 - interpolatedTime));
mView.getLayoutParams().height = newHeight;
mView.requestLayout();
if (mChangeListener != null) mChangeListener.onChanged(
(mMaxHeight - (float) mTopView.getLayoutParams().height) / (mMaxHeight - mNormalHeight)
);
}
}
}