From d5c9021b892185ce9e23a11515bffa1662fed6dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Sat, 13 Sep 2014 23:08:04 +0200 Subject: [PATCH] Hack to disable overscroll effect of swipe to update --- .../NoScrollableSwipeRefreshLayout.java | 473 ++++++++++++++++++ .../keychain/ui/KeyListFragment.java | 3 +- .../widget/ListAwareSwipeRefreshLayout.java | 25 +- 3 files changed, 486 insertions(+), 15 deletions(-) create mode 100644 OpenKeychain/src/main/java/android/support/v4/widget/NoScrollableSwipeRefreshLayout.java diff --git a/OpenKeychain/src/main/java/android/support/v4/widget/NoScrollableSwipeRefreshLayout.java b/OpenKeychain/src/main/java/android/support/v4/widget/NoScrollableSwipeRefreshLayout.java new file mode 100644 index 000000000..9bbcfe698 --- /dev/null +++ b/OpenKeychain/src/main/java/android/support/v4/widget/NoScrollableSwipeRefreshLayout.java @@ -0,0 +1,473 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.widget; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.support.v4.view.ViewCompat; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.Animation; +import android.view.animation.Animation.AnimationListener; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Transformation; +import android.widget.AbsListView; + + +/** + * Same as SwipeRefreshLayout, only updateContentOffsetTop and REFRESH_TRIGGER_DISTANCE + * have been modified! + */ +public class NoScrollableSwipeRefreshLayout extends ViewGroup { + private static final long RETURN_TO_ORIGINAL_POSITION_TIMEOUT = 300; + private static final float ACCELERATE_INTERPOLATION_FACTOR = 1.5f; + private static final float DECELERATE_INTERPOLATION_FACTOR = 2f; + private static final float PROGRESS_BAR_HEIGHT = 4; + private static final float MAX_SWIPE_DISTANCE_FACTOR = .6f; + private static final int REFRESH_TRIGGER_DISTANCE = 200; + + private SwipeProgressBar mProgressBar; //the thing that shows progress is going + private View mTarget; //the content that gets pulled down + private int mOriginalOffsetTop; + private OnRefreshListener mListener; + private MotionEvent mDownEvent; + private int mFrom; + private boolean mRefreshing = false; + private int mTouchSlop; + private float mDistanceToTriggerSync = -1; + private float mPrevY; + private int mMediumAnimationDuration; + private float mFromPercentage = 0; + private float mCurrPercentage = 0; + private int mProgressBarHeight; + private int mCurrentTargetOffsetTop; + // Target is returning to its start offset because it was cancelled or a + // refresh was triggered. + private boolean mReturningToStart; + private final DecelerateInterpolator mDecelerateInterpolator; + private final AccelerateInterpolator mAccelerateInterpolator; + private static final int[] LAYOUT_ATTRS = new int[] { + android.R.attr.enabled + }; + + private final Animation mAnimateToStartPosition = new Animation() { + @Override + public void applyTransformation(float interpolatedTime, Transformation t) { + int targetTop = 0; + if (mFrom != mOriginalOffsetTop) { + targetTop = (mFrom + (int)((mOriginalOffsetTop - mFrom) * interpolatedTime)); + } + int offset = targetTop - mTarget.getTop(); + final int currentTop = mTarget.getTop(); + if (offset + currentTop < 0) { + offset = 0 - currentTop; + } + setTargetOffsetTopAndBottom(offset); + } + }; + + private Animation mShrinkTrigger = new Animation() { + @Override + public void applyTransformation(float interpolatedTime, Transformation t) { + float percent = mFromPercentage + ((0 - mFromPercentage) * interpolatedTime); + mProgressBar.setTriggerPercentage(percent); + } + }; + + private final AnimationListener mReturnToStartPositionListener = new BaseAnimationListener() { + @Override + public void onAnimationEnd(Animation animation) { + // Once the target content has returned to its start position, reset + // the target offset to 0 + mCurrentTargetOffsetTop = 0; + } + }; + + private final AnimationListener mShrinkAnimationListener = new BaseAnimationListener() { + @Override + public void onAnimationEnd(Animation animation) { + mCurrPercentage = 0; + } + }; + + private final Runnable mReturnToStartPosition = new Runnable() { + + @Override + public void run() { + mReturningToStart = true; + animateOffsetToStartPosition(mCurrentTargetOffsetTop + getPaddingTop(), + mReturnToStartPositionListener); + } + + }; + + // Cancel the refresh gesture and animate everything back to its original state. + private final Runnable mCancel = new Runnable() { + + @Override + public void run() { + mReturningToStart = true; + // Timeout fired since the user last moved their finger; animate the + // trigger to 0 and put the target back at its original position + if (mProgressBar != null) { + mFromPercentage = mCurrPercentage; + mShrinkTrigger.setDuration(mMediumAnimationDuration); + mShrinkTrigger.setAnimationListener(mShrinkAnimationListener); + mShrinkTrigger.reset(); + mShrinkTrigger.setInterpolator(mDecelerateInterpolator); + startAnimation(mShrinkTrigger); + } + animateOffsetToStartPosition(mCurrentTargetOffsetTop + getPaddingTop(), + mReturnToStartPositionListener); + } + + }; + + /** + * Simple constructor to use when creating a SwipeRefreshLayout from code. + * @param context + */ + public NoScrollableSwipeRefreshLayout(Context context) { + this(context, null); + } + + /** + * Constructor that is called when inflating SwipeRefreshLayout from XML. + * @param context + * @param attrs + */ + public NoScrollableSwipeRefreshLayout(Context context, AttributeSet attrs) { + super(context, attrs); + + mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + + mMediumAnimationDuration = getResources().getInteger( + android.R.integer.config_mediumAnimTime); + + setWillNotDraw(false); + mProgressBar = new SwipeProgressBar(this); + final DisplayMetrics metrics = getResources().getDisplayMetrics(); + mProgressBarHeight = (int) (metrics.density * PROGRESS_BAR_HEIGHT); + mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR); + mAccelerateInterpolator = new AccelerateInterpolator(ACCELERATE_INTERPOLATION_FACTOR); + + final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS); + setEnabled(a.getBoolean(0, true)); + a.recycle(); + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + removeCallbacks(mCancel); + removeCallbacks(mReturnToStartPosition); + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + removeCallbacks(mReturnToStartPosition); + removeCallbacks(mCancel); + } + + private void animateOffsetToStartPosition(int from, AnimationListener listener) { + mFrom = from; + mAnimateToStartPosition.reset(); + mAnimateToStartPosition.setDuration(mMediumAnimationDuration); + mAnimateToStartPosition.setAnimationListener(listener); + mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator); + mTarget.startAnimation(mAnimateToStartPosition); + } + + /** + * Set the listener to be notified when a refresh is triggered via the swipe + * gesture. + */ + public void setOnRefreshListener(OnRefreshListener listener) { + mListener = listener; + } + + private void setTriggerPercentage(float percent) { + if (percent == 0f) { + // No-op. A null trigger means it's uninitialized, and setting it to zero-percent + // means we're trying to reset state, so there's nothing to reset in this case. + mCurrPercentage = 0; + return; + } + mCurrPercentage = percent; + mProgressBar.setTriggerPercentage(percent); + } + + /** + * Notify the widget that refresh state has changed. Do not call this when + * refresh is triggered by a swipe gesture. + * + * @param refreshing Whether or not the view should show refresh progress. + */ + public void setRefreshing(boolean refreshing) { + if (mRefreshing != refreshing) { + ensureTarget(); + mCurrPercentage = 0; + mRefreshing = refreshing; + if (mRefreshing) { + mProgressBar.start(); + } else { + mProgressBar.stop(); + } + } + } + + /** + * Set the four colors used in the progress animation. The first color will + * also be the color of the bar that grows in response to a user swipe + * gesture. + * + * @param colorRes1 Color resource. + * @param colorRes2 Color resource. + * @param colorRes3 Color resource. + * @param colorRes4 Color resource. + */ + public void setColorScheme(int colorRes1, int colorRes2, int colorRes3, int colorRes4) { + ensureTarget(); + final Resources res = getResources(); + final int color1 = res.getColor(colorRes1); + final int color2 = res.getColor(colorRes2); + final int color3 = res.getColor(colorRes3); + final int color4 = res.getColor(colorRes4); + mProgressBar.setColorScheme(color1, color2, color3,color4); + } + + /** + * @return Whether the SwipeRefreshWidget is actively showing refresh + * progress. + */ + public boolean isRefreshing() { + return mRefreshing; + } + + private void ensureTarget() { + // Don't bother getting the parent height if the parent hasn't been laid out yet. + if (mTarget == null) { + if (getChildCount() > 1 && !isInEditMode()) { + throw new IllegalStateException( + "SwipeRefreshLayout can host only one direct child"); + } + mTarget = getChildAt(0); + mOriginalOffsetTop = mTarget.getTop() + getPaddingTop(); + } + if (mDistanceToTriggerSync == -1) { + if (getParent() != null && ((View)getParent()).getHeight() > 0) { + final DisplayMetrics metrics = getResources().getDisplayMetrics(); + mDistanceToTriggerSync = (int) Math.min( + ((View) getParent()) .getHeight() * MAX_SWIPE_DISTANCE_FACTOR, + REFRESH_TRIGGER_DISTANCE * metrics.density); + } + } + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + mProgressBar.draw(canvas); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + final int width = getMeasuredWidth(); + final int height = getMeasuredHeight(); + mProgressBar.setBounds(0, 0, width, mProgressBarHeight); + if (getChildCount() == 0) { + return; + } + final View child = getChildAt(0); + final int childLeft = getPaddingLeft(); + final int childTop = mCurrentTargetOffsetTop + getPaddingTop(); + final int childWidth = width - getPaddingLeft() - getPaddingRight(); + final int childHeight = height - getPaddingTop() - getPaddingBottom(); + child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight); + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + if (getChildCount() > 1 && !isInEditMode()) { + throw new IllegalStateException("SwipeRefreshLayout can host only one direct child"); + } + if (getChildCount() > 0) { + getChildAt(0).measure( + MeasureSpec.makeMeasureSpec( + getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), + MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec( + getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), + MeasureSpec.EXACTLY)); + } + } + + /** + * @return Whether it is possible for the child view of this layout to + * scroll up. Override this if the child view is a custom view. + */ + public boolean canChildScrollUp() { + if (android.os.Build.VERSION.SDK_INT < 14) { + if (mTarget instanceof AbsListView) { + final AbsListView absListView = (AbsListView) mTarget; + return absListView.getChildCount() > 0 + && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0) + .getTop() < absListView.getPaddingTop()); + } else { + return mTarget.getScrollY() > 0; + } + } else { + return ViewCompat.canScrollVertically(mTarget, -1); + } + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + ensureTarget(); + boolean handled = false; + if (mReturningToStart && ev.getAction() == MotionEvent.ACTION_DOWN) { + mReturningToStart = false; + } + if (isEnabled() && !mReturningToStart && !canChildScrollUp()) { + handled = onTouchEvent(ev); + } + return !handled ? super.onInterceptTouchEvent(ev) : handled; + } + + @Override + public void requestDisallowInterceptTouchEvent(boolean b) { + // Nope. + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + final int action = event.getAction(); + boolean handled = false; + switch (action) { + case MotionEvent.ACTION_DOWN: + mCurrPercentage = 0; + mDownEvent = MotionEvent.obtain(event); + mPrevY = mDownEvent.getY(); + break; + case MotionEvent.ACTION_MOVE: + if (mDownEvent != null && !mReturningToStart) { + final float eventY = event.getY(); + float yDiff = eventY - mDownEvent.getY(); + if (yDiff > mTouchSlop) { + // User velocity passed min velocity; trigger a refresh + if (yDiff > mDistanceToTriggerSync) { + // User movement passed distance; trigger a refresh + startRefresh(); + handled = true; + break; + } else { + // Just track the user's movement + setTriggerPercentage( + mAccelerateInterpolator.getInterpolation( + yDiff / mDistanceToTriggerSync)); + float offsetTop = yDiff; + if (mPrevY > eventY) { + offsetTop = yDiff - mTouchSlop; + } + updateContentOffsetTop((int) (offsetTop)); + if (mPrevY > eventY && (mTarget.getTop() < mTouchSlop)) { + // If the user puts the view back at the top, we + // don't need to. This shouldn't be considered + // cancelling the gesture as the user can restart from the top. + removeCallbacks(mCancel); + } else { + updatePositionTimeout(); + } + mPrevY = event.getY(); + handled = true; + } + } + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + if (mDownEvent != null) { + mDownEvent.recycle(); + mDownEvent = null; + } + break; + } + return handled; + } + + private void startRefresh() { + removeCallbacks(mCancel); + mReturnToStartPosition.run(); + setRefreshing(true); + mListener.onRefresh(); + } + + private void updateContentOffsetTop(int targetTop) { + final int currentTop = mTarget.getTop(); + if (targetTop > mDistanceToTriggerSync) { + targetTop = (int) mDistanceToTriggerSync; + } else if (targetTop < 0) { + targetTop = 0; + } +// setTargetOffsetTopAndBottom(targetTop - currentTop); + } + + private void setTargetOffsetTopAndBottom(int offset) { + mTarget.offsetTopAndBottom(offset); + mCurrentTargetOffsetTop = mTarget.getTop(); + } + + private void updatePositionTimeout() { + removeCallbacks(mCancel); + postDelayed(mCancel, RETURN_TO_ORIGINAL_POSITION_TIMEOUT); + } + + /** + * Classes that wish to be notified when the swipe gesture correctly + * triggers a refresh should implement this interface. + */ + public interface OnRefreshListener { + public void onRefresh(); + } + + /** + * Simple AnimationListener to avoid having to implement unneeded methods in + * AnimationListeners. + */ + private class BaseAnimationListener implements AnimationListener { + @Override + public void onAnimationStart(Animation animation) { + } + + @Override + public void onAnimationEnd(Animation animation) { + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + } +} \ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java index 30db01fb4..db5c48da7 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java @@ -35,6 +35,7 @@ import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.support.v4.view.MenuItemCompat; import android.support.v4.widget.CursorAdapter; +import android.support.v4.widget.NoScrollableSwipeRefreshLayout; import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBarActivity; @@ -81,7 +82,7 @@ import se.emilsjolander.stickylistheaders.StickyListHeadersListView; */ public class KeyListFragment extends LoaderFragment implements SearchView.OnQueryTextListener, AdapterView.OnItemClickListener, - LoaderManager.LoaderCallbacks, SwipeRefreshLayout.OnRefreshListener { + LoaderManager.LoaderCallbacks, NoScrollableSwipeRefreshLayout.OnRefreshListener { private KeyListAdapter mAdapter; private StickyListHeadersListView mStickyList; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ListAwareSwipeRefreshLayout.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ListAwareSwipeRefreshLayout.java index 58e8e81e9..17954f827 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ListAwareSwipeRefreshLayout.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ListAwareSwipeRefreshLayout.java @@ -18,6 +18,7 @@ package org.sufficientlysecure.keychain.ui.widget; import android.content.Context; +import android.support.v4.widget.NoScrollableSwipeRefreshLayout; import android.support.v4.widget.SwipeRefreshLayout; import android.util.AttributeSet; @@ -25,8 +26,7 @@ import org.sufficientlysecure.keychain.util.Log; import se.emilsjolander.stickylistheaders.StickyListHeadersListView; -public class ListAwareSwipeRefreshLayout extends SwipeRefreshLayout { - +public class ListAwareSwipeRefreshLayout extends NoScrollableSwipeRefreshLayout { private StickyListHeadersListView mStickyListHeadersListView = null; private boolean mIsLocked = false; @@ -37,6 +37,7 @@ public class ListAwareSwipeRefreshLayout extends SwipeRefreshLayout { public ListAwareSwipeRefreshLayout(Context context) { super(context); } + public ListAwareSwipeRefreshLayout(Context context, AttributeSet attrs) { super(context, attrs); } @@ -47,6 +48,7 @@ public class ListAwareSwipeRefreshLayout extends SwipeRefreshLayout { public void setStickyListHeadersListView(StickyListHeadersListView stickyListHeadersListView) { mStickyListHeadersListView = stickyListHeadersListView; } + public StickyListHeadersListView getStickyListHeadersListView() { return mStickyListHeadersListView; } @@ -55,27 +57,22 @@ public class ListAwareSwipeRefreshLayout extends SwipeRefreshLayout { mIsLocked = locked; Log.d("ListAwareSwipeRefreshLayout", (mIsLocked ? "is locked" : "not locked")); } + public boolean getIsLocked() { return mIsLocked; } @Override public boolean canChildScrollUp() { - if (mStickyListHeadersListView == null) + if (mStickyListHeadersListView == null) { return super.canChildScrollUp(); + } - return ( - mIsLocked - || - ( + return (mIsLocked || ( mStickyListHeadersListView.getWrappedList().getChildCount() > 0 - && - ( - mStickyListHeadersListView.getTop() > 0 - || - mStickyListHeadersListView.getFirstVisiblePosition() > 0 - ) - ) + && (mStickyListHeadersListView.getTop() > 0 + || mStickyListHeadersListView.getFirstVisiblePosition() > 0 + )) ); } } \ No newline at end of file