android自定義下拉重新整理和上拉載入控制元件
阿新 • • 發佈:2019-01-03
import android.content.Context; import android.graphics.Point; import android.support.v4.view.MotionEventCompat; import android.support.v4.view.NestedScrollingChild; import android.support.v4.view.NestedScrollingChildHelper; import android.support.v4.view.NestedScrollingParent; import android.support.v4.view.NestedScrollingParentHelper; import android.support.v4.view.ViewCompat; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.widget.Scroller; import java.util.ArrayList; /** * @description 可能具有頂部重新整理和底部載入功能的佈局 * @note 檢視的新增順序為內容、頭部(非必要)、底部(非必要) **/ public class PullLayout extends ViewGroup implements NestedScrollingParent,NestedScrollingChild { //內容檢視 private View mContentView; //頂部重新整理的時候會顯示的檢視 private View mHeaderView; //底部載入的時候會顯示的檢視 private View mFooterView; //當前是否在觸控狀態下 private boolean isOnTouch; private PullLayoutOption mOption; //頭部檢視的高度 private int mHeaderHeight; //底部檢視的高度 private int mFooterHeight; //上次的觸控事件座標 private Point mLastPoint; //當前偏移量 private int mCurrentOffset; //上次的偏移量 private int mPrevOffset; private int mTouchSlop; //重新整理和載入更多的回撥 private ArrayList mRefreshListeners; private ArrayList mLoadMoreListeners; //當前是否在重新整理中 private boolean isRefreshing; //當前是否在載入中 private boolean isLoading; //緩慢滑動工作者 private ScrollerWorker mScroller; //主要用於標記當前事件的意義 private boolean canUpIntercept; private boolean canDownIntercept; //一次攔截事件的時候當前是否可以頂部或底部重新整理 private boolean canUp; private boolean canDown; //當前是否處於巢狀滑動中 private boolean isNestedScrolling; private NestedScrollingParentHelper mParentHelper; private NestedScrollingChildHelper mChildHelper; public PullLayout(Context context) { this(context, null); } public PullLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public PullLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initData(); } private void initData() { mOption = new PullLayoutOption(); mLastPoint = new Point(); ViewConfiguration configuration = ViewConfiguration.get(getContext()); mTouchSlop = configuration.getScaledTouchSlop(); mRefreshListeners = new ArrayList<>(); mLoadMoreListeners = new ArrayList<>(); mScroller = new ScrollerWorker(getContext()); mParentHelper = new NestedScrollingParentHelper(this); mChildHelper = new NestedScrollingChildHelper(this); } @Override protected void onFinishInflate() { super.onFinishInflate(); int childCount = getChildCount(); switch (childCount) { case 1://這種時候預設只有一個內容檢視 mContentView = getChildAt(0); break; case 2://預設優先支援頂部重新整理 mContentView = getChildAt(0); mHeaderView = getChildAt(1); break; case 3: mContentView = getChildAt(0); mHeaderView = getChildAt(1); mFooterView = getChildAt(2); break; default: throw new IllegalArgumentException("必須包括1到3個子檢視"); } checkHeaderAndFooterAndAddListener(); } /** * 檢查頭部和底部是否為監聽,是的話新增到監聽回撥列表中 */ private void checkHeaderAndFooterAndAddListener() { if (mHeaderView instanceof IRefreshListener) { mRefreshListeners.add((IRefreshListener) mHeaderView); } if (mFooterView instanceof ILoadMoreListener) { mLoadMoreListeners.add((ILoadMoreListener) mFooterView); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); measureChildWithMargins(mContentView, widthMeasureSpec, 0, heightMeasureSpec, 0); MarginLayoutParams lp = null; if (null != mHeaderView) { measureChildWithMargins(mHeaderView, widthMeasureSpec, 0, heightMeasureSpec, 0); lp = (MarginLayoutParams) mHeaderView.getLayoutParams(); mHeaderHeight = mHeaderView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; } if (null != mFooterView) { measureChildWithMargins(mFooterView, widthMeasureSpec, 0, heightMeasureSpec, 0); lp = (MarginLayoutParams) mFooterView.getLayoutParams(); mFooterHeight = mFooterView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int left, top; MarginLayoutParams lp; lp = (MarginLayoutParams) mContentView.getLayoutParams(); left = (l + getPaddingLeft() + lp.leftMargin); if (mOption.isContentFixed()) { top = (t + getPaddingTop() + lp.topMargin); }else{ top = (t + getPaddingTop() + lp.topMargin) + mCurrentOffset; } //畫內容佈局 mContentView.layout(left, top, left + mContentView.getMeasuredWidth(), top + mContentView.getMeasuredHeight()); //畫headerView佈局 if (null != mHeaderView) { lp = (MarginLayoutParams) mHeaderView.getLayoutParams(); left = (l + getPaddingLeft() + lp.leftMargin); top = (t + getPaddingTop() + lp.topMargin) - mHeaderHeight + mCurrentOffset; mHeaderView.layout(left, top, left + mHeaderView.getMeasuredWidth(), top + mHeaderView.getMeasuredHeight()); } //畫footerView佈局 if (null != mFooterView) { lp = (MarginLayoutParams) mFooterView.getLayoutParams(); left = (l + getPaddingLeft() + lp.leftMargin); top = (b - getPaddingBottom() + lp.topMargin) + mCurrentOffset; mFooterView.layout(left, top, left + mFooterView.getMeasuredWidth(), top + mFooterView.getMeasuredHeight()); } } /** * 事件分發的處理,判斷是否攔截滑動事件,當滿足下拉重新整理和上拉載入的時候,會返回true代表父佈局攔截滑動事件並呼叫onTouchvent消耗 * 這個時候會顯示出頭佈局或者底佈局 **/ @Override public boolean onInterceptTouchEvent(MotionEvent event) { if (!isEnabled() || !hasHeaderOrFooter() || isRefreshing || isLoading || isNestedScrolling) { return false; } switch (MotionEventCompat.getActionMasked(event)) { case MotionEvent.ACTION_MOVE: int x = (int) event.getX(); int y = (int) event.getY(); int deltaY = (y - mLastPoint.y); int dy = Math.abs(deltaY); int dx = Math.abs(x - mLastPoint.x); Log.d(getClass().getSimpleName(), "dx-->" + dx + "--dy-->" + dy + "--touchSlop-->" + mTouchSlop); if (dy > mTouchSlop && dy >= dx) { canUp = mOption.canUpToDown();//通過option檔案裡面定義能從上往下拉,即下拉重新整理。外部呼叫 canDown = mOption.canDownToUp();//通過option檔案裡面定義能從下往上拉,即上拉載入。外部呼叫 Log.d(getClass().getSimpleName(), "canUp-->" + canUp + "--canDown-->" + canDown + "--deltaY-->" + deltaY); canUpIntercept = (deltaY > 0 && canUp); canDownIntercept = (deltaY < 0 && canDown); return canUpIntercept || canDownIntercept;//能上拉重新整理或者下拉載入的時候攔截時間,父佈局消耗,否則底佈局消耗。 } return false; } mLastPoint.set((int) event.getX(), (int) event.getY()); return false; } /** *處理下拉重新整理和上拉載入 */ @Override public boolean onTouchEvent(MotionEvent event) { if (!isEnabled() || !hasHeaderOrFooter() || isRefreshing || isLoading || isNestedScrolling) { return false; } switch (MotionEventCompat.getActionMasked(event)) { case MotionEvent.ACTION_MOVE: isOnTouch = true; updatePos((int) (event.getY() - mLastPoint.y)); break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: isOnTouch = false; if (mCurrentOffset > 0) { tryPerformRefresh(); } else { tryPerformLoading(); } break; } mLastPoint.set((int) event.getX(), (int) event.getY()); return true; } /** * 修改偏移量,改變檢視位置 * * @param deltaY 當前位置的偏移量 */ private void updatePos(int deltaY) { if (!hasHeaderOrFooter() || deltaY == 0) {//不需要偏移 return; } if (isOnTouch) { if (!canUp && (mCurrentOffset + deltaY > 0)) {//此時偏移量不應該>0 deltaY = (0 - mCurrentOffset); } else if (!canDown && (mCurrentOffset + deltaY < 0)) {//此時偏移量不應該<0 deltaY = (0 - mCurrentOffset); } } mPrevOffset = mCurrentOffset; mCurrentOffset += deltaY; mCurrentOffset = Math.max(Math.min(mCurrentOffset, mOption.getMaxDownOffset()), mOption.getMaxUpOffset()); deltaY = mCurrentOffset - mPrevOffset; if (deltaY == 0) {//不需要偏移 return; } callUIPositionChangedListener(mPrevOffset, mCurrentOffset); if (!mOption.isContentFixed()) { mContentView.offsetTopAndBottom(deltaY); } if (null != mHeaderView) { mHeaderView.offsetTopAndBottom(deltaY); } if (null != mFooterView) { mFooterView.offsetTopAndBottom(deltaY); } invalidate(); } /** * 是否有頭部或者底部檢視 * * @return true是 */ private boolean hasHeaderOrFooter() { return null != mHeaderView || null != mFooterView; } /** * 嘗試處理載入更多 */ private void tryPerformLoading() { if (isOnTouch || isLoading || isNestedScrolling) { return; } if (mCurrentOffset <= mOption.getLoadMoreOffset()) { startLoading(); } else { mScroller.trySmoothScrollToOffset(0); } } /** * 嘗試處理重新整理回撥 */ private void tryPerformRefresh() { if (isOnTouch || isRefreshing || isNestedScrolling) {//觸控中或者重新整理中不進行回撥 return; } if (mCurrentOffset >= mOption.getRefreshOffset()) { startRefreshing(); } else {//沒有達到重新整理條件,還原狀態 mScroller.trySmoothScrollToOffset(0); } } /** * 處理重新整理 */ private void startRefreshing() { isRefreshing = true; callRefreshBeginListener(); mScroller.trySmoothScrollToOffset(mOption.getRefreshOffset()); } /** * 處理載入 */ private void startLoading() { isLoading = true; callLoadMoreBeginListener(); mScroller.trySmoothScrollToOffset(mOption.getLoadMoreOffset()); } /** * 回撥重新整理和載入的各種監聽 **/ private void callRefreshBeginListener() { for (IRefreshListener listener : mRefreshListeners) { listener.onRefreshBegin(); } } private void callRefreshCompleteListener() { for (IRefreshListener listener : mRefreshListeners) { listener.onRefreshComplete(); } } private void callUIPositionChangedListener(int oldOffset, int newOffset) { for (IRefreshListener listener : mRefreshListeners) { listener.onUIPositionChanged(oldOffset, newOffset); } for (ILoadMoreListener loadMoreListener : mLoadMoreListeners) { loadMoreListener.onUIPositionChanged(oldOffset, newOffset); } } private void callLoadMoreBeginListener() { for (ILoadMoreListener listener : mLoadMoreListeners) { listener.onLoadMoreBegin(); } } private void callLoadMoreCompleteListener() { for (ILoadMoreListener listener : mLoadMoreListeners) { listener.onLoadMoreComplete(); } } /** end **/ /** * 新增和移除監聽 **/ public void addRefreshListener(IRefreshListener listener) { mRefreshListeners.add(listener); } public void removeRefreshListener(IRefreshListener listener) { mRefreshListeners.remove(listener); } public void addLoadMoreListener(ILoadMoreListener listener) { mLoadMoreListeners.add(listener); } public void removeLoadMoreListener(ILoadMoreListener listener) { mLoadMoreListeners.remove(listener); } /** end **/ /** * 配置相關 **/ public void setOnCheckHandler(PullLayoutOption.OnCheckHandler handler) { mOption.setOnCheckHandler(handler); } @Override public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { //只接收豎直方向上面的巢狀滑動 boolean isVerticalScroll = (nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL); boolean canTouchMove = isEnabled() && hasHeaderOrFooter(); return isVerticalScroll && canTouchMove; } @Override public void onStopNestedScroll(View child) { mParentHelper.onStopNestedScroll(child); if (isNestedScrolling) { isNestedScrolling = false; isOnTouch = false; if (mCurrentOffset >= mOption.getRefreshOffset()) { startRefreshing(); } else if(mCurrentOffset <= mOption.getLoadMoreOffset()){ startLoading(); } else {//沒有達到重新整理條件,還原狀態 mScroller.trySmoothScrollToOffset(0); } } } @Override public void onNestedScrollAccepted(View child, View target, int axes) { mParentHelper.onNestedScrollAccepted(child, target, axes); } @Override public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { if (isNestedScrolling) { canUp = mOption.canUpToDown(); canDown = mOption.canDownToUp(); int minOffset = canDown?mOption.getMaxUpOffset():0; int maxOffset = canUp?mOption.getMaxDownOffset():0; int nextOffset = (mCurrentOffset - dy); int sureOffset = Math.min(Math.max(minOffset,nextOffset),maxOffset); int deltaY = sureOffset - mCurrentOffset; consumed[1] = (-deltaY); updatePos(deltaY); } dispatchNestedPreScroll(dx, dy, consumed, null); } @Override public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { boolean canTouch = !isLoading && !isRefreshing && !isOnTouch; if (dyUnconsumed != 0 && canTouch) { canUp = mOption.canUpToDown(); canDown = mOption.canDownToUp(); boolean canUpToDown = (canUp && dyUnconsumed < 0); boolean canDownToUp = (canDown && dyUnconsumed > 0); if(canUpToDown || canDownToUp){ isOnTouch = true; isNestedScrolling = true; updatePos(-dyUnconsumed); dyConsumed = dyUnconsumed; dyUnconsumed = 0; } } dispatchNestedScroll(dxConsumed,dxUnconsumed,dyConsumed,dyUnconsumed,null); } /** * 處理SmoothScroll */ private class ScrollerWorker implements Runnable { public static final int DEFAULT_SMOOTH_TIME = 400;//ms public static final int AUTO_REFRESH_SMOOTH_TIME = 200;//ms,自動重新整理和自動載入時佈局彈出時間 private int mSmoothScrollTime; private int mLastY;//上次的Y座標偏移量 private Scroller mScroller;//間隔計算執行者 private Context mContext;//上下文 private boolean isRunning;//當前是否執行中 public ScrollerWorker(Context mContext) { this.mContext = mContext; mScroller = new Scroller(mContext); mSmoothScrollTime = DEFAULT_SMOOTH_TIME; } public void setSmoothScrollTime(int mSmoothScrollTime) { this.mSmoothScrollTime = mSmoothScrollTime; } @Override public void run() { boolean isFinished = (!mScroller.computeScrollOffset() || mScroller.isFinished()); if (isFinished) { end(); } else { int y = mScroller.getCurrY(); int deltaY = (y - mLastY); boolean isDown = ((mPrevOffset == mOption.getRefreshOffset()) && deltaY > 0); boolean isUp = ((mPrevOffset == mOption.getLoadMoreOffset()) && deltaY < 0); if (isDown || isUp) {//不需要進行多餘的滑動 end(); return; } updatePos(deltaY); mLastY = y; post(this); } } /** * 嘗試緩慢滑動到指定偏移量 * * @param targetOffset 需要滑動到的偏移量 */ public void trySmoothScrollToOffset(int targetOffset) { if (!hasHeaderOrFooter()) { return; } endScroller(); removeCallbacks(this); mLastY = 0; int deltaY = (targetOffset - mCurrentOffset); mScroller.startScroll(0, 0, 0, deltaY, mSmoothScrollTime); isRunning = true; post(this); } /** * 結束Scroller */ private void endScroller() { if (!mScroller.isFinished()) { mScroller.forceFinished(true); } mScroller.abortAnimation(); } /** * 停止並且還原滑動工作 */ public void end() { removeCallbacks(this); endScroller(); isRunning = false; mLastY = 0; } } }