Java tutorial
/* * Copyright 2015 Hippo Seven * * 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 com.hippo.refreshlayout; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.support.annotation.ColorInt; import android.support.annotation.ColorRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.support.v4.content.ContextCompat; import android.support.v4.view.MotionEventCompat; import android.support.v4.view.NestedScrollingChildHelper; import android.support.v4.view.NestedScrollingParentHelper; import android.support.v4.view.ScrollingView; import android.support.v4.view.ViewCompat; import android.support.v4.widget.SwipeRefreshLayout; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.Log; 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; /** * The SwipeRefreshLayout should be used whenever the user can refresh the * contents of a view via a vertical swipe gesture. The activity that * instantiates this view should add an OnRefreshListener to be notified * whenever the swipe to refresh gesture is completed. The SwipeRefreshLayout * will notify the listener each and every time the gesture is completed again; * the listener is responsible for correctly determining when to actually * initiate a refresh of its content. If the listener determines there should * not be a refresh, it must call setRefreshing(false) to cancel any visual * indication of a refresh. If an activity wishes to show just the progress * animation, it should call setRefreshing(true). To disable the gesture and * progress animation, call setEnabled(false) on the view. * <p> * This layout should be made the parent of the view that will be refreshed as a * result of the gesture and can only support one direct child. This view will * also be made the target of the gesture and will be forced to match both the * width and the height supplied in this layout. The SwipeRefreshLayout does not * provide accessibility events; instead, a menu item must be provided to allow * refresh of the content wherever this gesture is used. * </p> */ public class RefreshLayout extends ViewGroup { // Maps to ProgressBar.Large style public static final int LARGE = MaterialProgressDrawable.LARGE; // Maps to ProgressBar default style public static final int DEFAULT = MaterialProgressDrawable.DEFAULT; @VisibleForTesting static final int CIRCLE_DIAMETER = 40; @VisibleForTesting static final int CIRCLE_DIAMETER_LARGE = 56; private static final String LOG_TAG = RefreshLayout.class.getSimpleName(); private static final int MAX_ALPHA = 255; private static final int STARTING_PROGRESS_ALPHA = (int) (.3f * MAX_ALPHA); private static final float DECELERATE_INTERPOLATION_FACTOR = 2f; private static final int INVALID_POINTER = -1; private static final float DRAG_RATE = .5f; // Max amount of circle that can be filled by progress during swipe gesture, // where 1.0 is a full circle private static final float MAX_PROGRESS_ANGLE = .8f; private static final int SCALE_DOWN_DURATION = 150; private static final int ALPHA_ANIMATION_DURATION = 300; private static final int ANIMATE_TO_TRIGGER_DURATION = 200; private static final int ANIMATE_TO_START_DURATION = 200; // Default background for the progress spinner private static final int CIRCLE_BG_LIGHT = 0xFFFAFAFA; // Default offset in dips from the top of the view to where the progress spinner should stop private static final int DEFAULT_CIRCLE_TARGET = 64; private View mTarget; // the target of the gesture OnRefreshListener mListener; boolean mHeaderRefreshing = false; private int mTouchSlop; private float mHeaderTotalDragDistance = -1; // If nested scrolling is enabled, the total amount that needed to be // consumed by this as the nested scrolling parent is used in place of the // overscroll determined by MOVE events in the onTouch handler private float mTotalUnconsumed; private final NestedScrollingParentHelper mNestedScrollingParentHelper; private final NestedScrollingChildHelper mNestedScrollingChildHelper; private final int[] mParentScrollConsumed = new int[2]; private final int[] mParentOffsetInWindow = new int[2]; private boolean mNestedScrollInProgress; private int mMediumAnimationDuration; int mHeaderCurrentTargetOffsetTop; private float mInitialMotionY; private float mInitialDownY; private boolean mIsHeaderBeingDragged; private int mActivePointerId = INVALID_POINTER; // Whether this item is scaled up rather than clipped boolean mHeaderScale; // Target is returning to its start offset because it was cancelled or a // refresh was triggered. private boolean mReturningToStart; private final DecelerateInterpolator mDecelerateInterpolator; private static final int[] LAYOUT_ATTRS = new int[] { android.R.attr.enabled }; CircleImageView mCircleView; private int mCircleViewIndex = -1; protected int mHeaderFrom; float mHeaderStartingScale; protected int mHeaderOriginalOffsetTop; int mHeaderSpinnerOffsetEnd; MaterialProgressDrawable mProgress; private Animation mHeaderScaleAnimation; private Animation mHeaderScaleDownAnimation; private Animation mHeaderAlphaStartAnimation; private Animation mHeaderAlphaMaxAnimation; private Animation mHeaderScaleDownToStartAnimation; boolean mHeaderNotify; private int mCircleDiameter; // Whether the client has set a custom starting position; boolean mHeaderUsingCustomStart; private OnChildScrollCallback mChildScrollCallback; private Animation.AnimationListener mHeaderRefreshListener = new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationRepeat(Animation animation) { } @SuppressLint("NewApi") @Override public void onAnimationEnd(Animation animation) { if (mHeaderRefreshing) { // Make sure the progress view is fully visible mProgress.setAlpha(MAX_ALPHA); mProgress.start(); if (mHeaderNotify) { if (mListener != null) { mListener.onHeaderRefresh(); } } mHeaderCurrentTargetOffsetTop = mCircleView.getTop(); } else { resetHeader(); } } }; private static final long RETURN_TO_ORIGINAL_POSITION_TIMEOUT = 300; private static final float ACCELERATE_INTERPOLATION_FACTOR = 1.5f; private static final float PROGRESS_BAR_HEIGHT = 4; private static final float MAX_SWIPE_DISTANCE_FACTOR = .6f; private static final int REFRESH_TRIGGER_DISTANCE = 120; private SwipeProgressBar mProgressBar; //the thing that shows progress is going private boolean mIsFooterBeingDragged; private int mFooterOriginalOffsetTop; private int mFooterFrom; private boolean mFooterRefreshing = false; private float mFooterDistanceToTriggerSync = -1; private float mFooterFromPercentage = 0; private float mFooterCurrPercentage = 0; private int mProgressBarHeight; private int mFooterCurrentTargetOffsetTop; private final AccelerateInterpolator mAccelerateInterpolator; private final Animation mAnimateFooterToStartPosition = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { int targetTop = 0; if (mFooterFrom != mFooterOriginalOffsetTop) { targetTop = (mFooterFrom + (int) ((mFooterOriginalOffsetTop - mFooterFrom) * interpolatedTime)); } int offset = targetTop - mTarget.getTop(); final int currentTop = mTarget.getTop(); if (offset + currentTop > 0) { offset = 0 - currentTop; } //setFooterTargetOffsetTopAndBottom(offset); } }; private Animation mShrinkTrigger = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { float percent = mFooterFromPercentage + ((0 - mFooterFromPercentage) * 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 mFooterCurrentTargetOffsetTop = 0; } }; private final AnimationListener mShrinkAnimationListener = new BaseAnimationListener() { @Override public void onAnimationEnd(Animation animation) { mFooterCurrPercentage = 0; } }; private final Runnable mReturnToStartPosition = new Runnable() { @Override public void run() { mReturningToStart = true; animateFooterOffsetToStartPosition(mFooterCurrentTargetOffsetTop + 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) { mFooterFromPercentage = mFooterCurrPercentage; mShrinkTrigger.setDuration(mMediumAnimationDuration); mShrinkTrigger.setAnimationListener(mShrinkAnimationListener); mShrinkTrigger.reset(); mShrinkTrigger.setInterpolator(mDecelerateInterpolator); startAnimation(mShrinkTrigger); } animateFooterOffsetToStartPosition(mFooterCurrentTargetOffsetTop + getPaddingTop(), mReturnToStartPositionListener); } }; private boolean mEnableSwipeHeader = true; private boolean mEnableSwipeFooter = true; void resetHeader() { mCircleView.clearAnimation(); mProgress.stop(); mCircleView.setVisibility(View.GONE); setHeaderColorViewAlpha(MAX_ALPHA); // Return the circle to its start position if (mHeaderScale) { setAnimationProgress(0 /* animation complete and view is hidden */); } else { setHeaderTargetOffsetTopAndBottom(mHeaderOriginalOffsetTop - mHeaderCurrentTargetOffsetTop, true /* requires update */); } mHeaderCurrentTargetOffsetTop = mCircleView.getTop(); } void resetFooter() { removeCallbacks(mCancel); removeCallbacks(mReturnToStartPosition); } @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); if (!enabled) { resetHeader(); resetFooter(); } } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); resetHeader(); resetFooter(); } @SuppressLint("NewApi") private void setHeaderColorViewAlpha(int targetAlpha) { mCircleView.getBackground().setAlpha(targetAlpha); mProgress.setAlpha(targetAlpha); } /** * The refresh indicator starting and resting position is always positioned * near the top of the refreshing content. This position is a consistent * location, but can be adjusted in either direction based on whether or not * there is a toolbar or actionbar present. * <p> * <strong>Note:</strong> Calling this will reset the position of the refresh indicator to * <code>start</code>. * </p> * * @param scale Set to true if there is no view at a higher z-order than where the progress * spinner is set to appear. Setting it to true will cause indicator to be scaled * up rather than clipped. * @param start The offset in pixels from the top of this view at which the * progress spinner should appear. * @param end The offset in pixels from the top of this view at which the * progress spinner should come to rest after a successful swipe * gesture. */ public void setHeaderProgressViewOffset(boolean scale, int start, int end) { mHeaderScale = scale; mHeaderOriginalOffsetTop = start; mHeaderSpinnerOffsetEnd = end; mHeaderUsingCustomStart = true; resetHeader(); mHeaderRefreshing = false; } /** * @return The offset in pixels from the top of this view at which the progress spinner should * appear. */ public int getHeaderProgressViewStartOffset() { return mHeaderOriginalOffsetTop; } /** * @return The offset in pixels from the top of this view at which the progress spinner should * come to rest after a successful swipe gesture. */ public int getHeaderProgressViewEndOffset() { return mHeaderSpinnerOffsetEnd; } /** * The refresh indicator resting position is always positioned near the top * of the refreshing content. This position is a consistent location, but * can be adjusted in either direction based on whether or not there is a * toolbar or actionbar present. * * @param scale Set to true if there is no view at a higher z-order than where the progress * spinner is set to appear. Setting it to true will cause indicator to be scaled * up rather than clipped. * @param end The offset in pixels from the top of this view at which the * progress spinner should come to rest after a successful swipe * gesture. */ public void setHeaderProgressViewEndTarget(boolean scale, int end) { mHeaderSpinnerOffsetEnd = end; mHeaderScale = scale; mCircleView.invalidate(); } /** * One of DEFAULT, or LARGE. */ public void setHeaderProgressCircleSize(int size) { if (size != MaterialProgressDrawable.LARGE && size != MaterialProgressDrawable.DEFAULT) { return; } final DisplayMetrics metrics = getResources().getDisplayMetrics(); if (size == MaterialProgressDrawable.LARGE) { mCircleDiameter = (int) (CIRCLE_DIAMETER_LARGE * metrics.density); } else { mCircleDiameter = (int) (CIRCLE_DIAMETER * metrics.density); } // force the bounds of the progress circle inside the circle view to // update by setting it to null before updating its size and then // re-setting it mCircleView.setImageDrawable(null); mProgress.updateSizes(size); mCircleView.setImageDrawable(mProgress); } /** * Simple constructor to use when creating a SwipeRefreshLayout from code. * * @param context */ public RefreshLayout(Context context) { this(context, null); } /** * Constructor that is called when inflating SwipeRefreshLayout from XML. * * @param context * @param attrs */ public RefreshLayout(Context context, AttributeSet attrs) { super(context, attrs); mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); mMediumAnimationDuration = getResources().getInteger(android.R.integer.config_mediumAnimTime); setWillNotDraw(false); mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR); mAccelerateInterpolator = new AccelerateInterpolator(ACCELERATE_INTERPOLATION_FACTOR); final DisplayMetrics metrics = getResources().getDisplayMetrics(); mCircleDiameter = (int) (CIRCLE_DIAMETER * metrics.density); createProgressView(); ViewCompat.setChildrenDrawingOrderEnabled(this, true); // the absolute offset has to take into account that the circle starts at an offset mHeaderSpinnerOffsetEnd = (int) (DEFAULT_CIRCLE_TARGET * metrics.density); mHeaderTotalDragDistance = mHeaderSpinnerOffsetEnd; mNestedScrollingParentHelper = new NestedScrollingParentHelper(this); mNestedScrollingChildHelper = new NestedScrollingChildHelper(this); setNestedScrollingEnabled(true); mHeaderOriginalOffsetTop = mHeaderCurrentTargetOffsetTop = -mCircleDiameter; moveToStart(1.0f); final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS); setEnabled(a.getBoolean(0, true)); a.recycle(); mProgressBar = new SwipeProgressBar(this); mProgressBarHeight = (int) (metrics.density * PROGRESS_BAR_HEIGHT); } @Override protected int getChildDrawingOrder(int childCount, int i) { if (mCircleViewIndex < 0) { return i; } else if (i == childCount - 1) { // Draw the selected child last return mCircleViewIndex; } else if (i >= mCircleViewIndex) { // Move the children after the selected child earlier one return i + 1; } else { // Keep the children before the selected child the same return i; } } private void createProgressView() { mCircleView = new CircleImageView(getContext(), CIRCLE_BG_LIGHT); mProgress = new MaterialProgressDrawable(getContext(), this); mProgress.setBackgroundColor(CIRCLE_BG_LIGHT); mCircleView.setImageDrawable(mProgress); mCircleView.setVisibility(View.GONE); addView(mCircleView); } /** * Set the listener to be notified when a refresh is triggered via the swipe * gesture. */ public void setOnRefreshListener(OnRefreshListener listener) { mListener = listener; } /** * Pre API 11, alpha is used to make the progress circle appear instead of scale. */ private boolean isAlphaUsedForScale() { return android.os.Build.VERSION.SDK_INT < 11; } /** * Is user allowed to swipe header */ public boolean isEnableSwipeHeader() { return mEnableSwipeHeader; } /** * Set user can swipe header or not * * @param enable true for enable */ public void setEnableSwipeHeader(boolean enable) { mEnableSwipeHeader = enable; } /** * Is user allowed to swipe footer */ public boolean isEnableSwipeFooter() { return mEnableSwipeFooter; } /** * Set user can swipe footer or not * * @param enable true for enable */ public void setEnableSwipeFooter(boolean enable) { mEnableSwipeFooter = enable; } /** * 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 setHeaderRefreshing(boolean refreshing) { if (mFooterRefreshing && refreshing) { // Can't header and footer refresh both return; } if (refreshing && mHeaderRefreshing != refreshing) { // scale and show mHeaderRefreshing = refreshing; int endTarget = 0; if (!mHeaderUsingCustomStart) { endTarget = mHeaderSpinnerOffsetEnd + mHeaderOriginalOffsetTop; } else { endTarget = mHeaderSpinnerOffsetEnd; } setHeaderTargetOffsetTopAndBottom(endTarget - mHeaderCurrentTargetOffsetTop, true /* requires update */); mHeaderNotify = false; startScaleUpAnimation(mHeaderRefreshListener); } else { setHeaderRefreshing(refreshing, false /* notify */); } } /** * 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 setFooterRefreshing(boolean refreshing) { if (mHeaderRefreshing && refreshing) { // Can't header and footer refresh both return; } if (mFooterRefreshing != refreshing) { ensureTarget(); mFooterCurrPercentage = 0; mFooterRefreshing = refreshing; if (mFooterRefreshing) { mProgressBar.start(); } else { mProgressBar.stop(); } } } @SuppressLint("NewApi") private void startScaleUpAnimation(AnimationListener listener) { mCircleView.setVisibility(View.VISIBLE); if (android.os.Build.VERSION.SDK_INT >= 11) { // Pre API 11, alpha is used in place of scale up to show the // progress circle appearing. // Don't adjust the alpha during appearance otherwise. mProgress.setAlpha(MAX_ALPHA); } mHeaderScaleAnimation = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { setAnimationProgress(interpolatedTime); } }; mHeaderScaleAnimation.setDuration(mMediumAnimationDuration); if (listener != null) { mCircleView.setAnimationListener(listener); } mCircleView.clearAnimation(); mCircleView.startAnimation(mHeaderScaleAnimation); } /** * Pre API 11, this does an alpha animation. * @param progress */ void setAnimationProgress(float progress) { if (isAlphaUsedForScale()) { setHeaderColorViewAlpha((int) (progress * MAX_ALPHA)); } else { ViewCompat.setScaleX(mCircleView, progress); ViewCompat.setScaleY(mCircleView, progress); } } private void setHeaderRefreshing(boolean refreshing, final boolean notify) { if (mFooterRefreshing && refreshing) { // Can't header and footer refresh both return; } if (mHeaderRefreshing != refreshing) { mHeaderNotify = notify; ensureTarget(); mHeaderRefreshing = refreshing; if (mHeaderRefreshing) { animateHeaderOffsetToCorrectPosition(mHeaderCurrentTargetOffsetTop, mHeaderRefreshListener); } else { startScaleDownAnimation(mHeaderRefreshListener); } } } void startScaleDownAnimation(Animation.AnimationListener listener) { mHeaderScaleDownAnimation = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { setAnimationProgress(1 - interpolatedTime); } }; mHeaderScaleDownAnimation.setDuration(SCALE_DOWN_DURATION); mCircleView.setAnimationListener(listener); mCircleView.clearAnimation(); mCircleView.startAnimation(mHeaderScaleDownAnimation); } @SuppressLint("NewApi") private void startProgressAlphaStartAnimation() { mHeaderAlphaStartAnimation = startAlphaAnimation(mProgress.getAlpha(), STARTING_PROGRESS_ALPHA); } @SuppressLint("NewApi") private void startProgressAlphaMaxAnimation() { mHeaderAlphaMaxAnimation = startAlphaAnimation(mProgress.getAlpha(), MAX_ALPHA); } @SuppressLint("NewApi") private Animation startAlphaAnimation(final int startingAlpha, final int endingAlpha) { // Pre API 11, alpha is used in place of scale. Don't also use it to // show the trigger point. if (mHeaderScale && isAlphaUsedForScale()) { return null; } Animation alpha = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { mProgress.setAlpha((int) (startingAlpha + ((endingAlpha - startingAlpha) * interpolatedTime))); } }; alpha.setDuration(ALPHA_ANIMATION_DURATION); // Clear out the previous animation listeners. mCircleView.setAnimationListener(null); mCircleView.clearAnimation(); mCircleView.startAnimation(alpha); return alpha; } /** * Set the background color of the progress spinner disc. * * @param colorRes Resource id of the color. */ public void setHeaderProgressBackgroundColorSchemeResource(@ColorRes int colorRes) { setHeaderProgressBackgroundColorSchemeColor(ContextCompat.getColor(getContext(), colorRes)); } /** * Set the background color of the progress spinner disc. * * @param color */ public void setHeaderProgressBackgroundColorSchemeColor(@ColorInt int color) { mCircleView.setBackgroundColor(color); mProgress.setBackgroundColor(color); } /** * Set the color resources used in the progress animation from color resources. * The first color will also be the color of the bar that grows in response * to a user swipe gesture. * * @param colorResIds */ public void setHeaderColorSchemeResources(@ColorRes int... colorResIds) { final Context context = getContext(); int[] colorRes = new int[colorResIds.length]; for (int i = 0; i < colorResIds.length; i++) { colorRes[i] = ContextCompat.getColor(context, colorResIds[i]); } setHeaderColorSchemeColors(colorRes); } /** * Set the 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 colors */ public void setHeaderColorSchemeColors(@ColorInt int... colors) { ensureTarget(); mProgress.setColorSchemeColors(colors); } /** * Set the colors used in the progress animation from color resources. * The first color will also be the color of the bar that grows in response * to a user swipe gesture. */ public void setFooterColorSchemeResources(int... colorResIds) { final Context context = getContext(); int[] colorRes = new int[colorResIds.length]; for (int i = 0; i < colorResIds.length; i++) { colorRes[i] = ContextCompat.getColor(context, colorResIds[i]); } setFooterColorSchemeColors(colorRes); } /** * Set the 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. */ public void setFooterColorSchemeColors(int... colors) { ensureTarget(); mProgressBar.setColorScheme(colors); } /** * @return Whether the SwipeRefreshWidget is actively showing refresh * progress. */ public boolean isRefreshing() { return mHeaderRefreshing || mFooterRefreshing; } public boolean isHeaderRefreshing() { return mHeaderRefreshing; } public boolean isFooterRefreshing() { return mFooterRefreshing; } private void ensureTarget() { // Don't bother getting the parent height if the parent hasn't been laid // out yet. if (mTarget == null) { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); if (!child.equals(mCircleView)) { mTarget = child; break; } } } if (mFooterDistanceToTriggerSync == -1) { if (getParent() != null && ((View) getParent()).getHeight() > 0) { final DisplayMetrics metrics = getResources().getDisplayMetrics(); mFooterDistanceToTriggerSync = (int) Math.min( ((View) getParent()).getHeight() * MAX_SWIPE_DISTANCE_FACTOR, REFRESH_TRIGGER_DISTANCE * metrics.density); } } } /** * Set the distance to trigger a sync in dips * * @param distance */ public void setHeaderDistanceToTriggerSync(int distance) { mHeaderTotalDragDistance = distance; } 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. mFooterCurrPercentage = 0; return; } */ mFooterCurrPercentage = percent; mProgressBar.setTriggerPercentage(percent); } @Override public void draw(@NonNull 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(); if (getChildCount() == 0) { return; } if (mTarget == null) { ensureTarget(); } if (mTarget == null) { return; } final View child = mTarget; final int childLeft = getPaddingLeft(); final int childTop = getPaddingTop(); final int childWidth = width - getPaddingLeft() - getPaddingRight(); final int childHeight = height - getPaddingTop() - getPaddingBottom(); child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight); int circleWidth = mCircleView.getMeasuredWidth(); int circleHeight = mCircleView.getMeasuredHeight(); mCircleView.layout((width / 2 - circleWidth / 2), mHeaderCurrentTargetOffsetTop, (width / 2 + circleWidth / 2), mHeaderCurrentTargetOffsetTop + circleHeight); mProgressBar.setBounds(0, height - mProgressBarHeight, width, height); } @Override public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (mTarget == null) { ensureTarget(); } if (mTarget == null) { return; } mTarget.measure( MeasureSpec.makeMeasureSpec(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY)); mCircleView.measure(MeasureSpec.makeMeasureSpec(mCircleDiameter, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(mCircleDiameter, MeasureSpec.EXACTLY)); mCircleViewIndex = -1; // Get the index of the circleview. for (int index = 0; index < getChildCount(); index++) { if (getChildAt(index) == mCircleView) { mCircleViewIndex = index; break; } } } /** * Get the diameter of the progress circle that is displayed as part of the * swipe to refresh layout. * * @return Diameter in pixels of the progress circle view. */ public int getHeaderProgressCircleDiameter() { return mCircleDiameter; } /** * @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 (mChildScrollCallback != null) { return mChildScrollCallback.canChildScrollUp(this, mTarget); } 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 ViewCompat.canScrollVertically(mTarget, -1) || mTarget.getScrollY() > 0; } } else { return ViewCompat.canScrollVertically(mTarget, -1); } } /** * Set a callback to override {@link SwipeRefreshLayout#canChildScrollUp()} method. Non-null * callback will return the value provided by the callback and ignore all internal logic. * @param callback Callback that should be called when canChildScrollUp() is called. */ public void setOnChildScrollUpCallback(@Nullable OnChildScrollCallback callback) { mChildScrollCallback = callback; } /** * @return Whether it is possible for the child view of this layout to * scroll down. Override this if the child view is a custom view. */ public boolean canChildScrollDown() { if (mChildScrollCallback != null) { return mChildScrollCallback.canChildScrollDown(this, mTarget); } if (mTarget instanceof AbsListView) { final AbsListView absListView = (AbsListView) mTarget; return absListView.getChildCount() > 0 && (absListView.getLastVisiblePosition() < absListView.getCount() - 1 || absListView.getChildAt(absListView.getChildCount() - 1) .getBottom() < absListView.getHeight() - absListView.getPaddingBottom()); } else { return ViewCompat.canScrollVertically(mTarget, 1); } } /** * @return {@code true} if child view almost scroll to bottom. */ public boolean isAlmostBottom() { if (null == mTarget) { return false; } if (mTarget instanceof AbsListView) { final AbsListView absListView = (AbsListView) mTarget; return absListView.getLastVisiblePosition() >= absListView.getCount() - 1; } else if (mTarget instanceof ScrollingView) { final ScrollingView scrollingView = (ScrollingView) mTarget; final int offset = scrollingView.computeVerticalScrollOffset(); final int range = scrollingView.computeVerticalScrollRange() - scrollingView.computeVerticalScrollExtent(); return offset >= range; } else { return !ViewCompat.canScrollVertically(mTarget, 1); } } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { try { ensureTarget(); final int action = MotionEventCompat.getActionMasked(ev); if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { mReturningToStart = false; } boolean mIsBeingDragged = false; if (isEnabled() && !mReturningToStart && !mHeaderRefreshing && !mFooterRefreshing) { if (!mIsFooterBeingDragged && mEnableSwipeHeader && !canChildScrollUp()) { mIsBeingDragged = headerInterceptTouchEvent(ev); } if (!mIsHeaderBeingDragged && mEnableSwipeFooter && !canChildScrollDown()) { mIsBeingDragged |= footerInterceptTouchEvent(ev); } } return mIsBeingDragged; } catch (Exception e) { return false; } } private boolean headerInterceptTouchEvent(MotionEvent ev) { int pointerIndex; final int action = MotionEventCompat.getActionMasked(ev); switch (action) { case MotionEvent.ACTION_DOWN: setHeaderTargetOffsetTopAndBottom(mHeaderOriginalOffsetTop - mCircleView.getTop(), true); mActivePointerId = ev.getPointerId(0); mIsHeaderBeingDragged = false; pointerIndex = ev.findPointerIndex(mActivePointerId); if (pointerIndex < 0) { return false; } mInitialDownY = ev.getY(pointerIndex); break; case MotionEvent.ACTION_MOVE: if (mActivePointerId == INVALID_POINTER) { Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id."); return false; } pointerIndex = ev.findPointerIndex(mActivePointerId); if (pointerIndex < 0) { return false; } final float y = ev.getY(pointerIndex); startHeaderDragging(y); break; case MotionEventCompat.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: mIsHeaderBeingDragged = false; mActivePointerId = INVALID_POINTER; break; } return mIsHeaderBeingDragged; } private boolean footerInterceptTouchEvent(MotionEvent ev) { int pointerIndex; final int action = MotionEventCompat.getActionMasked(ev); switch (action) { case MotionEvent.ACTION_DOWN: mActivePointerId = ev.getPointerId(0); mIsFooterBeingDragged = false; mFooterCurrPercentage = 0; pointerIndex = ev.findPointerIndex(mActivePointerId); if (pointerIndex < 0) { return false; } mInitialDownY = ev.getY(pointerIndex); break; case MotionEvent.ACTION_MOVE: if (mActivePointerId == INVALID_POINTER) { Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id."); return false; } pointerIndex = ev.findPointerIndex(mActivePointerId); if (pointerIndex < 0) { Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id."); return false; } final float y = ev.getY(pointerIndex); final float yDiff = y - mInitialDownY; if (yDiff < -mTouchSlop) { mIsFooterBeingDragged = true; mInitialMotionY = mInitialDownY - mTouchSlop; } break; case MotionEventCompat.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: mIsFooterBeingDragged = false; mFooterCurrPercentage = 0; mActivePointerId = INVALID_POINTER; break; } return mIsFooterBeingDragged; } @Override public void requestDisallowInterceptTouchEvent(boolean b) { // if this is a List < L or another view that doesn't support nested // scrolling, ignore this request so that the vertical scroll event // isn't stolen if ((android.os.Build.VERSION.SDK_INT < 21 && mTarget instanceof AbsListView) || (mTarget != null && !ViewCompat.isNestedScrollingEnabled(mTarget))) { // Nope. } else { super.requestDisallowInterceptTouchEvent(b); } } // NestedScrollingParent @Override public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { return isEnabled() && !mReturningToStart && !mHeaderRefreshing && (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; } @Override public void onNestedScrollAccepted(View child, View target, int axes) { // Reset the counter of how much leftover scroll needs to be consumed. mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes); // Dispatch up to the nested parent startNestedScroll(axes & ViewCompat.SCROLL_AXIS_VERTICAL); mTotalUnconsumed = 0; mNestedScrollInProgress = true; } @Override public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { // If we are in the middle of consuming, a scroll, then we want to move the spinner back up // before allowing the list to scroll if (dy > 0 && mTotalUnconsumed > 0) { if (dy > mTotalUnconsumed) { consumed[1] = dy - (int) mTotalUnconsumed; mTotalUnconsumed = 0; } else { mTotalUnconsumed -= dy; consumed[1] = dy; } moveSpinner(mTotalUnconsumed); } // If a client layout is using a custom start position for the circle // view, they mean to hide it again before scrolling the child view // If we get back to mTotalUnconsumed == 0 and there is more to go, hide // the circle so it isn't exposed if its blocking content is moved if (mHeaderUsingCustomStart && dy > 0 && mTotalUnconsumed == 0 && Math.abs(dy - consumed[1]) > 0) { mCircleView.setVisibility(View.GONE); } // Now let our nested parent consume the leftovers final int[] parentConsumed = mParentScrollConsumed; if (dispatchNestedPreScroll(dx - consumed[0], dy - consumed[1], parentConsumed, null)) { consumed[0] += parentConsumed[0]; consumed[1] += parentConsumed[1]; } } @Override public int getNestedScrollAxes() { return mNestedScrollingParentHelper.getNestedScrollAxes(); } @Override public void onStopNestedScroll(View target) { mNestedScrollingParentHelper.onStopNestedScroll(target); mNestedScrollInProgress = false; // Finish the spinner for nested scrolling if we ever consumed any // unconsumed nested scroll if (mTotalUnconsumed > 0) { finishSpinner(mTotalUnconsumed); mTotalUnconsumed = 0; } // Dispatch up our nested parent stopNestedScroll(); } @Override public void onNestedScroll(final View target, final int dxConsumed, final int dyConsumed, final int dxUnconsumed, final int dyUnconsumed) { // Dispatch up to the nested parent first dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, mParentOffsetInWindow); // This is a bit of a hack. Nested scrolling works from the bottom up, and as we are // sometimes between two nested scrolling views, we need a way to be able to know when any // nested scrolling parent has stopped handling events. We do that by using the // 'offset in window 'functionality to see if we have been moved from the event. // This is a decent indication of whether we should take over the event stream or not. final int dy = dyUnconsumed + mParentOffsetInWindow[1]; if (dy < 0 && !canChildScrollUp()) { mTotalUnconsumed += Math.abs(dy); moveSpinner(mTotalUnconsumed); } } // NestedScrollingChild @Override public void setNestedScrollingEnabled(boolean enabled) { mNestedScrollingChildHelper.setNestedScrollingEnabled(enabled); } @Override public boolean isNestedScrollingEnabled() { return mNestedScrollingChildHelper.isNestedScrollingEnabled(); } @Override public boolean startNestedScroll(int axes) { return mNestedScrollingChildHelper.startNestedScroll(axes); } @Override public void stopNestedScroll() { mNestedScrollingChildHelper.stopNestedScroll(); } @Override public boolean hasNestedScrollingParent() { return mNestedScrollingChildHelper.hasNestedScrollingParent(); } @Override public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { return mNestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow); } @Override public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { return mNestedScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); } @Override public boolean onNestedPreFling(View target, float velocityX, float velocityY) { return dispatchNestedPreFling(velocityX, velocityY); } @Override public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { return dispatchNestedFling(velocityX, velocityY, consumed); } @Override public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { return mNestedScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); } @Override public boolean dispatchNestedPreFling(float velocityX, float velocityY) { return mNestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY); } private boolean isAnimationRunning(Animation animation) { return animation != null && animation.hasStarted() && !animation.hasEnded(); } @SuppressLint("NewApi") private void moveSpinner(float overscrollTop) { mProgress.showArrow(true); float originalDragPercent = overscrollTop / mHeaderTotalDragDistance; float dragPercent = Math.min(1f, Math.abs(originalDragPercent)); float adjustedPercent = (float) Math.max(dragPercent - .4, 0) * 5 / 3; float extraOS = Math.abs(overscrollTop) - mHeaderTotalDragDistance; float slingshotDist = mHeaderUsingCustomStart ? mHeaderSpinnerOffsetEnd - mHeaderOriginalOffsetTop : mHeaderSpinnerOffsetEnd; float tensionSlingshotPercent = Math.max(0, Math.min(extraOS, slingshotDist * 2) / slingshotDist); float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow((tensionSlingshotPercent / 4), 2)) * 2f; float extraMove = (slingshotDist) * tensionPercent * 2; int targetY = mHeaderOriginalOffsetTop + (int) ((slingshotDist * dragPercent) + extraMove); // where 1.0f is a full circle if (mCircleView.getVisibility() != View.VISIBLE) { mCircleView.setVisibility(View.VISIBLE); } if (!mHeaderScale) { ViewCompat.setScaleX(mCircleView, 1f); ViewCompat.setScaleY(mCircleView, 1f); } if (mHeaderScale) { setAnimationProgress(Math.min(1f, overscrollTop / mHeaderTotalDragDistance)); } if (overscrollTop < mHeaderTotalDragDistance) { if (mProgress.getAlpha() > STARTING_PROGRESS_ALPHA && !isAnimationRunning(mHeaderAlphaStartAnimation)) { // Animate the alpha startProgressAlphaStartAnimation(); } } else { if (mProgress.getAlpha() < MAX_ALPHA && !isAnimationRunning(mHeaderAlphaMaxAnimation)) { // Animate the alpha startProgressAlphaMaxAnimation(); } } float strokeStart = adjustedPercent * .8f; mProgress.setStartEndTrim(0f, Math.min(MAX_PROGRESS_ANGLE, strokeStart)); mProgress.setArrowScale(Math.min(1f, adjustedPercent)); float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f; mProgress.setProgressRotation(rotation); setHeaderTargetOffsetTopAndBottom(targetY - mHeaderCurrentTargetOffsetTop, true /* requires update */); } private void finishSpinner(float overscrollTop) { if (overscrollTop > mHeaderTotalDragDistance) { setHeaderRefreshing(true, true /* notify */); } else { // cancel refresh mHeaderRefreshing = false; mProgress.setStartEndTrim(0f, 0f); Animation.AnimationListener listener = null; if (!mHeaderScale) { listener = new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { if (!mHeaderScale) { startScaleDownAnimation(null); } } @Override public void onAnimationRepeat(Animation animation) { } }; } animateHeaderOffsetToStartPosition(mHeaderCurrentTargetOffsetTop, listener); mProgress.showArrow(false); } } @Override public boolean onTouchEvent(@NonNull MotionEvent ev) { try { final int action = MotionEventCompat.getActionMasked(ev); if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { mReturningToStart = false; } if (isEnabled() && !mReturningToStart && !mHeaderRefreshing && !mFooterRefreshing) { if (!mIsFooterBeingDragged && mEnableSwipeHeader && !canChildScrollUp()) { headerTouchEvent(ev); } if (!mIsHeaderBeingDragged && mEnableSwipeFooter && !canChildScrollDown()) { footerTouchEvent(ev); } } return true; } catch (Exception e) { return false; } } private boolean headerTouchEvent(MotionEvent ev) { int pointerIndex; final int action = MotionEventCompat.getActionMasked(ev); switch (action) { case MotionEvent.ACTION_DOWN: mActivePointerId = ev.getPointerId(0); mIsHeaderBeingDragged = false; break; case MotionEvent.ACTION_MOVE: { pointerIndex = ev.findPointerIndex(mActivePointerId); if (pointerIndex < 0) { Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id."); return false; } final float y = ev.getY(pointerIndex); startHeaderDragging(y); if (mIsHeaderBeingDragged) { final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE; if (overscrollTop > 0) { moveSpinner(overscrollTop); } else { return false; } } break; } case MotionEventCompat.ACTION_POINTER_DOWN: { pointerIndex = MotionEventCompat.getActionIndex(ev); if (pointerIndex < 0) { Log.e(LOG_TAG, "Got ACTION_POINTER_DOWN event but have an invalid action index."); return false; } mActivePointerId = ev.getPointerId(pointerIndex); break; } case MotionEventCompat.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; case MotionEvent.ACTION_UP: { pointerIndex = ev.findPointerIndex(mActivePointerId); if (pointerIndex < 0) { Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id."); return false; } if (mIsHeaderBeingDragged) { final float y = ev.getY(pointerIndex); final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE; mIsHeaderBeingDragged = false; finishSpinner(overscrollTop); } mActivePointerId = INVALID_POINTER; return false; } case MotionEvent.ACTION_CANCEL: pointerIndex = ev.findPointerIndex(mActivePointerId); if (pointerIndex < 0) { Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id."); return false; } if (mIsHeaderBeingDragged) { mIsHeaderBeingDragged = false; finishSpinner(0); } mActivePointerId = INVALID_POINTER; return false; } return true; } @SuppressLint("NewApi") private void startHeaderDragging(float y) { final float yDiff = y - mInitialDownY; if (yDiff > mTouchSlop && !mIsHeaderBeingDragged) { mInitialMotionY = mInitialDownY + mTouchSlop; mIsHeaderBeingDragged = true; mProgress.setAlpha(STARTING_PROGRESS_ALPHA); } } private void startFooterRefresh() { removeCallbacks(mCancel); mReturnToStartPosition.run(); setFooterRefreshing(true); mListener.onFooterRefresh(); } private boolean footerTouchEvent(MotionEvent ev) { final int action = MotionEventCompat.getActionMasked(ev); int pointerIndex; float y; float yDiff; switch (action) { case MotionEvent.ACTION_DOWN: mActivePointerId = ev.getPointerId(0); mIsFooterBeingDragged = false; mFooterCurrPercentage = 0; break; case MotionEvent.ACTION_MOVE: pointerIndex = ev.findPointerIndex(mActivePointerId); if (pointerIndex < 0) { Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id."); return false; } y = ev.getY(pointerIndex); if (!mIsFooterBeingDragged) { yDiff = y - mInitialDownY; if (yDiff < -mTouchSlop) { mIsFooterBeingDragged = true; mInitialMotionY = mInitialDownY - mTouchSlop; } } if (mIsFooterBeingDragged) { yDiff = y - mInitialMotionY; setTriggerPercentage(mAccelerateInterpolator.getInterpolation( MathUtils.clamp(-yDiff, 0, mFooterDistanceToTriggerSync) / mFooterDistanceToTriggerSync)); } break; case MotionEventCompat.ACTION_POINTER_DOWN: { final int index = MotionEventCompat.getActionIndex(ev); mActivePointerId = ev.getPointerId(index); break; } case MotionEventCompat.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: pointerIndex = ev.findPointerIndex(mActivePointerId); if (mActivePointerId == INVALID_POINTER && pointerIndex < 0) { if (action == MotionEvent.ACTION_UP) { Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id."); } return false; } try { y = ev.getY(pointerIndex); } catch (Throwable e) { y = 0; } yDiff = y - mInitialMotionY; if (action == MotionEvent.ACTION_UP && -yDiff > mFooterDistanceToTriggerSync) { // User movement passed distance; trigger a refresh startFooterRefresh(); } else { mCancel.run(); } mIsFooterBeingDragged = false; mFooterCurrPercentage = 0; mActivePointerId = INVALID_POINTER; return false; } return mIsFooterBeingDragged; } private void animateHeaderOffsetToCorrectPosition(int from, AnimationListener listener) { mHeaderFrom = from; mAnimateToCorrectPosition.reset(); mAnimateToCorrectPosition.setDuration(ANIMATE_TO_TRIGGER_DURATION); mAnimateToCorrectPosition.setInterpolator(mDecelerateInterpolator); if (listener != null) { mCircleView.setAnimationListener(listener); } mCircleView.clearAnimation(); mCircleView.startAnimation(mAnimateToCorrectPosition); } private void animateHeaderOffsetToStartPosition(int from, AnimationListener listener) { if (mHeaderScale) { // Scale the item back down startScaleDownReturnToStartAnimation(from, listener); } else { mHeaderFrom = from; mAnimateToStartPosition.reset(); mAnimateToStartPosition.setDuration(ANIMATE_TO_START_DURATION); mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator); if (listener != null) { mCircleView.setAnimationListener(listener); } mCircleView.clearAnimation(); mCircleView.startAnimation(mAnimateToStartPosition); } } private final Animation mAnimateToCorrectPosition = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { int targetTop = 0; int endTarget = 0; if (!mHeaderUsingCustomStart) { endTarget = mHeaderSpinnerOffsetEnd - Math.abs(mHeaderOriginalOffsetTop); } else { endTarget = mHeaderSpinnerOffsetEnd; } targetTop = (mHeaderFrom + (int) ((endTarget - mHeaderFrom) * interpolatedTime)); int offset = targetTop - mCircleView.getTop(); setHeaderTargetOffsetTopAndBottom(offset, false /* requires update */); mProgress.setArrowScale(1 - interpolatedTime); } }; void moveToStart(float interpolatedTime) { int targetTop = 0; targetTop = (mHeaderFrom + (int) ((mHeaderOriginalOffsetTop - mHeaderFrom) * interpolatedTime)); int offset = targetTop - mCircleView.getTop(); setHeaderTargetOffsetTopAndBottom(offset, false /* requires update */); } private final Animation mAnimateToStartPosition = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { moveToStart(interpolatedTime); } }; @SuppressLint("NewApi") private void startScaleDownReturnToStartAnimation(int from, Animation.AnimationListener listener) { mHeaderFrom = from; if (isAlphaUsedForScale()) { mHeaderStartingScale = mProgress.getAlpha(); } else { mHeaderStartingScale = ViewCompat.getScaleX(mCircleView); } mHeaderScaleDownToStartAnimation = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { float targetScale = (mHeaderStartingScale + (-mHeaderStartingScale * interpolatedTime)); setAnimationProgress(targetScale); moveToStart(interpolatedTime); } }; mHeaderScaleDownToStartAnimation.setDuration(SCALE_DOWN_DURATION); if (listener != null) { mCircleView.setAnimationListener(listener); } mCircleView.clearAnimation(); mCircleView.startAnimation(mHeaderScaleDownToStartAnimation); } void setHeaderTargetOffsetTopAndBottom(int offset, boolean requiresUpdate) { mCircleView.bringToFront(); ViewCompat.offsetTopAndBottom(mCircleView, offset); mHeaderCurrentTargetOffsetTop = mCircleView.getTop(); if (requiresUpdate && android.os.Build.VERSION.SDK_INT < 11) { invalidate(); } } private void animateFooterOffsetToStartPosition(int from, AnimationListener listener) { mFooterFrom = from; mAnimateFooterToStartPosition.reset(); mAnimateFooterToStartPosition.setDuration(mMediumAnimationDuration); mAnimateFooterToStartPosition.setAnimationListener(listener); mAnimateFooterToStartPosition.setInterpolator(mDecelerateInterpolator); mTarget.startAnimation(mAnimateFooterToStartPosition); } private void updatePositionTimeout() { removeCallbacks(mCancel); postDelayed(mCancel, RETURN_TO_ORIGINAL_POSITION_TIMEOUT); } private void onSecondaryPointerUp(MotionEvent ev) { final int pointerIndex = MotionEventCompat.getActionIndex(ev); final int pointerId = ev.getPointerId(pointerIndex); if (pointerId == mActivePointerId) { // This was our active pointer going up. Choose a new // active pointer and adjust accordingly. final int newPointerIndex = pointerIndex == 0 ? 1 : 0; mActivePointerId = ev.getPointerId(newPointerIndex); } } /** * Classes that wish to be notified when the swipe gesture correctly * triggers a refresh should implement this interface. */ public interface OnRefreshListener { void onHeaderRefresh(); void onFooterRefresh(); } /** * Classes that wish to override {@link RefreshLayout#canChildScrollUp()} method * and {@link RefreshLayout#canChildScrollDown()} method behavior should implement this interface. */ public interface OnChildScrollCallback { /** * Callback that will be called when {@link RefreshLayout#canChildScrollUp()} method * is called to allow the implementer to override its behavior. * * @param parent SwipeRefreshLayout that this callback is overriding. * @param child The child view of SwipeRefreshLayout. * * @return Whether it is possible for the child view of parent layout to scroll up. */ boolean canChildScrollUp(RefreshLayout parent, @Nullable View child); /** * Callback that will be called when {@link RefreshLayout#canChildScrollDown()} method * is called to allow the implementer to override its behavior. * * @param parent SwipeRefreshLayout that this callback is overriding. * @param child The child view of SwipeRefreshLayout. * * @return Whether it is possible for the child view of parent layout to scroll down. */ boolean canChildScrollDown(RefreshLayout parent, @Nullable View child); } /** * Simple AnimationListener to avoid having to implement unneeded methods in * AnimationListeners. */ private static class BaseAnimationListener implements AnimationListener { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { } @Override public void onAnimationRepeat(Animation animation) { } } }