文章目錄
前言
目前主流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 theNestedScrollingParentHelper
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 theNestedScrollingChildHelper
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
- 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) {
}
}
- 初始成員變量
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);
}
}
- 判斷處理滑動的方向
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方法中緩存的進行攔截處理的座標軸。
- 攔截滑動處理
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攔截滑動處理是在onNestedPreScroll和onNestedScroll回調方法中,其中onNestedPreScroll觸發時機是在子view進行滑動之前,onNestedScroll是在子view滑動之後。
本例在onNestedPreScroll方法中,會判斷當前是否需要由ComboScrollLayout進行滾動,若判斷成立,則會消耗掉所有偏移量,子view將不再處理滾動,從而達到攔截的目的。
- 滑動停止
public class ComboScrollLayout extends LinearLayout implements NestedScrollingParent2 {
@Override
public void onStopNestedScroll(@NonNull View target, int type) {
parentHelper.onStopNestedScroll(target, type);
}
}
當滑動結束時,將會觸發onStopNestedScroll方法,可以做一些收尾工作。本例中委託給NestedScrollingParentHelper的同名方法。
- 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接口,才能形成完整的交互。
- 定義關鍵成員變量
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);
}
}
- 繼承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輔助類相應的同名方法執行。
- 重寫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
- ComboChildLayout使用場景
ComboChildLayout不是用來包裝ViewPager2,而是當ViewPager2有某一頁爲普通view時,用來包裹該頁的根佈局。
本例在ViewPager2中添加了三個Fragment,分別演示ViewPager2下爲普通線性佈局、滾動佈局、列表佈局的情況。其中普通線性佈局需要使用ComboChildLayout進行包裹,而滾動佈局和列表佈局因爲使用NestedScrollView和RecyclerView,其自身實現了NestedScrollingChild2接口,所以不需要額外操作。