diff --git a/README.md b/README.md index cca33bb..555b98b 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,10 @@ This library encapsulates scrolling logic when implementing *scrolling tabs* (ho compile 'ru.noties:scrollable:x.x.x' ``` +## What new (1.1.0) +* Improved interception of ghost touches when `ScrollableLayout.getScrollY()` > 0 && < max scroll y +* Added `Close-up logic` (turned off by default) + ## Howto Simply wrap your views in `ru.noties.scrollable.ScrollableLayout`. The final xml might be looking something like that (don't copy, it's not valid): @@ -34,7 +38,7 @@ Simply wrap your views in `ru.noties.scrollable.ScrollableLayout`. The final xml + app:scrollable_maxScroll="@dimen/header_height" + app:scrollable_considerIdleMillis="125" + app:scrollable_friction="0.075" + app:scrollable_closeUpAnimationMillis="250" + app:scrollable_defaultCloseUp="true" + app:scrollable_scrollerFlywheel="false" + app:scrollable_closeUpAnimatorInterpolator="@android:anim/accelerate_decelerate_interpolator"> + app:scrollable_maxScroll="@dimen/header_height" + app:scrollable_defaultCloseUp="true"> } +// used for debugging +//dependencies { +// compile 'ru.noties:debug:1.1.3' +//} + apply from: 'https://raw.githubusercontent.com/chrisbanes/gradle-mvn-push/master/gradle-mvn-push.gradle' diff --git a/library/src/main/java/ru/noties/scrollable/CloseUpAlgorithm.java b/library/src/main/java/ru/noties/scrollable/CloseUpAlgorithm.java new file mode 100644 index 0000000..6ce7332 --- /dev/null +++ b/library/src/main/java/ru/noties/scrollable/CloseUpAlgorithm.java @@ -0,0 +1,32 @@ +package ru.noties.scrollable; + +/** + * Use this interface to handle specific *close-up* logic for {@link ScrollableLayout}, + * use with {@link ScrollableLayout#setCloseUpAlgorithm(CloseUpAlgorithm)} + * @see DefaultCloseUpAlgorithm + * Created by Dimitry Ivanov on 22.05.2015. + */ +public interface CloseUpAlgorithm { + + /** + * This method computes end scroll y after fling event was detected + * @param layout {@link ScrollableLayout} + * @param isScrollingBottom whether {@link ScrollableLayout} would scroll to top or bottom + * @param nowY current scroll y of the *layout* + * @param suggestedY scroll y that is suggested + * @param maxY current max scroll y of the *layout* + * @return end scroll y value for the *layout* to animate to + */ + int getFlingFinalY(ScrollableLayout layout, boolean isScrollingBottom, int nowY, int suggestedY, int maxY); + + /** + * This method will be fired after scroll state of a {@link ScrollableLayout} would be considered idle + * @param layout {@link ScrollableLayout} + * @param nowY current scroll y of the *layout* + * @param maxY current max scroll y of the *layout* + * @see ScrollableLayout#getConsiderIdleMillis() + * @see ScrollableLayout#setConsiderIdleMillis(long) + * @return end scroll y value for the *layout* to animate to + */ + int getIdleFinalY(ScrollableLayout layout, int nowY, int maxY); +} diff --git a/library/src/main/java/ru/noties/scrollable/CloseUpAnimatorConfigurator.java b/library/src/main/java/ru/noties/scrollable/CloseUpAnimatorConfigurator.java new file mode 100644 index 0000000..8c68143 --- /dev/null +++ b/library/src/main/java/ru/noties/scrollable/CloseUpAnimatorConfigurator.java @@ -0,0 +1,18 @@ +package ru.noties.scrollable; + +import android.animation.ObjectAnimator; + +/** + * This interface might be used to customize {@link android.animation.ObjectAnimator} behavior during close-up animation + * @see android.animation.ObjectAnimator + * @see InterpolatorCloseUpAnimatorConfigurator + * Created by Dimitry Ivanov on 22.05.2015. + */ +public interface CloseUpAnimatorConfigurator { + + /** + * Note that {@link android.animation.ObjectAnimator#setDuration(long)} would erase current value set by {@link CloseUpIdleAnimationTime} if any present + * @param animator current {@link android.animation.ObjectAnimator} object to animate close-up animation of a {@link ScrollableLayout} + */ + void configure(ObjectAnimator animator); +} diff --git a/library/src/main/java/ru/noties/scrollable/CloseUpIdleAnimationTime.java b/library/src/main/java/ru/noties/scrollable/CloseUpIdleAnimationTime.java new file mode 100644 index 0000000..6c27815 --- /dev/null +++ b/library/src/main/java/ru/noties/scrollable/CloseUpIdleAnimationTime.java @@ -0,0 +1,19 @@ +package ru.noties.scrollable; + +/** + * This interface might be used to dynamically compute close-up animation time of a {@link ScrollableLayout} + * @see ScrollableLayout#setCloseUpIdleAnimationTime(CloseUpIdleAnimationTime) + * @see SimpleCloseUpIdleAnimationTime + * Created by Dimitry Ivanov on 22.05.2015. + */ +public interface CloseUpIdleAnimationTime { + + /** + * @param layout {@link ScrollableLayout} + * @param nowY current scroll y of the *layout* + * @param endY scroll y value to which *layout* would scroll to + * @param maxY current max scroll y value of the *layout* + * @return animation duration for a close-up animation + */ + long compute(ScrollableLayout layout, int nowY, int endY, int maxY); +} diff --git a/library/src/main/java/ru/noties/scrollable/DefaultCloseUpAlgorithm.java b/library/src/main/java/ru/noties/scrollable/DefaultCloseUpAlgorithm.java new file mode 100644 index 0000000..649b74f --- /dev/null +++ b/library/src/main/java/ru/noties/scrollable/DefaultCloseUpAlgorithm.java @@ -0,0 +1,27 @@ +package ru.noties.scrollable; + +/** + * Default implementation of the {@link CloseUpAlgorithm} + * With this implementation {@link ScrollableLayout} would have only two states - collapsed & expanded + * @see ScrollableLayout#setCloseUpAlgorithm(CloseUpAlgorithm) + * Created by Dimitry Ivanov on 23.05.2015. + */ +public class DefaultCloseUpAlgorithm implements CloseUpAlgorithm { + + /** + * {@inheritDoc} + */ + @Override + public int getFlingFinalY(ScrollableLayout layout, boolean isScrollingBottom, int nowY, int suggestedY, int maxY) { + return isScrollingBottom ? 0 : maxY; + } + + /** + * {@inheritDoc} + */ + @Override + public int getIdleFinalY(ScrollableLayout layout, int nowY, int maxY) { + final boolean shouldScrollToTop = nowY < (maxY / 2); + return shouldScrollToTop ? 0 : maxY; + } +} diff --git a/library/src/main/java/ru/noties/scrollable/DipUtils.java b/library/src/main/java/ru/noties/scrollable/DipUtils.java new file mode 100644 index 0000000..2e40b13 --- /dev/null +++ b/library/src/main/java/ru/noties/scrollable/DipUtils.java @@ -0,0 +1,18 @@ +package ru.noties.scrollable; + +import android.content.Context; +import android.content.res.Resources; + +/** + * Created by Dimitry Ivanov on 23.05.2015. + */ +class DipUtils { + + private DipUtils() {} + + static int dipToPx(Context context, int dip) { + final Resources r = context.getResources(); + final float scale = r.getDisplayMetrics().density; + return (int) (dip * scale + .5F); + } +} diff --git a/library/src/main/java/ru/noties/scrollable/InterpolatorCloseUpAnimatorConfigurator.java b/library/src/main/java/ru/noties/scrollable/InterpolatorCloseUpAnimatorConfigurator.java new file mode 100644 index 0000000..dfc2ee6 --- /dev/null +++ b/library/src/main/java/ru/noties/scrollable/InterpolatorCloseUpAnimatorConfigurator.java @@ -0,0 +1,24 @@ +package ru.noties.scrollable; + +import android.animation.ObjectAnimator; +import android.view.animation.Interpolator; + +/** + * Created by Dimitry Ivanov on 23.05.2015. + */ +public class InterpolatorCloseUpAnimatorConfigurator implements CloseUpAnimatorConfigurator { + + private final Interpolator mInterpolator; + + public InterpolatorCloseUpAnimatorConfigurator(Interpolator interpolator) { + this.mInterpolator = interpolator; + } + + /** + * {@inheritDoc} + */ + @Override + public void configure(ObjectAnimator animator) { + animator.setInterpolator(mInterpolator); + } +} diff --git a/library/src/main/java/ru/noties/scrollable/ScrollableLayout.java b/library/src/main/java/ru/noties/scrollable/ScrollableLayout.java index 719e09b..bffb8fd 100644 --- a/library/src/main/java/ru/noties/scrollable/ScrollableLayout.java +++ b/library/src/main/java/ru/noties/scrollable/ScrollableLayout.java @@ -1,13 +1,20 @@ package ru.noties.scrollable; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Rect; +import android.os.Parcel; +import android.os.Parcelable; import android.util.AttributeSet; +import android.util.Property; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; +import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import android.widget.FrameLayout; import android.widget.Scroller; @@ -99,6 +106,9 @@ */ public class ScrollableLayout extends FrameLayout { + private static final long DEFAULT_IDLE_CLOSE_UP_ANIMATION = 200L; + private static final int DEFAULT_CONSIDER_IDLE_MILLIS = 100; + private Scroller mScroller; private GestureDetector mScrollDetector; private GestureDetector mFlingDetector; @@ -109,6 +119,18 @@ public class ScrollableLayout extends FrameLayout { private int mMaxScrollY; private boolean mIsScrolling; + private boolean mIsFlinging; + + private MotionEventHook mMotionEventHook; + + private CloseUpAlgorithm mCloseUpAlgorithm; + private ObjectAnimator mCloseUpAnimator; + + private boolean mSelfUpdateScroll; + private boolean mSelfUpdateFling; + + private CloseUpIdleAnimationTime mCloseUpIdleAnimationTime; + private CloseUpAnimatorConfigurator mCloseAnimatorConfigurator; private View mDraggableView; private boolean mIsDraggingDraggable; @@ -117,6 +139,8 @@ public class ScrollableLayout extends FrameLayout { mDraggableRect = new Rect(); } + private long mConsiderIdleMillis; + public ScrollableLayout(Context context) { super(context); init(context, null); @@ -138,11 +162,37 @@ private void init(Context context, AttributeSet attributeSet) { try { final boolean flyWheel = array.getBoolean(R.styleable.ScrollableLayout_scrollable_scrollerFlywheel, false); - mScroller = initScroller(context, null, flyWheel); + final float friction = array.getFloat(R.styleable.ScrollableLayout_scrollable_friction, Float.NaN); + if (friction == friction) { + setFriction(friction); + } + mMaxScrollY = array.getDimensionPixelSize(R.styleable.ScrollableLayout_scrollable_maxScroll, 0); + final long considerIdleMillis = array.getInteger( + R.styleable.ScrollableLayout_scrollable_considerIdleMillis, + DEFAULT_CONSIDER_IDLE_MILLIS + ); + setConsiderIdleMillis(considerIdleMillis); + + final boolean useDefaultCloseUp = array.getBoolean(R.styleable.ScrollableLayout_scrollable_defaultCloseUp, false); + if (useDefaultCloseUp) { + setCloseUpAlgorithm(new DefaultCloseUpAlgorithm()); + } + + final int closeUpAnimationMillis = array.getInteger(R.styleable.ScrollableLayout_scrollable_closeUpAnimationMillis, -1); + if (closeUpAnimationMillis != -1) { + setCloseUpIdleAnimationTime(new SimpleCloseUpIdleAnimationTime(closeUpAnimationMillis)); + } + + final int interpolatorResId = array.getResourceId(R.styleable.ScrollableLayout_scrollable_closeUpAnimatorInterpolator, 0); + if (interpolatorResId != 0) { + final Interpolator interpolator = AnimationUtils.loadInterpolator(context, interpolatorResId); + setCloseAnimatorConfigurator(new InterpolatorCloseUpAnimatorConfigurator(interpolator)); + } + } finally { array.recycle(); } @@ -150,7 +200,14 @@ private void init(Context context, AttributeSet attributeSet) { setVerticalScrollBarEnabled(true); mScrollDetector = new GestureDetector(context, new ScrollGestureListener()); - mFlingDetector = new GestureDetector(context, new FlingGestureListener()); + mFlingDetector = new GestureDetector(context, new FlingGestureListener(context)); + + mMotionEventHook = new MotionEventHook(new MotionEventHookCallback() { + @Override + public void apply(MotionEvent event) { + ScrollableLayout.super.dispatchTouchEvent(event); + } + }); } /** @@ -198,6 +255,25 @@ public int getMaxScrollY() { return mMaxScrollY; } + /** + * Note that this value might be set with xml definition (
{@code app:scrollable_considerIdleMillis="100"}
) + * @param millis millis after which current scroll + * state would be considered idle and thus firing close up logic if set + * @see #getConsiderIdleMillis() + * @see #DEFAULT_CONSIDER_IDLE_MILLIS + */ + public void setConsiderIdleMillis(long millis) { + mConsiderIdleMillis = millis; + } + + /** + * @return current value of millis after which scroll state would be considered idle + * @see #setConsiderIdleMillis(long) + */ + public long getConsiderIdleMillis() { + return mConsiderIdleMillis; + } + /** * Pass an {@link ru.noties.scrollable.OnScrollChangedListener} * if you wish to get notifications when scroll state of this View has changed. @@ -212,12 +288,65 @@ public void setOnScrollChangedListener(OnScrollChangedListener listener) { /** * @see android.view.View#onScrollChanged(int, int, int, int) * @see ru.noties.scrollable.OnScrollChangedListener#onScrollChanged(int, int, int) + * @see CloseUpAlgorithm */ @Override public void onScrollChanged(int l, int t, int oldL, int oldT) { - if (mOnScrollChangedListener != null) { + + final boolean changed = t != oldT; + + if (changed && mOnScrollChangedListener != null) { mOnScrollChangedListener.onScrollChanged(t, oldT, mMaxScrollY); } + + if (mCloseUpAlgorithm != null) { + removeCallbacks(mIdleRunnable); + if (!mSelfUpdateScroll && changed) { + postDelayed(mIdleRunnable, mConsiderIdleMillis); + } + } + } + + protected void setSelfUpdateScroll(boolean value) { + mSelfUpdateScroll = value; + } + + protected boolean isSelfUpdateScroll() { + return mSelfUpdateScroll; + } + + /** + * Note that {@link DefaultCloseUpAlgorithm} might be set with + * xml definition (
{@code app:scrollable_defaultCloseUp="true"}
) + * @param closeUpAlgorithm {@link CloseUpAlgorithm} implementation, might be null + * @see CloseUpAlgorithm + * @see DefaultCloseUpAlgorithm + */ + public void setCloseUpAlgorithm(CloseUpAlgorithm closeUpAlgorithm) { + this.mCloseUpAlgorithm = closeUpAlgorithm; + } + + /** + * Note that {@link SimpleCloseUpIdleAnimationTime} might be set with xml definition + * (
{@code app:scrollable_closeUpAnimationMillis="200"}
) + * @param closeUpIdleAnimationTime {@link CloseUpIdleAnimationTime} implementation, might be null + * @see CloseUpIdleAnimationTime + * @see SimpleCloseUpIdleAnimationTime + * @see #DEFAULT_IDLE_CLOSE_UP_ANIMATION + */ + public void setCloseUpIdleAnimationTime(CloseUpIdleAnimationTime closeUpIdleAnimationTime) { + this.mCloseUpIdleAnimationTime = closeUpIdleAnimationTime; + } + + /** + * @param configurator {@link CloseUpAnimatorConfigurator} implementation + * to process current close up + * {@link android.animation.ObjectAnimator}, might be null + * @see CloseUpAnimatorConfigurator + * @see android.animation.ObjectAnimator + */ + public void setCloseAnimatorConfigurator(CloseUpAnimatorConfigurator configurator) { + this.mCloseAnimatorConfigurator = configurator; } /** @@ -229,6 +358,7 @@ public void onScrollChanged(int l, int t, int oldL, int oldT) { public void scrollTo(int x, int y) { final int newY = getNewY(y); + if (newY < 0) { return; } @@ -245,7 +375,7 @@ protected int getNewY(int y) { } final int direction = y - currentY; - final boolean isScrollingBottomTop = y - currentY < 0; + final boolean isScrollingBottomTop = direction < 0; if (mCanScrollVerticallyDelegate != null) { @@ -253,6 +383,7 @@ protected int getNewY(int y) { // if not dragging draggable then return, else do not return if (!mIsDraggingDraggable + && !mSelfUpdateScroll && mCanScrollVerticallyDelegate.canScrollVertically(direction)) { return -1; } @@ -288,11 +419,9 @@ public void setDraggableView(View view) { public boolean dispatchTouchEvent(@SuppressWarnings("NullableProblems") MotionEvent event) { final int action = event.getActionMasked(); - if (action == MotionEvent.ACTION_DOWN) { mScroller.abortAnimation(); - removeCallbacks(mScrollRunnable); if (mDraggableView != null && mDraggableView.getGlobalVisibleRect(mDraggableRect)) { final int x = (int) (event.getRawX() + .5F); @@ -303,45 +432,49 @@ public boolean dispatchTouchEvent(@SuppressWarnings("NullableProblems") MotionEv } } - final boolean prevScrollingState = mIsScrolling; - mIsScrolling = mScrollDetector.onTouchEvent(event); - final boolean flingResult = mFlingDetector.onTouchEvent(event); - - if (mIsDraggingDraggable && mIsScrolling - || (prevScrollingState && action == MotionEvent.ACTION_UP && flingResult)) { - final MotionEvent cancelEvent = MotionEvent.obtain(event); - try { - cancelEvent.setAction(MotionEvent.ACTION_CANCEL); - super.dispatchTouchEvent(cancelEvent); - } finally { - cancelEvent.recycle(); + final boolean isPrevScrolling = mIsScrolling; + final boolean isPrevFlinging = mIsFlinging; + + mIsFlinging = mFlingDetector .onTouchEvent(event); + mIsScrolling = mScrollDetector.onTouchEvent(event); + + removeCallbacks(mScrollRunnable); + post(mScrollRunnable); + + final boolean isIntercepted = mIsScrolling || mIsFlinging; + final boolean isPrevIntercepted = isPrevScrolling || isPrevFlinging; + + final boolean shouldRedirectDownTouch = action == MotionEvent.ACTION_MOVE + && (!isIntercepted && isPrevIntercepted) + && getScrollY() == mMaxScrollY; + + if (isIntercepted || isPrevIntercepted) { + + mMotionEventHook.hook(event, MotionEvent.ACTION_CANCEL); + + if (!isPrevIntercepted) { + return true; } - return true; } - super.dispatchTouchEvent(event); + if (shouldRedirectDownTouch) { + mMotionEventHook.hook(event, MotionEvent.ACTION_DOWN); + } + super.dispatchTouchEvent(event); return true; } - private final Runnable mScrollRunnable = new Runnable() { - @Override - public void run() { - - if (mScroller.computeScrollOffset()) { + private void cancelIdleAnimationIfRunning(boolean removeCallbacks) { - final int y = mScroller.getCurrY(); - final int nowY = getScrollY(); - final int diff = y - nowY; - - if (diff != 0) { - scrollBy(0, diff); - } + if (removeCallbacks) { + removeCallbacks(mIdleRunnable); + } - post(this); - } + if (mCloseUpAnimator != null && mCloseUpAnimator.isRunning()) { + mCloseUpAnimator.cancel(); } - }; + } @Override public void computeScroll() { @@ -371,6 +504,85 @@ protected void onLayout(boolean changed, int left, int top, int right, int botto } } + private final Runnable mScrollRunnable = new Runnable() { + @Override + public void run() { + + final boolean isContinue = mScroller.computeScrollOffset(); + mSelfUpdateFling = isContinue; + + if (isContinue) { + + final int y = mScroller.getCurrY(); + final int nowY = getScrollY(); + final int diff = y - nowY; + + if (diff != 0) { + scrollBy(0, diff); + } + + post(this); + } + } + }; + + private final Runnable mIdleRunnable = new Runnable() { + @Override + public void run() { + + cancelIdleAnimationIfRunning(false); + + if (mSelfUpdateScroll || mSelfUpdateFling) { + return; + } + + final int nowY = getScrollY(); + + if (nowY == 0 + || nowY == mMaxScrollY) { + return; + } + + final int endY = mCloseUpAlgorithm.getIdleFinalY(ScrollableLayout.this, nowY, mMaxScrollY); + + if (nowY == endY) { + return; + } + + mCloseUpAnimator = ObjectAnimator.ofInt(ScrollableLayout.this, mCloseUpAnimationProperty, nowY, endY); + + final long duration = mCloseUpIdleAnimationTime != null + ? mCloseUpIdleAnimationTime.compute(ScrollableLayout.this, nowY, endY, mMaxScrollY) + : DEFAULT_IDLE_CLOSE_UP_ANIMATION; + + mCloseUpAnimator.setDuration(duration); + mCloseUpAnimator.addListener(new AnimatorListenerAdapter() { + + @Override + public void onAnimationStart(Animator animation) { + ; + mSelfUpdateScroll = true; + } + + @Override + public void onAnimationEnd(Animator animation) { + mSelfUpdateScroll = false; + } + + @Override + public void onAnimationCancel(Animator animation) { + mSelfUpdateScroll = false; + } + }); + + if (mCloseAnimatorConfigurator != null) { + mCloseAnimatorConfigurator.configure(mCloseUpAnimator); + } + + mCloseUpAnimator.start(); + } + }; + private class ScrollGestureListener extends GestureListenerAdapter { private final int mTouchSlop; @@ -390,17 +602,34 @@ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float d return false; } - scrollBy(0, (int) distanceY); + final int now = getScrollY(); + scrollBy(0, (int) (distanceY + .5F)); - return true; + return now != getScrollY(); } } private class FlingGestureListener extends GestureListenerAdapter { + private static final int MIN_FLING_DISTANCE_DIP = 12; + + private final int mMinFlingDistance; + private final float mMinVelocity; + + FlingGestureListener(Context context) { + this.mMinFlingDistance = DipUtils.dipToPx(context, MIN_FLING_DISTANCE_DIP); + + final ViewConfiguration configuration = ViewConfiguration.get(context); + this.mMinVelocity = configuration.getScaledMinimumFlingVelocity(); + } + @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + if (Math.abs(velocityY) < mMinVelocity) { + return false; + } + if (Math.abs(velocityX) > Math.abs(velocityY)) { return false; } @@ -410,13 +639,31 @@ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float ve return false; } - removeCallbacks(mScrollRunnable); mScroller.fling(0, nowY, 0, -(int) (velocityY + .5F), 0, 0, 0, mMaxScrollY); - post(mScrollRunnable); if (mScroller.computeScrollOffset()) { - final int finalY = mScroller.getFinalY(); + final int suggestedY = mScroller.getFinalY(); + + if (Math.abs(nowY - suggestedY) < mMinFlingDistance) { + mScroller.abortAnimation(); + return false; + } + + final int finalY; + if (suggestedY == nowY || mCloseUpAlgorithm == null) { + finalY = suggestedY; + } else { + finalY = mCloseUpAlgorithm.getFlingFinalY( + ScrollableLayout.this, + suggestedY - nowY < 0, + nowY, + suggestedY, + mMaxScrollY + ); + mScroller.setFinalY(finalY); + } + final int newY = getNewY(finalY); return !(finalY == nowY || newY < 0); @@ -425,4 +672,98 @@ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float ve return false; } } + + private static class MotionEventHook { + + final MotionEventHookCallback callback; + + MotionEventHook(MotionEventHookCallback callback) { + this.callback = callback; + } + + void hook(MotionEvent event, int action) { + final int historyAction = event.getAction(); + event.setAction(action); + callback.apply(event); + event.setAction(historyAction); + } + } + + private interface MotionEventHookCallback { + void apply(MotionEvent event); + } + + private final Property mCloseUpAnimationProperty + = new Property(Integer.class, "scrollY") { + + @Override + public Integer get(ScrollableLayout object) { + return object.getScrollY(); + } + + @Override + public void set(final ScrollableLayout layout, final Integer value) { + layout.setScrollY(value); + } + }; + + @Override + public Parcelable onSaveInstanceState() { + final Parcelable superState = super.onSaveInstanceState(); + final ScrollableLayoutSavedState savedState = new ScrollableLayoutSavedState(superState); + + savedState.scrollY = getScrollY(); + + return savedState; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + + if (!(state instanceof ScrollableLayoutSavedState)) { + super.onRestoreInstanceState(state); + return; + } + + final ScrollableLayoutSavedState in = (ScrollableLayoutSavedState) state; + super.onRestoreInstanceState(in.getSuperState()); + + setScrollY(in.scrollY); + } + + private static class ScrollableLayoutSavedState extends BaseSavedState { + + int scrollY; + + public ScrollableLayoutSavedState(Parcel source) { + super(source); + + scrollY = source.readInt(); + } + + public ScrollableLayoutSavedState(Parcelable superState) { + super(superState); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + + out.writeInt(scrollY); + } + + public static final Creator CREATOR + = new Creator() { + + @Override + public ScrollableLayoutSavedState createFromParcel(Parcel in) { + return new ScrollableLayoutSavedState(in); + } + + @Override + public ScrollableLayoutSavedState[] newArray(int size) { + return new ScrollableLayoutSavedState[size]; + } + }; + } } diff --git a/library/src/main/java/ru/noties/scrollable/SimpleCloseUpIdleAnimationTime.java b/library/src/main/java/ru/noties/scrollable/SimpleCloseUpIdleAnimationTime.java new file mode 100644 index 0000000..8a98ee7 --- /dev/null +++ b/library/src/main/java/ru/noties/scrollable/SimpleCloseUpIdleAnimationTime.java @@ -0,0 +1,25 @@ +package ru.noties.scrollable; + +/** + * Created by Dimitry Ivanov on 23.05.2015. + */ +public class SimpleCloseUpIdleAnimationTime implements CloseUpIdleAnimationTime { + + private final long mDuration; + + public SimpleCloseUpIdleAnimationTime(long duration) { + this.mDuration = duration; + } + + /** + * {@inheritDoc} + */ + @Override + public long compute(ScrollableLayout layout, int nowY, int endY, int maxY) { + return mDuration; + } + + public long getDuration() { + return mDuration; + } +} diff --git a/library/src/main/res/values/attrs.xml b/library/src/main/res/values/attrs.xml index 4f30463..8fe6ea1 100644 --- a/library/src/main/res/values/attrs.xml +++ b/library/src/main/res/values/attrs.xml @@ -4,6 +4,12 @@ + + + + + + \ No newline at end of file