記一個自己寫的湊合能用的上拉加載下拉刷新控件

學習了別人自定義的文章,然後整了這個滿滿都是bug的控件,自己湊合能用,姑且記錄一下

最開始悶着頭沒想明白就開始寫,結果各種不好使,於是畫了個流程圖梳理了一下思路,終於能用了

思路如上圖,具體在代碼的註釋中

package com.example.myaccount.widget;

import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.RelativeLayout;
import android.widget.Scroller;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.core.view.NestedScrollingParent;
import androidx.core.view.NestedScrollingParentHelper;
import androidx.core.view.ViewCompat;
import androidx.recyclerview.widget.RecyclerView;

import com.example.myaccount.R;

public class PullableLayout extends ViewGroup implements NestedScrollingParent {

    private View mTarget; // the target of the gesture,可滑動目標
    private View mHeader,mFooter;
    private Scroller mLayoutScroller;
    private static final int SCROLL_RATIO = 2;

    // 普通狀態
    private static final int NORMAL = 0;
    // 意圖刷新
    private static final int TRY_REFRESH = 1;
    // 刷新狀態
    private static final int REFRESH = 2;
    // 意圖加載
    private static final int TRY_LOAD_MORE = 3;
    // 加載狀態
    private static final int LOAD_MORE = 4;
    private int status = NORMAL;

    private final NestedScrollingParentHelper mNestedScrollingParentHelper;



    public PullableLayout(Context context) {
        super(context);
        mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
    }

//    從xml中加載view時調用,view都加載完成後會調用onFinishInflate()
    public PullableLayout(Context context, AttributeSet attrs) {
        super(context, attrs);

        mHeader = LayoutInflater.from(context).inflate(R.layout.view_head,null);
        mFooter = LayoutInflater.from(context).inflate(R.layout.view_foot,null);

        mLayoutScroller = new Scroller(context);
        mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);

    }

    public PullableLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
    }

    public PullableLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
    }

    private TextView tvHead,tvFoot;
    private RoundView rvHead,rvFoot;

//    當我們的XML佈局被加載完後,就會回調onFinshInfalte這個方法,在這個方法中我們可以初始化控件和數據。
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();

        tvHead = (TextView) mHeader.findViewById(R.id.tv_head);
        rvHead = (RoundView) mHeader.findViewById(R.id.rv_head);

        tvFoot = (TextView) mFooter.findViewById(R.id.tv_foot);
        rvFoot = (RoundView) mFooter.findViewById(R.id.rv_foot);

        RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(LayoutParams.MATCH_PARENT
                , LayoutParams.MATCH_PARENT);
        mHeader.setLayoutParams(params);
        mFooter.setLayoutParams(params);

        addView(mHeader);
        addView(mFooter);
    }

    private void ensureTarget(){
        for(int i = 0;i<getChildCount();i++){
            View child = getChildAt(i);
            if(child != mHeader && child != mFooter){
                mTarget = child;
                break;
            }
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        if (mTarget == null) {
            ensureTarget();
        }
        if (mTarget == null) {
            return;
        }

        for(int i = 0; i<getChildCount();i++){
            View child = getChildAt(i);
            child.measure(MeasureSpec.makeMeasureSpec(
                getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
                MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(
                getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY));
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        if (getChildCount() == 0) {
            return;
        }
        if (mTarget == null) {
            ensureTarget();
        }
        if (mTarget == null) {
            return;
        }

        // 置位
        for (int i = 0; i < getChildCount(); i++){
            View child = getChildAt(i);
            if (child == mHeader) { // 頭視圖隱藏在頂端
                child.layout(0, 0 - child.getMeasuredHeight(), child.getMeasuredWidth(), 0);
            } else if (child == mFooter) { // 尾視圖隱藏在layout所有內容視圖之後
                child.layout(0, mTarget.getMeasuredHeight(), child.getMeasuredWidth(), mTarget.getMeasuredHeight() + child.getMeasuredHeight());
            } else {
                child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight());
            }
        }
    }

    //  是否接收嵌套滾動
    @Override
    public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes) {
//        縱向返回true
        return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

    @Override
    public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes) {

    }

    @Override
    public void onStopNestedScroll(@NonNull View target) {
        Log.d("zyyu","nested stop");
        pullFinish();
        mNestedScrollingParentHelper.onStopNestedScroll(target);

    }

    // 父view是否先消耗滾動參數
    @Override
    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed) {
        Log.d("zyyd","onNestedPreScroll dy:"+dy);
        Log.d("zyyd","onNestedPreScroll getScrollY():"+getScrollY());


        //如果是上滑且頂部控件未完全隱藏,則消耗掉dy,即consumed[1]=dy;
        //以mTop的bottom=0時爲原點,只要mTop露頭了,scrollY就是負值
        boolean hiddenTop = (dy > 0 && getScrollY() < 0) || status == REFRESH;//下拉過程中的上拉 
        //-1檢查view向上=手指向下滑動,1向下
//        如果是下滑且內部View已經無法繼續下拉,則消耗掉dy,即consumed[1]=dy,
        boolean showTop = dy < 0 && !mTarget.canScrollVertically(-1);

        boolean hiddenBottom = (dy < 0 && getScrollY() > 0)|| status == LOAD_MORE;
        boolean showBottom = dy > 0 && !mTarget.canScrollVertically(1);
//
//        消耗掉的意思,就是自己去執行scrollBy,實際上就是我們的StickNavLayout滑動
        if(hiddenTop || showTop || hiddenBottom || showBottom){
            Log.d("zyyd","onNestedPreScroll consumed");
            if(hiddenTop || showBottom){
                pullUp(dy);
            }

            if (showTop || hiddenBottom){
                pullDown(dy);
            }

            consumed[1] = dy;
        }


    }

    //子View主動將消費的距離與未消費的距離通知父View
    @Override
    public void onNestedScroll(final View target, final int dxConsumed, final int dyConsumed,
                               final int dxUnconsumed, final int dyUnconsumed) {
        if(dyUnconsumed > 0){
            pullUp(dyUnconsumed);
        }else if(dyUnconsumed < 0){
            pullDown(dyUnconsumed);
        }
    }

    //    是否消耗fling事件
    @Override
    public boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed) {
//        當頂部控件顯示時,fling可以讓頂部控件隱藏或者顯示。
        return false;
    }

    //    處理慣性事件
    @Override
    public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) {

        return false;
    }

    @Override
    public int getNestedScrollAxes() {
        return mNestedScrollingParentHelper.getNestedScrollAxes();
    }

//    //    限制滑動範圍
//    @Override
//    public void scrollTo(int x, int y) {
//
//        if(y>0){
//            y=0;
//        }
//
//        int dy = y-getScrollY();
//        //down
//        if(dy <= 0){
//            if (Math.abs(y) >= mHeader.getHeight()){
//                y = -mHeader.getHeight();
//            }
//        }else {
//
//        }
//
//        super.scrollTo(x,y);
//
//    }

    private int mLastMoveY;
    private int effectiveScrollY = 150;

//    important!
    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mLayoutScroller.computeScrollOffset()) {
            scrollTo(0, mLayoutScroller.getCurrY());
        }
        postInvalidate();
    }

//    important 優化AbsListView滑到頂(底)部後不能直接繼續下(上)拉操作
//    原因是AbsListView開始處理滑動事件時,會禁止父view攔截視圖
//// Time to start stealing events! Once we've stolen them, don't let anyone
//// steal from us
//   final ViewParent parent = getParent();
//            if (parent != null) {
//        parent.requestDisallowInterceptTouchEvent(true);
//    }
//    如下是Swiperefreshlayout中的解決辦法
    @Override
    public void requestDisallowInterceptTouchEvent(boolean b) {
        // if this is a List < L or another view that doesn't support nested
        // scrolling, ignore this request so that the vertical scroll event
        // isn't stolen
        if ((Build.VERSION.SDK_INT < 21 && mTarget instanceof AbsListView)
                || (mTarget != null && !ViewCompat.isNestedScrollingEnabled(mTarget))) {
            // Nope.
        } else {
            super.requestDisallowInterceptTouchEvent(b);
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        boolean intercept = false;

        //當前位置
        int y = (int) ev.getY();

        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                mLastMoveY = y;
                intercept = false;
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d("zyyd"," intercept move");
                if(status == REFRESH || status == LOAD_MORE){
                    //如果不用NestedScrollingParent則不能直接攔截,因爲list滾到頭後應該滾list本身,所以應該是父view滾一段距離把head隱藏後交給list滾

                    return false;
                }
                //下拉
                if(y > mLastMoveY){
                    Log.d("zyyd"," intercept move down");

                    if(mTarget instanceof AbsListView){ //ListView、GridView
                        intercept = true;
                        AbsListView adapterChild = (AbsListView) mTarget;
                        //getFirstVisiblePosition():當前顯示的第一個item的position
                        //getTop():該item的Top位置
                        if(adapterChild.getFirstVisiblePosition() != 0
                                ||adapterChild.getChildAt(0).getTop() != 0){
                            intercept = false;
                        }
                    }else {
//                       intercept = false;
//                       RecyclerView recyclerView = (RecyclerView)mTarget;
//
//                       //offset是當前屏幕劃過的距離
//                       if(recyclerView.computeVerticalScrollOffset()<= 0){
//                           intercept = true;
//                        }
                         intercept = !mTarget.canScrollVertically(-1);
                    }
                }else if(y < mLastMoveY){  //上拉
                    Log.d("zyyd"," intercept move up");

                    if(mTarget instanceof AbsListView){
                        intercept = true;
                        AbsListView adapterChild = (AbsListView) mTarget;

                        if(adapterChild.getLastVisiblePosition() != adapterChild.getCount() -1
                                ||adapterChild.getChildAt(adapterChild.getChildCount() - 1).getBottom() != getMeasuredHeight()){
                            intercept = false;
                        }
                    }else {
//                        intercept = false;
//                        RecyclerView recyclerView = (RecyclerView) mTarget;
//                        //extent是當前屏幕顯示的距離;range是整個view可滑動的高度
//                        if (recyclerView.computeVerticalScrollExtent() + recyclerView.computeVerticalScrollOffset()
//                                >= recyclerView.computeVerticalScrollRange()){
//                            intercept = true;
//                        }
                        //false表示已滾動底部,攔截
                        intercept = !mTarget.canScrollVertically(1);
                    }
                }
                Log.d("zyyd"," intercept up"+intercept);
                break;
            case MotionEvent.ACTION_UP:
                intercept = false;
                break;
        }

        mLastMoveY = y;

        return intercept;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        //當前位置
        int y = (int) event.getY();

        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                //保存按下的位置
                mLastMoveY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d("zyyd","touch move");
                //獲取滑動距離
                int dy = mLastMoveY - y;

                //下拉
                if (dy < 0){
                    Log.d("zyyd","touch move dy<0");

                    //1.上拉一段距離後,此時dy<0,然後向下滾動還原getScrollY()>0,此時有可能也超過有效距離,因此需要處理
                    // 2.|| 滑動不超過頭部1/2
                    pullDown(dy);
//                    if(getScrollY()>0||Math.abs(getScrollY()) <= mHeader.getMeasuredHeight()/2){
                        //滾動
//                        scrollBy(0, dy);

//                        if(status != LOAD_MORE && status != TRY_LOAD_MORE) {
//                            //滑動超過有效距離
//                            if (Math.abs(getScrollY()) >= effectiveScrollY) {
//                                tvHead.setText("鬆開刷新");
//                                status = REFRESH;
//                            } else {
//                                status = TRY_REFRESH;
//                            }
//                        } else {
//                            status = LOAD_MORE;
//                        }
//                    }

                } else if(dy > 0){  //上拉
                    Log.d("zyyd","touch move dy>0");

                    pullUp(dy);

                }
                Log.d("zyyd","touch status:"+status);

                break;
            case MotionEvent.ACTION_UP:
                Log.d("zyyu","touch up status:"+status);
                pullFinish();
                break;
        }

        mLastMoveY = y;
//        postInvalidate();

        return true;

    }

    private int REFRESH_EFFECTIVE = 300;
    private int LOAD_EFFECTIVE = 300;

    public void pullDown(int dy){
        Log.d("zyyd","pullDown getScrollY:"+getScrollY());

        if(getScrollY() >= 0){
            scrollBy(0,dy);
            if(getScrollY() == 0){
                updateStatus(NORMAL);
            }else if(Math.abs(getScrollY()) < LOAD_EFFECTIVE){
                updateStatus(TRY_LOAD_MORE);
            }else {
                updateStatus(LOAD_MORE);
            }
        } else if(Math.abs(getScrollY()) < REFRESH_EFFECTIVE){
            scrollBy(0,dy/SCROLL_RATIO);
            updateStatus(TRY_REFRESH);
        }else {
            scrollBy(0,dy/(SCROLL_RATIO*2));
            updateStatus(REFRESH);
        }
    }

    public void pullUp(int dy){
        Log.d("zyyd","pullUp getScrollY:"+getScrollY());

        if(getScrollY() <= 0){
            scrollBy(0,dy);

            if(getScrollY() == 0){
                updateStatus(NORMAL);
            }else if(Math.abs(getScrollY()) < REFRESH_EFFECTIVE){
                updateStatus(TRY_REFRESH);
            }else {
                updateStatus(REFRESH);
            }
        } else if(Math.abs(getScrollY()) <= LOAD_EFFECTIVE){
            scrollBy(0,dy/SCROLL_RATIO);
            updateStatus(TRY_LOAD_MORE);
        }else {
            scrollBy(0,dy/(SCROLL_RATIO*2));
            updateStatus(LOAD_MORE);
        }
    }

    public void pullFinish(){
        //滑動超過有效距離
        if(status == REFRESH){
            //滑動到可以顯示頭部的位置
            //getScroll,當前點爲0,getScroll是以當前點爲原點滑動的距離,下正,上負
            mLayoutScroller.startScroll(0, getScrollY(), 0, -(getScrollY() + effectiveScrollY));
            tvHead.setVisibility(GONE);
            rvHead.setVisibility(VISIBLE);

            if(onRefreshListener != null)
                onRefreshListener.onRefresh();
        } else if( status == LOAD_MORE){
            //
            mLayoutScroller.startScroll(0,getScrollY(),0,-(getScrollY()-effectiveScrollY));
            tvFoot.setVisibility(GONE);
            rvFoot.setVisibility(VISIBLE);

            if(onRefreshListener != null)
                onRefreshListener.onLoad();
        }else{
            //滑動距離過短,恢復
            mLayoutScroller.startScroll(0, getScrollY(), 0, -getScrollY());
        }
    }

    public void updateStatus(int status){
        switch (status){
            case NORMAL:
                break;
            case REFRESH:
                tvHead.setVisibility(VISIBLE);
                tvHead.setText("鬆開刷新");
                rvHead.setVisibility(GONE);
                break;
            case TRY_REFRESH:
                tvHead.setVisibility(VISIBLE);
                tvHead.setText("下拉刷新");
                rvHead.setVisibility(GONE);
                break;
            case LOAD_MORE:
                tvFoot.setVisibility(VISIBLE);
                rvFoot.setVisibility(GONE);
                tvFoot.setText("鬆開加載更多");
                break;
            case TRY_LOAD_MORE:
                tvFoot.setVisibility(VISIBLE);
                rvFoot.setVisibility(GONE);
                tvFoot.setText("上拉加載更多");
                break;
        }
        this.status = status;
    }

    public void refreshDone(){
        mLayoutScroller.startScroll(0, getScrollY(), 0, -getScrollY());
        rvHead.setVisibility(View.GONE);
        tvHead.setText("下拉刷新");
        tvHead.setVisibility(View.VISIBLE);
        status = NORMAL;

    }

    public void loadDone(){
        mLayoutScroller.startScroll(0, getScrollY(), 0, -getScrollY());
        rvFoot.setVisibility(View.GONE);
        tvFoot.setText("上拉加載");
        tvFoot.setVisibility(View.VISIBLE);
        status = NORMAL;

    }

    private onRefreshListener onRefreshListener;

    public void setRefreshListener(PullableLayout.onRefreshListener onRefreshListener) {
        this.onRefreshListener = onRefreshListener;
    }

    public interface onRefreshListener{
        void onRefresh();
        void onLoad();

    }

}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章