Welcome Buddy, Android does not provide horizontal SwipeRefreshLayout in android. So I brought a complete solution for horizontal SwipeRefreshLayout in Android. In this tutorial, I will explain, about Horizontal SwipeRefreshLayout widget, How does work? After that, I will prepare a sample app.
Prerequisite
You have to need basic idea about SwipeRefreshLayout and how its work with RecyclerView. Apart from this, You have to knowledge of RecyclerView Implementation.
Horizontal SwipeRefreshLayout Sample App
Create Horizontal SwipeRefreshLayout Widget
First of all, We will prepare Horizontal SwipeRefreshLayout widget, So do that create a widget folder in the main package. Inside this package create a new class which extends ViewGroup named is HorizontalSwipeRefreshLayout .
package com.wave.pagination.horizontalswiperefreshlayout.widget; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.support.annotation.ColorInt; import android.support.annotation.ColorRes; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.support.v4.view.MotionEventCompat; import android.support.v4.view.NestedScrollingChild; import android.support.v4.view.NestedScrollingChildHelper; import android.support.v4.view.NestedScrollingParent; import android.support.v4.view.NestedScrollingParentHelper; import android.support.v4.view.ViewCompat; 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.Animation; import android.view.animation.Animation.AnimationListener; import android.view.animation.DecelerateInterpolator; import android.view.animation.Transformation; import android.widget.AbsListView; /** * Created on : May 04, 2019 * Author : AndroidWave */ public class HorizontalSwipeRefreshLayout extends ViewGroup implements NestedScrollingParent, NestedScrollingChild { // 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 = HorizontalSwipeRefreshLayout.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 mRefreshing = false; private int mTouchSlop; private float mTotalDragDistance = -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 mCurrentTargetOffsetLeft; private float mInitialMotionX; private float mInitialDownX; private boolean mIsBeingDragged; private int mActivePointerId = INVALID_POINTER; // Whether this item is scaled up rather than clipped boolean mScale; // 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 mFrom; float mStartingScale; protected int mOriginalOffsetLeft; MaterialProgressDrawable mProgress; private Animation mScaleAnimation; private Animation mScaleDownAnimation; private Animation mAlphaStartAnimation; private Animation mAlphaMaxAnimation; private Animation mScaleDownToStartAnimation; float mSpinnerFinalOffset; boolean mNotify; private int mCircleDiameter; // Whether the client has set a custom starting position; boolean mUsingCustomStart; private OnChildScrollUpCallback mChildScrollUpCallback; private AnimationListener mRefreshListener = new AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationRepeat(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { if (mRefreshing) { // Make sure the progress view is fully visible mProgress.setAlpha(MAX_ALPHA); mProgress.start(); if (mNotify) { if (mListener != null) { mListener.onRefresh(); } } mCurrentTargetOffsetLeft = mCircleView.getLeft(); } else { reset(); } } }; void reset() { mCircleView.clearAnimation(); mProgress.stop(); mCircleView.setVisibility(View.GONE); setColorViewAlpha(MAX_ALPHA); // Return the circle to its start position if (mScale) { setAnimationProgress(0 /* animation complete and view is hidden */); } else { setTargetOffsetLeftAndRight(mOriginalOffsetLeft - mCurrentTargetOffsetLeft, true /* requires update */); } mCurrentTargetOffsetLeft = mCircleView.getLeft(); } @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); if (!enabled) { reset(); } } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); reset(); } private void setColorViewAlpha(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 setProgressViewOffset(boolean scale, int start, int end) { mScale = scale; mOriginalOffsetLeft = start; mSpinnerFinalOffset = end; mUsingCustomStart = true; reset(); mRefreshing = false; } /** * 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 setProgressViewEndTarget(boolean scale, int end) { mSpinnerFinalOffset = end; mScale = scale; mCircleView.invalidate(); } /** * One of DEFAULT, or LARGE. */ public void setSize(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 HorizontalSwipeRefreshLayout(Context context) { this(context, null); } /** * Constructor that is called when inflating SwipeRefreshLayout from XML. * * @param context * @param attrs */ public HorizontalSwipeRefreshLayout(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); 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 mSpinnerFinalOffset = DEFAULT_CIRCLE_TARGET * metrics.density; mTotalDragDistance = mSpinnerFinalOffset; mNestedScrollingParentHelper = new NestedScrollingParentHelper(this); mNestedScrollingChildHelper = new NestedScrollingChildHelper(this); setNestedScrollingEnabled(true); mOriginalOffsetLeft = mCurrentTargetOffsetLeft = -mCircleDiameter; moveToStart(1.0f); final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS); setEnabled(a.getBoolean(0, true)); a.recycle(); } @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; } /** * 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 (refreshing && mRefreshing != refreshing) { // scale and show mRefreshing = refreshing; int endTarget = 0; if (!mUsingCustomStart) { endTarget = (int) (mSpinnerFinalOffset + mOriginalOffsetLeft); } else { endTarget = (int) mSpinnerFinalOffset; } setTargetOffsetLeftAndRight(endTarget - mCurrentTargetOffsetLeft, true /* requires update */); mNotify = false; startScaleUpAnimation(mRefreshListener); } else { setRefreshing(refreshing, false /* notify */); } } 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); } mScaleAnimation = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { setAnimationProgress(interpolatedTime); } }; mScaleAnimation.setDuration(mMediumAnimationDuration); if (listener != null) { mCircleView.setAnimationListener(listener); } mCircleView.clearAnimation(); mCircleView.startAnimation(mScaleAnimation); } /** * Pre API 11, this does an alpha animation. * * @param progress */ void setAnimationProgress(float progress) { if (isAlphaUsedForScale()) { setColorViewAlpha((int) (progress * MAX_ALPHA)); } else { ViewCompat.setScaleX(mCircleView, progress); ViewCompat.setScaleY(mCircleView, progress); } } private void setRefreshing(boolean refreshing, final boolean notify) { if (mRefreshing != refreshing) { mNotify = notify; ensureTarget(); mRefreshing = refreshing; if (mRefreshing) { animateOffsetToCorrectPosition(mCurrentTargetOffsetLeft, mRefreshListener); } else { startScaleDownAnimation(mRefreshListener); } } } void startScaleDownAnimation(AnimationListener listener) { mScaleDownAnimation = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { setAnimationProgress(1 - interpolatedTime); } }; mScaleDownAnimation.setDuration(SCALE_DOWN_DURATION); mCircleView.setAnimationListener(listener); mCircleView.clearAnimation(); mCircleView.startAnimation(mScaleDownAnimation); } private void startProgressAlphaStartAnimation() { mAlphaStartAnimation = startAlphaAnimation(mProgress.getAlpha(), STARTING_PROGRESS_ALPHA); } private void startProgressAlphaMaxAnimation() { mAlphaMaxAnimation = startAlphaAnimation(mProgress.getAlpha(), MAX_ALPHA); } 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 (mScale && 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; } /** * @deprecated Use {@link #setProgressBackgroundColorSchemeResource(int)} */ @Deprecated public void setProgressBackgroundColor(int colorRes) { setProgressBackgroundColorSchemeResource(colorRes); } /** * Set the background color of the progress spinner disc. * * @param colorRes Resource id of the color. */ public void setProgressBackgroundColorSchemeResource(@ColorRes int colorRes) { setProgressBackgroundColorSchemeColor(getResources().getColor(colorRes)); } /** * Set the background color of the progress spinner disc. * * @param color */ public void setProgressBackgroundColorSchemeColor(@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 setColorSchemeResources(@ColorRes int... colorResIds) { final Resources res = getResources(); int[] colorRes = new int[colorResIds.length]; for (int i = 0; i < colorResIds.length; i++) { colorRes[i] = res.getColor(colorResIds[i]); } setColorSchemeColors(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 setColorSchemeColors(@ColorInt int... colors) { ensureTarget(); mProgress.setColorSchemeColors(colors); } /** * @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) { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); if (!child.equals(mCircleView)) { mTarget = child; break; } } } } /** * Set the distance to trigger a sync in dips * * @param distance */ public void setDistanceToTriggerSync(int distance) { mTotalDragDistance = distance; } @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), mCurrentTargetOffsetLeft, (width / 2 + circleWidth / 2), mCurrentTargetOffsetLeft + circleHeight); mCircleView.layout(mCurrentTargetOffsetLeft, (height / 2 - circleHeight / 2), mCurrentTargetOffsetLeft + circleWidth, (height / 2 + circleHeight / 2)); } @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 getProgressCircleDiameter() { 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 (mChildScrollUpCallback != null) { return mChildScrollUpCallback.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) .getLeft() < absListView.getPaddingLeft()); } else { return ViewCompat.canScrollHorizontally(mTarget, -1) || mTarget.getScrollX() > 0; } } else { return ViewCompat.canScrollHorizontally(mTarget, -1); } } /** * Set a callback to override {@link HorizontalSwipeRefreshLayout #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 OnChildScrollUpCallback callback) { mChildScrollUpCallback = callback; } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { ensureTarget(); final int action = MotionEventCompat.getActionMasked(ev); int pointerIndex; if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { mReturningToStart = false; } if (!isEnabled() || mReturningToStart || canChildScrollUp() || mRefreshing || mNestedScrollInProgress) { // Fail fast if we're not in a state where a swipe is possible return false; } switch (action) { case MotionEvent.ACTION_DOWN: setTargetOffsetLeftAndRight(mOriginalOffsetLeft - mCircleView.getLeft(), true); mActivePointerId = ev.getPointerId(0); mIsBeingDragged = false; pointerIndex = ev.findPointerIndex(mActivePointerId); if (pointerIndex < 0) { return false; } mInitialDownX = ev.getX(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 x = ev.getX(pointerIndex); startDragging(x); break; case MotionEventCompat.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: mIsBeingDragged = false; mActivePointerId = INVALID_POINTER; break; } return mIsBeingDragged; } @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 && !mRefreshing && (nestedScrollAxes & ViewCompat.SCROLL_AXIS_HORIZONTAL) != 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_HORIZONTAL); 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 (dx > 0 && mTotalUnconsumed > 0) { if (dx > mTotalUnconsumed) { consumed[0] = dx - (int) mTotalUnconsumed; mTotalUnconsumed = 0; } else { mTotalUnconsumed -= dx; consumed[0] = dx; } 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 (mUsingCustomStart && dx > 0 && mTotalUnconsumed == 0 && Math.abs(dx - consumed[0]) > 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 dx = dxUnconsumed + mParentOffsetInWindow[0]; if (dx < 0 && !canChildScrollUp()) { mTotalUnconsumed += Math.abs(dx); 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(); } private void moveSpinner(float overscrollLeft) { mProgress.showArrow(true); float originalDragPercent = overscrollLeft / mTotalDragDistance; float dragPercent = Math.min(1f, Math.abs(originalDragPercent)); float adjustedPercent = (float) Math.max(dragPercent - .4, 0) * 5 / 3; float extraOS = Math.abs(overscrollLeft) - mTotalDragDistance; float slingshotDist = mUsingCustomStart ? mSpinnerFinalOffset - mOriginalOffsetLeft : mSpinnerFinalOffset; 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 targetX = mOriginalOffsetLeft + (int) ((slingshotDist * dragPercent) + extraMove); // where 1.0f is a full circle if (mCircleView.getVisibility() != View.VISIBLE) { mCircleView.setVisibility(View.VISIBLE); } if (!mScale) { ViewCompat.setScaleX(mCircleView, 1f); ViewCompat.setScaleY(mCircleView, 1f); } if (mScale) { setAnimationProgress(Math.min(1f, overscrollLeft / mTotalDragDistance)); } if (overscrollLeft < mTotalDragDistance) { if (mProgress.getAlpha() > STARTING_PROGRESS_ALPHA && !isAnimationRunning(mAlphaStartAnimation)) { // Animate the alpha startProgressAlphaStartAnimation(); } } else { if (mProgress.getAlpha() < MAX_ALPHA && !isAnimationRunning(mAlphaMaxAnimation)) { // 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); setTargetOffsetLeftAndRight(targetX - mCurrentTargetOffsetLeft, true /* requires update */); } private void finishSpinner(float overscrollLeft) { if (overscrollLeft > mTotalDragDistance) { setRefreshing(true, true /* notify */); } else { // cancel refresh mRefreshing = false; mProgress.setStartEndTrim(0f, 0f); AnimationListener listener = null; if (!mScale) { listener = new AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { if (!mScale) { startScaleDownAnimation(null); } } @Override public void onAnimationRepeat(Animation animation) { } }; } animateOffsetToStartPosition(mCurrentTargetOffsetLeft, listener); mProgress.showArrow(false); } } @Override public boolean onTouchEvent(MotionEvent ev) { final int action = MotionEventCompat.getActionMasked(ev); int pointerIndex = -1; if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { mReturningToStart = false; } if (!isEnabled() || mReturningToStart || canChildScrollUp() || mRefreshing || mNestedScrollInProgress) { // Fail fast if we're not in a state where a swipe is possible return false; } switch (action) { case MotionEvent.ACTION_DOWN: mActivePointerId = ev.getPointerId(0); mIsBeingDragged = 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 x = ev.getX(pointerIndex); startDragging(x); if (mIsBeingDragged) { final float overscrollLeft = (x - mInitialMotionX) * DRAG_RATE; if (overscrollLeft > 0) { moveSpinner(overscrollLeft); } 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 (mIsBeingDragged) { final float x = ev.getX(pointerIndex); final float overscrollLeft = (x - mInitialMotionX) * DRAG_RATE; mIsBeingDragged = false; finishSpinner(overscrollLeft); } mActivePointerId = INVALID_POINTER; return false; } case MotionEvent.ACTION_CANCEL: return false; } return true; } private void startDragging(float x) { final float xDiff = x - mInitialDownX; if (xDiff > mTouchSlop && !mIsBeingDragged) { mInitialMotionX = mInitialDownX + mTouchSlop; mIsBeingDragged = true; mProgress.setAlpha(STARTING_PROGRESS_ALPHA); } } private void animateOffsetToCorrectPosition(int from, AnimationListener listener) { mFrom = 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 animateOffsetToStartPosition(int from, AnimationListener listener) { if (mScale) { // Scale the item back down startScaleDownReturnToStartAnimation(from, listener); } else { mFrom = 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 targetLeft = 0; int endTarget = 0; if (!mUsingCustomStart) { endTarget = (int) (mSpinnerFinalOffset - Math.abs(mOriginalOffsetLeft)); } else { endTarget = (int) mSpinnerFinalOffset; } targetLeft = (mFrom + (int) ((endTarget - mFrom) * interpolatedTime)); int offset = targetLeft - mCircleView.getLeft(); setTargetOffsetLeftAndRight(offset, false /* requires update */); mProgress.setArrowScale(1 - interpolatedTime); } }; void moveToStart(float interpolatedTime) { int targetLeft = 0; targetLeft = (mFrom + (int) ((mOriginalOffsetLeft - mFrom) * interpolatedTime)); int offset = targetLeft - mCircleView.getLeft(); setTargetOffsetLeftAndRight(offset, false /* requires update */); } private final Animation mAnimateToStartPosition = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { moveToStart(interpolatedTime); } }; private void startScaleDownReturnToStartAnimation(int from, AnimationListener listener) { mFrom = from; if (isAlphaUsedForScale()) { mStartingScale = mProgress.getAlpha(); } else { mStartingScale = ViewCompat.getScaleX(mCircleView); } mScaleDownToStartAnimation = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { float targetScale = (mStartingScale + (-mStartingScale * interpolatedTime)); setAnimationProgress(targetScale); moveToStart(interpolatedTime); } }; mScaleDownToStartAnimation.setDuration(SCALE_DOWN_DURATION); if (listener != null) { mCircleView.setAnimationListener(listener); } mCircleView.clearAnimation(); mCircleView.startAnimation(mScaleDownToStartAnimation); } void setTargetOffsetLeftAndRight(int offset, boolean requiresUpdate) { mCircleView.bringToFront(); ViewCompat.offsetLeftAndRight(mCircleView, offset); mCurrentTargetOffsetLeft = mCircleView.getLeft(); if (requiresUpdate && android.os.Build.VERSION.SDK_INT < 11) { invalidate(); } } 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 { /** * Called when a swipe gesture triggers a refresh. */ void onRefresh(); } /** * Classes that wish to override {@link HorizontalSwipeRefreshLayout #canChildScrollUp()} method * behavior should implement this interface. */ public interface OnChildScrollUpCallback { /** * Callback that will be called when {@link HorizontalSwipeRefreshLayout #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(HorizontalSwipeRefreshLayout parent, @Nullable View child); } }
Create MaterialProgressDrawable utility class
In above class we are using two utility class. So create a new class named is MaterialProgressDrawable and paste all code inside that.
package com.wave.pagination.horizontalswiperefreshlayout.widget; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.v4.view.animation.FastOutSlowInInterpolator; import android.util.DisplayMetrics; import android.view.View; import android.view.animation.Animation; import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; import android.view.animation.Transformation; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; /** * Created on : May 04, 2019 * Author : AndroidWave */ public class MaterialProgressDrawable extends Drawable implements Animatable { private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); static final Interpolator MATERIAL_INTERPOLATOR = new FastOutSlowInInterpolator(); private static final float FULL_ROTATION = 1080.0f; @Retention(RetentionPolicy.SOURCE) @IntDef({LARGE, DEFAULT}) public @interface ProgressDrawableSize {} // Maps to ProgressBar.Large style static final int LARGE = 0; // Maps to ProgressBar default style static final int DEFAULT = 1; // Maps to ProgressBar default style private static final int CIRCLE_DIAMETER = 40; private static final float CENTER_RADIUS = 8.75f; //should add up to 10 when + stroke_width private static final float STROKE_WIDTH = 2.5f; // Maps to ProgressBar.Large style private static final int CIRCLE_DIAMETER_LARGE = 56; private static final float CENTER_RADIUS_LARGE = 12.5f; private static final float STROKE_WIDTH_LARGE = 3f; private static final int[] COLORS = new int[] { Color.BLACK }; /** * The value in the linear interpolator for animating the drawable at which * the color transition should start */ private static final float COLOR_START_DELAY_OFFSET = 0.75f; private static final float END_TRIM_START_DELAY_OFFSET = 0.5f; private static final float START_TRIM_DURATION_OFFSET = 0.5f; /** The duration of a single progress spin in milliseconds. */ private static final int ANIMATION_DURATION = 1332; /** The number of points in the progress "star". */ private static final float NUM_POINTS = 5f; /** The list of animators operating on this drawable. */ private final ArrayList<Animation> mAnimators = new ArrayList<Animation>(); /** The indicator ring, used to manage animation state. */ private final Ring mRing; /** Canvas rotation in degrees. */ private float mRotation; /** Layout info for the arrowhead in dp */ private static final int ARROW_WIDTH = 10; private static final int ARROW_HEIGHT = 5; private static final float ARROW_OFFSET_ANGLE = 5; /** Layout info for the arrowhead for the large spinner in dp */ private static final int ARROW_WIDTH_LARGE = 12; private static final int ARROW_HEIGHT_LARGE = 6; private static final float MAX_PROGRESS_ARC = .8f; private Resources mResources; private View mParent; private Animation mAnimation; float mRotationCount; private double mWidth; private double mHeight; boolean mFinishing; MaterialProgressDrawable(Context context, View parent) { mParent = parent; mResources = context.getResources(); mRing = new Ring(mCallback); mRing.setColors(COLORS); updateSizes(DEFAULT); setupAnimators(); } private void setSizeParameters(double progressCircleWidth, double progressCircleHeight, double centerRadius, double strokeWidth, float arrowWidth, float arrowHeight) { final Ring ring = mRing; final DisplayMetrics metrics = mResources.getDisplayMetrics(); final float screenDensity = metrics.density; mWidth = progressCircleWidth * screenDensity; mHeight = progressCircleHeight * screenDensity; ring.setStrokeWidth((float) strokeWidth * screenDensity); ring.setCenterRadius(centerRadius * screenDensity); ring.setColorIndex(0); ring.setArrowDimensions(arrowWidth * screenDensity, arrowHeight * screenDensity); ring.setInsets((int) mWidth, (int) mHeight); } public void updateSizes(@ProgressDrawableSize int size) { if (size == LARGE) { setSizeParameters(CIRCLE_DIAMETER_LARGE, CIRCLE_DIAMETER_LARGE, CENTER_RADIUS_LARGE, STROKE_WIDTH_LARGE, ARROW_WIDTH_LARGE, ARROW_HEIGHT_LARGE); } else { setSizeParameters(CIRCLE_DIAMETER, CIRCLE_DIAMETER, CENTER_RADIUS, STROKE_WIDTH, ARROW_WIDTH, ARROW_HEIGHT); } } /** * @param show Set to true to display the arrowhead on the progress spinner. */ public void showArrow(boolean show) { mRing.setShowArrow(show); } /** * @param scale Set the scale of the arrowhead for the spinner. */ public void setArrowScale(float scale) { mRing.setArrowScale(scale); } /** * Set the start and end trim for the progress spinner arc. * * @param startAngle start angle * @param endAngle end angle */ public void setStartEndTrim(float startAngle, float endAngle) { mRing.setStartTrim(startAngle); mRing.setEndTrim(endAngle); } /** * Set the amount of rotation to apply to the progress spinner. * * @param rotation Rotation is from [0..1] */ public void setProgressRotation(float rotation) { mRing.setRotation(rotation); } /** * Update the background color of the circle image view. */ public void setBackgroundColor(int color) { mRing.setBackgroundColor(color); } /** * 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. * * @param colors */ public void setColorSchemeColors(int... colors) { mRing.setColors(colors); mRing.setColorIndex(0); } @Override public int getIntrinsicHeight() { return (int) mHeight; } @Override public int getIntrinsicWidth() { return (int) mWidth; } @Override public void draw(Canvas c) { final Rect bounds = getBounds(); final int saveCount = c.save(); c.rotate(mRotation, bounds.exactCenterX(), bounds.exactCenterY()); mRing.draw(c, bounds); c.restoreToCount(saveCount); } @Override public void setAlpha(int alpha) { mRing.setAlpha(alpha); } public int getAlpha() { return mRing.getAlpha(); } @Override public void setColorFilter(ColorFilter colorFilter) { mRing.setColorFilter(colorFilter); } @SuppressWarnings("unused") void setRotation(float rotation) { mRotation = rotation; invalidateSelf(); } @SuppressWarnings("unused") private float getRotation() { return mRotation; } @Override public int getOpacity() { return PixelFormat.TRANSLUCENT; } @Override public boolean isRunning() { final ArrayList<Animation> animators = mAnimators; final int N = animators.size(); for (int i = 0; i < N; i++) { final Animation animator = animators.get(i); if (animator.hasStarted() && !animator.hasEnded()) { return true; } } return false; } @Override public void start() { mAnimation.reset(); mRing.storeOriginals(); // Already showing some part of the ring if (mRing.getEndTrim() != mRing.getStartTrim()) { mFinishing = true; mAnimation.setDuration(ANIMATION_DURATION / 2); mParent.startAnimation(mAnimation); } else { mRing.setColorIndex(0); mRing.resetOriginals(); mAnimation.setDuration(ANIMATION_DURATION); mParent.startAnimation(mAnimation); } } @Override public void stop() { mParent.clearAnimation(); setRotation(0); mRing.setShowArrow(false); mRing.setColorIndex(0); mRing.resetOriginals(); } float getMinProgressArc(Ring ring) { return (float) Math.toRadians( ring.getStrokeWidth() / (2 * Math.PI * ring.getCenterRadius())); } // Adapted from ArgbEvaluator.java private int evaluateColorChange(float fraction, int startValue, int endValue) { int startInt = (Integer) startValue; int startA = (startInt >> 24) & 0xff; int startR = (startInt >> 16) & 0xff; int startG = (startInt >> 8) & 0xff; int startB = startInt & 0xff; int endInt = (Integer) endValue; int endA = (endInt >> 24) & 0xff; int endR = (endInt >> 16) & 0xff; int endG = (endInt >> 8) & 0xff; int endB = endInt & 0xff; return (int) ((startA + (int) (fraction * (endA - startA))) << 24) | (int) ((startR + (int) (fraction * (endR - startR))) << 16) | (int) ((startG + (int) (fraction * (endG - startG))) << 8) | (int) ((startB + (int) (fraction * (endB - startB)))); } /** * Update the ring color if this is within the last 25% of the animation. * The new ring color will be a translation from the starting ring color to * the next color. */ void updateRingColor(float interpolatedTime, Ring ring) { if (interpolatedTime > COLOR_START_DELAY_OFFSET) { // scale the interpolatedTime so that the full // transformation from 0 - 1 takes place in the // remaining time ring.setColor(evaluateColorChange((interpolatedTime - COLOR_START_DELAY_OFFSET) / (1.0f - COLOR_START_DELAY_OFFSET), ring.getStartingColor(), ring.getNextColor())); } } void applyFinishTranslation(float interpolatedTime, Ring ring) { // shrink back down and complete a full rotation before // starting other circles // Rotation goes between [0..1]. updateRingColor(interpolatedTime, ring); float targetRotation = (float) (Math.floor(ring.getStartingRotation() / MAX_PROGRESS_ARC) + 1f); final float minProgressArc = getMinProgressArc(ring); final float startTrim = ring.getStartingStartTrim() + (ring.getStartingEndTrim() - minProgressArc - ring.getStartingStartTrim()) * interpolatedTime; ring.setStartTrim(startTrim); ring.setEndTrim(ring.getStartingEndTrim()); final float rotation = ring.getStartingRotation() + ((targetRotation - ring.getStartingRotation()) * interpolatedTime); ring.setRotation(rotation); } private void setupAnimators() { final Ring ring = mRing; final Animation animation = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { if (mFinishing) { applyFinishTranslation(interpolatedTime, ring); } else { // The minProgressArc is calculated from 0 to create an // angle that matches the stroke width. final float minProgressArc = getMinProgressArc(ring); final float startingEndTrim = ring.getStartingEndTrim(); final float startingTrim = ring.getStartingStartTrim(); final float startingRotation = ring.getStartingRotation(); updateRingColor(interpolatedTime, ring); // Moving the start trim only occurs in the first 50% of a // single ring animation if (interpolatedTime <= START_TRIM_DURATION_OFFSET) { // scale the interpolatedTime so that the full // transformation from 0 - 1 takes place in the // remaining time final float scaledTime = (interpolatedTime) / (1.0f - START_TRIM_DURATION_OFFSET); final float startTrim = startingTrim + ((MAX_PROGRESS_ARC - minProgressArc) * MATERIAL_INTERPOLATOR .getInterpolation(scaledTime)); ring.setStartTrim(startTrim); } // Moving the end trim starts after 50% of a single ring // animation completes if (interpolatedTime > END_TRIM_START_DELAY_OFFSET) { // scale the interpolatedTime so that the full // transformation from 0 - 1 takes place in the // remaining time final float minArc = MAX_PROGRESS_ARC - minProgressArc; float scaledTime = (interpolatedTime - START_TRIM_DURATION_OFFSET) / (1.0f - START_TRIM_DURATION_OFFSET); final float endTrim = startingEndTrim + (minArc * MATERIAL_INTERPOLATOR.getInterpolation(scaledTime)); ring.setEndTrim(endTrim); } final float rotation = startingRotation + (0.25f * interpolatedTime); ring.setRotation(rotation); float groupRotation = ((FULL_ROTATION / NUM_POINTS) * interpolatedTime) + (FULL_ROTATION * (mRotationCount / NUM_POINTS)); setRotation(groupRotation); } } }; animation.setRepeatCount(Animation.INFINITE); animation.setRepeatMode(Animation.RESTART); animation.setInterpolator(LINEAR_INTERPOLATOR); animation.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { mRotationCount = 0; } @Override public void onAnimationEnd(Animation animation) { // do nothing } @Override public void onAnimationRepeat(Animation animation) { ring.storeOriginals(); ring.goToNextColor(); ring.setStartTrim(ring.getEndTrim()); if (mFinishing) { // finished closing the last ring from the swipe gesture; go // into progress mode mFinishing = false; animation.setDuration(ANIMATION_DURATION); ring.setShowArrow(false); } else { mRotationCount = (mRotationCount + 1) % (NUM_POINTS); } } }); mAnimation = animation; } private final Callback mCallback = new Callback() { @Override public void invalidateDrawable(Drawable d) { invalidateSelf(); } @Override public void scheduleDrawable(Drawable d, Runnable what, long when) { scheduleSelf(what, when); } @Override public void unscheduleDrawable(Drawable d, Runnable what) { unscheduleSelf(what); } }; private static class Ring { private final RectF mTempBounds = new RectF(); private final Paint mPaint = new Paint(); private final Paint mArrowPaint = new Paint(); private final Callback mCallback; private float mStartTrim = 0.0f; private float mEndTrim = 0.0f; private float mRotation = 0.0f; private float mStrokeWidth = 5.0f; private float mStrokeInset = 2.5f; private int[] mColors; // mColorIndex represents the offset into the available mColors that the // progress circle should currently display. As the progress circle is // animating, the mColorIndex moves by one to the next available color. private int mColorIndex; private float mStartingStartTrim; private float mStartingEndTrim; private float mStartingRotation; private boolean mShowArrow; private Path mArrow; private float mArrowScale; private double mRingCenterRadius; private int mArrowWidth; private int mArrowHeight; private int mAlpha; private final Paint mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG); private int mBackgroundColor; private int mCurrentColor; Ring(Callback callback) { mCallback = callback; mPaint.setStrokeCap(Paint.Cap.SQUARE); mPaint.setAntiAlias(true); mPaint.setStyle(Paint.Style.STROKE); mArrowPaint.setStyle(Paint.Style.FILL); mArrowPaint.setAntiAlias(true); } public void setBackgroundColor(int color) { mBackgroundColor = color; } /** * Set the dimensions of the arrowhead. * * @param width Width of the hypotenuse of the arrow head * @param height Height of the arrow point */ public void setArrowDimensions(float width, float height) { mArrowWidth = (int) width; mArrowHeight = (int) height; } /** * Draw the progress spinner */ public void draw(Canvas c, Rect bounds) { final RectF arcBounds = mTempBounds; arcBounds.set(bounds); arcBounds.inset(mStrokeInset, mStrokeInset); final float startAngle = (mStartTrim + mRotation) * 360; final float endAngle = (mEndTrim + mRotation) * 360; float sweepAngle = endAngle - startAngle; mPaint.setColor(mCurrentColor); c.drawArc(arcBounds, startAngle, sweepAngle, false, mPaint); drawTriangle(c, startAngle, sweepAngle, bounds); if (mAlpha < 255) { mCirclePaint.setColor(mBackgroundColor); mCirclePaint.setAlpha(255 - mAlpha); c.drawCircle(bounds.exactCenterX(), bounds.exactCenterY(), bounds.width() / 2, mCirclePaint); } } private void drawTriangle(Canvas c, float startAngle, float sweepAngle, Rect bounds) { if (mShowArrow) { if (mArrow == null) { mArrow = new Path(); mArrow.setFillType(Path.FillType.EVEN_ODD); } else { mArrow.reset(); } // Adjust the position of the triangle so that it is inset as // much as the arc, but also centered on the arc. float inset = (int) mStrokeInset / 2 * mArrowScale; float x = (float) (mRingCenterRadius * Math.cos(0) + bounds.exactCenterX()); float y = (float) (mRingCenterRadius * Math.sin(0) + bounds.exactCenterY()); // Update the path each time. This works around an issue in SKIA // where concatenating a rotation matrix to a scale matrix // ignored a starting negative rotation. This appears to have // been fixed as of API 21. mArrow.moveTo(0, 0); mArrow.lineTo(mArrowWidth * mArrowScale, 0); mArrow.lineTo((mArrowWidth * mArrowScale / 2), (mArrowHeight * mArrowScale)); mArrow.offset(x - inset, y); mArrow.close(); // draw a triangle mArrowPaint.setColor(mCurrentColor); c.rotate(startAngle + sweepAngle - ARROW_OFFSET_ANGLE, bounds.exactCenterX(), bounds.exactCenterY()); c.drawPath(mArrow, mArrowPaint); } } /** * Set the colors the progress spinner alternates between. * * @param colors Array of integers describing the colors. Must be non-<code>null</code>. */ public void setColors(@NonNull int[] colors) { mColors = colors; // if colors are reset, make sure to reset the color index as well setColorIndex(0); } /** * Set the absolute color of the progress spinner. This is should only * be used when animating between current and next color when the * spinner is rotating. * * @param color int describing the color. */ public void setColor(int color) { mCurrentColor = color; } /** * @param index Index into the color array of the color to display in * the progress spinner. */ public void setColorIndex(int index) { mColorIndex = index; mCurrentColor = mColors[mColorIndex]; } /** * @return int describing the next color the progress spinner should use when drawing. */ public int getNextColor() { return mColors[getNextColorIndex()]; } private int getNextColorIndex() { return (mColorIndex + 1) % (mColors.length); } /** * Proceed to the next available ring color. This will automatically * wrap back to the beginning of colors. */ public void goToNextColor() { setColorIndex(getNextColorIndex()); } public void setColorFilter(ColorFilter filter) { mPaint.setColorFilter(filter); invalidateSelf(); } /** * @param alpha Set the alpha of the progress spinner and associated arrowhead. */ public void setAlpha(int alpha) { mAlpha = alpha; } /** * @return Current alpha of the progress spinner and arrowhead. */ public int getAlpha() { return mAlpha; } /** * @param strokeWidth Set the stroke width of the progress spinner in pixels. */ public void setStrokeWidth(float strokeWidth) { mStrokeWidth = strokeWidth; mPaint.setStrokeWidth(strokeWidth); invalidateSelf(); } @SuppressWarnings("unused") public float getStrokeWidth() { return mStrokeWidth; } @SuppressWarnings("unused") public void setStartTrim(float startTrim) { mStartTrim = startTrim; invalidateSelf(); } @SuppressWarnings("unused") public float getStartTrim() { return mStartTrim; } public float getStartingStartTrim() { return mStartingStartTrim; } public float getStartingEndTrim() { return mStartingEndTrim; } public int getStartingColor() { return mColors[mColorIndex]; } @SuppressWarnings("unused") public void setEndTrim(float endTrim) { mEndTrim = endTrim; invalidateSelf(); } @SuppressWarnings("unused") public float getEndTrim() { return mEndTrim; } @SuppressWarnings("unused") public void setRotation(float rotation) { mRotation = rotation; invalidateSelf(); } @SuppressWarnings("unused") public float getRotation() { return mRotation; } public void setInsets(int width, int height) { final float minEdge = (float) Math.min(width, height); float insets; if (mRingCenterRadius <= 0 || minEdge < 0) { insets = (float) Math.ceil(mStrokeWidth / 2.0f); } else { insets = (float) (minEdge / 2.0f - mRingCenterRadius); } mStrokeInset = insets; } @SuppressWarnings("unused") public float getInsets() { return mStrokeInset; } /** * @param centerRadius Inner radius in px of the circle the progress * spinner arc traces. */ public void setCenterRadius(double centerRadius) { mRingCenterRadius = centerRadius; } public double getCenterRadius() { return mRingCenterRadius; } /** * @param show Set to true to show the arrow head on the progress spinner. */ public void setShowArrow(boolean show) { if (mShowArrow != show) { mShowArrow = show; invalidateSelf(); } } /** * @param scale Set the scale of the arrowhead for the spinner. */ public void setArrowScale(float scale) { if (scale != mArrowScale) { mArrowScale = scale; invalidateSelf(); } } /** * @return The amount the progress spinner is currently rotated, between [0..1]. */ public float getStartingRotation() { return mStartingRotation; } /** * If the start / end trim are offset to begin with, store them so that * animation starts from that offset. */ public void storeOriginals() { mStartingStartTrim = mStartTrim; mStartingEndTrim = mEndTrim; mStartingRotation = mRotation; } /** * Reset the progress spinner to default rotation, start and end angles. */ public void resetOriginals() { mStartingStartTrim = 0; mStartingEndTrim = 0; mStartingRotation = 0; setStartTrim(0); setEndTrim(0); setRotation(0); } private void invalidateSelf() { mCallback.invalidateDrawable(null); } } }
Second Utility class is CircleImageView
In widget folder create a new class and paste below code inside that.
package com.wave.pagination.horizontalswiperefreshlayout.widget; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.RadialGradient; import android.graphics.Shader; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.OvalShape; import android.support.v4.view.ViewCompat; import android.support.v7.widget.AppCompatImageView; import android.view.animation.Animation; /** * Created on : May 04, 2019 * Author : AndroidWave */ public class CircleImageView extends AppCompatImageView { private static final int KEY_SHADOW_COLOR = 0x1E000000; private static final int FILL_SHADOW_COLOR = 0x3D000000; // PX private static final float X_OFFSET = 0f; private static final float Y_OFFSET = 1.75f; private static final float SHADOW_RADIUS = 3.5f; private static final int SHADOW_ELEVATION = 4; private Animation.AnimationListener mListener; int mShadowRadius; CircleImageView(Context context, int color) { super(context); final float density = getContext().getResources().getDisplayMetrics().density; final int shadowYOffset = (int) (density * Y_OFFSET); final int shadowXOffset = (int) (density * X_OFFSET); mShadowRadius = (int) (density * SHADOW_RADIUS); ShapeDrawable circle; if (elevationSupported()) { circle = new ShapeDrawable(new OvalShape()); ViewCompat.setElevation(this, SHADOW_ELEVATION * density); } else { OvalShape oval = new OvalShadow(mShadowRadius); circle = new ShapeDrawable(oval); ViewCompat.setLayerType(this, ViewCompat.LAYER_TYPE_SOFTWARE, circle.getPaint()); circle.getPaint().setShadowLayer(mShadowRadius, shadowXOffset, shadowYOffset, KEY_SHADOW_COLOR); final int padding = mShadowRadius; // set padding so the inner image sits correctly within the shadow. setPadding(padding, padding, padding, padding); } circle.getPaint().setColor(color); setBackgroundDrawable(circle); } private boolean elevationSupported() { return android.os.Build.VERSION.SDK_INT >= 21; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (!elevationSupported()) { setMeasuredDimension(getMeasuredWidth() + mShadowRadius * 2, getMeasuredHeight() + mShadowRadius * 2); } } public void setAnimationListener(Animation.AnimationListener listener) { mListener = listener; } @Override public void onAnimationStart() { super.onAnimationStart(); if (mListener != null) { mListener.onAnimationStart(getAnimation()); } } @Override public void onAnimationEnd() { super.onAnimationEnd(); if (mListener != null) { mListener.onAnimationEnd(getAnimation()); } } /** * Update the background color of the circle image view. * * @param colorRes Id of a color resource. */ public void setBackgroundColorRes(int colorRes) { setBackgroundColor(getContext().getResources().getColor(colorRes)); } @Override public void setBackgroundColor(int color) { if (getBackground() instanceof ShapeDrawable) { ((ShapeDrawable) getBackground()).getPaint().setColor(color); } } private class OvalShadow extends OvalShape { private RadialGradient mRadialGradient; private Paint mShadowPaint; OvalShadow(int shadowRadius) { super(); mShadowPaint = new Paint(); mShadowRadius = shadowRadius; updateRadialGradient((int) rect().width()); } @Override protected void onResize(float width, float height) { super.onResize(width, height); updateRadialGradient((int) width); } @Override public void draw(Canvas canvas, Paint paint) { final int viewWidth = CircleImageView.this.getWidth(); final int viewHeight = CircleImageView.this.getHeight(); canvas.drawCircle(viewWidth / 2, viewHeight / 2, viewWidth / 2, mShadowPaint); canvas.drawCircle(viewWidth / 2, viewHeight / 2, viewWidth / 2 - mShadowRadius, paint); } private void updateRadialGradient(int diameter) { mRadialGradient = new RadialGradient(diameter / 2, diameter / 2, mShadowRadius, new int[]{FILL_SHADOW_COLOR, Color.TRANSPARENT}, null, Shader.TileMode.CLAMP); mShadowPaint.setShader(mRadialGradient); } } }
Now your widget is ready to use. Now I will tell you how to use it
How to use HorizontalSwipeRefreshLayout
Now I’m going to explain how to use this widget. For doing that you have to follow these step.
- Setup Project and add dependency
- Create list item pojo class
- Prepare RecyclerView Adapter
- Create layout for list item
- In MainActivity, use HorizontalSwipeRefreshLayout with RecyclerView
1. Setup Project and add dependency
Open app/build.gradle and add below dependency
implementation 'com.android.support:recyclerview-v7:28.0.0' implementation 'com.android.support:design:28.0.0' implementation 'com.google.code.gson:gson:2.8.5' implementation 'com.github.bumptech.glide:glide:4.9.0' annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0' implementation 'com.android.support:cardview-v7:28.0.0'
2. Create list item POJO class
Create a model class which have getter setter named is ListItem.
package com.wave.pagination.horizontalswiperefreshlayout; import com.google.gson.annotations.SerializedName; public class ListItem { @SerializedName("description") private String mDescription; @SerializedName("title") private String mTitle; public ListItem(String mDescription, String mTitle) { this.mDescription = mDescription; this.mTitle = mTitle; } public String getDescription() { return mDescription; } public void setDescription(String description) { mDescription = description; } public String getTitle() { return mTitle; } public void setTitle(String title) { mTitle = title; } }
3. Prepare RecyclerView Adapter
As per previous tutorial, I’m creating just RecyclerView Adapter. For do that create a class named RecyclerViewAdapter and paste below code. For BaseViewHolder goes here.
package com.wave.pagination.horizontalswiperefreshlayout; import android.support.annotation.NonNull; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import java.util.List; /** * Created on : May 04, 2019 * Author : AndroidWave */ public class RecyclerViewAdapter extends RecyclerView.Adapter<BaseViewHolder> { private List<ListItem> mListItems; public RecyclerViewAdapter(List<ListItem> postItems) { this.mListItems = postItems; } @Override public BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { return new ViewHolder( LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_layout, parent, false)); } @Override public void onBindViewHolder(@NonNull BaseViewHolder holder, int position) { holder.onBind(position); } @Override public int getItemViewType(int position) { return 0; } @Override public int getItemCount() { return mListItems.size(); } public void addItems(List<ListItem> listItems) { mListItems = listItems; notifyDataSetChanged(); } public class ViewHolder extends BaseViewHolder { TextView textViewTitle; TextView textViewDescription; ViewHolder(View itemView) { super(itemView); textViewTitle = itemView.findViewById(R.id.tvTitle); textViewDescription = itemView.findViewById(R.id.tvInfo); } protected void clear() { } public void onBind(int position) { super.onBind(position); ListItem item = mListItems.get(position); textViewTitle.setText(item.getTitle()); textViewDescription.setText(item.getDescription()); } } }
List item layout file
Create a new XML file inside layout directory and paste below code
<?xml version="1.0" encoding="utf-8"?> <android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:card_view="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center" android:layout_margin="32dp" android:background="#AA000000" card_view:cardCornerRadius="10dp"> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/tvTitle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginEnd="8dp" android:gravity="center" android:text="Baseball" android:textColor="#161616" android:textSize="18sp" app:layout_constraintBottom_toTopOf="@+id/tvInfo" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_chainStyle="packed" /> <TextView android:id="@+id/tvInfo" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:layout_marginEnd="8dp" android:gravity="center" android:text="Nike Enters 10-Year Partnership With Major League Baseball!" android:textSize="16sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/tvTitle" /> </android.support.constraint.ConstraintLayout> </android.support.v7.widget.CardView>
Finally open the MainActivity and do below operation
package com.wave.pagination.horizontalswiperefreshlayout; import android.os.Bundle; import android.os.Handler; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.DefaultItemAnimator; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import com.wave.pagination.horizontalswiperefreshlayout.widget.HorizontalSwipeRefreshLayout; import java.util.ArrayList; public class MainActivity extends AppCompatActivity { HorizontalSwipeRefreshLayout mSwipeRefreshLayout; RecyclerView mRecyclerView; LinearLayoutManager mLayoutManager; RecyclerViewAdapter mAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mSwipeRefreshLayout = findViewById(R.id.mHorizontalSwipeRefreshLayout); mRecyclerView = findViewById(R.id.mRecyclerView); setUp(); mSwipeRefreshLayout.setOnRefreshListener(new HorizontalSwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh() { prepareDemoContent(); } }); } private void setUp() { mLayoutManager = new LinearLayoutManager(this); mLayoutManager.setOrientation(RecyclerView.HORIZONTAL); mRecyclerView.setLayoutManager(mLayoutManager); mRecyclerView.setItemAnimator(new DefaultItemAnimator()); mAdapter = new RecyclerViewAdapter(new ArrayList<ListItem>()); /* * Prepare demo content */ prepareDemoContent(); } private void prepareDemoContent() { mSwipeRefreshLayout.setRefreshing(true); new Handler().postDelayed(new Runnable() { @Override public void run() { mSwipeRefreshLayout.setRefreshing(false); ArrayList<ListItem> mSports = new ArrayList<>(); String[] titleList = getResources().getStringArray(R.array.title_array); String[] infoList = getResources().getStringArray(R.array.info_array); for (int i = 0; i < titleList.length; i++) { mSports.add(new ListItem(infoList[i], titleList[i])); } mAdapter.addItems(mSports); mRecyclerView.setAdapter(mAdapter); } }, 2000); } }
You almost finished. That’s all for Horizontal SwipeRefreshLayout in Android Tutorial. I hope it’s helpful for you, then help me by sharing this post with all your friends who learning android app development.
Get Solution Code
Still having troubles? you can leave your comments here. Thank You 🙂