1. 程式人生 > >android自定義下拉重新整理和上拉載入控制元件

android自定義下拉重新整理和上拉載入控制元件


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;
        }

    }

}