手把手實現ScrollView+ViewPager+RecyclerView常規嵌套首頁佈局

前言

目前主流APP常見首頁佈局有頂部banner+列表、頂部banner+ViewPager等形式,如果有刷新需求,再通過外層嵌套SwipeRefreshLayout以實現刷新需求。若能掌握這些佈局方式,就能應對大部分APP需求。

XML佈局

佈局用到系統控件,需要添加依賴:

implementation ‘androidx.appcompat:appcompat:1.1.0’
implementation ‘androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-alpha03’
implementation “androidx.viewpager2:viewpager2:1.0.0”
implementation ‘com.google.android.material:material:1.1.0-beta02’

一. SwipeRefreshLayout+頂部banner+RecyclerView

效果預覽:
在這裏插入圖片描述

xml實現:

<?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/swipe_refresh"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".NestedScrollViewActivity">

    <androidx.core.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".NestedScrollViewActivity">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">

            <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/banner_view"
                android:layout_width="match_parent"
                android:layout_height="300dp" />

            <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/recycler_view"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />
        </LinearLayout>
    </androidx.core.widget.NestedScrollView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

使用androidx.swiperefreshlayout.widget.SwipeRefreshLayout、androidx.core.widget.NestedScrollView、androidx.recyclerview.widget.RecyclerView按照這種排列布局即可實現,不需要自定義view處理滑動衝突。so easy~

ps:RecyclerView初始化、adapter初始化、模擬數據填充等代碼,可以參考NestedRecyclerViewActivity.java

二. SwipeRefreshLayout+頂部banner+ViewPager

效果預覽:
在這裏插入圖片描述

xml實現:

<?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/swipe_refresh"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".NestedScrollViewActivity">

    <androidx.core.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".NestedScrollViewActivity">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">

            <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/banner_view"
                android:layout_width="match_parent"
                android:layout_height="300dp" />

            <com.google.android.material.tabs.TabLayout
                android:id="@+id/tablayout"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"/>

            <androidx.viewpager2.widget.ViewPager2
                android:id="@id/viewpager"
                android:layout_width="match_parent"
                android:layout_height="match_parent"/>
        </LinearLayout>
    </androidx.core.widget.NestedScrollView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

佈局方式和上面類似,只是將RecyclerView替換成TabLayout和ViewPager2。
接下來運行,會發現…有點不對勁,滑動卡殼了。怎麼回事,爲什麼同樣是系統控件,TabLayout和ViewPager2就不支持嵌套滑動呢?

點開SwipeRefreshLayout和NestedScrollView源碼,發現他們均實現了NestedScrollingParent2、NestedScrollingChild2接口,RecyclerView實現了NestedScrollingChild2接口。而TabLayout和ViewPager2沒有實現NestedScrollingParent2、NestedScrollingChild2任一接口。

關於NestedScrollingParent2、NestedScrollingChild2接口

  • NestedScrollingParent2官方註釋:

This interface should be implemented by {@link android.view.ViewGroup ViewGroup} subclasses that wish to support scrolling operations delegated by a nested child view.
Classes implementing this interface should create a final instance of a {@link NestedScrollingParentHelper} as a field and delegate any View or ViewGroup methods to the NestedScrollingParentHelper methods of the same signature.

簡單說就是希望支持嵌套滑動的父容器需要實現此接口,處理來自子視圖傳遞的滑動事件。可以藉助NestedScrollingParentHelper輔助類來執行相應方法。

  • NestedScrollingChild2官方註釋:

This interface should be implemented by {@link View View} subclasses that wish to support dispatching nested scrolling operations to a cooperating parent {@link android.view.ViewGroup ViewGroup}.
Classes implementing this interface should create a final instance of a {@link NestedScrollingChildHelper} as a field and delegate any View methods to the NestedScrollingChildHelper methods of the same signature.

簡單說就是希望支持嵌套滑動的子視圖需要實現此接口,優先將滾動事件向上傳遞給父容器處理。可以藉助NestedScrollingChildHelper輔助類來執行相應方法。

  • NestedScrollingParent2接口方法說明:
/**
 * 子視圖觸發滑動時會回調該方法,父容器在該方法中根據子view、滑動方向、觸摸類型等判斷自己是否支持接收,
 * 若接收返回true,否則返回false。(可由NestedScrollingChild2的startNestedScroll方法觸發)
 */
boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes, @NestedScrollType int type);

/**
 * onStartNestedScroll返回true後會回調該方法,可在此方法中做一些初始配置操作。
 */
void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes, @NestedScrollType int type);

/**
 * 開始滑動時,子視圖會優先回調該方法。父容器可以處理自己的滾動操作,之後將剩餘的滾動偏移量
 * 傳回給子視圖。(可由NestedScrollingChild2的dispatchNestedPreScroll方法觸發)
 */
void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, @NestedScrollType int type);

/**
 * 子視圖處理完剩餘的滾動偏移量後,若還有剩餘,則將剩餘的滾動偏移量再通過該回調傳給
 * 父容器處理。(可由NestedScrollingChild2的dispatchNestedScroll方法觸發)
 */
void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @NestedScrollType int type);

/**
 * 當滑動結束時,回調該方法。(可由NestedScrollingChild2的stopNestedScroll方法觸發)
 */
void onStopNestedScroll(@NonNull View target, @NestedScrollType int type);

(ps:接口方法詳細的說明可以查看源碼註釋或者百度谷歌。)

  • NestedScrollingChild2接口方法說明:
/**
 * 通知開始滑動,會回調父容器的onStartNestedScroll方法。
 */
boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type);

/**
 * 通知停止滑動,會回調父容器的onStopNestedScroll方法。
 */
void stopNestedScroll(@NestedScrollType int type);

/**
 * 查詢是否有父容器支持指定類型的嵌套滑動。
 */
boolean hasNestedScrollingParent(@NestedScrollType int type);

/**
 * 在子視圖處理滑動前,先將滾動偏移量傳遞給父容器。
 */
boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow, @NestedScrollType int type);

/**
 * 子視圖處理滑動後,再將剩餘的滾動偏移量傳遞給父容器。
 */
boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, @NestedScrollType int type);

(ps:接口方法詳細的說明可以查看源碼註釋或者百度谷歌。)

方法執行流程規範:
child.startNestedScroll -> parent.onStartNestedScroll -> parent.onNestedScrollAccepted -> child.dispatchNestedPreScroll -> parent.onNestedPreScroll -> child.dispatchNestedScroll -> parent.onNestedScroll -> child.stopNestedScroll -> parent.onStopNestedScroll

簡而言之,滑動產生時,由child主動通知,parent被動接收判斷處理。這裏的child和parent不必是直接父子關係,child會向上遍歷parent。

部分參數含義說明:

  • child:表示包含target的當前容器的直接子view。
  • target:表示調用startNestedScroll觸發onStartNestedScroll回調的那個子view。
  • axes:表示即將滑動的座標軸方向,通過位運算求出方向。
  • type:表示觸摸類型,有TYPE_TOUCH(用戶觸摸)、TYPE_NON_TOUCH(慣性滑動)兩種類型。
  • dx:水平滑動偏移量。<0表示手指向右劃,>0則相反。
  • dy:垂直滑動偏移量。<0表示手指向下劃,>0則相反。
  • consumed:保存父容器滑動消耗的偏移量(索引0存x軸偏移,1存y軸偏移)。在父容器滑動後,子view會將原偏移量減去consumed中的值得到剩餘偏移量,再進行自身的滾動處理。
  • dxConsumed:子view消耗的水平偏移量。
  • dyConsumed:子view消耗的垂直偏移量。
  • dxUnconsumed:子view滑動後還剩下的水平偏移量。
  • dyUnconsumed:子view滑動後還剩下的垂直偏移量。

注意:若有用戶觸摸滑動到慣性滑動,會走兩遍方法執行流程,即不同type各觸發一次流程。

因爲TabLayout和ViewPager2不支持這種佈局下的嵌套滑動,所以只能通過自定義view來處理滑動和事件分發。

滑動邏輯分析

首先將佈局拆分成上下兩部分(即父容器包含top_view和content_view兩個子視圖):

  • 當手指向上滑動時,若top_view仍然可見,則父容器需要進行滾動處理直至top_view不可見
  • 當手指向下滑動時,若top_view不完全可見(即之前向上滑動過),且content_view不可向下滑動(即content_view自身內容已經滑動至自身頂部),則父容器需要進行滾動處理直至top_view完全可見

代碼實現

自定義父容器ComboScrollLayout

  1. ComboScrollLayout繼承NestedScrollingParent2
public class ComboScrollLayout extends LinearLayout implements NestedScrollingParent2 {

    private View topView;
    private View contentView;

    private int topHeight;

    private NestedScrollingParentHelper parentHelper = new NestedScrollingParentHelper(this);
    
    @Override
    protected void onFinishInflate() {
    }
    
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    }
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    }
    
    @Override
    public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
    }
    
    @Override
    public int getNestedScrollAxes() {
    }
    
    @Override
    public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) {
    }
    
    @Override
    public void onStopNestedScroll(@NonNull View target, int type) {
    }
    
    @Override
    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
    }
    
    @Override
    public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
    }
}
  1. 初始成員變量
public class ComboScrollLayout extends LinearLayout implements NestedScrollingParent2 {
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        // 獲取top_view和content_view
        if (getChildCount() > 0) {
            topView = getChildAt(0);
        }
        if (getChildCount() > 1) {
            contentView = getChildAt(1);
        }
        if (topView == null || contentView == null) {
            throw new AndroidRuntimeException("容器中至少需要兩個子view");
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        // 獲取top_view的高度
        if (topView != null) {
            topHeight = topView.getMeasuredHeight();
        }
    }
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 調整contentView的高度爲父容器高度,使之填充佈局,避免父容器滾動後出現空白
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        ViewGroup.LayoutParams lp = contentView.getLayoutParams();
        lp.height = getMeasuredHeight();
        contentView.setLayoutParams(lp);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
}
  1. 判斷處理滑動的方向
public class ComboScrollLayout extends LinearLayout implements NestedScrollingParent2 {
    @Override
    public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
        if (contentView != null) {
            // 開始滾動前先停止滾動
            if (contentView instanceof RecyclerView) {
                ((RecyclerView) contentView).stopScroll();
            } else if (contentView instanceof NestedScrollView) {
                ((NestedScrollView) contentView).stopNestedScroll();
            } else if (contentView instanceof ViewPager2) {
                ((ViewPager2) contentView).stopNestedScroll();
            }
        }
        topView.stopNestedScroll();
        // 處理垂直方向的滑動
        boolean handled = (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
        return handled;
    }

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

    @Override
    public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) {
        parentHelper.onNestedScrollAccepted(child, target, axes, type);
    }
}

ComboScrollLayout需要在onStartNestedScroll方法中判斷即將開始滑動的方向是否是自己想要處理的。本例中判斷若爲垂直方向滾動就返回true,表示將接收處理垂直方向滑動事件。

onNestedScrollAccepted方法中,調用了NestedScrollingParentHelper輔助類的同樣方法簽名的方法,用以緩存前一步onStartNestedScroll方法中判斷條件攔截的結果。

getNestedScrollAxes方法中,調用NestedScrollingParentHelper輔助類的同名方法,返回前一步onNestedScrollAccepted方法中緩存的進行攔截處理的座標軸。

  1. 攔截滑動處理
public class ComboScrollLayout extends LinearLayout implements NestedScrollingParent2 {
    @Override
    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
        // 向上滑動。若當前topview可見,需要將topview滑動至不可見
        boolean hideTop = dy > 0 && getScrollY() < topHeight;
        // 向下滑動。若contentView滑動至頂,已不可再滑動,且當前topview未完全可見,則將topview滑動至完全可見
        boolean showTop = dy < 0 &&
                getScrollY() > 0 &&
                !ViewCompat.canScrollVertically(target, -1) &&
                !ViewCompat.canScrollVertically(contentView, -1);

        if (hideTop || showTop) {
            // 若需要滑動topview,則滑動dy偏移量
            scrollBy(0, dy);
            // 將ComboScrollLayout消耗的偏移量賦值給consumed數組
            consumed[1] = dy;
        }
    }

    @Override
    public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
        if (dyUnconsumed > 0) {
            if (target == topView) {
                // 由topView發起的向上滑動,繼續讓contentView滑動剩餘的未消耗完的偏移量
                scrollBy(0, dyUnconsumed);
            }
        }
    }

    @Override
    public void scrollTo(int x, int y) {
        // 將ComboScrollLayout自身的滾動範圍限制在0~topHeight(即在topview完全可見至完全不可見的範圍內滑動)
        if (y < 0) {
            y = 0;
        }
        if (y > topHeight) {
            y = topHeight;
        }
        super.scrollTo(x, y);
    }
}

ComboScrollLayout攔截滑動處理是在onNestedPreScrollonNestedScroll回調方法中,其中onNestedPreScroll觸發時機是在子view進行滑動之前,onNestedScroll是在子view滑動之後。

本例在onNestedPreScroll方法中,會判斷當前是否需要由ComboScrollLayout進行滾動,若判斷成立,則會消耗掉所有偏移量,子view將不再處理滾動,從而達到攔截的目的。

  1. 滑動停止
public class ComboScrollLayout extends LinearLayout implements NestedScrollingParent2 {
@Override
    public void onStopNestedScroll(@NonNull View target, int type) {
        parentHelper.onStopNestedScroll(target, type);
    }
}

當滑動結束時,將會觸發onStopNestedScroll方法,可以做一些收尾工作。本例中委託給NestedScrollingParentHelper的同名方法。

  1. SwipeRefreshLayout衝突處理

若要支持下拉刷新,需要在ComboScrollLayout外套一層SwipeRefreshLayout,需要處理和SwipeRefreshLayout下拉的滑動衝突。

首先獲取SwipeRefreshLayout引用:

public class ComboScrollLayout extends LinearLayout implements NestedScrollingParent2 {
    private SwipeRefreshLayout refreshLayout;
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (topView != null) {
            topHeight = topView.getMeasuredHeight();
        }
        // 獲取外層SwipeRefreshLayout
        if (refreshLayout == null && getParent() != null && getParent() instanceof SwipeRefreshLayout) {
            refreshLayout = (SwipeRefreshLayout) getParent();
        }
    }
}

在滑動開始/結束時啓用/禁用SwipeRefreshLayout:

public class ComboScrollLayout extends LinearLayout implements NestedScrollingParent2 {
	@Override
    public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
        // 省略部分代碼
        ...
        
        boolean handled = (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
        // 若爲垂直滾動方向,且topView未完全可見,應由ComboScrollLayout處理滑動,禁用SwipeRefreshLayout。
        if (handled && refreshLayout != null && getScrollY() != 0) {
            refreshLayout.setEnabled(false);
        }

        return handled;
    }
	
	@Override
    public void onStopNestedScroll(@NonNull View target, int type) {
        // 滑動結束,啓用SwipeRefreshLayout
        if (refreshLayout != null) {
            refreshLayout.setEnabled(true);
        }
        parentHelper.onStopNestedScroll(target, type);
    }
}

至此完成了自定義容器編寫,ComboScrollLayout實現了NestedScrollingParent2接口,從而能夠響應處理子view滑動事件,優先消耗掉滑動事件。

(ps:完整源碼見ComboScrollLayout.java

修改XML佈局

<?xml version="1.0" encoding="utf-8"?>
<com.cdh.nestedscrolling.widget.ComboSwipeRefreshLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/swipe_refresh"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".NestedScrollViewActivity">

    <com.cdh.nestedscrolling.widget.ComboScrollLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@id/combo_top_view"
            android:layout_width="match_parent"
            android:layout_height="300dp"
            app:layout_constraintTop_toTopOf="parent"/>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">

            <com.google.android.material.tabs.TabLayout
                android:id="@+id/tablayout"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"/>

            <androidx.viewpager2.widget.ViewPager2
                android:id="@id/combo_content_view"
                android:layout_width="match_parent"
                android:layout_height="match_parent"/>
        </LinearLayout>

    </com.cdh.nestedscrolling.widget.ComboScrollLayout>
</com.cdh.nestedscrolling.widget.ComboSwipeRefreshLayout>

(ps:ComboSwipeRefreshLayout繼承自SwipeRefreshLayout,重寫了onInterceptTouchEvent方法,用於處理嵌套橫向banner時的滑動衝突。完整源碼可見ComboSwipeRefreshLayout.java

自定義子視圖ComboChildLayout

父容器實現了NestedScrollingParent2接口,子view也需要實現NestedScrollingChild2接口,才能形成完整的交互。

  1. 定義關鍵成員變量
public class ComboChildLayout extends LinearLayout implements NestedScrollingChild2 {

	private int orientation;
    // touch滑動相關參數
    private int lastX = -1, lastY = -1;
    private final int[] offset = new int[2];
    private final int[] consumed = new int[2];

    // fling滑動相關參數
    private boolean isFling;
    private final int minFlingVelocity, maxFlingVelocity;
    private Scroller scroller;
    private VelocityTracker velocityTracker;
    private int lastFlingX, lastFlingY;
    private final int[] flingConsumed = new int[2];
    
	private NestedScrollingChildHelper childHelper = new NestedScrollingChildHelper(this);
    
	public ComboChildLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
		// 獲取佈局排布方向
        orientation = getOrientation();
        setNestedScrollingEnabled(true);
        // 獲取當前頁面配置信息
        ViewConfiguration config = ViewConfiguration.get(context);
        // 設置系統默認最小和最大加速度
        minFlingVelocity = config.getScaledMinimumFlingVelocity();
        maxFlingVelocity = config.getScaledMaximumFlingVelocity();
        scroller = new Scroller(context);
    }
}
  1. 繼承NestedScrollingChild2
public class ComboChildLayout extends LinearLayout implements NestedScrollingChild2 {
	@Override
    public boolean startNestedScroll(int axes, int type) {
        return childHelper.startNestedScroll(axes, type);
    }

    @Override
    public void stopNestedScroll(int type) {
        childHelper.stopNestedScroll(type);
    }

    @Override
    public boolean hasNestedScrollingParent(int type) {
        return childHelper.hasNestedScrollingParent(type);
    }
    
	@Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, int type) {
        if (orientation == VERTICAL) {
            dxUnconsumed = 0;
        } else {
            dyUnconsumed = 0;
        }
        return childHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, type);
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow, int type) {
        if (orientation == VERTICAL) {
            dx = 0;
        } else {
            dy = 0;
        }
        return childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
    }

    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        childHelper.setNestedScrollingEnabled(enabled);
    }

    @Override
    public boolean isNestedScrollingEnabled() {
        return childHelper.isNestedScrollingEnabled();
    }
}

這些方法中全部委託給NestedScrollingChildHelper輔助類相應的同名方法執行。

  1. 重寫onTouchEvent分發事件
public class ComboChildLayout extends LinearLayout implements NestedScrollingChild2 {
    @Override
    public boolean onTouchEvent(MotionEvent event) {
    	// 重置fling相關參數
        cancelFling();
        // 獲取VelocityTracker,用於加速度計算
        if (velocityTracker == null) {
            velocityTracker = VelocityTracker.obtain();
        }
        // 追蹤觸摸點移動加速度
        velocityTracker.addMovement(event);

        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                // 初始化值
                consumed[0] = 0;
                consumed[1] = 0;
                offset[0] = 0;
                offset[1] = 0;
                lastX = (int) event.getX();
                lastY = (int) event.getY();
                // 調用startNestedScroll通知parent根據滑動方向和滑動類型進行啓用嵌套滑動,
                // 當前屬於用戶觸摸滑動,type傳TYPE_TOUCH。
                if (orientation == VERTICAL) {
                	// 垂直方向滑動
                    startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
                } else {
                	// 水平方向滑動
                    startNestedScroll(ViewCompat.SCROLL_AXIS_HORIZONTAL, ViewCompat.TYPE_TOUCH);
                }
                break;

            case MotionEvent.ACTION_MOVE:
                int curX = (int) event.getX();
                int curY = (int) event.getY();
                // 計算滑動偏移量,起始座標-當前座標
                int dx = lastX - curX;
                int dy = lastY - curY;

                // 優先將滑動偏移量交由parent處理,
                if (dispatchNestedPreScroll(dx, dy, consumed, offset, ViewCompat.TYPE_TOUCH)) {
                    // parent滑動完後
                    // 滑動偏移量減去parent消耗的量
                    dx -= consumed[0];
                    dy -= consumed[1];
                }

                // 用於記錄子view自身滑動消耗的偏移量
                int consumedX = 0;
                int consumedY = 0;
                // 自身或child進行滑動
                if (orientation == VERTICAL) {
                    consumedY = childConsumedY(dy);
                } else {
                    consumedX = childConsumedX(dx);
                }

                // 滑動偏移量減去自身或child消耗的量,然後再交由parent處理
                dispatchNestedScroll(consumedX, consumedY, dx-consumedX, dy-consumedY, null, ViewCompat.TYPE_TOUCH);

                lastX = curX;
                lastY = curY;
                break;

            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                // 通知parent滑動結束
                stopNestedScroll(ViewCompat.TYPE_TOUCH);

                if (velocityTracker != null) {
                    // 計算觸摸點加速度
                    velocityTracker.computeCurrentVelocity(1000, maxFlingVelocity);
                    // 獲取xy軸加速度
                    int vx = (int) velocityTracker.getXVelocity();
                    int vy = (int) velocityTracker.getYVelocity();
                    // 進行fling
                    fling(vx, vy);
                    velocityTracker.clear();
                }

                lastX = -1;
                lastY = -1;
                break;

            default:
                break;
        }

        return true;
    }
}

在onTouchEvent方法中,首先在DOWN時通過通知parent對滑動進行判斷響應。之後在ACTION_MOVE過程中,計算滑動偏移量,優先交由parent進行消耗處理,若有parent接收處理,則在parent滑動後,減去parent消耗的偏移量,在交給自身或子view進行剩餘偏移量的滑動。若自身或子view滑動後還有剩餘的偏移量,則再交由parent處理。最後在UP/CANCEL通知parent滑動結束。

(ps:在本例中,UP/CANCEL後有進行fling操作,在fling中會再觸發startNestedScroll到stopNestedScroll的過程,不同的是傳遞的type變爲TYPE_NON_TOUCH。)

自定義子視圖完整源碼可見ComboChildLayout.java

  1. ComboChildLayout使用場景
    ComboChildLayout不是用來包裝ViewPager2,而是當ViewPager2有某一頁爲普通view時,用來包裹該頁的根佈局。
    本例在ViewPager2中添加了三個Fragment,分別演示ViewPager2下爲普通線性佈局、滾動佈局、列表佈局的情況。其中普通線性佈局需要使用ComboChildLayout進行包裹,而滾動佈局和列表佈局因爲使用NestedScrollView和RecyclerView,其自身實現了NestedScrollingChild2接口,所以不需要額外操作。

完整項目地址

github:ComboScrollLayout

發佈了27 篇原創文章 · 獲贊 2 · 訪問量 8506
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章