Java tutorial
/* * Copyright 2016 Lixplor * 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.lixplor.rocketpulltorefresh; import android.animation.ValueAnimator; import android.content.Context; import android.support.v4.view.ViewCompat; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.widget.FrameLayout; import android.widget.LinearLayout; import com.lixplor.rocketpulltorefresh.footer.AbsFooter; import com.lixplor.rocketpulltorefresh.header.AbsHeader; /** * Created : 2016-10-28 * Author : Lixplor * Web : http://blog.lixplor.com * Email : me@lixplor.com */ public class RefreshLayout extends LinearLayout { public static final int STATE_NORMAL = 0; public static final int STATE_PULL_DOWN_TO_REFRESH = 1; public static final int STATE_RELEASE_TO_REFRESH = 2; public static final int STATE_REFRESHING = 3; public static final int STATE_REFRESH_FINISH = 4; public static final int STATE_PULL_UP_TO_LOAD = -1; public static final int STATE_RELEASE_TO_LOAD = -2; public static final int STATE_LOADING = -3; public static final int STATE_LOAD_FINISH = -4; private Context mContext; /** * Touch tolerance of system */ private int mTouchSlop = ViewConfiguration.get(this.getContext()).getScaledTouchSlop(); /** * Current pulling state */ private int mCurrentState = STATE_NORMAL; /** * Callback when pulling state changed */ private OnStateChangedListener mOnStateChangedListener; /** * Callback when pulling distance changed */ private OnPullListener mOnPullListener; /** * View that shows content */ private View mContentView; /** * Header view which can be pull down */ private View mHeaderView; /** * Footer view which can be pull up */ private View mFooterView; /** * Header abstract class */ private AbsHeader mAbsHeader; /** * Footer abstract class */ private AbsFooter mAbsFooter; /** * Height of header view */ private int mHeaderHeight; /** * Height of footer view */ private int mFooterHeight; /** * Raw Y px in the last touch event */ private float mLastTouchRawY; /** * Raw Y px of touch down event */ private float mDownRawY; /** * Y px in touch down event of onInterceptTouchEvent */ private float mInterceptDownY; /** * Y px in touch move event of onInterceptTouchEvent */ private float mInterceptMoveY; /** * Shrink back animation of header view */ private ValueAnimator mHeaderBackAnimator; /** * Shrink back animation of footer view */ private ValueAnimator mFooterBackAnimator; // configs /** * Pulling resistor. 1f means same speed as touch movement; less than 1f means slower; more than 1f means faster */ private float mPullResistor = 0.7f; /** * Shrink back animation duration */ private long mBackAnimDuration = 200; /** * Whether to enable pull down to refresh */ private boolean mEnableRefresh = false; /** * Whether to enable pull up to load more */ private boolean mEnableLoadMore = false; /** * Pull distance of header limitation */ private float mHeaderPullDistanceLimit = 1.5f; /** * Pull distance of footer limitation */ private float mFooterPullDistanceLimit = 1.5f; public RefreshLayout(Context context) { this(context, null); } public RefreshLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public RefreshLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } @Override protected void onFinishInflate() { super.onFinishInflate(); if (getChildCount() > 1) { throw new IllegalStateException("Only 1 child view allowed!"); } // get content view mContentView = getChildAt(0); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { return shouldInterceptTouchEvent(ev, mContentView); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mDownRawY = event.getRawY(); mLastTouchRawY = mDownRawY; break; case MotionEvent.ACTION_MOVE: float moveRawX = event.getRawX(); float moveRawY = event.getRawY(); float dy = moveRawY - mLastTouchRawY; float scrollY = getScrollY(); // move entire layout to display header or footer scrollWithLimit(dy, scrollY); if (scrollY < 0) { performScrollDown(scrollY, moveRawX, moveRawY); } else if (scrollY > 0) { performScrollUp(scrollY, moveRawX, moveRawY); } mLastTouchRawY = moveRawY; break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: performTouchFinish(); break; } return true; } /** * Scroll with evaluation if reach the limit * @param dy change of move Y * @param scrollY scroll Y */ private void scrollWithLimit(float dy, float scrollY) { // move entire layout to display header or footer int targetScrollY = (int) (-dy * mPullResistor + scrollY); boolean isReachLimit = mHeaderPullDistanceLimit > 1f && -targetScrollY > mHeaderPullDistanceLimit * mHeaderHeight || mFooterPullDistanceLimit > 1f && targetScrollY > mFooterPullDistanceLimit * mFooterHeight; if (!isReachLimit) { scrollTo(0, targetScrollY); } } /** * Perform scroll down logic * @param scrollY scroll Y * @param moveRawX move raw X * @param moveRawY move raw Y */ private void performScrollDown(float scrollY, float moveRawX, float moveRawY) { // evaluate if head appears fully, then change to release mode float outHeight = getY() - scrollY; float outPercent = outHeight / mHeaderHeight; if (mOnPullListener != null) { mOnPullListener.onPullDown(moveRawX, moveRawY, outPercent); } if (outHeight >= mHeaderHeight && mCurrentState != STATE_RELEASE_TO_REFRESH) { mCurrentState = STATE_RELEASE_TO_REFRESH; mAbsHeader.changeToRelease(); } else if (outHeight < mHeaderHeight && mCurrentState != STATE_PULL_DOWN_TO_REFRESH) { mCurrentState = STATE_PULL_DOWN_TO_REFRESH; mAbsHeader.changeToPullDown(); } } /** * Perform scroll up logic * @param scrollY scroll Y * @param moveRawX move raw X * @param moveRawY move raw Y */ private void performScrollUp(float scrollY, float moveRawX, float moveRawY) { // evaluate if foot appears fully, then change to release mode float outHeight = scrollY - getY(); float outPercent = outHeight / mFooterHeight; if (mOnPullListener != null) { mOnPullListener.onPullUp(moveRawX, moveRawY, outPercent); } if (outHeight >= mFooterHeight && mCurrentState != STATE_RELEASE_TO_LOAD) { mCurrentState = STATE_RELEASE_TO_LOAD; mAbsFooter.changeToRelease(); } else if (outHeight < mFooterHeight && mCurrentState != STATE_PULL_UP_TO_LOAD) { mCurrentState = STATE_PULL_UP_TO_LOAD; mAbsFooter.changeToPullUp(); } } /** * Perform touch finish logic */ private void performTouchFinish() { if (mCurrentState == STATE_PULL_DOWN_TO_REFRESH) { finishRefresh(); } else if (mCurrentState == STATE_RELEASE_TO_REFRESH) { mCurrentState = STATE_REFRESHING; mAbsHeader.changeToRefreshing(); if (mOnStateChangedListener != null) { mOnStateChangedListener.onRefresh(); animHeader(getScrollY(), -mHeaderHeight); } } else if (mCurrentState == STATE_PULL_UP_TO_LOAD) { finishLoad(); } else if (mCurrentState == STATE_RELEASE_TO_LOAD) { mCurrentState = STATE_LOADING; mAbsFooter.changeToLoading(); if (mOnStateChangedListener != null) { mOnStateChangedListener.onLoad(); animFooter(getScrollY(), mFooterHeight); } } } /** * Init of view * @param context Context * @param attrs AttributeSet */ private void init(Context context, AttributeSet attrs) { mContext = context; setOrientation(LinearLayout.VERTICAL); } /** * Init header container view */ private void initHeaderContainer() { FrameLayout headerContainer = new FrameLayout(mContext); LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); params.width = LayoutParams.MATCH_PARENT; params.height = LayoutParams.WRAP_CONTENT; mHeaderView.measure(0, 0); mHeaderHeight = mHeaderView.getMeasuredHeight(); params.topMargin = -mHeaderHeight; headerContainer.addView(mHeaderView); addView(headerContainer, 0, params); } /** * Init footer container view */ private void initFooterContainer() { FrameLayout footerContainer = new FrameLayout(mContext); LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); params.width = LayoutParams.MATCH_PARENT; params.height = LayoutParams.WRAP_CONTENT; mFooterView.measure(0, 0); mFooterHeight = mFooterView.getMeasuredHeight(); params.bottomMargin = -mFooterHeight; footerContainer.addView(mFooterView); addView(footerContainer, params); } /** * Evaluate if this should intercept touch event to enable pull actions * @param ev MotionEvent * @param contentView content view to help evaluate * @return true if intercepts; false otherwise */ private boolean shouldInterceptTouchEvent(MotionEvent ev, View contentView) { boolean shouldIntercept; switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_POINTER_DOWN: mInterceptDownY = ev.getRawY(); break; case MotionEvent.ACTION_MOVE: mInterceptMoveY = ev.getRawY(); mLastTouchRawY = mInterceptMoveY; if (Math.abs(mInterceptMoveY - mInterceptDownY) > mTouchSlop) { if (mEnableRefresh && mInterceptMoveY - mInterceptDownY > 0) { // pull down, evaluate if content view is at top shouldIntercept = !ViewCompat.canScrollVertically(contentView, -1); return shouldIntercept; } else if (mEnableLoadMore && mInterceptMoveY - mInterceptDownY < 0) { // pull up, evaluate if content view is at bottom shouldIntercept = !ViewCompat.canScrollVertically(contentView, 1); return shouldIntercept; } } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: break; } return super.onInterceptTouchEvent(ev); } /** * Perform header view shrink back animation * @param from animation start Y px * @param to animation stop Y px */ private void animHeader(int from, final int to) { if (mHeaderBackAnimator == null) { mHeaderBackAnimator = new ValueAnimator(); mHeaderBackAnimator.setDuration(mBackAnimDuration); mHeaderBackAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { int destY = (int) animation.getAnimatedValue(); scrollTo(0, destY); if (destY == 0) { // when back finished, reset header mCurrentState = STATE_PULL_DOWN_TO_REFRESH; mAbsHeader.reset(); } } }); mHeaderBackAnimator.setTarget(this); } mHeaderBackAnimator.setIntValues(from, to); mHeaderBackAnimator.start(); } /** * Perform footer view shrink back animation * @param from animation start Y px * @param to animation stop Y px */ private void animFooter(int from, int to) { if (mFooterBackAnimator == null) { mFooterBackAnimator = new ValueAnimator(); mFooterBackAnimator.setDuration(mBackAnimDuration); mFooterBackAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { int destY = (int) animation.getAnimatedValue(); scrollTo(0, destY); if (destY == 0) { // when back finished, reset footer mCurrentState = STATE_PULL_UP_TO_LOAD; mAbsFooter.reset(); } } }); mFooterBackAnimator.setTarget(this); } mFooterBackAnimator.setIntValues(from, to); mFooterBackAnimator.start(); } /** * Set custom header view. This will also enable pull down to refresh * @param header class that extends from AbsHeader */ public void setHeaderView(AbsHeader header) { setEnableRefresh(true); mAbsHeader = header; if (mAbsHeader instanceof OnPullListener) { mOnPullListener = (OnPullListener) mAbsHeader; } mHeaderView = mAbsHeader.getHeader(); initHeaderContainer(); } /** * Set custom footer view. This will also enable pull up to load more * @param footer class that extends from AbsFooter */ public void setFooterView(AbsFooter footer) { setEnableLoadMore(true); mAbsFooter = footer; mFooterView = mAbsFooter.getFooter(); initFooterContainer(); } /** * Set pulling resistor * @param resistor 1f means same speed as touch movement; less than 1f means slower; more than 1f means faster */ public void setPullResistor(float resistor) { if (resistor > 0f && resistor <= 2f) { mPullResistor = resistor; } } /** * Set header or footer shrink back animation duration * @param duration duration in milli seconds */ public void setShrinkBackAnimDuration(long duration) { if (duration < 0) { duration = 0; } mBackAnimDuration = duration; } /** * Whether to enable pull to refresh. <br/> * This is normally used to enable/disable fresh at runtime. Be sure that you called setFooterView() first! * @param enable true to enable; false otherwise */ public void setEnableRefresh(boolean enable) { mEnableRefresh = enable; } /** * Whether to enable pull to load. <br/> * This is normally used to enable/disable load at runtime. Be sure that you called setHeaderView() first! * @param enable true to enable; false otherwise */ public void setEnableLoadMore(boolean enable) { mEnableLoadMore = enable; } /** * Set pull distance of header limitation * @param percent percent of header height */ public void setHeaderPullDistanceLimit(float percent) { if (percent < 1f) { percent = 1f; } mHeaderPullDistanceLimit = percent; } /** * Set pull distance of footer limitation * @param percent percent of footer height */ public void setFooterPullDistanceLimit(float percent) { if (percent < 1f) { percent = 1f; } mFooterPullDistanceLimit = percent; } /** * Call this when you have finished refreshing. This will shrink back header with animation */ public void finishRefresh() { mCurrentState = STATE_REFRESH_FINISH; mAbsHeader.changeToFinish(); animHeader(getScrollY(), 0); } /** * Call this when you have finished loading. This will shrink back footer view with animation */ public void finishLoad() { mCurrentState = STATE_LOAD_FINISH; mAbsFooter.changeToFinish(); animFooter(getScrollY(), 0); } /** * Set a listener to listen pulling distance changes * @param onPullListener OnPullListener */ public void setOnPullListener(OnPullListener onPullListener) { mOnPullListener = onPullListener; } /** * Set a listener to listen to pull state change. * @param onStateChangedListener OnStateChangedListener */ public void setOnStateChangedListener(OnStateChangedListener onStateChangedListener) { mOnStateChangedListener = onStateChangedListener; } /** * Listener that callbacks pull state */ public interface OnStateChangedListener { /** * Called when header state changes to refreshing */ void onRefresh(); /** * Called when footer state changes to loading */ void onLoad(); } /** * Listener to listen pulling distance changes */ public interface OnPullListener { /** * Called when header pull down distance changes * @param touchX touch X position * @param touchY touch Y position * @param percent percent of appearence of header */ void onPullDown(float touchX, float touchY, float percent); /** * Called when footer pull up distance changes * @param touchX touch X position * @param touchY touch Y position * @param percent percent of appearence of footer */ void onPullUp(float touchX, float touchY, float percent); } }