package in.srain.cube.views.ptr; import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; import android.view.*; import android.widget.Scroller; import android.widget.TextView; import java.util.ArrayList; import in.srain.cube.views.ptr.indicator.PtrIndicator; import in.srain.cube.views.ptr.util.PtrCLog; /** * This layout view for "Pull to Refresh(Ptr)" support all of the view, you can contain everything you want. * support: pull to refresh / release to refresh / auto refresh / keep header view while refreshing / hide header view while refreshing * It defines {@link in.srain.cube.views.ptr.PtrUIHandler}, which allows you customize the UI easily. */ public class PtrFrameLayout extends ViewGroup { public enum Mode { NONE, REFRESH, LOAD_MORE, BOTH } private byte mStatus = PTR_STATUS_INIT; // status enum public final static byte PTR_STATUS_INIT = 1; public final static byte PTR_STATUS_PREPARE = 2; public final static byte PTR_STATUS_LOADING = 3; public final static byte PTR_STATUS_COMPLETE = 4; private static final boolean DEBUG_LAYOUT = true; public static boolean DEBUG = true; private static int ID = 1; protected final String LOG_TAG = "ptr-frame-" + ++ID; // auto refresh status private static byte FLAG_AUTO_REFRESH_AT_ONCE = 0x01; private static byte FLAG_AUTO_REFRESH_BUT_LATER = 0x01 << 1; private static byte FLAG_ENABLE_NEXT_PTR_AT_ONCE = 0x01 << 2; private static byte FLAG_PIN_CONTENT = 0x01 << 3; private static byte MASK_AUTO_REFRESH = 0x03; protected View mContent; // optional config for define header and content in xml file private int mHeaderId = 0; private int mContainerId = 0; private int mFooterId = 0; // config private Mode mMode = Mode.BOTH; private int mDurationToClose = 200; private int mDurationToCloseHeader = 1000; private boolean mKeepHeaderWhenRefresh = true; private boolean mPullToRefresh = false; private View mHeaderView; private View mFooterView; private PtrUIHandlerHolder mPtrUIHandlerHolder = PtrUIHandlerHolder.create(); private PtrHandler mPtrHandler; // working parameters private ScrollChecker mScrollChecker; private int mPagingTouchSlop; private int mHeaderHeight; private int mFooterHeight; private boolean mDisableWhenHorizontalMove = false; private int mFlag = 0x00; // disable when detect moving horizontally private boolean mPreventForHorizontal = false; private MotionEvent mLastMoveEvent; private PtrUIHandlerHook mRefreshCompleteHook; private int mLoadingMinTime = 500; private long mLoadingStartTime = 0; private PtrIndicator mPtrIndicator; private boolean mHasSendCancelEvent = false; private Runnable mPerformRefreshCompleteDelay = new Runnable() { @Override public void run() { performRefreshComplete(); } }; public PtrFrameLayout(Context context) { this(context, null); } public PtrFrameLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public PtrFrameLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mPtrIndicator = new PtrIndicator(); TypedArray arr = context.obtainStyledAttributes(attrs, R.styleable.PtrFrameLayout, 0, 0); if (arr != null) { mHeaderId = arr.getResourceId(R.styleable.PtrFrameLayout_ptr_header, mHeaderId); mContainerId = arr.getResourceId(R.styleable.PtrFrameLayout_ptr_content, mContainerId); mFooterId = arr.getResourceId(R.styleable.PtrFrameLayout_ptr_footer, mFooterId); mPtrIndicator.setResistance(arr.getFloat(R.styleable.PtrFrameLayout_ptr_resistance, mPtrIndicator.getResistance())); mDurationToClose = arr.getInt(R.styleable.PtrFrameLayout_ptr_duration_to_close, mDurationToClose); mDurationToCloseHeader = arr.getInt(R.styleable.PtrFrameLayout_ptr_duration_to_close_header, mDurationToCloseHeader); float ratio = mPtrIndicator.getRatioOfHeaderToHeightRefresh(); ratio = arr.getFloat(R.styleable.PtrFrameLayout_ptr_ratio_of_header_height_to_refresh, ratio); mPtrIndicator.setRatioOfHeaderHeightToRefresh(ratio); mKeepHeaderWhenRefresh = arr.getBoolean(R.styleable.PtrFrameLayout_ptr_keep_header_when_refresh, mKeepHeaderWhenRefresh); mPullToRefresh = arr.getBoolean(R.styleable.PtrFrameLayout_ptr_pull_to_fresh, mPullToRefresh); mMode = getModeFromIndex(arr.getInt(R.styleable.PtrFrameLayout_ptr_mode, 4)); arr.recycle(); } mScrollChecker = new ScrollChecker(); final ViewConfiguration conf = ViewConfiguration.get(getContext()); mPagingTouchSlop = conf.getScaledTouchSlop() * 2; } private Mode getModeFromIndex(int index) { switch (index) { case 0: return Mode.NONE; case 1: return Mode.REFRESH; case 2: return Mode.LOAD_MORE; case 3: return Mode.BOTH; default: return Mode.BOTH; } } @Override protected void onFinishInflate() { final int childCount = getChildCount(); if (childCount > 3) { throw new IllegalStateException("PtrFrameLayout only can host 3 elements"); } else if (childCount == 3) { if (mHeaderId != 0 && mHeaderView == null) { mHeaderView = findViewById(mHeaderId); } if (mContainerId != 0 && mContent == null) { mContent = findViewById(mContainerId); } if (mFooterId != 0 && mFooterView == null) { mFooterView = findViewById(mFooterId); } // not specify header or content or footer if (mContent == null || mHeaderView == null || mFooterView == null) { final View child1 = getChildAt(0); final View child2 = getChildAt(1); final View child3 = getChildAt(2); // all are not specified if (mContent == null && mHeaderView == null && mFooterView == null) { mHeaderView = child1; mContent = child2; mFooterView = child3; } // only some are specified else { ArrayList<View> view = new ArrayList<View>(3) {{ add(child1); add(child2); add(child3); }}; if (mHeaderView != null) { view.remove(mHeaderView); } if (mContent != null) { view.remove(mContent); } if (mFooterView != null) { view.remove(mFooterView); } if (mHeaderView == null && view.size() > 0) { mHeaderView = view.get(0); view.remove(0); } if (mContent == null && view.size() > 0) { mContent = view.get(0); view.remove(0); } if (mFooterView == null && view.size() > 0) { mFooterView = view.get(0); view.remove(0); } } } } else if (childCount == 2) { // ignore the footer by default if (mHeaderId != 0 && mHeaderView == null) { mHeaderView = findViewById(mHeaderId); } if (mContainerId != 0 && mContent == null) { mContent = findViewById(mContainerId); } // not specify header or content if (mContent == null || mHeaderView == null) { View child1 = getChildAt(0); View child2 = getChildAt(1); if (child1 instanceof PtrUIHandler) { mHeaderView = child1; mContent = child2; } else if (child2 instanceof PtrUIHandler) { mHeaderView = child2; mContent = child1; } else { // both are not specified if (mContent == null && mHeaderView == null) { mHeaderView = child1; mContent = child2; } // only one is specified else { if (mHeaderView == null) { mHeaderView = mContent == child1 ? child2 : child1; } else { mContent = mHeaderView == child1 ? child2 : child1; } } } } } else if (childCount == 1) { mContent = getChildAt(0); } else { TextView errorView = new TextView(getContext()); errorView.setClickable(true); errorView.setTextColor(0xffff6600); errorView.setGravity(Gravity.CENTER); errorView.setTextSize(20); errorView.setText("The content view in PtrFrameLayout is empty. Do you forget to specify its id in xml layout file?"); mContent = errorView; addView(mContent); } if (mHeaderView != null) { mHeaderView.bringToFront(); } if (mFooterView != null) { mFooterView.bringToFront(); } super.onFinishInflate(); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); if (mScrollChecker != null) { mScrollChecker.destroy(); } if (mPerformRefreshCompleteDelay != null) { removeCallbacks(mPerformRefreshCompleteDelay); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (DEBUG && DEBUG_LAYOUT) { PtrCLog.d(LOG_TAG, "onMeasure frame: width: %s, height: %s, padding: %s %s %s %s", getMeasuredHeight(), getMeasuredWidth(), getPaddingLeft(), getPaddingRight(), getPaddingTop(), getPaddingBottom()); } if (mHeaderView != null) { measureChildWithMargins(mHeaderView, widthMeasureSpec, 0, heightMeasureSpec, 0); MarginLayoutParams lp = (MarginLayoutParams) mHeaderView.getLayoutParams(); mHeaderHeight = mHeaderView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; mPtrIndicator.setHeaderHeight(mHeaderHeight); } if (mFooterView != null) { measureChildWithMargins(mFooterView, widthMeasureSpec, 0, heightMeasureSpec, 0); MarginLayoutParams lp = (MarginLayoutParams) mFooterView.getLayoutParams(); mFooterHeight = mFooterView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; mPtrIndicator.setFooterHeight(mFooterHeight); } if (mContent != null) { measureContentView(mContent, widthMeasureSpec, heightMeasureSpec); if (DEBUG && DEBUG_LAYOUT) { ViewGroup.MarginLayoutParams lp = (MarginLayoutParams) mContent.getLayoutParams(); PtrCLog.d(LOG_TAG, "onMeasure content, width: %s, height: %s, margin: %s %s %s %s", getMeasuredWidth(), getMeasuredHeight(), lp.leftMargin, lp.topMargin, lp.rightMargin, lp.bottomMargin); PtrCLog.d(LOG_TAG, "onMeasure, currentPos: %s, lastPos: %s, top: %s", mPtrIndicator.getCurrentPosY(), mPtrIndicator.getLastPosY(), mContent.getTop()); } } } private void measureContentView(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, getPaddingTop() + getPaddingBottom() + lp.topMargin, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } @Override protected void onLayout(boolean flag, int i, int j, int k, int l) { layoutChildren(); } private void layoutChildren() { // because the header and footer can not show at the same time, so when header has a offset, the footer's offset should be 0, vice versa.. int offsetHeaderY; int offsetFooterY; if (mPtrIndicator.isHeader()) { offsetHeaderY = mPtrIndicator.getCurrentPosY(); offsetFooterY = 0; } else { offsetHeaderY = 0; offsetFooterY = mPtrIndicator.getCurrentPosY(); } int paddingLeft = getPaddingLeft(); int paddingTop = getPaddingTop(); int contentBottom = 0; if (DEBUG && DEBUG_LAYOUT) { PtrCLog.d(LOG_TAG, "onLayout offset: %s %s %s %s", offsetHeaderY, offsetFooterY, isPinContent(), mPtrIndicator.isHeader()); } if (mHeaderView != null) { MarginLayoutParams lp = (MarginLayoutParams) mHeaderView.getLayoutParams(); final int left = paddingLeft + lp.leftMargin; final int top = paddingTop + lp.topMargin + offsetHeaderY - mHeaderHeight; final int right = left + mHeaderView.getMeasuredWidth(); final int bottom = top + mHeaderView.getMeasuredHeight(); mHeaderView.layout(left, top, right, bottom); if (DEBUG && DEBUG_LAYOUT) { PtrCLog.d(LOG_TAG, "onLayout header: %s %s %s %s %s", left, top, right, bottom, mHeaderView.getMeasuredHeight()); } } if (mContent != null) { MarginLayoutParams lp = (MarginLayoutParams) mContent.getLayoutParams(); int left; int top; int right; int bottom; if (mPtrIndicator.isHeader()) { left = paddingLeft + lp.leftMargin; top = paddingTop + lp.topMargin + (isPinContent() ? 0 : offsetHeaderY); right = left + mContent.getMeasuredWidth(); bottom = top + mContent.getMeasuredHeight(); } else { left = paddingLeft + lp.leftMargin; top = paddingTop + lp.topMargin - (isPinContent() ? 0 : offsetFooterY); right = left + mContent.getMeasuredWidth(); bottom = top + mContent.getMeasuredHeight(); } contentBottom = bottom; if (DEBUG && DEBUG_LAYOUT) { PtrCLog.d(LOG_TAG, "onLayout content: %s %s %s %s %s", left, top, right, bottom, mContent.getMeasuredHeight()); } mContent.layout(left, top, right, bottom); } if (mFooterView != null) { MarginLayoutParams lp = (MarginLayoutParams) mFooterView.getLayoutParams(); final int left = paddingLeft + lp.leftMargin; final int top = paddingTop + lp.topMargin + contentBottom - (isPinContent() ? offsetFooterY : 0); final int right = left + mFooterView.getMeasuredWidth(); final int bottom = top + mFooterView.getMeasuredHeight(); mFooterView.layout(left, top, right, bottom); if (DEBUG && DEBUG_LAYOUT) { PtrCLog.d(LOG_TAG, "onLayout footer: %s %s %s %s %s", left, top, right, bottom, mFooterView.getMeasuredHeight()); } } } public boolean dispatchTouchEventSupper(MotionEvent e) { return super.dispatchTouchEvent(e); } @Override public boolean dispatchTouchEvent(MotionEvent e) { if (!isEnabled() || mContent == null || mHeaderView == null) { return dispatchTouchEventSupper(e); } int action = e.getAction(); switch (action) { case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: mPtrIndicator.onRelease(); if (mPtrIndicator.hasLeftStartPosition()) { if (DEBUG) { PtrCLog.d(LOG_TAG, "call onRelease when user release"); } onRelease(false); if (mPtrIndicator.hasMovedAfterPressedDown()) { sendCancelEvent(); return true; } return dispatchTouchEventSupper(e); } else { return dispatchTouchEventSupper(e); } case MotionEvent.ACTION_DOWN: mHasSendCancelEvent = false; mPtrIndicator.onPressDown(e.getX(), e.getY()); mScrollChecker.abortIfWorking(); mPreventForHorizontal = false; // The cancel event will be sent once the position is moved. // So let the event pass to children. // fix #93, #102 dispatchTouchEventSupper(e); return true; case MotionEvent.ACTION_MOVE: mLastMoveEvent = e; mPtrIndicator.onMove(e.getX(), e.getY()); float offsetX = mPtrIndicator.getOffsetX(); float offsetY = mPtrIndicator.getOffsetY(); if (mDisableWhenHorizontalMove && !mPreventForHorizontal && (Math.abs(offsetX) > mPagingTouchSlop && Math.abs(offsetX) > Math.abs(offsetY))) { if (mPtrIndicator.isInStartPosition()) { mPreventForHorizontal = true; } } if (mPreventForHorizontal) { return dispatchTouchEventSupper(e); } boolean moveDown = offsetY > 0; boolean moveUp = !moveDown; boolean canMoveUp = mPtrIndicator.isHeader() && mPtrIndicator.hasLeftStartPosition(); // if the header is showing boolean canMoveDown = mFooterView != null && !mPtrIndicator.isHeader() && mPtrIndicator.hasLeftStartPosition(); // if the footer is showing boolean canHeaderMoveDown = mPtrHandler != null && mPtrHandler.checkCanDoRefresh(this, mContent, mHeaderView) && (mMode.ordinal() & 1) > 0; boolean canFooterMoveUp = mPtrHandler != null && mFooterView != null // The footer view could be null, so need double check && mPtrHandler instanceof PtrHandler2 && ((PtrHandler2) mPtrHandler).checkCanDoLoadMore(this, mContent, mFooterView) && (mMode.ordinal() & 2) > 0; if (DEBUG) { PtrCLog.v(LOG_TAG, "ACTION_MOVE: offsetY:%s, currentPos: %s, moveUp: %s, canMoveUp: %s, moveDown: %s: canMoveDown: %s canHeaderMoveDown: %s canFooterMoveUp: %s", offsetY, mPtrIndicator.getCurrentPosY(), moveUp, canMoveUp, moveDown, canMoveDown, canHeaderMoveDown, canFooterMoveUp); } // if either the header and footer are not showing if (!canMoveUp && !canMoveDown) { // disable move when header not reach top if (moveDown && !canHeaderMoveDown) { return dispatchTouchEventSupper(e); } if (moveUp && !canFooterMoveUp) { return dispatchTouchEventSupper(e); } // should show up header if (moveDown) { moveHeaderPos(offsetY); return true; } // should show up footer if (moveUp) { moveFooterPos(offsetY); return true; } } // if header is showing, then no need to move footer if (canMoveUp) { moveHeaderPos(offsetY); return true; } // if footer is showing, then no need to move header // When status is completing, that is, the footer is hiding, disable pull up if (canMoveDown && mStatus != PTR_STATUS_COMPLETE) { moveFooterPos(offsetY); return true; } } return dispatchTouchEventSupper(e); } private void moveFooterPos(float deltaY) { mPtrIndicator.setIsHeader(false); // to keep the consistence with refresh, need to converse the deltaY movePos(-deltaY); } private void moveHeaderPos(float deltaY) { mPtrIndicator.setIsHeader(true); movePos(deltaY); } /** * if deltaY > 0, move the content down * * @param deltaY */ private void movePos(float deltaY) { // has reached the top if ((deltaY < 0 && mPtrIndicator.isInStartPosition())) { if (DEBUG) { PtrCLog.e(LOG_TAG, String.format("has reached the top")); } return; } int to = mPtrIndicator.getCurrentPosY() + (int) deltaY; // over top if (mPtrIndicator.willOverTop(to)) { if (DEBUG) { PtrCLog.e(LOG_TAG, String.format("over top")); } to = PtrIndicator.POS_START; } mPtrIndicator.setCurrentPos(to); int change = to - mPtrIndicator.getLastPosY(); updatePos(mPtrIndicator.isHeader() ? change : -change); } private void updatePos(int change) { if (change == 0) { return; } boolean isUnderTouch = mPtrIndicator.isUnderTouch(); // once moved, cancel event will be sent to child if (isUnderTouch && !mHasSendCancelEvent && mPtrIndicator.hasMovedAfterPressedDown()) { mHasSendCancelEvent = true; sendCancelEvent(); } // leave initiated position or just refresh complete if ((mPtrIndicator.hasJustLeftStartPosition() && mStatus == PTR_STATUS_INIT) || (mPtrIndicator.goDownCrossFinishPosition() && mStatus == PTR_STATUS_COMPLETE && isEnabledNextPtrAtOnce())) { mStatus = PTR_STATUS_PREPARE; mPtrUIHandlerHolder.onUIRefreshPrepare(this); if (DEBUG) { PtrCLog.i(LOG_TAG, "PtrUIHandler: onUIRefreshPrepare, mFlag %s", mFlag); } } // back to initiated position if (mPtrIndicator.hasJustBackToStartPosition()) { tryToNotifyReset(); // recover event to children if (isUnderTouch) { sendDownEvent(); } } // Pull to Refresh if (mStatus == PTR_STATUS_PREPARE) { // reach fresh height while moving from top to bottom if (!isUnderTouch && !isAutoRefresh() && mPullToRefresh && mPtrIndicator.crossRefreshLineFromTopToBottom()) { tryToPerformRefresh(); } // reach header height while auto refresh if (performAutoRefreshButLater() && mPtrIndicator.hasJustReachedHeaderHeightFromTopToBottom()) { tryToPerformRefresh(); } } if (DEBUG) { PtrCLog.v(LOG_TAG, "updatePos: change: %s, current: %s last: %s, top: %s, headerHeight: %s", change, mPtrIndicator.getCurrentPosY(), mPtrIndicator.getLastPosY(), mContent.getTop(), mHeaderHeight); } if (mPtrIndicator.isHeader()) { mHeaderView.offsetTopAndBottom(change); } else { mFooterView.offsetTopAndBottom(change); } if (!isPinContent()) { mContent.offsetTopAndBottom(change); } invalidate(); if (mPtrUIHandlerHolder.hasHandler()) { mPtrUIHandlerHolder.onUIPositionChange(this, isUnderTouch, mStatus, mPtrIndicator); } onPositionChange(isUnderTouch, mStatus, mPtrIndicator); } protected void onPositionChange(boolean isInTouching, byte status, PtrIndicator mPtrIndicator) { } @SuppressWarnings("unused") public int getHeaderHeight() { return mHeaderHeight; } public int getFooterHeight() { return mFooterHeight; } private void onRelease(boolean stayForLoading) { tryToPerformRefresh(); if (mStatus == PTR_STATUS_LOADING) { // keep header for fresh if (mKeepHeaderWhenRefresh) { // scroll header back if (mPtrIndicator.isOverOffsetToKeepHeaderWhileLoading() && !stayForLoading) { mScrollChecker.tryToScrollTo(mPtrIndicator.getOffsetToKeepHeaderWhileLoading(), mDurationToClose); } else { // do nothing } } else { tryScrollBackToTopWhileLoading(); } } else { if (mStatus == PTR_STATUS_COMPLETE) { notifyUIRefreshComplete(false); } else { tryScrollBackToTopAbortRefresh(); } } } /** * please DO REMEMBER resume the hook * * @param hook */ public void setRefreshCompleteHook(PtrUIHandlerHook hook) { mRefreshCompleteHook = hook; hook.setResumeAction(new Runnable() { @Override public void run() { if (DEBUG) { PtrCLog.d(LOG_TAG, "mRefreshCompleteHook resume."); } notifyUIRefreshComplete(true); } }); } /** * Scroll back to to if is not under touch */ private void tryScrollBackToTop() { if (!mPtrIndicator.isUnderTouch() && mPtrIndicator.hasLeftStartPosition()) { mScrollChecker.tryToScrollTo(PtrIndicator.POS_START, mDurationToCloseHeader); } } /** * just make easier to understand */ private void tryScrollBackToTopWhileLoading() { tryScrollBackToTop(); } /** * just make easier to understand */ private void tryScrollBackToTopAfterComplete() { tryScrollBackToTop(); } /** * just make easier to understand */ private void tryScrollBackToTopAbortRefresh() { tryScrollBackToTop(); } private boolean tryToPerformRefresh() { if (mStatus != PTR_STATUS_PREPARE) { return false; } // if ((mPtrIndicator.isOverOffsetToKeepHeaderWhileLoading() && isAutoRefresh()) || mPtrIndicator.isOverOffsetToRefresh()) { mStatus = PTR_STATUS_LOADING; performRefresh(); } return false; } private void performRefresh() { mLoadingStartTime = System.currentTimeMillis(); if (mPtrUIHandlerHolder.hasHandler()) { mPtrUIHandlerHolder.onUIRefreshBegin(this); if (DEBUG) { PtrCLog.i(LOG_TAG, "PtrUIHandler: onUIRefreshBegin"); } } if (mPtrHandler != null) { if (mPtrIndicator.isHeader()) { mPtrHandler.onRefreshBegin(this); } else { if (mPtrHandler instanceof PtrHandler2) { ((PtrHandler2) mPtrHandler).onLoadMoreBegin(this); } } } } /** * If at the top and not in loading, reset */ private boolean tryToNotifyReset() { if ((mStatus == PTR_STATUS_COMPLETE || mStatus == PTR_STATUS_PREPARE) && mPtrIndicator.isInStartPosition()) { if (mPtrUIHandlerHolder.hasHandler()) { mPtrUIHandlerHolder.onUIReset(this); if (DEBUG) { PtrCLog.i(LOG_TAG, "PtrUIHandler: onUIReset"); } } mStatus = PTR_STATUS_INIT; clearFlag(); return true; } return false; } protected void onPtrScrollAbort() { if (mPtrIndicator.hasLeftStartPosition() && isAutoRefresh()) { if (DEBUG) { PtrCLog.d(LOG_TAG, "call onRelease after scroll abort"); } onRelease(true); } } protected void onPtrScrollFinish() { if (mPtrIndicator.hasLeftStartPosition() && isAutoRefresh()) { if (DEBUG) { PtrCLog.d(LOG_TAG, "call onRelease after scroll finish"); } onRelease(true); } } /** * Detect whether is refreshing. * * @return */ public boolean isRefreshing() { return mStatus == PTR_STATUS_LOADING; } /** * Call this when data is loaded. * The UI will perform complete at once or after a delay, depends on the time elapsed is greater then {@link #mLoadingMinTime} or not. */ final public void refreshComplete() { if (DEBUG) { PtrCLog.i(LOG_TAG, "refreshComplete"); } if (mRefreshCompleteHook != null) { mRefreshCompleteHook.reset(); } int delay = (int) (mLoadingMinTime - (System.currentTimeMillis() - mLoadingStartTime)); if (delay <= 0) { if (DEBUG) { PtrCLog.d(LOG_TAG, "performRefreshComplete at once"); } performRefreshComplete(); } else { postDelayed(mPerformRefreshCompleteDelay, delay); if (DEBUG) { PtrCLog.d(LOG_TAG, "performRefreshComplete after delay: %s", delay); } } } /** * Do refresh complete work when time elapsed is greater than {@link #mLoadingMinTime} */ private void performRefreshComplete() { mStatus = PTR_STATUS_COMPLETE; // if is auto refresh do nothing, wait scroller stop if (mScrollChecker.mIsRunning && isAutoRefresh()) { // do nothing if (DEBUG) { PtrCLog.d(LOG_TAG, "performRefreshComplete do nothing, scrolling: %s, auto refresh: %s", mScrollChecker.mIsRunning, mFlag); } return; } notifyUIRefreshComplete(false); } /** * Do real refresh work. If there is a hook, execute the hook first. * * @param ignoreHook */ private void notifyUIRefreshComplete(boolean ignoreHook) { /** * After hook operation is done, {@link #notifyUIRefreshComplete} will be call in resume action to ignore hook. */ if (mPtrIndicator.hasLeftStartPosition() && !ignoreHook && mRefreshCompleteHook != null) { if (DEBUG) { PtrCLog.d(LOG_TAG, "notifyUIRefreshComplete mRefreshCompleteHook run."); } mRefreshCompleteHook.takeOver(); return; } if (mPtrUIHandlerHolder.hasHandler()) { if (DEBUG) { PtrCLog.i(LOG_TAG, "PtrUIHandler: onUIRefreshComplete"); } mPtrUIHandlerHolder.onUIRefreshComplete(this); } mPtrIndicator.onUIRefreshComplete(); tryScrollBackToTopAfterComplete(); tryToNotifyReset(); } public void autoRefresh() { autoRefresh(true, mDurationToCloseHeader); } public void autoRefresh(boolean atOnce) { autoRefresh(atOnce, mDurationToCloseHeader); } private void clearFlag() { // remove auto fresh flag mFlag = mFlag & ~MASK_AUTO_REFRESH; } public void autoLoadMore() { autoRefresh(true, mDurationToCloseHeader, false); } public void autoLoadMore(boolean atOnce) { autoRefresh(atOnce, mDurationToCloseHeader, false); } public void autoRefresh(boolean atOnce, int duration) { autoRefresh(atOnce, duration, true); } public void autoRefresh(boolean atOnce, int duration, boolean isHeader) { if (mStatus != PTR_STATUS_INIT) { return; } mFlag |= atOnce ? FLAG_AUTO_REFRESH_AT_ONCE : FLAG_AUTO_REFRESH_BUT_LATER; mStatus = PTR_STATUS_PREPARE; if (mPtrUIHandlerHolder.hasHandler()) { mPtrUIHandlerHolder.onUIRefreshPrepare(this); if (DEBUG) { PtrCLog.i(LOG_TAG, "PtrUIHandler: onUIRefreshPrepare, mFlag %s", mFlag); } } mPtrIndicator.setIsHeader(isHeader); mScrollChecker.tryToScrollTo(mPtrIndicator.getOffsetToRefresh(), duration); if (atOnce) { mStatus = PTR_STATUS_LOADING; performRefresh(); } } public boolean isAutoRefresh() { return (mFlag & MASK_AUTO_REFRESH) > 0; } private boolean performAutoRefreshButLater() { return (mFlag & MASK_AUTO_REFRESH) == FLAG_AUTO_REFRESH_BUT_LATER; } public boolean isEnabledNextPtrAtOnce() { return (mFlag & FLAG_ENABLE_NEXT_PTR_AT_ONCE) > 0; } /** * If @param enable has been set to true. The user can perform next PTR at once. * * @param enable */ public void setEnabledNextPtrAtOnce(boolean enable) { if (enable) { mFlag = mFlag | FLAG_ENABLE_NEXT_PTR_AT_ONCE; } else { mFlag = mFlag & ~FLAG_ENABLE_NEXT_PTR_AT_ONCE; } } public boolean isPinContent() { return (mFlag & FLAG_PIN_CONTENT) > 0; } /** * The content view will now move when {@param pinContent} set to true. * * @param pinContent */ public void setPinContent(boolean pinContent) { if (pinContent) { mFlag = mFlag | FLAG_PIN_CONTENT; } else { mFlag = mFlag & ~FLAG_PIN_CONTENT; } } /** * It's useful when working with viewpager. * * @param disable */ public void disableWhenHorizontalMove(boolean disable) { mDisableWhenHorizontalMove = disable; } /** * loading will last at least for so long * * @param time */ public void setLoadingMinTime(int time) { mLoadingMinTime = time; } /** * Not necessary any longer. Once moved, cancel event will be sent to child. * * @param yes */ @Deprecated public void setInterceptEventWhileWorking(boolean yes) { } @SuppressWarnings({"unused"}) public View getContentView() { return mContent; } public void setPtrHandler(PtrHandler ptrHandler) { mPtrHandler = ptrHandler; } public void addPtrUIHandler(PtrUIHandler ptrUIHandler) { PtrUIHandlerHolder.addHandler(mPtrUIHandlerHolder, ptrUIHandler); } @SuppressWarnings({"unused"}) public void removePtrUIHandler(PtrUIHandler ptrUIHandler) { mPtrUIHandlerHolder = PtrUIHandlerHolder.removeHandler(mPtrUIHandlerHolder, ptrUIHandler); } public void setPtrIndicator(PtrIndicator slider) { if (mPtrIndicator != null && mPtrIndicator != slider) { slider.convertFrom(mPtrIndicator); } mPtrIndicator = slider; } public void setMode(Mode mode) { mMode = mode; } public Mode getMode() { return mMode; } @SuppressWarnings({"unused"}) public float getResistance() { return mPtrIndicator.getResistance(); } public void setResistance(float resistance) { mPtrIndicator.setResistance(resistance); } @SuppressWarnings({"unused"}) public float getDurationToClose() { return mDurationToClose; } /** * The duration to return back to the refresh position * * @param duration */ public void setDurationToClose(int duration) { mDurationToClose = duration; } @SuppressWarnings({"unused"}) public long getDurationToCloseHeader() { return mDurationToCloseHeader; } /** * The duration to close time * * @param duration */ public void setDurationToCloseHeader(int duration) { mDurationToCloseHeader = duration; } public void setRatioOfHeaderHeightToRefresh(float ratio) { mPtrIndicator.setRatioOfHeaderHeightToRefresh(ratio); } public int getOffsetToRefresh() { return mPtrIndicator.getOffsetToRefresh(); } @SuppressWarnings({"unused"}) public void setOffsetToRefresh(int offset) { mPtrIndicator.setOffsetToRefresh(offset); } @SuppressWarnings({"unused"}) public float getRatioOfHeaderToHeightRefresh() { return mPtrIndicator.getRatioOfHeaderToHeightRefresh(); } @SuppressWarnings({"unused"}) public int getOffsetToKeepHeaderWhileLoading() { return mPtrIndicator.getOffsetToKeepHeaderWhileLoading(); } @SuppressWarnings({"unused"}) public void setOffsetToKeepHeaderWhileLoading(int offset) { mPtrIndicator.setOffsetToKeepHeaderWhileLoading(offset); } @SuppressWarnings({"unused"}) public boolean isKeepHeaderWhenRefresh() { return mKeepHeaderWhenRefresh; } public void setKeepHeaderWhenRefresh(boolean keepOrNot) { mKeepHeaderWhenRefresh = keepOrNot; } public boolean isPullToRefresh() { return mPullToRefresh; } public void setPullToRefresh(boolean pullToRefresh) { mPullToRefresh = pullToRefresh; } @SuppressWarnings({"unused"}) public View getHeaderView() { return mHeaderView; } public void setHeaderView(View header) { if (mHeaderView != null && header != null && mHeaderView != header) { removeView(mHeaderView); } ViewGroup.LayoutParams lp = header.getLayoutParams(); if (lp == null) { lp = new LayoutParams(-1, -2); header.setLayoutParams(lp); } mHeaderView = header; addView(header); } public void setFooterView(View footer) { if (mFooterView != null && footer != null && mHeaderView != footer) { removeView(mHeaderView); } ViewGroup.LayoutParams lp = footer.getLayoutParams(); if (lp == null) { lp = new LayoutParams(-1, -2); footer.setLayoutParams(lp); } mFooterView = footer; addView(footer); } @Override protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { return p != null && p instanceof LayoutParams; } @Override protected ViewGroup.LayoutParams generateDefaultLayoutParams() { return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); } @Override protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { return new LayoutParams(p); } @Override public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { return new LayoutParams(getContext(), attrs); } private void sendCancelEvent() { if (DEBUG) { PtrCLog.d(LOG_TAG, "send cancel event"); } // The ScrollChecker will update position and lead to send cancel event when mLastMoveEvent is null. // fix #104, #80, #92 if (mLastMoveEvent == null) { return; } MotionEvent last = mLastMoveEvent; MotionEvent e = MotionEvent.obtain(last.getDownTime(), last.getEventTime() + ViewConfiguration.getLongPressTimeout(), MotionEvent.ACTION_CANCEL, last.getX(), last.getY(), last.getMetaState()); dispatchTouchEventSupper(e); } private void sendDownEvent() { if (DEBUG) { PtrCLog.d(LOG_TAG, "send down event"); } final MotionEvent last = mLastMoveEvent; MotionEvent e = MotionEvent.obtain(last.getDownTime(), last.getEventTime(), MotionEvent.ACTION_DOWN, last.getX(), last.getY(), last.getMetaState()); dispatchTouchEventSupper(e); } public static class LayoutParams extends MarginLayoutParams { public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); } public LayoutParams(int width, int height) { super(width, height); } @SuppressWarnings({"unused"}) public LayoutParams(MarginLayoutParams source) { super(source); } public LayoutParams(ViewGroup.LayoutParams source) { super(source); } } class ScrollChecker implements Runnable { private int mLastFlingY; private Scroller mScroller; private boolean mIsRunning = false; private int mStart; private int mTo; public ScrollChecker() { mScroller = new Scroller(getContext()); } public void run() { boolean finish = !mScroller.computeScrollOffset() || mScroller.isFinished(); int curY = mScroller.getCurrY(); int deltaY = curY - mLastFlingY; if (DEBUG) { if (deltaY != 0) { PtrCLog.v(LOG_TAG, "scroll: %s, start: %s, to: %s, currentPos: %s, current :%s, last: %s, delta: %s", finish, mStart, mTo, mPtrIndicator.getCurrentPosY(), curY, mLastFlingY, deltaY); } } if (!finish) { mLastFlingY = curY; if (mPtrIndicator.isHeader()) { moveHeaderPos(deltaY); } else { moveFooterPos(-deltaY); } post(this); } else { finish(); } } public boolean isRunning() { return mScroller.isFinished(); } private void finish() { if (DEBUG) { PtrCLog.v(LOG_TAG, "finish, currentPos:%s", mPtrIndicator.getCurrentPosY()); } reset(); onPtrScrollFinish(); } private void reset() { mIsRunning = false; mLastFlingY = 0; removeCallbacks(this); } private void destroy() { reset(); if (!mScroller.isFinished()) { mScroller.forceFinished(true); } } public void abortIfWorking() { if (mIsRunning) { if (!mScroller.isFinished()) { mScroller.forceFinished(true); } onPtrScrollAbort(); reset(); } } public void tryToScrollTo(int to, int duration) { if (mPtrIndicator.isAlreadyHere(to)) { return; } mStart = mPtrIndicator.getCurrentPosY(); mTo = to; int distance = to - mStart; if (DEBUG) { PtrCLog.d(LOG_TAG, "tryToScrollTo: start: %s, distance:%s, to:%s", mStart, distance, to); } removeCallbacks(this); mLastFlingY = 0; // fix #47: Scroller should be reused, https://github.com/liaohuqiu/android-Ultra-Pull-To-Refresh/issues/47 if (!mScroller.isFinished()) { mScroller.forceFinished(true); } mScroller.startScroll(0, 0, 0, distance, duration); post(this); mIsRunning = true; } } }