Forked from NikolaDespotoski/OverscrollScalingViewAppBarLayoutBehavior.java
Created
November 20, 2015 02:45
-
-
Save goodev/7c6a90db5ec36b939e48 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="utf-8"?> | |
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" | |
xmlns:app="http://schemas.android.com/apk/res-auto" | |
xmlns:tools="http://schemas.android.com/tools" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
android:fitsSystemWindows="true" | |
tools:context=".MainActivity"> | |
<android.support.design.widget.AppBarLayout | |
android:id="@+id/app_bar" | |
android:layout_width="match_parent" | |
android:layout_height="@dimen/app_bar_height" | |
android:fitsSystemWindows="true" | |
android:theme="@style/AppTheme.AppBarOverlay"> | |
<ImageView | |
android:id="@+id/image" | |
android:layout_width="match_parent" | |
android:layout_height="200dp" | |
android:src="@drawable/placeholder" | |
android:tag="overScrollScale" | |
app:layout_scrollFlags="scroll|exitUntilCollapsed" /> | |
</android.support.design.widget.AppBarLayout> | |
<include layout="@layout/content_main" /> | |
<android.support.design.widget.FloatingActionButton | |
android:id="@+id/fab" | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:layout_margin="@dimen/fab_margin" | |
android:src="@android:drawable/ic_dialog_email" | |
app:layout_anchor="@id/app_bar" | |
app:layout_anchorGravity="bottom|end" /> | |
</android.support.design.widget.CoordinatorLayout> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="utf-8"?> | |
<android.support.v4.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android" | |
xmlns:app="http://schemas.android.com/apk/res-auto" | |
xmlns:tools="http://schemas.android.com/tools" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
app:layout_behavior="despotoski.nikola.appbarlayoutsamples.view.OverscrollScalingImageAppBarLayoutBehavior" | |
tools:context=".MainActivity" | |
tools:showIn="@layout/activity_main"> | |
<TextView | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:layout_margin="@dimen/text_margin" | |
android:text="@string/large_text" /> | |
</android.support.v4.widget.NestedScrollView> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package despotoski.nikola.appbarlayoutsamples.view; | |
import android.animation.IntEvaluator; | |
import android.animation.ValueAnimator; | |
import android.content.Context; | |
import android.content.res.TypedArray; | |
import android.support.design.widget.AppBarLayout; | |
import android.support.design.widget.CoordinatorLayout; | |
import android.support.v4.view.ViewCompat; | |
import android.support.v4.view.ViewPropertyAnimatorCompat; | |
import android.support.v4.view.ViewPropertyAnimatorListener; | |
import android.support.v4.view.ViewPropertyAnimatorListenerAdapter; | |
import android.util.AttributeSet; | |
import android.view.View; | |
import android.view.ViewGroup; | |
import android.widget.ImageView; | |
import despotoski.nikola.appbarlayoutsamples.R; | |
/** | |
* Created by Nikola on 11/17/2015. | |
*/ | |
public class OverscrollScalingViewAppBarLayoutBehavior extends AppBarLayout.ScrollingViewBehavior { | |
private static final String TAG = "overScrollScale"; | |
private View mTargetScalingView; | |
private int mPivotX; | |
private int mPivotY; | |
private Scaler mScaleImpl; | |
private static final int[] SCROLLVIEW_STYLEABLE = new int[]{ | |
R.attr.targetScalingViewId | |
}; | |
public OverscrollScalingViewAppBarLayoutBehavior(Context context, AttributeSet attrs) { | |
super(context, attrs); | |
mScaleImpl = mScaleImpl == null ? new ViewScaler() : mScaleImpl; | |
} | |
public OverscrollScalingViewAppBarLayoutBehavior() { | |
super(); | |
mScaleImpl = mScaleImpl == null ? new ViewScaler() : mScaleImpl; | |
} | |
private int mTotalDyUnconsumed = 0; | |
private int mTotalTargetDyUnconsumed; | |
public void setScaler(Scaler scaler) { | |
this.mScaleImpl = scaler; | |
} | |
@Override | |
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) { | |
return dependency instanceof AppBarLayout; | |
} | |
@Override | |
public boolean onLayoutChild(CoordinatorLayout parent, View abl, int layoutDirection) { | |
boolean superLayout = super.onLayoutChild(parent, abl, layoutDirection); | |
if (mTargetScalingView == null) { | |
mTargetScalingView = parent.findViewWithTag(TAG); | |
if (mTargetScalingView != null) { | |
mScaleImpl.obtainInitialValues(); | |
} | |
} | |
return superLayout; | |
} | |
@Override | |
public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { | |
if (mTargetScalingView == null || dyConsumed != 0) { | |
mScaleImpl.cancelAnimations(); | |
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed); | |
return; | |
} | |
if (dyUnconsumed < 0 && getTopAndBottomOffset() >= mScaleImpl.getInitialParentBottom()) { | |
int absDyUnconsumed = Math.abs(dyUnconsumed); | |
mTotalDyUnconsumed += absDyUnconsumed; | |
mTotalDyUnconsumed = Math.min(mTotalDyUnconsumed, mTotalTargetDyUnconsumed); | |
mScaleImpl.updateViewScale(); | |
} else { | |
mTotalDyUnconsumed = 0; | |
mScaleImpl.setShouldRestore(false); | |
if (dyConsumed != 0) { | |
mScaleImpl.cancelAnimations(); | |
} | |
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed); | |
} | |
} | |
@Override | |
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) { | |
return nestedScrollAxes == View.SCROLL_AXIS_VERTICAL; | |
} | |
@Override | |
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target) { | |
mScaleImpl.retractScale(); | |
super.onStopNestedScroll(coordinatorLayout, child, target); | |
} | |
private class ViewScaler extends ParentScaler { | |
private boolean mRetracting = false; | |
private ViewPropertyAnimatorListener mShouldRestoreListener = new ViewPropertyAnimatorListenerAdapter() { | |
@Override | |
public void onAnimationEnd(View view) { | |
mShouldRestore = false; | |
} | |
}; | |
private ViewPropertyAnimatorCompat mScaleAnimator; | |
@Override | |
public void updateViewScale() { | |
float scale = getScale(); | |
setScale(scale); | |
mCurrentScale = scale; | |
mShouldRestore = true; | |
} | |
@Override | |
public void retractScale() { | |
super.retractScale(); | |
if (mShouldRestore) { | |
mRetracting = true; | |
mScaleAnimator = ViewCompat.animate(mTargetScalingView).setListener(mShouldRestoreListener).scaleY(1f).scaleX(1f); | |
mScaleAnimator.start(); | |
mTotalDyUnconsumed = 0; | |
} | |
} | |
@Override | |
public void setScale(float scale) { | |
super.setScale(scale); | |
ViewCompat.setScaleX(mTargetScalingView, scale); | |
ViewCompat.setScaleY(mTargetScalingView, scale); | |
} | |
@Override | |
public void obtainInitialValues() { | |
super.obtainInitialValues(); | |
mInitialScale = Math.max(ViewCompat.getScaleY(mTargetScalingView), ViewCompat.getScaleX(mTargetScalingView)); | |
} | |
@Override | |
public boolean isRetracting() { | |
return mRetracting; | |
} | |
@Override | |
public void cancelAnimations() { | |
super.cancelAnimations(); | |
mShouldRestore = false; | |
ViewCompat.animate(mTargetScalingView).cancel(); | |
ViewCompat.setScaleY(mTargetScalingView, 1f); | |
ViewCompat.setScaleX(mTargetScalingView, 1f); | |
} | |
} | |
private class ParentScaler implements Scaler { | |
float mCurrentScale; | |
float mInitialScale; | |
boolean mShouldRestore = false; | |
private int mInitialParentBottom; | |
private ViewGroup mParent; | |
IntEvaluator mIntEvaluator = new IntEvaluator(); | |
private int mTargetParentBottom; | |
private ValueAnimator mBottomAnimator; | |
@Override | |
public void setShouldRestore(boolean restore) { | |
mShouldRestore = restore; | |
} | |
public float getCurrentScale() { | |
return mCurrentScale; | |
} | |
@Override | |
public void cancelAnimations() { | |
mShouldRestore = false; | |
if (mBottomAnimator != null && mBottomAnimator.isRunning()) | |
mBottomAnimator.cancel(); | |
} | |
@Override | |
public int getInitialParentBottom() { | |
return mInitialParentBottom; | |
} | |
@Override | |
public boolean isShouldRestore() { | |
return mShouldRestore; | |
} | |
@Override | |
public boolean isRetracting() { | |
return false; | |
} | |
public float getScale() { | |
float ratio = (float) mTotalDyUnconsumed / mTotalTargetDyUnconsumed; | |
float scale = 1f + ratio; | |
return scale; | |
} | |
@Override | |
public void updateViewScale() { | |
} | |
@Override | |
public void retractScale() { | |
final View parent = mParent; | |
if (parent.getBottom() > mInitialParentBottom) { | |
mBottomAnimator = ValueAnimator.ofInt(parent.getBottom(), mInitialParentBottom); | |
mBottomAnimator.setEvaluator(mIntEvaluator); | |
mBottomAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { | |
@Override | |
public void onAnimationUpdate(ValueAnimator animation) { | |
int bottom = (int) animation.getAnimatedValue(); | |
parent.setBottom(bottom); | |
} | |
}); | |
mBottomAnimator.start(); | |
} | |
} | |
@Override | |
public void setScale(float scale) { | |
final View parent = mParent; | |
Integer evaluate = mIntEvaluator.evaluate(scale, mInitialParentBottom, mTargetParentBottom); | |
parent.setBottom(evaluate); | |
parent.postInvalidate(); | |
} | |
@Override | |
public void obtainInitialValues() { | |
mParent = (ViewGroup) mTargetScalingView.getParent(); | |
mInitialParentBottom = mParent.getHeight(); | |
mTargetParentBottom = (int) (mInitialParentBottom * 1.1); | |
mPivotX = mTargetScalingView.getWidth() / 2; | |
mPivotY = mTargetScalingView.getHeight() / 2; | |
ViewCompat.setPivotX(mTargetScalingView, mPivotX); | |
ViewCompat.setPivotY(mTargetScalingView, mPivotY); | |
mTotalTargetDyUnconsumed = 50; | |
} | |
} | |
/* private class MatrixScaler extends DefaultScaler { | |
private int mIntialViewBottom; | |
private double mTargetParentBottom; | |
@Override | |
public boolean isRetracting() { | |
return mRetracting; | |
} | |
@Override | |
public float getCurrentScale() { | |
return mCurrentScale; | |
} | |
private boolean mRetracting = false; | |
private AnimatorListenerAdapter mShouldRestoreListener = new AnimatorListenerAdapter() { | |
public void onAnimationEnd(Animation a) { | |
mShouldRestore = false; | |
} | |
}; | |
private ValueAnimator.AnimatorUpdateListener mRestoreScaleListener = new ValueAnimator.AnimatorUpdateListener() { | |
@Override | |
public void onAnimationUpdate(ValueAnimator animation) { | |
float animatedValue = (float) animation.getAnimatedValue(); | |
setScale(animatedValue); | |
} | |
}; | |
@Override | |
public void updateViewScale() { | |
float scale = getScale(); | |
setScale(scale); | |
mCurrentScale = scale; | |
mShouldRestore = true; | |
} | |
@Override | |
public void retractScale() { | |
super.retractScale(); | |
if (mShouldRestore) { | |
mRetracting = true; | |
mShouldRestore = false; | |
ValueAnimator valueAnimator = ValueAnimator.ofFloat(mCurrentScale, mInitialScale); | |
valueAnimator.addListener(mShouldRestoreListener); | |
valueAnimator.addUpdateListener(mRestoreScaleListener); | |
valueAnimator.start(); | |
mTotalDyUnconsumed = 0; | |
} | |
} | |
@Override | |
public void setScale(float scale) { | |
super.setScale(scale); | |
Integer evaluate = mIntEvaluator.evaluate(scale, mIntialViewBottom, (int) mTargetParentBottom); | |
mTargetScalingView.setBottom(evaluate); | |
Matrix matrix = new Matrix(); | |
matrix.setScale(scale, scale, mPivotX, mPivotY); | |
mTargetScalingView.setImageMatrix(matrix); | |
} | |
@Override | |
public void obtainInitialValues() { | |
super.obtainInitialValues(); | |
mIntialViewBottom = mTargetScalingView.getBottom(); | |
mTargetParentBottom = mIntialViewBottom * 1.1; | |
float[] values = new float[9]; | |
mTargetScalingView.getImageMatrix().getValues(values); | |
mInitialScale = Math.min(values[Matrix.MSCALE_X], values[Matrix.MSCALE_Y]); | |
} | |
} | |
*/ | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package despotoski.nikola.appbarlayoutsamples.view; | |
public interface Scaler { | |
public void setShouldRestore(boolean restore); | |
public float getCurrentScale(); | |
public void cancelAnimations(); | |
public int getInitialParentBottom(); | |
public boolean isShouldRestore(); | |
public boolean isRetracting(); | |
public float getScale(); | |
public void updateViewScale(); | |
public void retractScale(); | |
public void setScale(float scale); | |
public void obtainInitialValues(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment