Java tutorial
/* * Copyright 2015 FauDroids * * 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. * * Courtesy to Reginald (xyxyLiu) * https://github.com/xyxyLiu/SwipeRefreshLayout/blob/master/library/src/main/java/com/reginald/swiperefresh/CustomSwipeRefreshLayout.java */ package org.faudroids.boredrudolf.ui; import android.content.Context; import android.graphics.Rect; import android.support.v4.view.ViewCompat; import android.support.v4.view.ViewPager; import android.util.AttributeSet; 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; import org.faudroids.boredrudolf.R; /** * The CustomSwipeRefreshLayout should be used whenever the user can refresh the * contents of a view via a vertical swipe gesture. The activity that * instantiates this view should add an OnRefreshListener to be notified * whenever the swipe to refresh gesture is completed. And refreshComplete() * should be called whenever the refreshing is complete. The CustomSwipeRefreshLayout * will notify the listener each and every time the gesture is completed again. */ public class CustomSwipeRefreshLayout extends ViewGroup { private static final boolean DEBUG = false; private static final String TAG = CustomSwipeRefreshHeadLayout.class.getName(); // time out for no movements during swipe action private static final int RETURN_TO_ORIGINAL_POSITION_TIMEOUT = 500; // Duration of the animation from the top of the content view to parent top private static final int RETURN_TO_TOP_DURATION = 500; // Duration of the animation from the top of the content view to the height of header private static final int RETURN_TO_HEADER_DURATION = 500; // deceleration of progress bar private static final float DECELERATE_INTERPOLATION_FACTOR = 2f; // maximum swipe distance( percent of parent container) private static final float MAX_SWIPE_DISTANCE_FACTOR = .9f; // swipe resistance factor private static final float RESISTANCE_FACTOR = .9f; private final DecelerateInterpolator mDecelerateInterpolator; private final Animation mAnimateStayComplete = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { // DO NOTHING } }; private State currentState = new State(State.STATE_NORMAL); private State lastState = new State(-1); private ScrollUpHandler mScrollUpHandler; private ScrollLeftOrRightHandler mScrollLeftOrRightHandler; private int mRefreshCompleteTimeout; private View mHeadView; //the content that gets pulled down private View mTarget = null; private int mTargetOriginalTop; private OnRefreshListener mListener; private MotionEvent mDownEvent; private int mFrom; private boolean mRefreshing = false; private int mTouchSlop; private int mDistanceToTriggerSync = -1; private float mPrevY; private boolean enableHorizontalScroll = true; private boolean isHorizontalScroll; private boolean checkHorizontalMove; private boolean mCheckValidMotionFlag = true; private int mCurrentTargetOffsetTop = 0; private final AnimationListener mReturningAnimationListener = new BaseAnimationListener() { @Override public void onAnimationEnd(Animation animation) { // Once the target content has returned to its start position, reset // the target offset to 0 // mCurrentTargetOffsetTop = 0; mInReturningAnimation = false; } }; private RefreshCheckHandler mRefreshCheckHandler; private boolean mInReturningAnimation; private int mTriggerOffset = 0; private final Runnable mReturnToTriggerPosition = new Runnable() { @Override public void run() { mInReturningAnimation = true; animateOffsetToTriggerPosition(mTarget.getTop(), mReturningAnimationListener); } }; private final Runnable mReturnToStartPosition = new Runnable() { @Override public void run() { mInReturningAnimation = true; animateOffsetToStartPosition(mTarget.getTop(), mReturningAnimationListener); } }; private final AnimationListener mStayCompleteListener = new BaseAnimationListener() { @Override public void onAnimationEnd(Animation animation) { mReturnToStartPosition.run(); mRefreshing = false; } }; // Cancel the refresh gesture and animate everything back to its original state. private final Runnable mCancel = new Runnable() { @Override public void run() { mInReturningAnimation = true; animateOffsetToStartPosition(mTarget.getTop(), mReturningAnimationListener); } }; private final Animation mAnimateToStartPosition = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { int targetTop = mTargetOriginalTop; if (mFrom != mTargetOriginalTop) { targetTop = (mFrom + (int) ((mTargetOriginalTop - mFrom) * interpolatedTime)); } int offset = targetTop - mTarget.getTop(); final int currentTop = mTarget.getTop(); if (offset + currentTop < 0) { offset = 0 - currentTop; } setTargetOffsetTop(offset, true); } }; private final Animation mAnimateToTriggerPosition = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { int targetTop = mDistanceToTriggerSync; if (mFrom > mDistanceToTriggerSync) { targetTop = (mFrom + (int) ((mDistanceToTriggerSync - mFrom) * interpolatedTime)); } int offset = targetTop - mTarget.getTop(); final int currentTop = mTarget.getTop(); if (offset + currentTop < 0) { offset = 0 - currentTop; } setTargetOffsetTop(offset, true); } }; /** * Simple constructor to use when creating a CustomSwipeRefreshLayout from code. * * @param context */ public CustomSwipeRefreshLayout(Context context) { this(context, null); } /** * Constructor that is called when inflating CustomSwipeRefreshLayout from XML. * * @param context * @param attrs */ public CustomSwipeRefreshLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public CustomSwipeRefreshLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); setWillNotDraw(false); mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR); mRefreshCompleteTimeout = context.getResources().getInteger(R.integer.game_shutdown_delay); mTriggerOffset = getResources().getDimensionPixelOffset(R.dimen.game_height); } private void animateStayComplete(AnimationListener listener) { mAnimateStayComplete.reset(); mAnimateStayComplete.setDuration(mRefreshCompleteTimeout); mAnimateStayComplete.setAnimationListener(listener); mTarget.startAnimation(mAnimateStayComplete); } private void animateOffsetToTriggerPosition(int from, AnimationListener listener) { mFrom = from; mAnimateToTriggerPosition.reset(); mAnimateToTriggerPosition.setDuration(RETURN_TO_HEADER_DURATION); mAnimateToTriggerPosition.setAnimationListener(listener); mAnimateToTriggerPosition.setInterpolator(mDecelerateInterpolator); mTarget.startAnimation(mAnimateToTriggerPosition); } private void animateOffsetToStartPosition(int from, AnimationListener listener) { mFrom = from; mAnimateToStartPosition.reset(); mAnimateToStartPosition.setDuration(RETURN_TO_TOP_DURATION); mAnimateToStartPosition.setAnimationListener(listener); mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator); mTarget.startAnimation(mAnimateToStartPosition); } /** * @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. */ private boolean canViewScrollUp(View view, MotionEvent event) { boolean ret; event.offsetLocation(view.getScrollX() - view.getLeft(), view.getScrollY() - view.getTop()); if (mScrollUpHandler != null) { boolean canViewScrollUp = mScrollUpHandler.canScrollUp(view); if (canViewScrollUp) return true; } if (android.os.Build.VERSION.SDK_INT < 14) { if (view instanceof AbsListView) { final AbsListView absListView = (AbsListView) view; ret = absListView.getChildCount() > 0 && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0).getTop() < absListView.getPaddingTop()); } else { ret = view.getScrollY() > 0 || canChildrenScrollUp(view, event); } } else { ret = ViewCompat.canScrollVertically(view, -1) || canChildrenScrollUp(view, event); } if (DEBUG) Log.d(TAG, "canViewScrollUp " + view.getClass().getName() + " " + ret); return ret; } private boolean canChildrenScrollUp(View view, MotionEvent event) { if (view instanceof ViewGroup) { final ViewGroup viewgroup = (ViewGroup) view; int count = viewgroup.getChildCount(); for (int i = 0; i < count; ++i) { View child = viewgroup.getChildAt(i); Rect bounds = new Rect(); child.getHitRect(bounds); if (bounds.contains((int) event.getX(), (int) event.getY())) { return canViewScrollUp(child, event); } } } return false; } /** * @param direction Negative to check scrolling left, positive to check scrolling right. * @return Whether it is possible for the child view of this layout to * scroll left or right. Override this if the child view is a custom view. */ private boolean canViewScrollHorizontally(View view, MotionEvent event, int direction) { boolean ret; event.offsetLocation(view.getScrollX() - view.getLeft(), view.getScrollY() - view.getTop()); if (mScrollLeftOrRightHandler != null) { boolean canViewScrollLeftOrRight = mScrollLeftOrRightHandler.canScrollLeftOrRight(view, direction); if (canViewScrollLeftOrRight) return true; } if (android.os.Build.VERSION.SDK_INT < 14) { if (view instanceof ViewPager) { ret = ((ViewPager) view).canScrollHorizontally(direction); } else { ret = view.getScrollX() * direction > 0; } } else { ret = ViewCompat.canScrollHorizontally(view, direction); } ret = ret || canChildrenScrollHorizontally(view, event, direction); if (DEBUG) Log.d(TAG, "canViewScrollHorizontally " + view.getClass().getName() + " " + ret); return ret; } private boolean canChildrenScrollHorizontally(View view, MotionEvent event, int direction) { if (view instanceof ViewGroup) { final ViewGroup viewgroup = (ViewGroup) view; int count = viewgroup.getChildCount(); for (int i = 0; i < count; ++i) { View child = viewgroup.getChildAt(i); Rect bounds = new Rect(); child.getHitRect(bounds); if (bounds.contains((int) event.getX(), (int) event.getY())) { if (DEBUG) Log.d(TAG, "in child " + child.getClass().getName()); return canViewScrollHorizontally(child, event, direction); } } } return false; } @Override public void onAttachedToWindow() { super.onAttachedToWindow(); removeCallbacks(mCancel); removeCallbacks(mReturnToStartPosition); } @Override public void onDetachedFromWindow() { super.onDetachedFromWindow(); removeCallbacks(mReturnToStartPosition); removeCallbacks(mCancel); } /** * Set the listener to be notified when a refresh is triggered via the swipe * gesture. */ public void setOnRefreshListener(OnRefreshListener listener) { mListener = listener; } // for headview private void setRefreshState(int state) { currentState.update(state, mCurrentTargetOffsetTop, mTriggerOffset); ((CustomSwipeRefreshHeadLayout) mHeadView).onStateChange(currentState, lastState); lastState.update(state, mCurrentTargetOffsetTop, mTriggerOffset); } private void updateHeadViewState(boolean changeHeightOnly) { if (changeHeightOnly) { setRefreshState(currentState.getRefreshState()); } else { if (mTarget.getTop() > mDistanceToTriggerSync) { setRefreshState(State.STATE_READY); } else { setRefreshState(State.STATE_NORMAL); } } } public void refreshComplete() { setRefreshing(false); } /** * @return Whether the SwipeRefreshWidget is actively showing refresh * progress. */ public boolean isRefreshing() { return mRefreshing; } /** * 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. */ protected void setRefreshing(boolean refreshing) { if (mRefreshing != refreshing) { ensureTarget(); mRefreshing = refreshing; if (mRefreshing) { mReturnToTriggerPosition.run(); } else { // keep refreshing state for refresh complete mRefreshing = true; removeCallbacks(mReturnToStartPosition); removeCallbacks(mCancel); animateStayComplete(mStayCompleteListener); setRefreshState(State.STATE_COMPLETE); } } } private View getContentView() { return getChildAt(0) == mHeadView ? getChildAt(1) : getChildAt(0); } private void ensureTarget() { // Don't bother getting the parent height if the parent hasn't been laid out yet. if (mTarget == null) { if (getChildCount() > 2 && !isInEditMode()) { throw new IllegalStateException("CustomSwipeRefreshLayout can host ONLY one direct child"); } mTarget = getContentView(); mTargetOriginalTop = mTarget.getTop(); int mOriginalOffsetBottom = mTargetOriginalTop + mTarget.getHeight(); if (DEBUG) { Log.d(TAG, "mTargetOriginalTop = " + mTargetOriginalTop + ", mOriginalOffsetBottom = " + mOriginalOffsetBottom); } } if (mDistanceToTriggerSync == -1) { if (getParent() != null && ((View) getParent()).getHeight() > 0) { mDistanceToTriggerSync = (int) Math.min( ((View) getParent()).getHeight() * MAX_SWIPE_DISTANCE_FACTOR, mTriggerOffset + mTargetOriginalTop); } } } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { if (getChildCount() == 0) { return; } MarginLayoutParams lp = (MarginLayoutParams) mHeadView.getLayoutParams(); final int headViewLeft = getPaddingLeft() + lp.leftMargin; final int headViewTop = mCurrentTargetOffsetTop - mHeadView.getMeasuredHeight() + getPaddingTop() + lp.topMargin; final int headViewRight = headViewLeft + mHeadView.getMeasuredWidth(); final int headViewBottom = headViewTop + mHeadView.getMeasuredHeight(); mHeadView.layout(headViewLeft, headViewTop, headViewRight, headViewBottom); if (DEBUG) Log.d(TAG, String.format("@@ onLayout() : mHeadview [%d,%d,%d,%d] ", headViewLeft, headViewTop, headViewRight, headViewBottom)); final View content = getContentView(); lp = (MarginLayoutParams) content.getLayoutParams(); final int childLeft = getPaddingLeft() + lp.leftMargin; final int childTop = mCurrentTargetOffsetTop + getPaddingTop() + lp.topMargin; final int childRight = childLeft + content.getMeasuredWidth(); final int childBottom = childTop + content.getMeasuredHeight(); content.layout(childLeft, childTop, childRight, childBottom); if (DEBUG) Log.d(TAG, String.format("@@ onLayout() %d : content [%d,%d,%d,%d] ", getChildAt(0) == mHeadView ? 1 : 0, childLeft, childTop, childRight, childBottom)); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (mHeadView == null) { mHeadView = new GameView(getContext()); addView(mHeadView, new MarginLayoutParams(LayoutParams.MATCH_PARENT, getResources().getDimensionPixelOffset(R.dimen.game_height))); } if (getChildCount() > 2 && !isInEditMode()) { throw new IllegalStateException("CustomSwipeRefreshLayout can host one child content view."); } measureChildWithMargins(mHeadView, widthMeasureSpec, 0, heightMeasureSpec, 0); final View content = getContentView(); if (getChildCount() > 0) { MarginLayoutParams lp = (MarginLayoutParams) content.getLayoutParams(); content.measure( MeasureSpec.makeMeasureSpec(getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - lp.leftMargin - lp.rightMargin, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(getMeasuredHeight() - getPaddingTop() - getPaddingBottom() - lp.topMargin - lp.bottomMargin, MeasureSpec.EXACTLY)); } if (DEBUG) { Log.d(TAG, String.format("onMeasure(): swiperefreshlayout: width=%d, height=%d", getMeasuredWidth(), getMeasuredHeight())); Log.d(TAG, String.format("onMeasure(): headview: width=%d, height=%d", mHeadView.getMeasuredWidth(), mHeadView.getMeasuredHeight())); Log.d(TAG, String.format("onMeasure(): content: width=%d, height=%d", content.getMeasuredWidth(), content.getMeasuredHeight())); } } @Override protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { return p instanceof MarginLayoutParams; } @Override protected ViewGroup.LayoutParams generateDefaultLayoutParams() { return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); } @Override protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { return new MarginLayoutParams(p); } @Override public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(), attrs); } @Override public void addView(View child, int index, LayoutParams params) { if (getChildCount() > 1 && !isInEditMode()) { throw new IllegalStateException("CustomSwipeRefreshLayout can host ONLY one child content view"); } super.addView(child, index, params); } private boolean checkCanDoRefresh() { if (mRefreshCheckHandler != null) { return mRefreshCheckHandler.canRefresh(); } return true; } @Override public boolean dispatchTouchEvent(MotionEvent event) { if (DEBUG) Log.d(TAG, "dispatchTouchEvent() start "); boolean ret = super.dispatchTouchEvent(event); if (event.getAction() == MotionEvent.ACTION_DOWN) ret = true; if (DEBUG) Log.d(TAG, "dispatchTouchEvent() " + ret); return ret; } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (DEBUG) Log.d(TAG, "onInterceptTouchEvent() start " + ev); ensureTarget(); boolean handled = false; float curY = ev.getY(); if (!isEnabled()) { return false; } // record the first event: if (ev.getAction() == MotionEvent.ACTION_DOWN) { mDownEvent = MotionEvent.obtain(ev); mPrevY = mDownEvent.getY(); mCheckValidMotionFlag = true; checkHorizontalMove = true; } else if (ev.getAction() == MotionEvent.ACTION_MOVE) { float yDiff = Math.abs(curY - mDownEvent.getY()); if (enableHorizontalScroll) { MotionEvent event = MotionEvent.obtain(ev); int horizontalScrollDirection = ev.getX() > mDownEvent.getX() ? -1 : 1; float xDiff = Math.abs(ev.getX() - mDownEvent.getX()); if (isHorizontalScroll) { if (DEBUG) Log.d(TAG, "onInterceptTouchEvent(): in horizontal scroll"); mPrevY = curY; checkHorizontalMove = false; return false; } else if (xDiff <= mTouchSlop) { checkHorizontalMove = true; //return false; } else if (canViewScrollHorizontally(mTarget, event, horizontalScrollDirection) && checkHorizontalMove && xDiff > 2 * yDiff) { if (DEBUG) Log.d(TAG, "onInterceptTouchEvent(): start horizontal scroll"); mPrevY = curY; isHorizontalScroll = true; checkHorizontalMove = false; return false; } else { checkHorizontalMove = false; } } if (yDiff < mTouchSlop) { mPrevY = curY; return false; } } else if (ev.getAction() == MotionEvent.ACTION_UP) { float yDiff = Math.abs(curY - mDownEvent.getY()); if (enableHorizontalScroll && isHorizontalScroll) { if (DEBUG) Log.d(TAG, "onInterceptTouchEvent(): finish horizontal scroll"); isHorizontalScroll = false; mPrevY = ev.getY(); return false; } else if (yDiff < mTouchSlop) { mPrevY = curY; return false; } } MotionEvent event = MotionEvent.obtain(ev); if (!mInReturningAnimation && !canViewScrollUp(mTarget, event)) { handled = onTouchEvent(ev); if (DEBUG) Log.d(TAG, "onInterceptTouchEvent(): handled = onTouchEvent(event);" + handled); } else { // keep updating last Y position when the event is not intercepted! mPrevY = ev.getY(); } boolean ret = !handled ? super.onInterceptTouchEvent(ev) : handled; if (DEBUG) Log.d(TAG, "onInterceptTouchEvent() " + ret); return ret; } @Override public void requestDisallowInterceptTouchEvent(boolean b) { // Nope. } @Override public boolean onTouchEvent(MotionEvent event) { if (DEBUG) Log.d(TAG, "onTouchEvent() start"); if (!isEnabled()) { return false; } final int action = event.getAction(); boolean handled = false; int curTargetTop = mTarget.getTop(); mCurrentTargetOffsetTop = curTargetTop - mTargetOriginalTop; switch (action) { case MotionEvent.ACTION_MOVE: if (mDownEvent != null && !mInReturningAnimation) { final float eventY = event.getY(); float yDiff = eventY - mDownEvent.getY(); boolean isScrollUp = eventY - mPrevY > 0; // if yDiff is large enough to be counted as one move event if (mCheckValidMotionFlag && (yDiff > mTouchSlop || yDiff < -mTouchSlop)) { mCheckValidMotionFlag = false; } // keep refresh head above mTarget when refreshing if (isRefreshing()) { mPrevY = event.getY(); handled = false; break; } // curTargetTop is bigger than trigger if (curTargetTop >= mDistanceToTriggerSync) { // User movement passed distance; trigger a refresh removeCallbacks(mCancel); } // curTargetTop is not bigger than trigger else { // Just track the user's movement if (!isScrollUp && (curTargetTop < mTargetOriginalTop + 1)) { removeCallbacks(mCancel); mPrevY = event.getY(); handled = false; break; } else { updatePositionTimeout(true); } } handled = true; if (curTargetTop >= mTargetOriginalTop && !isRefreshing()) setTargetOffsetTop((int) ((eventY - mPrevY) * RESISTANCE_FACTOR), false); else setTargetOffsetTop((int) ((eventY - mPrevY)), true); mPrevY = event.getY(); } break; case MotionEvent.ACTION_UP: if (mRefreshing) break; if (mCurrentTargetOffsetTop >= mTriggerOffset) { startRefresh(); } else { updatePositionTimeout(false); } handled = true; break; case MotionEvent.ACTION_CANCEL: if (mDownEvent != null) { mDownEvent.recycle(); mDownEvent = null; } break; } if (DEBUG) Log.d(TAG, "onTouchEvent() " + handled); return handled; } private void startRefresh() { if (!checkCanDoRefresh()) { updatePositionTimeout(false); return; } removeCallbacks(mCancel); setRefreshState(State.STATE_REFRESHING); setRefreshing(true); if (mListener != null) mListener.onRefresh(); } private void updateContentOffsetTop(int targetTop, boolean changeHeightOnly) { final int currentTop = mTarget.getTop(); if (targetTop < mTargetOriginalTop) { targetTop = mTargetOriginalTop; } setTargetOffsetTop(targetTop - currentTop, changeHeightOnly); } private void setTargetOffsetTop(int offset, boolean changeHeightOnly) { if (offset == 0) return; // check whether the mTarget total top offset is going to be smaller than 0 if (mCurrentTargetOffsetTop + offset >= 0) { mTarget.offsetTopAndBottom(offset); mHeadView.offsetTopAndBottom(offset); mCurrentTargetOffsetTop += offset; invalidate(); } else { updateContentOffsetTop(mTargetOriginalTop, changeHeightOnly); } updateHeadViewState(changeHeightOnly); } private void updatePositionTimeout(boolean isDelayed) { removeCallbacks(mCancel); postDelayed(mCancel, isDelayed ? RETURN_TO_ORIGINAL_POSITION_TIMEOUT : 0); } public void setEnableHorizontalScroll(boolean isEnable) { enableHorizontalScroll = isEnable; } public void setScroolUpHandler(ScrollUpHandler handler) { mScrollUpHandler = handler; } public void setScroolLeftOrRightHandler(ScrollLeftOrRightHandler handler) { mScrollLeftOrRightHandler = handler; } public void setRefreshCheckHandler(RefreshCheckHandler handler) { mRefreshCheckHandler = handler; } /** * Classes that wish to be notified when the swipe gesture correctly * triggers a refresh should implement this interface. */ public interface OnRefreshListener { void onRefresh(); } /** * Classes that checking whether refresh can be triggered */ public interface RefreshCheckHandler { boolean canRefresh(); } public interface ScrollUpHandler { boolean canScrollUp(View view); } public interface ScrollLeftOrRightHandler { boolean canScrollLeftOrRight(View view, int direction); } /** * Classes that must be implemented by for custom headview */ public interface CustomSwipeRefreshHeadLayout { void onStateChange(State currentState, State lastState); } /** * Refresh state */ public static class State { public final static int STATE_NORMAL = 0; public final static int STATE_READY = 1; public final static int STATE_REFRESHING = 2; public final static int STATE_COMPLETE = 3; /** * detailed refresh state code */ private int refreshState = STATE_NORMAL; /** * scroll distance relative to refresh trigger distance * percent = top / trigger; */ private float percent; /** * distance from header top to parent top. */ private int headerTop; /** * distance from header top to parent top to trigger refresh */ private int trigger; public State(int refreshState) { this.refreshState = refreshState; } void update(int refreshState, int top, int trigger) { this.refreshState = refreshState; this.headerTop = top; this.trigger = trigger; this.percent = (float) top / trigger; } public int getRefreshState() { return refreshState; } public float getPercent() { return percent; } } /** * Simple AnimationListener to avoid having to implement unneeded methods in * AnimationListeners. */ private class BaseAnimationListener implements AnimationListener { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { } @Override public void onAnimationRepeat(Animation animation) { } } }