Java tutorial
/* * Copyright (C) 2015 baitouwei. * * 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.baitouwei.swiperefresh; import android.animation.Animator; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.content.Context; import android.support.v4.view.MotionEventCompat; import android.support.v4.view.ViewCompat; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.StaggeredGridLayoutManager; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; import android.widget.AbsListView; import android.widget.FrameLayout; import com.baitouwei.swiperefresh.internal.DefaultSwipeView; import com.baitouwei.swiperefresh.internal.LayoutDirection; import com.baitouwei.swiperefresh.internal.LayoutLayer; import com.baitouwei.swiperefresh.internal.OffsetOrientation; import com.baitouwei.swiperefresh.internal.SwipeStatus; import com.baitouwei.swiperefresh.internal.SwipeView; import com.baitouwei.swiperefresh.utils.SwipeRefreshHelp; import com.baitouwei.swiperefresh.utils.Utils; import com.baitouwei.swiperefresh.utils.ValueAnimatorCompat; import java.math.BigDecimal; import java.math.RoundingMode; /** * ASwipeRefreshLayout(Android SwipeRefreshLayout), * A simple and flexible layout support swipe down refresh and swipe up load more. * Features: * <ul> * <li>1.Easy to custom header or footer</li> * <li>2.Easy to custom the way header or footer appear</li> * <li>3.Support auto refresh and auto load more(auto load more only work for AbsListView or RecyclerView)</li> * </ul> * * @author baitouwei */ public class ASwipeRefreshLayout extends FrameLayout { private static final String TAG = ASwipeRefreshLayout.class.getSimpleName(); /////////////////////////////////////////////////////////////////////////// // params can configurate by user /////////////////////////////////////////////////////////////////////////// /** * interpolator user for header animate */ private Interpolator headerInterpolator; /** * interpolator user for footer animate */ private Interpolator footerInterpolator; /** * {@link ASwipeRefreshLayout} */ private SwipeLayoutConfigListener swipeLayoutConfigListener; /** * damp rate for swipe down, * range:[0,1],The number more smaller,the drag more different */ private static final float swipeDownDampRate = .5f; /** * damp rate for swipe up, * range:[0,1],The number more smaller,the drag more different */ private static final float swipeUpDampRate = .5f; /** * if true,Content will auto scroll to {@link #lastVisiblePos},only work if content is AbsListView or RecyclerView */ private boolean autoScrollAfterLoadMore = true; //swipe down /** * {@link SwipeView.Config#offsetRange} for swipe down */ private int contentViewSwipeDownOffsetRange; /** * {@link SwipeView.Config#startOffset} for swipe down */ private int contentViewSwipeDownStartOffset; /** * {@link SwipeView.Config#endOffset} for swipe down */ private int contentViewSwipeDownEndOffset; /** * {@link SwipeView.Config#parallaxFactor} for swipe down */ private float parallaxFactorOfContentSwipeDown = 1f; /** * is dampable for swipe down */ private boolean dampAbleOfSwipeDown = false; private SwipeView headerView; /** * swipe down refresh listener.If null,the layout can not perform swipe down refresh */ private OnSwipeRefreshListener swipeDownRefreshListener; /** * whether auto perform swipe down refresh the layout show */ private boolean isAutoSwipeDownRefresh = false; //swipe up /** * {@link SwipeView.Config#offsetRange} for swipe up */ private int contentViewSwipeUpOffsetRange; /** * {@link SwipeView.Config#startOffset} for swipe up */ private int contentViewSwipeUpStartOffset; /** * {@link SwipeView.Config#endOffset} for swipe up */ private int contentViewSwipeUpEndOffset; /** * {@link SwipeView.Config#parallaxFactor} for swipe up */ private float parallaxFactorOfContentSwipeUp = 1; /** * is dampable for swipe up */ private boolean dampAbleOfSwipeUp = false; private SwipeView footerView; /** * swipe up refresh listener.If null,the layout can not perform swipe up refresh */ private OnSwipeRefreshListener swipeUpRefreshListener; /** * whether auto perform swipe up refresh the layout show */ private boolean isAutoLoadMore = false; /** * whether can perform swipe up refresh the layout show.If false it can't swipe up. * eg:You can set if true after load more from api and find date have not more */ private boolean canLoadMore = true; /** * skip {@link #loadMoreSkipNum} item to perform swipe up refresh * only work if {@link #contentView} is {@link AbsListView} or {@link RecyclerView} */ private int loadMoreSkipNum = 0; /** * only use when content view is AbsListView */ private AbsListView.OnScrollListener onScrollListener; /////////////////////////////////////////////////////////////////////////// // params user inner /////////////////////////////////////////////////////////////////////////// private static final int INVALID_POINTER = -1; private boolean isDealAutoLoadMoreListenerDone = false; /** * true:current is swipe down;false:current is swipe up */ private boolean isSwipeDown = true; private boolean isDragging = false; /** * the last visible position when begin load more,default is {@linkplain Utils#INVALID_POSITIONS} */ private int lastVisiblePos = Utils.INVALID_POSITIONS; /** * {@link SwipeView.Config#currentOffset} */ private int currentContentOffset; private View contentView; private ValueAnimator animatorOfContent; /** * drag offset percent,equal to dragOffset/{@link #contentViewSwipeDownOffsetRange} or dragOffset/{@link #contentViewSwipeUpOffsetRange} */ private float dragPercent = 0; /** * */ private float offsetPercent = 0; private int touchSlop; private int activePointerId = INVALID_POINTER; private float initialMotionY; //Header /** * whether have been swiped down refresh */ private boolean autoRefreshed = false; private long durationOfSwipeDown = 1400; private ValueAnimator animatorOfHeader; //Footer private long durationOfSwipeUp = 1400; private ValueAnimator animatorOfFooter; public ASwipeRefreshLayout(Context context) { super(context); init(); } public ASwipeRefreshLayout(Context context, AttributeSet attrs) { super(context, attrs); init(); } public ASwipeRefreshLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { if (headerInterpolator == null) { headerInterpolator = new DecelerateInterpolator(); } if (footerInterpolator == null) { footerInterpolator = new DecelerateInterpolator(); } touchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); if (headerView == null || footerView == null) { headerView = new DefaultSwipeView(getContext()); footerView = new DefaultSwipeView(getContext()); SwipeRefreshHelp.buildDrawerSwipeRefreshLayout(this); } addView(headerView); addView(footerView); headerView.setVisibility(INVISIBLE); footerView.setVisibility(INVISIBLE); setWillNotDraw(true); ViewCompat.setChildrenDrawingOrderEnabled(this, true); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); pauseAnimate(); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (ensureTarget()) { int pWidth = getMeasuredWidth(); int pHeight = getMeasuredHeight(); int pPaddingL = getPaddingLeft(); int pPaddingT = getPaddingTop(); int pPaddingR = getPaddingRight(); int pPaddingB = getPaddingBottom(); int l, t, r, b; //header LayoutDirection layoutModeOfHeader = headerView.getSwipeLayoutDirection(); MarginLayoutParams headerParams = (MarginLayoutParams) headerView.getLayoutParams(); //default LayoutDirection is CENTER l = pPaddingL + headerParams.leftMargin; t = pPaddingT + headerParams.topMargin; r = l + headerView.getMeasuredWidth() - pPaddingR - headerParams.rightMargin; b = t + headerView.getMeasuredHeight() - pPaddingB - headerParams.bottomMargin; if (headerView.getOffsetOrientation() == OffsetOrientation.VERTICAL) { t += headerView.getCurrentOffset(); b += headerView.getCurrentOffset(); } else { l += headerView.getCurrentOffset(); r += headerView.getCurrentOffset(); } if (layoutModeOfHeader == LayoutDirection.CENTER_OFFSET) { t = t - headerView.getMeasuredHeight(); b = b - headerView.getMeasuredHeight(); } else if (layoutModeOfHeader == LayoutDirection.LEFT) { l = l - headerView.getMeasuredWidth(); r = r - headerView.getMeasuredWidth(); } else if (layoutModeOfHeader == LayoutDirection.RIGHT) { l = l + pWidth; r = r + pWidth; } headerView.layout(l, t, r, b); //if view is out of content,we need to force draw it headerView.postInvalidate(); //footer LayoutDirection layoutModeOfFooter = footerView.getSwipeLayoutDirection(); MarginLayoutParams footerParams = (MarginLayoutParams) footerView.getLayoutParams(); //default LayoutDirection is CENTER l = pPaddingL + footerParams.leftMargin; t = pHeight - footerView.getMeasuredHeight() + footerParams.topMargin; r = l + footerView.getMeasuredWidth() - pPaddingR - footerParams.rightMargin; b = t + footerView.getMeasuredHeight() - pPaddingB - footerParams.bottomMargin; if (footerView.getOffsetOrientation() == OffsetOrientation.VERTICAL) { t += footerView.getCurrentOffset(); b += footerView.getCurrentOffset(); } else { l += footerView.getCurrentOffset(); r += footerView.getCurrentOffset(); } if (layoutModeOfFooter == LayoutDirection.CENTER_OFFSET) { t = t + footerView.getMeasuredHeight(); b = b + footerView.getMeasuredHeight(); } else if (layoutModeOfFooter == LayoutDirection.LEFT) { l = l - footerView.getMeasuredWidth(); r = r - footerView.getMeasuredWidth(); } else if (layoutModeOfFooter == LayoutDirection.RIGHT) { l = l + pWidth; r = r + pWidth; } //if view is out of content,we need to force draw it footerView.layout(l, t, r, b); footerView.postInvalidate(); int currentContentOffset = getCurrentContentOffset(); contentView.layout(pPaddingL, pPaddingT + currentContentOffset, pPaddingL + contentView.getMeasuredWidth() - pPaddingR, pPaddingT + currentContentOffset + pHeight - pPaddingB); dealAutoRefresh(); if (swipeLayoutConfigListener != null) { swipeLayoutConfigListener.onLayout(this, left, top, right, bottom); swipeLayoutConfigListener.setUpOffset(this); } addAutoLoadMoreListener(); resumeAnimate(); } } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (!isEnabled() || (canChildScrollUp() && canChildScrollDown()) || footerView.getSwipeStatus() == SwipeStatus.REFRESHING || headerView.getSwipeStatus() == SwipeStatus.REFRESHING || (swipeDownRefreshListener == null && swipeUpRefreshListener == null) || isAnimating()) { return false; } final int action = MotionEventCompat.getActionMasked(ev); switch (action) { case MotionEvent.ACTION_DOWN: isDragging = false; //reset currentContentOffset = 0; headerView.setCurrentOffset(0); footerView.setCurrentOffset(0); headerView.updateStatus(SwipeStatus.NORMAL); footerView.updateStatus(SwipeStatus.NORMAL); activePointerId = MotionEventCompat.getPointerId(ev, ev.getActionIndex()); initialMotionY = getMotionEventY(ev, activePointerId); break; case MotionEvent.ACTION_MOVE: if (activePointerId == INVALID_POINTER) { return false; } final float diffY = getMotionEventY(ev, activePointerId) - initialMotionY; //deal border if ((diffY > 0 && !canChildScrollUp() || (diffY < 0 && !canChildScrollDown()))) { if (Math.abs(diffY) > touchSlop && !isDragging) { isDragging = true; } } break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: isDragging = false; activePointerId = INVALID_POINTER; break; } return isDragging; } /** * if we ensure current is swipe down or swipe up */ private boolean isSwipeStatusEnsure = false; @Override public boolean onTouchEvent(MotionEvent ev) { if (!isDragging) { return super.onTouchEvent(ev); } final int action = MotionEventCompat.getActionMasked(ev); switch (action) { case MotionEvent.ACTION_MOVE: final float y = getMotionEventY(ev, activePointerId); final float dragOffset = (int) (y - initialMotionY); if (isSwipeStatusEnsure) { if ((dragOffset < 0 && isSwipeDown == true) || (dragOffset > 0 && isSwipeDown == false)) { return false; } } if (dragOffset > 0) {//SwipeDown,PS:dragPercent and dragOffset is positive if (headerView.getVisibility() != VISIBLE) { headerView.setVisibility(VISIBLE); } isSwipeDown = true; dragPercent = dragOffset / (contentViewSwipeDownOffsetRange); if (dampAbleOfSwipeDown) { int targetY = dampCompute(Math.abs(dragOffset * parallaxFactorOfContentSwipeDown), contentViewSwipeDownOffsetRange, swipeDownDampRate); offsetContent(targetY - currentContentOffset, 1); } else { if (dragOffset < contentViewSwipeDownOffsetRange) { offsetContent((int) dragOffset - currentContentOffset, parallaxFactorOfContentSwipeDown); } } offsetHeader((int) (dragPercent * headerView.getOffsetRange() - headerView.getCurrentOffset())); if (swipeLayoutConfigListener.isReadyToRefresh(this, dragOffset)) { headerView.updateStatus(SwipeStatus.READY); } else { headerView.updateStatus(SwipeStatus.NORMAL); } headerView.updatePercent(dragPercent); } else {//SwipeUp,PS:dragPercent and dragOffset is negative if (footerView.getVisibility() != VISIBLE) { footerView.setVisibility(VISIBLE); } isSwipeDown = false; dragPercent = dragOffset / (contentViewSwipeUpOffsetRange); if (dampAbleOfSwipeUp) { int targetY = dampCompute(Math.abs(dragOffset * parallaxFactorOfContentSwipeUp), Math.abs(contentViewSwipeUpOffsetRange), swipeUpDampRate); offsetContent(-targetY - currentContentOffset, 1); } else { if (dragOffset > contentViewSwipeUpOffsetRange) { offsetContent((int) dragOffset - currentContentOffset, parallaxFactorOfContentSwipeUp); } } offsetFooter((int) (dragPercent * footerView.getOffsetRange() - footerView.getCurrentOffset())); if (swipeLayoutConfigListener.isReadyToLoadMore(this, dragOffset)) { footerView.updateStatus(SwipeStatus.READY); } else { footerView.updateStatus(SwipeStatus.NORMAL); } footerView.updatePercent(dragPercent); } if (isSwipeStatusEnsure == false) { isSwipeStatusEnsure = true; } break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: if (isSwipeDown) { if (headerView.getSwipeStatus() == SwipeStatus.READY) { refreshStart(); } else { animateContentOffsetToPos(true, true, new OnAnimateContentOffsetListener() { @Override public void onOffsetEnd() { headerView.updateStatus(SwipeStatus.NORMAL); } }); animateHeaderOffsetToPos(true, new OnAnimateContentOffsetListener() { @Override public void onOffsetEnd() { headerView.updateStatus(SwipeStatus.NORMAL); } }); } } else { if (footerView.getSwipeStatus() == SwipeStatus.READY) { loadMoreStart(); } else { animateContentOffsetToPos(true, false, new OnAnimateContentOffsetListener() { @Override public void onOffsetEnd() { footerView.updateStatus(SwipeStatus.NORMAL); } }); animateFooterOffsetToPos(true, new OnAnimateContentOffsetListener() { @Override public void onOffsetEnd() { footerView.updateStatus(SwipeStatus.NORMAL); } }); } } isDragging = false; activePointerId = INVALID_POINTER; isSwipeStatusEnsure = false; break; } return true; } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); swipeDownRefreshListener = null; swipeUpRefreshListener = null; } /** * compute damp * * @param yDiff Y axis offset * @param MAX max offset * @param dampRate damp rate,range:[0,1],The number more smaller,the drag more different * @return target Y axis */ private int dampCompute(float yDiff, int MAX, float dampRate) { final float scrollTop = yDiff * dampRate; dragPercent = scrollTop / MAX; float boundedDragPercent = Math.min(1f, (dragPercent)); float extraOS = Math.abs(scrollTop) - MAX; float slingshotDist = MAX; 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;//result is 1/4 * MAX int targetY = (int) ((slingshotDist * boundedDragPercent) + extraMove); return targetY; } public void setLayerOfHeader(LayoutLayer layerOfHeader) { headerView.setLayoutLayer(layerOfHeader); } public void setLayerOfFooter(LayoutLayer layerOfFooter) { footerView.setLayoutLayer(layerOfFooter); } //Animate /** * animate content view specific location * * @param isToStart true:move to the start location,false:move to the end location * @param isSwipeDown true:current is swipe down,false:current is swipe up * @param listener {@link OnAnimateContentOffsetListener} */ private void animateContentOffsetToPos(final boolean isToStart, final boolean isSwipeDown, final OnAnimateContentOffsetListener listener) { if (animatorOfContent != null && animatorOfContent.isRunning()) { animatorOfContent.cancel(); } if (isToStart) { if (isSwipeDown) { animatorOfContent = ObjectAnimator .ofInt((int) currentContentOffset, contentViewSwipeDownStartOffset) .setDuration(durationOfSwipeDown); animatorOfContent.setInterpolator(headerInterpolator); } else { animatorOfContent = ObjectAnimator.ofInt((int) currentContentOffset, contentViewSwipeUpStartOffset) .setDuration(durationOfSwipeUp); animatorOfContent.setInterpolator(footerInterpolator); } } else { if (isSwipeDown) { animatorOfContent = ObjectAnimator.ofInt((int) currentContentOffset, contentViewSwipeDownEndOffset) .setDuration(durationOfSwipeDown); animatorOfContent.setInterpolator(headerInterpolator); } else { animatorOfContent = ObjectAnimator.ofInt((int) currentContentOffset, contentViewSwipeUpEndOffset) .setDuration(durationOfSwipeUp); animatorOfContent.setInterpolator(footerInterpolator); } } final float currentPercent = headerView.getPercent(); animatorOfContent.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { if (isSwipeDown) { offsetContent((int) animation.getAnimatedValue() - currentContentOffset, parallaxFactorOfContentSwipeDown); } else { offsetContent((int) animation.getAnimatedValue() - currentContentOffset, parallaxFactorOfContentSwipeUp); } } }); animatorOfContent.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { if (listener != null) { listener.onOffsetEnd(); } } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }); animatorOfContent.start(); } /** * animate header view specific location * * @param isToStart true:move to the start location,false:move to the end location * @param listener {@link OnAnimateContentOffsetListener} */ private void animateHeaderOffsetToPos(final boolean isToStart, final OnAnimateContentOffsetListener listener) { if (animatorOfHeader != null && animatorOfHeader.isRunning()) { animatorOfHeader.cancel(); } if (headerView.getVisibility() != VISIBLE) { headerView.setVisibility(VISIBLE); } if (isToStart) { animatorOfHeader = ObjectAnimator.ofInt(headerView.getCurrentOffset(), headerView.getStartOffset()) .setDuration(durationOfSwipeDown); } else { animatorOfHeader = ObjectAnimator.ofInt(headerView.getCurrentOffset(), headerView.getEndOffset()) .setDuration(durationOfSwipeDown); } final float currentPercent = headerView.getPercent(); animatorOfHeader.setInterpolator(headerInterpolator); animatorOfHeader.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { offsetHeader((int) animation.getAnimatedValue() - headerView.getCurrentOffset()); computeOffsetPercent(currentPercent, isToStart, animation.getAnimatedFraction()); } }); animatorOfHeader.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { if (listener != null) { listener.onOffsetEnd(); } if (isToStart) { headerView.setVisibility(INVISIBLE); footerView.setVisibility(INVISIBLE); } else { if (!headerView.isRunning()) { headerView.start(); } } } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }); animatorOfHeader.start(); } /** * animate footer view specific location * * @param isToStart true:move to the start location,false:move to the end location * @param listener {@link OnAnimateContentOffsetListener} */ private void animateFooterOffsetToPos(final boolean isToStart, final OnAnimateContentOffsetListener listener) { if (animatorOfFooter != null && animatorOfFooter.isRunning()) { animatorOfFooter.cancel(); } if (footerView.getVisibility() != VISIBLE) { footerView.setVisibility(VISIBLE); } if (isToStart) { animatorOfFooter = ObjectAnimator.ofInt(footerView.getCurrentOffset(), footerView.getStartOffset()) .setDuration(durationOfSwipeUp); } else { animatorOfFooter = ObjectAnimator.ofInt(footerView.getCurrentOffset(), footerView.getEndOffset()) .setDuration(durationOfSwipeUp); } animatorOfFooter.setInterpolator(footerInterpolator); final float currentPercent = footerView.getPercent(); animatorOfFooter.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { offsetFooter((int) animation.getAnimatedValue() - footerView.getCurrentOffset()); computeOffsetPercent(currentPercent, isToStart, animation.getAnimatedFraction()); } }); animatorOfFooter.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { if (listener != null) { listener.onOffsetEnd(); } if (isToStart) { headerView.setVisibility(INVISIBLE); footerView.setVisibility(INVISIBLE); } else { if (!footerView.isRunning()) { footerView.start(); } } } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }); animatorOfFooter.start(); } private boolean isAnimating() { if ((animatorOfContent != null && animatorOfContent.isRunning()) || (animatorOfHeader != null && animatorOfHeader.isRunning()) || (animatorOfFooter != null && animatorOfFooter.isRunning())) { return true; } else { return false; } } private void pauseAnimate() { if (animatorOfContent != null) { ValueAnimatorCompat.pause(animatorOfContent); } if (animatorOfHeader != null) { ValueAnimatorCompat.pause(animatorOfHeader); } if (animatorOfFooter != null) { ValueAnimatorCompat.pause(animatorOfFooter); } } private void resumeAnimate() { if (animatorOfContent != null) { ValueAnimatorCompat.resume(animatorOfContent); } if (animatorOfHeader != null) { ValueAnimatorCompat.resume(animatorOfHeader); } if (animatorOfFooter != null) { ValueAnimatorCompat.resume(animatorOfFooter); } } private void offsetContent(int offset, float parallaxFactor) { offset *= parallaxFactor; ViewCompat.offsetTopAndBottom(contentView, offset); currentContentOffset += offset; } private void offsetHeader(int offset) { offset = (int) (offset * headerView.getParallaxFactor()); headerView.setCurrentOffset(offset); } private void offsetFooter(int offset) { offset = (int) (offset * footerView.getParallaxFactor()); footerView.setCurrentOffset(offset); } //Utils private float getMotionEventY(MotionEvent ev, int activePointerId) { final int index = MotionEventCompat.findPointerIndex(ev, activePointerId); if (index < 0) { return -1; } return MotionEventCompat.getY(ev, index); } public boolean canChildScrollUp() { if (android.os.Build.VERSION.SDK_INT < 14) { if (contentView instanceof AbsListView) { final AbsListView absListView = (AbsListView) contentView; return absListView.getChildCount() > 0 && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0).getTop() < absListView.getPaddingTop()); } else { return contentView.getScrollY() > 0; } } else { return ViewCompat.canScrollVertically(contentView, -1); } } public boolean canChildScrollDown() { return ViewCompat.canScrollVertically(contentView, 1); } private boolean ensureTarget() { if (contentView != null) { return true; } if (getChildCount() == 3) { for (int i = 0; i < getChildCount(); i++) { View childView = getChildAt(i); if (childView instanceof SwipeView || childView instanceof SwipeView) { continue; } else { contentView = childView; } } LayoutLayer layerOfHeader = headerView.getLayoutLayer(); LayoutLayer layerOfFooter = footerView.getLayoutLayer(); if (contentView != null) { if (layerOfHeader == LayoutLayer.ABOVE && layerOfFooter == LayoutLayer.ABOVE) { bringChildToFront(headerView); bringChildToFront(footerView); } else if (layerOfHeader == LayoutLayer.BOTTOM && layerOfFooter == LayoutLayer.BOTTOM) { bringChildToFront(contentView); } else if (layerOfHeader == LayoutLayer.ABOVE && layerOfFooter == LayoutLayer.BOTTOM) { bringChildToFront(contentView); bringChildToFront(headerView); } else if (layerOfHeader == LayoutLayer.BOTTOM && layerOfFooter == LayoutLayer.ABOVE) { bringChildToFront(contentView); bringChildToFront(footerView); } return true; } } return false; } /** * where we can swipe down refresh * * @return */ private boolean canSwipeDownRefresh() { if (ViewCompat.isAttachedToWindow(this) && (headerView.getSwipeStatus() == SwipeStatus.READY || headerView.getSwipeStatus() == SwipeStatus.NORMAL) && swipeDownRefreshListener != null) { if (!canChildScrollDown() && !canChildScrollUp()) { return true; } else if (!canChildScrollUp()) { return true; } } return false; } /** * where we can swipe up refresh * * @return */ private boolean canSwipeUpRefresh() { if (ViewCompat.isAttachedToWindow(this) && (footerView.getSwipeStatus() == SwipeStatus.READY || footerView.getSwipeStatus() == SwipeStatus.NORMAL) && canLoadMore && swipeUpRefreshListener != null) { if (!canChildScrollDown() && !canChildScrollUp()) { return true; } else if (loadMoreSkipNum > 0) { if (contentView instanceof AbsListView) { if (((AbsListView) contentView) .getLastVisiblePosition() >= ((AbsListView) contentView).getAdapter().getCount() - loadMoreSkipNum - 1) { return true; } } else if (contentView instanceof RecyclerView) { RecyclerView recyclerView = (RecyclerView) contentView; RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); if (layoutManager instanceof LinearLayoutManager) { if (((LinearLayoutManager) layoutManager) .findLastVisibleItemPosition() >= recyclerView.getAdapter().getItemCount() - loadMoreSkipNum - 1) { return true; } } else if (layoutManager instanceof StaggeredGridLayoutManager) { StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager) layoutManager; if (staggeredGridLayoutManager.getOrientation() == StaggeredGridLayoutManager.VERTICAL) { int[] lastVisiblePos = staggeredGridLayoutManager.findLastVisibleItemPositions(null); int spanNum = staggeredGridLayoutManager.getSpanCount(); if (lastVisiblePos[lastVisiblePos.length - 1] >= recyclerView.getAdapter() .getItemCount() - loadMoreSkipNum - 1) { return true; } } } } } else if (!canChildScrollDown()) { return true; } } return false; } /** * compute offset percent when animating * * @param currentPercent * @param isToStart * @param animatedFraction */ private void computeOffsetPercent(float currentPercent, boolean isToStart, float animatedFraction) { float percent = currentPercent; if (isSwipeDown) { if (isToStart) { percent = new BigDecimal(0).subtract(new BigDecimal(percent)) .multiply(new BigDecimal(animatedFraction)).add(new BigDecimal(percent)) .setScale(3, RoundingMode.HALF_UP).floatValue(); percent = Math.max(0, percent); } else { percent = new BigDecimal(1).subtract(new BigDecimal(percent)) .multiply(new BigDecimal(animatedFraction)).add(new BigDecimal(percent)) .setScale(3, RoundingMode.HALF_UP).floatValue(); percent = Math.min(1, percent); } if (percent != headerView.getPercent()) { headerView.updatePercent(percent); } } else { if (isToStart) { percent = new BigDecimal(0).subtract(new BigDecimal(percent)) .multiply(new BigDecimal(animatedFraction)).add(new BigDecimal(percent)) .setScale(3, RoundingMode.HALF_UP).floatValue(); percent = Math.max(0, percent); } else { percent = new BigDecimal(1).subtract(new BigDecimal(dragPercent)) .multiply(new BigDecimal(animatedFraction)).add(new BigDecimal(percent)) .setScale(3, RoundingMode.HALF_UP).floatValue(); percent = Math.min(1, percent); } footerView.updatePercent(percent); } } //Listener /** * listener call when animate end */ private interface OnAnimateContentOffsetListener { /** * call when view offset is finish */ public void onOffsetEnd(); } /** * swipe refresh listener */ public interface OnSwipeRefreshListener { /** * begin refresh */ public void onSwipeRefresh(ASwipeRefreshLayout aSwipeRefreshLayout); /** * begin refresh success */ public void onSwipeSuccess(ASwipeRefreshLayout aSwipeRefreshLayout); /** * begin refresh fail */ public void onSwipeFail(ASwipeRefreshLayout aSwipeRefreshLayout); } /** * listener use to config {@link ASwipeRefreshLayout} */ public interface SwipeLayoutConfigListener { /** * setup header and footer offset config,Such as {@link SwipeView.Config#offsetRange}, * {@link SwipeView.Config#startOffset},{@link SwipeView.Config#endOffset}. * this call every time when {@link ASwipeRefreshLayout} layout,at this point,{@link ASwipeRefreshLayout} and it's child views measure ready * * @param layout */ public void setUpOffset(ASwipeRefreshLayout layout); /** * you can custom layout {@link #headerView} or {@link #footerView} or {@link #contentView} hear. * In general,you do not need to write this * * @param layout */ public void onLayout(ASwipeRefreshLayout layout, int left, int top, int right, int bottom); /** * use for decide when to refresh * * @param layout * @param dragOffset offset of finger swipe,it can be positive or negative * @return true:ready for refresh;false:can not refresh */ public boolean isReadyToRefresh(ASwipeRefreshLayout layout, float dragOffset); /** * use for decide when to loadMore * * @param layout * @param dragOffset offset of finger swipe,it can be positive or negative * @return true:ready for loadMore;false:can not loadMore */ public boolean isReadyToLoadMore(ASwipeRefreshLayout layout, float dragOffset); } private void dealAutoLoadMore() { if (!isDragging && isAutoLoadMore && !isRefreshingOrLoadingMore() && canSwipeUpRefresh()) { if (!canChildScrollDown()) { loadMoreStart(true); } else { loadMoreStart(false); } } } private void addAutoLoadMoreListener() { if (!isDealAutoLoadMoreListenerDone) { if (contentView instanceof AbsListView) { AbsListView absListView = (AbsListView) contentView; onScrollListener = Utils.getScrollListener(absListView); absListView.setOnScrollListener(new AbsListView.OnScrollListener() { @Override public void onScrollStateChanged(AbsListView view, int scrollState) { if (onScrollListener != null) { onScrollListener.onScrollStateChanged(view, scrollState); } } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { dealAutoLoadMore(); if (onScrollListener != null) { onScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount); } } }); } else if (contentView instanceof RecyclerView) { ((RecyclerView) contentView).addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); } @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); dealAutoLoadMore(); } }); } isDealAutoLoadMoreListenerDone = true; } } /** * we use delay to implement auto refresh,because we do not know what time the view is ready. * the implement is strange,so you'd better not use auto refresh, * just call {@link #refreshStart()} when you need or you can sure the view is ready. */ private void dealAutoRefresh() { if (isAutoSwipeDownRefresh && !autoRefreshed) { postDelayed(new Runnable() { @Override public void run() { refreshStart(); } }, 300); autoRefreshed = true; } } private boolean isRefreshingOrLoadingMore() { return headerView.getSwipeStatus() == SwipeStatus.REFRESHING || footerView.getSwipeStatus() == SwipeStatus.REFRESHING; } /////////////////////////////////////////////////////////////////////////// // Action method /////////////////////////////////////////////////////////////////////////// public void refreshStart() { if (canSwipeDownRefresh()) { swipeDownRefreshListener.onSwipeRefresh(this); headerView.updateStatus(SwipeStatus.REFRESHING); animateContentOffsetToPos(false, true, null); animateHeaderOffsetToPos(false, null); } } public void refreshSuccess() { if (headerView.getSwipeStatus() == SwipeStatus.REFRESHING && swipeDownRefreshListener != null) { headerView.updateStatus(SwipeStatus.SUCCESS); swipeDownRefreshListener.onSwipeSuccess(this); animateContentOffsetToPos(true, true, new OnAnimateContentOffsetListener() { @Override public void onOffsetEnd() { headerView.updateStatus(SwipeStatus.NORMAL); dealAutoLoadMore(); } }); animateHeaderOffsetToPos(true, new OnAnimateContentOffsetListener() { @Override public void onOffsetEnd() { headerView.updateStatus(SwipeStatus.NORMAL); } }); } } public void refreshFail() { if (headerView.getSwipeStatus() == SwipeStatus.REFRESHING && swipeDownRefreshListener != null) { swipeDownRefreshListener.onSwipeFail(this); headerView.updateStatus(SwipeStatus.FAIL); animateContentOffsetToPos(true, true, new OnAnimateContentOffsetListener() { @Override public void onOffsetEnd() { headerView.updateStatus(SwipeStatus.NORMAL); dealAutoLoadMore(); } }); animateHeaderOffsetToPos(true, new OnAnimateContentOffsetListener() { @Override public void onOffsetEnd() { headerView.updateStatus(SwipeStatus.NORMAL); } }); } } public void loadMoreStart() { loadMoreStart(true); } private void loadMoreStart(boolean animateAble) { if (canSwipeUpRefresh()) { lastVisiblePos = Utils.findLastVisibleItemPositions(contentView); swipeUpRefreshListener.onSwipeRefresh(this); if (animateAble) { animateContentOffsetToPos(false, false, null); animateFooterOffsetToPos(false, null); } footerView.updateStatus(SwipeStatus.REFRESHING); } } public void loadMoreSuccess() { if (footerView.getSwipeStatus() == SwipeStatus.REFRESHING && swipeUpRefreshListener != null) { footerView.updateStatus(SwipeStatus.SUCCESS); swipeUpRefreshListener.onSwipeSuccess(this); animateContentOffsetToPos(true, false, new OnAnimateContentOffsetListener() { @Override public void onOffsetEnd() { footerView.updateStatus(SwipeStatus.NORMAL); if (autoScrollAfterLoadMore) { Utils.scrollToItem(contentView, lastVisiblePos); } } }); animateFooterOffsetToPos(true, new OnAnimateContentOffsetListener() { @Override public void onOffsetEnd() { footerView.updateStatus(SwipeStatus.NORMAL); } }); } } public void loadMoreFail() { if (footerView.getSwipeStatus() == SwipeStatus.REFRESHING && swipeUpRefreshListener != null) { swipeUpRefreshListener.onSwipeFail(this); footerView.updateStatus(SwipeStatus.FAIL); animateContentOffsetToPos(true, false, new OnAnimateContentOffsetListener() { @Override public void onOffsetEnd() { footerView.updateStatus(SwipeStatus.NORMAL); } }); animateFooterOffsetToPos(true, new OnAnimateContentOffsetListener() { @Override public void onOffsetEnd() { footerView.updateStatus(SwipeStatus.NORMAL); } }); } } /////////////////////////////////////////////////////////////////////////// // Config or status method /////////////////////////////////////////////////////////////////////////// /** * @return Interpolator for header animate and swipe down */ public Interpolator getHeaderInterpolator() { return headerInterpolator; } /** * @return Interpolator for footer animate and swipe up */ public Interpolator getFooterInterpolator() { return footerInterpolator; } /** * @return damp rate for when drag */ public static float getSwipeDownDampRate() { return swipeDownDampRate; } /** * @return {@link #autoScrollAfterLoadMore} */ public boolean isAutoScrollAfterLoadMore() { return autoScrollAfterLoadMore; } /** * @return {@link #dampAbleOfSwipeDown} */ public boolean isDampAbleOfSwipeDown() { return dampAbleOfSwipeDown; } /** * @return {@link #headerView} */ public SwipeView getHeaderView() { return headerView; } /** * @return {@link #swipeDownRefreshListener} */ public OnSwipeRefreshListener getSwipeDownRefreshListener() { return swipeDownRefreshListener; } /** * @return {@link #parallaxFactorOfContentSwipeUp} */ public float getParallaxFactorOfContentSwipeUp() { return parallaxFactorOfContentSwipeUp; } /** * @return {@link #dampAbleOfSwipeUp} */ public boolean isDampAbleOfSwipeUp() { return dampAbleOfSwipeUp; } /** * @return {@link #footerView} */ public SwipeView getFooterView() { return footerView; } /** * @return {@link #swipeUpRefreshListener} */ public OnSwipeRefreshListener getSwipeUpRefreshListener() { return swipeUpRefreshListener; } /** * @return {@link #canLoadMore} */ public boolean isCanLoadMore() { return canLoadMore; } public boolean isAutoLoadMore() { return isAutoLoadMore; } public boolean isAutoSwipeDownRefresh() { return isAutoSwipeDownRefresh; } public int getLoadMoreSkipNum() { return loadMoreSkipNum; } public int getCurrentContentOffset() { return currentContentOffset; } public View getContentView() { return contentView; } public float getParallaxFactorOfContentSwipeDown() { return parallaxFactorOfContentSwipeDown; } public int getContentViewSwipeDownOffsetRange() { return contentViewSwipeDownOffsetRange; } public int getContentViewSwipeDownStartOffset() { return contentViewSwipeDownStartOffset; } public int getContentViewSwipeDownEndOffset() { return contentViewSwipeDownEndOffset; } public int getContentViewSwipeUpOffsetRange() { return contentViewSwipeUpOffsetRange; } public int getContentViewSwipeUpStartOffset() { return contentViewSwipeUpStartOffset; } public int getContentViewSwipeUpEndOffset() { return contentViewSwipeUpEndOffset; } public long getDurationOfSwipeUp() { return durationOfSwipeUp; } public ASwipeRefreshLayout setSwipeDownRefreshListener(OnSwipeRefreshListener swipeDownRefreshListener) { this.swipeDownRefreshListener = swipeDownRefreshListener; return this; } public ASwipeRefreshLayout setSwipeUpRefreshListener(OnSwipeRefreshListener swipeUpRefreshListener) { this.swipeUpRefreshListener = swipeUpRefreshListener; return this; } public ASwipeRefreshLayout setAutoRefresh(boolean isAutoRefresh) { this.isAutoSwipeDownRefresh = isAutoRefresh; return this; } public ASwipeRefreshLayout setAutoLoadMore(boolean isAutoLoadMore) { this.isAutoLoadMore = isAutoLoadMore; return this; } public ASwipeRefreshLayout setHaveMore(boolean isHaveMore) { this.canLoadMore = isHaveMore; return this; } public ASwipeRefreshLayout setSwipeLayoutConfigListener(SwipeLayoutConfigListener swipeLayoutConfigListener) { this.swipeLayoutConfigListener = swipeLayoutConfigListener; return this; } public ASwipeRefreshLayout setDampAbleOfSwipeDown(boolean dampAbleOfSwipeDown) { this.dampAbleOfSwipeDown = dampAbleOfSwipeDown; return this; } public ASwipeRefreshLayout setDampAbleOfSwipeUp(boolean dampAbleOfSwipeUp) { this.dampAbleOfSwipeUp = dampAbleOfSwipeUp; return this; } public ASwipeRefreshLayout setHeaderInterpolator(Interpolator headerInterpolator) { this.headerInterpolator = headerInterpolator; return this; } public ASwipeRefreshLayout setFooterInterpolator(Interpolator footerInterpolator) { this.footerInterpolator = footerInterpolator; return this; } public ASwipeRefreshLayout setTouchSlop(int touchSlop) { this.touchSlop = touchSlop; return this; } public ASwipeRefreshLayout setParallaxFactorOfContentSwipeUp(float parallaxFactorOfContentSwipeUp) { this.parallaxFactorOfContentSwipeUp = parallaxFactorOfContentSwipeUp; return this; } public ASwipeRefreshLayout setFooterView(SwipeView footerView) { SwipeView.Config config = null; try { config = this.footerView.getConfig().clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } int pos = indexOfChild(this.footerView); removeView(this.footerView); addView(footerView, pos); this.footerView = footerView; this.footerView.setConfig(config); return this; } public ASwipeRefreshLayout setLoadMoreSkipNum(int loadMoreSkipNum) { this.loadMoreSkipNum = loadMoreSkipNum; return this; } public ASwipeRefreshLayout setParallaxFactorOfContentSwipeDown(float parallaxFactorOfContentSwipeDown) { this.parallaxFactorOfContentSwipeDown = parallaxFactorOfContentSwipeDown; return this; } public ASwipeRefreshLayout setContentViewSwipeDownOffsetRange(int contentViewSwipeDownOffsetRange) { this.contentViewSwipeDownOffsetRange = contentViewSwipeDownOffsetRange; return this; } public ASwipeRefreshLayout setContentViewSwipeDownStartOffset(int contentViewSwipeDownStartOffset) { this.contentViewSwipeDownStartOffset = contentViewSwipeDownStartOffset; return this; } public ASwipeRefreshLayout setContentViewSwipeDownEndOffset(int contentViewSwipeDownEndOffset) { this.contentViewSwipeDownEndOffset = contentViewSwipeDownEndOffset; return this; } public ASwipeRefreshLayout setContentViewSwipeUpOffsetRange(int contentViewSwipeUpOffsetRange) { this.contentViewSwipeUpOffsetRange = contentViewSwipeUpOffsetRange; return this; } public ASwipeRefreshLayout setContentViewSwipeUpStartOffset(int contentViewSwipeUpStartOffset) { this.contentViewSwipeUpStartOffset = contentViewSwipeUpStartOffset; return this; } public ASwipeRefreshLayout setContentViewSwipeUpEndOffset(int contentViewSwipeUpEndOffset) { this.contentViewSwipeUpEndOffset = contentViewSwipeUpEndOffset; return this; } public ASwipeRefreshLayout setDurationOfSwipeDown(long durationOfSwipeDown) { this.durationOfSwipeDown = durationOfSwipeDown; return this; } public ASwipeRefreshLayout setDurationOfSwipeUp(long durationOfSwipeUp) { this.durationOfSwipeUp = durationOfSwipeUp; return this; } /** * @param autoScrollAfterLoadMore {@link #autoScrollAfterLoadMore} */ public ASwipeRefreshLayout setAutoScrollAfterLoadMore(boolean autoScrollAfterLoadMore) { this.autoScrollAfterLoadMore = autoScrollAfterLoadMore; return this; } public ASwipeRefreshLayout setHeaderView(SwipeView headerView) { SwipeView.Config config = null; try { config = this.headerView.getConfig().clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } int pos = indexOfChild(this.headerView); removeView(this.headerView); addView(headerView, pos); this.headerView = headerView; this.headerView.setConfig(config); return this; } }