XComponent-XStickyNavContainer彈性左滑下一頁

zhuxh發表於2019-01-21

XComponent簡介

XComponent (原始碼地址)整合了多種android自定義控制元件,包括但不限於:

  • XStickyNavContainer
  • TimeTextView
  • ExpandableTextView
  • SingleFitTextView
  • XTabLayout
  • SineWaveView ...

本文介紹XStickyNavContainer自定義控制元件, 該控制元件實現"右拉檢視更多", 釋放還原, 等功能.

0. 原始碼地址

github.com/zhxhcoder/X…

1. 引用方法

implementation 'com.zhxh:xcomponentlib:3.1'
複製程式碼

2. 使用方法

舉個例子:

    <com.zhxh.xcomponentlib.xstickyhorizon.XStickyNavContainer
        android:id="@+id/head_home_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toBottomOf="@+id/sineView">

        <android.support.v7.widget.RecyclerView
            android:id="@+id/head_home_recyclerview"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#ffffff"
            android:overScrollMode="never"
            android:scrollbars="none" />

    </com.zhxh.xcomponentlib.xstickyhorizon.XStickyNavContainer>

複製程式碼
        XStickyNavContainer layout = findViewById(R.id.head_home_layout);

        layout.setOnStartActivity(() -> {
            startActivity(new Intent(MainActivity.this, TabHomeActivity.class));

        });
複製程式碼

部分效果:

xcomponent.gif

3. 原始碼分析

該類繼承LinearLayout並實現NestedScrollingParent介面,因為這個容器的子view是一個可以橫向滑動的RecycleView所以不可避免的要解決滑動衝突問題。我們一般解決滑動衝突的方法:外部攔截法內部攔截法。(在Android開發藝術探索第3章也有詳細描述)

1,外部攔截法: 即父View根據需要對事件進行攔截。邏輯處理放在父View的onInterceptTouchEvent方法中。我們只需要重寫父View的onInterceptTouchEvent方法,並根據邏輯需要做相應的攔截即可。

public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean intercepted = false;
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                intercepted = false;
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                if (需要攔截) {
                    intercepted = true;
                } else {
                    intercepted = false;
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                intercepted = false;
                break;
            }
            default:
                break;
        }
        mLastXIntercept = x;
        mLastYIntercept = y;
        return intercepted;
    }
複製程式碼

主要事項:

  • ACTION_DOWN 一定返回false,不要攔截它,否則根據View事件分發機制,後續ACTION_MOVE 與 ACTION_UP事件都將預設交給父View去處理!
  • 原則上ACTION_UP也需要返回false,如果返回true,並且滑動事件交給子View處理,那麼子View將接收不到ACTION_UP事件,子View的onClick事件也無法觸發。而父View不一樣,如果父View在ACTION_MOVE中開始攔截事件,那麼後續ACTION_UP也將預設交給父View處理!

1,內部攔截法: 即父View不攔截任何事件,所有事件都傳遞給子View,子View根據需要決定是自己消費事件還是給父View處理。這需要子View使用requestDisallowInterceptTouchEvent方法才能正常工作。下面是子View的dispatchTouchEvent方法的虛擬碼:

public boolean dispatchTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                parent.requestDisallowInterceptTouchEvent(true);
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                if (父容器需要攔截) {
                    parent.requestDisallowInterceptTouchEvent(false);
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                break;
            }
            default:
                break;
        }

        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(event);
    }

複製程式碼

父View需要重寫onInterceptTouchEvent方法:

public boolean onInterceptTouchEvent(MotionEvent event) {

        int action = event.getAction();
        if (action == MotionEvent.ACTION_DOWN) {
            return false;
        } else {
            return true;
        }
    }

複製程式碼

注意事項:

  • 內部攔截法要求父View不能攔截ACTION_DOWN事件,由於ACTION_DOWN不受FLAG_DISALLOW_INTERCEPT標誌位控制,一旦父容器攔截ACTION_DOWN那麼所有的事件都不會傳遞給子View。
  • 滑動策略的邏輯放在子View的dispatchTouchEvent方法的ACTION_MOVE中,如果父容器需要獲取點選事件則呼叫 parent.requestDisallowInterceptTouchEvent(false)方法,讓父容器去攔截事件

但是還有更簡單的方法解決滑動衝突,NestedScrolling。 NestedScrolling,在 V4 包下面,在 22.10 版本的時候新增進來,支援 5.0 及 5.0 以上的系統。 在傳統的事件分發機制 中,一旦某個 View 或者 ViewGroup 消費了事件,就很難將事件交給父 View 進行共同處理。而 NestedScrolling 機制很好地幫助我們解決了這一問題。我們只需要按照規範實現相應的介面即可,子 View 實現 NestedScrollingChild,父 View 實現 NestedScrollingParent ,通過 NestedScrollingChildHelper 或者 NestedScrollingParentHelper 完成互動。

NestedScrolling機制 能夠讓 父view 和 子view 在滾動時進行配合,其基本流程如下:

  1. 當 子view 開始滾動之前,可以通知 父view,讓其先於自己進行滾動;
  2. 子view 自己進行滾動
  3. 子view 滾動之後,還可以通知 父view 繼續滾動

要實現這樣的互動,父View 需要實現 NestedScrollingParent介面,而 子View 需要實現NestedScrollingChild介面。

在這套互動機制中,child 是動作的發起者,parent 只是接受回撥並作出響應。

另外:父view 和 子view 並不需要是直接的父子關係,即如果 "parent1 包含 parent2,parent2 包含child”,則 parent1 和child 仍能通過 NestedScrolling機制 進行互動

具體程式碼如下:


    /**
     * 返回true代表處理本次事件
     * 在執行動畫時間裡不能處理本次事件
     */
    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        return target instanceof RecyclerView && !isRunAnim;
    }
    /**
     * 必須要複寫 onStartNestedScroll後呼叫
     */
    @Override
    public void onNestedScrollAccepted(View child, View target, int axes) {
        mParentHelper.onNestedScrollAccepted(child, target, axes);
    }

    /**
     * 復位初始位置
     * scrollTo 移動到指定座標
     * scrollBy 在原有座標上面移動
     */
    @Override
    public void onStopNestedScroll(View target) {
        mParentHelper.onStopNestedScroll(target);
        // 如果不在RecyclerView滑動範圍內
        if (maxWidth != getScrollX()) {
            startAnimation(new ProgressAnimation());
        }

        if (getScrollX() > maxWidth + maxWidth / 2 && mlistener != null) {
            mlistener.onStart();
        }
    }
    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {

    }

    /**
     * @param dx       水平滑動距離
     * @param dy       垂直滑動距離
     * @param consumed 父類消耗掉的距離
     */
    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        getParent().requestDisallowInterceptTouchEvent(true);
        // dx>0 往左滑動 dx<0往右滑動
        //System.out.println("dx:" + dx + "=======getScrollX:" + getScrollX() + "==========canScrollHorizontally:" + !ViewCompat.canScrollHorizontally(target, -1));
        boolean hiddenLeft = dx > 0 && getScrollX() < maxWidth && !ViewCompat.canScrollHorizontally(target, -1);
        boolean showLeft = dx < 0 && !ViewCompat.canScrollHorizontally(target, -1);
        boolean hiddenRight = dx < 0 && getScrollX() > maxWidth && !ViewCompat.canScrollHorizontally(target, 1);
        boolean showRight = dx > 0 && !ViewCompat.canScrollHorizontally(target, 1);
        if (hiddenLeft || showLeft || hiddenRight || showRight) {
            scrollBy(dx / DRAG, 0);
            consumed[0] = dx;
        }

        if (hiddenRight || showRight) {
            mFooterView.setRefresh(dx / DRAG);
        }

        // 限制錯位問題
        if (dx > 0 && getScrollX() > maxWidth && !ViewCompat.canScrollHorizontally(target, -1)) {
            scrollTo(maxWidth, 0);
        }
        if (dx < 0 && getScrollX() < maxWidth && !ViewCompat.canScrollHorizontally(target, 1)) {
            scrollTo(maxWidth, 0);
        }
    }

    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
        return false;
    }

    /**
     * 子view是否可以有慣性 解決右滑時快速左滑顯示錯位問題
     *
     * @return true不可以  false可以
     */
    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        // 當RecyclerView在介面之內交給它自己慣性滑動
        return getScrollX() != maxWidth;
    }

    @Override
    public int getNestedScrollAxes() {
        return 0;
    }
複製程式碼
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes)
child:ViewParent包含觸發巢狀滾動的view的物件
target:觸發巢狀滾動的view (在這裡如果不涉及多層巢狀的話,child和target)是相同的
nestedScrollAxes:就是巢狀滾動的滾動方向了.
當子view的呼叫NestedScrollingChild的方法startNestedScroll時,會呼叫該方法
該方法決定了當前控制元件是否能接收到其內部View(並非是直接子View)滑動時的引數
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
如果onStartNestedScroll方法返回true,之後就會呼叫該方法.它是讓巢狀滾動在開始滾動之前,讓佈局容器(viewGroup)或者它的父類執行一些配置的初始化(React to the successful claiming of a nested scroll operation)
public void onStopNestedScroll(View target)
當子view呼叫stopNestedScroll時會呼叫該方法,停止滾動
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed)
target:同上
dxConsumed:表示target已經消費的x方向的距離
dyConsumed:表示target已經消費的x方向的距離
dxUnconsumed:表示x方向剩下的滑動距離
dyUnconsumed:表示y方向剩下的滑動距離
當子view呼叫dispatchNestedScroll方法時,會呼叫該方法
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed)
target:同上
dx:表示target本次滾動產生的x方向的滾動總距離
dy:表示target本次滾動產生的y方向的滾動總距離
consumed:表示父佈局要消費的滾動距離,consumed[0]和consumed[1]分別表示父佈局在x和y方向上消費的距離.
當子view呼叫dispatchNestedPreScroll方法是,會呼叫該方法

複製程式碼
    /**
     * @param dx       水平滑動距離
     * @param dy       垂直滑動距離
     * @param consumed 父類消耗掉的距離
     */
    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        getParent().requestDisallowInterceptTouchEvent(true);
        // dx>0 往左滑動 dx<0往右滑動
        //System.out.println("dx:" + dx + "=======getScrollX:" + getScrollX() + "==========canScrollHorizontally:" + !ViewCompat.canScrollHorizontally(target, -1));
        boolean hiddenLeft = dx > 0 && getScrollX() < maxWidth && !ViewCompat.canScrollHorizontally(target, -1);
        boolean showLeft = dx < 0 && !ViewCompat.canScrollHorizontally(target, -1);
        boolean hiddenRight = dx < 0 && getScrollX() > maxWidth && !ViewCompat.canScrollHorizontally(target, 1);
        boolean showRight = dx > 0 && !ViewCompat.canScrollHorizontally(target, 1);
        if (hiddenLeft || showLeft || hiddenRight || showRight) {
            scrollBy(dx / DRAG, 0);
            consumed[0] = dx;
        }

        if (hiddenRight || showRight) {
            mFooterView.setRefresh(dx / DRAG);
        }

        // 限制錯位問題
        if (dx > 0 && getScrollX() > maxWidth && !ViewCompat.canScrollHorizontally(target, -1)) {
            scrollTo(maxWidth, 0);
        }
        if (dx < 0 && getScrollX() < maxWidth && !ViewCompat.canScrollHorizontally(target, 1)) {
            scrollTo(maxWidth, 0);
        }
    }
複製程式碼

回彈動畫的實現:

    private class ProgressAnimation extends Animation {

        private ProgressAnimation() {
            isRunAnim = true;
        }

        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            scrollBy((int) ((maxWidth - getScrollX()) * interpolatedTime), 0);
            if (interpolatedTime == 1) {
                isRunAnim = false;
                mFooterView.setRelease();
            }
        }

        @Override
        public void initialize(int width, int height, int parentWidth, int parentHeight) {
            super.initialize(width, height, parentWidth, parentHeight);
            setDuration(300);
            setInterpolator(new AccelerateInterpolator());
        }
    }
複製程式碼

我們自動為子view RecycleView上加了footerview


    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        setOrientation(LinearLayout.HORIZONTAL);

        if (getChildAt(0) instanceof RecyclerView) {
            mChildView = (RecyclerView) getChildAt(0);
            LayoutParams layoutParams = new LayoutParams(maxWidth, LayoutParams.MATCH_PARENT);
            addView(mHeaderView, 0, layoutParams);
            addView(mFooterView, getChildCount(), layoutParams);
            // 左移
            scrollBy(maxWidth, 0);

            mChildView.setOnTouchListener(new OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    // 保證動畫狀態中 子view不能滑動
                    return isRunAnim;
                }
            });
        }
    }
複製程式碼

footerview同樣是一個自定義view,CYAnimatorView

    public void setRefresh(int width) {
        mMove += width;
        if (mMove < 0) {
            mMove = 0;
        } else if (mMove > XStickyNavContainer.maxWidth) {
            mMove = XStickyNavContainer.maxWidth;
        }
        mView.getLayoutParams().width = mMove;
        mView.getLayoutParams().height = LinearLayout.LayoutParams.MATCH_PARENT;

        if (mMove > XStickyNavContainer.maxWidth / 2) {
            animator_text.setText("釋放檢視更多");
            animator_arrow.setImageResource(R.drawable.tactics_more_right);
        } else {
            animator_text.setText("滑動檢視更多");
            animator_arrow.setImageResource(R.drawable.tactics_more_left);
        }
        requestLayout();
    }
複製程式碼

根據移動的距離,判斷footer顯示的文字和圖示。

相關文章