在前面的兩片文章中咱們瞭解了 NestedScroll 的相關接口及通常處理邏輯。在本篇文章中就實現一個具體的聯合滑動需求。php
Android中常常在佈局中嵌入 WebView 來展現網頁內容,並且WebView內部還有交互邏輯(滾動之類的),若是外部佈局也要處理滾動邏輯,就會有滑動衝突,這種場景在實際項目開發中很常見,例如在含有 AppBarLayout
的 CoordinatorLayout
中嵌入一個 WebView , WebView 底部再放一個 footer 放置收藏按鈕等,須要在向上滑動時首先保持 WebView 跟隨 AppBarLayout
滑動,在 AppBarLayout
滑出屏幕以後, WebView 全屏展現,繼續滑動 WebView ,WebView 劃到底以後將 WebView 及 footer 一塊兒向上繼續滑動。實際效果以下圖:java
針對此需求,根據 CoordinatorLayout
及 AppBarLayout
的瞭解,咱們能夠將 WebView 放在 CoordinatorLayout
的一個子layout裏,並將該layout的 layout_behavior
設爲 appbar_scrolling_view_behavior
,便可實現滑動時維持 WebView 在 AppBarLayout
底部並跟隨滑動直至 AppBarLayout
滑出頂部 WebView 全屏展現。android
可是如何在 WebView 全屏展現以後可以繼續滑動 WebView 內容直至不能滑動,拖動出 footer 呢。git
一種比較簡單的作法是,將 WebView 及 footer 放在一個自定義的layout裏,編程實現WebView的內容滾動及整個佈局的滾動( WebView 劃到底以後滾動佈局)。layout文件以下:github
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/activity_root" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:clipChildren="false" android:background="#ffffff" android:fitsSystemWindows="true">
<android.support.design.widget.CoordinatorLayout android:id="@+id/preview_coordinator_container" android:layout_width="match_parent" android:layout_height="match_parent" android:clipChildren="false" android:fitsSystemWindows="true">
<android.support.design.widget.AppBarLayout android:id="@+id/preview_app_bar" android:layout_width="match_parent" android:layout_height="wrap_content" android:clipChildren="false" app:elevation="0dp">
<RelativeLayout android:id="@+id/title_bar" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/colorPrimary" app:layout_scrollFlags="scroll">
<TextView android:id="@+id/text_title" android:layout_width="wrap_content" android:layout_height="50dp" android:layout_alignParentTop="true" android:gravity="center" android:textColor="#ffffff" android:textStyle="bold" android:textSize="18sp" android:text="隨便寫個標題" android:layout_centerHorizontal="true"/>
</RelativeLayout>
</android.support.design.widget.AppBarLayout>
<FrameLayout android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior">
<com.lwons.nestedscrollexample.ScrollingContent android:id="@+id/scrolling_content" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#ffffff" android:orientation="vertical">
</com.lwons.nestedscrollexample.ScrollingContent>
</FrameLayout>
</android.support.design.widget.CoordinatorLayout>
</LinearLayout>
複製代碼
這裏 com.lwons.nestedscrollexample.ScrollingContent
是基於 LinearLayout
的自定義佈局。裏面放置了height爲 MATCH_PARENT
的 WebView 及height爲實際高度的 footer 。編程
而爲了避免影響 WebView 及 footer 的點擊事件,咱們須要儘可能只攔截處理滑動相關的事件,這裏須要在自定義佈局的 onInterceptTouchEvent
中過濾 MotionEvent
。以下:app
private int mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
private float mLastY;
private boolean mIsDraging;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
mIsDraging = false;
return false;
}
switch (action) {
case MotionEvent.ACTION_MOVE: {
if (mIsDraging) {
return true;
}
final float yoff = Math.abs(mLastY - ev.getRawY());
if (yoff > mTouchSlop) {
// 只有手指滑動距離大於閾值時,纔會開始攔截
// Start scrolling!
getParent().requestDisallowInterceptTouchEvent(true);
return true;
}
break;
}
case MotionEvent.ACTION_DOWN:
mLastY = ev.getRawY();
break;
}
return false;
}
複製代碼
這樣自定義佈局就可以攔截到滑動事件,並可以獲得每一步的滑動距離,可是如何處理這個滑動距離呢。ide
回顧 NestedScroll 接口的使用方式及特色,咱們在自定義佈局中攔截了滑動事件以後須要與外部佈局聯動,而發起聯動及控制聯動的一方是 NestedScrollingChild
(後面簡稱NC),所以咱們須要在自定義佈局裏實現 NestedScrollingChild
相關的接口並控制滑動邏輯,而 NestedScrollingParent
(後面簡稱NP)是哪一個佈局呢, 從CoordinatorLayout
的代碼中咱們能夠得知NP就是CoordinatorLayout
,它會處理AppBarLayout
的滑動。佈局
聯繫需求的滑動交互詳情,咱們在向上滑動時,首先須要滑動AppBarLayout
並使 WebView 跟隨滑動,這一部分CoordinatorLayout
會幫咱們實現,咱們只須要調用dispatchNestedPreScroll
通知CoordinatorLayout
就好了。而後AppBarLayout
滑出頂部以後,須要繼續滾動 WebView ,這一部分須要咱們本身處理,只須要調用 WebView 的scrollBy
接口便可。在 WebView 沒法滑動時,咱們須要滾動整個自定義佈局,這裏也簡單,調用自定義佈局的scrollBy
接口便可,它會使得 WebView 和 footer 總體向上滾動。spa
分析到這裏,整個向上滑動的操做過程就已經很清楚了。而向下的過程與向上基本相同。
處理邏輯的代碼以下:
@Override
public boolean onTouchEvent(MotionEvent ev) {
boolean returnValue = false;
MotionEvent event = MotionEvent.obtain(ev);
final int action = MotionEventCompat.getActionMasked(event);
float eventY = event.getRawY();
switch (action) {
case MotionEvent.ACTION_MOVE:
if (getScrollState() == SCROLL_STAT_SCROLLING) {
stopScroll();
}
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
if (!mIsDraging) {
mIsDraging = true;
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
}
// 滑動距離
int deltaY = (int) (mLastY - eventY);
mLastY = eventY;
// 通知NP先進行滑動,這裏CoordinatorLayout會滾動AppBarLayout及當前ScrollingContent佈局
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset, ViewCompat.TYPE_TOUCH)) {
deltaY -= mScrollConsumed[1]; // mScrollConsumed[1]爲CoordinatorLayout消耗掉的距離
event.offsetLocation(0, -mScrollOffset[1]);
}
mVelocityTracker.addMovement(event);
// 處理當前佈局自己的滾動邏輯
int scrollInternalY = 0;
if (deltaY != 0) {
scrollInternalY = scrollY(deltaY);
deltaY -= scrollInternalY;
}
// 若是滑動距離尚未消耗徹底,通知NP繼續處理(NP能夠選擇處理或者不處理)
if (deltaY != 0) {
dispatchNestedScroll(0, mScrollConsumed[1]+scrollInternalY, 0, deltaY, mScrollOffset, ViewCompat.TYPE_TOUCH);
}
returnValue = true;
break;
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
returnValue = true;
mVelocityTracker.computeCurrentVelocity(1000);
// fling邏輯
onFlingY((int) -mVelocityTracker.getYVelocity());
mVelocityTracker.clear();
mIsDraging = false;
// 中止手指拖拽的滑動
stopNestedScroll(ViewCompat.TYPE_TOUCH);
break;
}
return returnValue;
}
/** * 內部滾動邏輯 * @param deltaY 當前未消耗的滑動距離 * @return 內部滾動消耗掉的滑動距離 */
private int scrollY(int deltaY) {
int remainY = deltaY;
int consumedY = 0;
if (remainY > 0) {
// 向上滑動
if (mWebview != null && mWebview.canScrollUp() > 0) {
// WebView還能繼續向上滾動
int readerScroll = Math.min(mWebview.canScrollUp(), remainY);
mWebview.scrollBy(0, readerScroll);
remainY -= readerScroll;
consumedY += readerScroll;
}
if (remainY > 0 && getScrollY() < mFooter.getHeight()) {
// 當前佈局還能繼續向上滾動
int layoutScroll = Math.min(mFooter.getHeight() - getScrollY(), remainY);
scrollBy(0, layoutScroll);
consumedY += layoutScroll;
}
} else {
// 向下滑動
if (getScrollY() > 0) {
// 當前佈局還能繼續向下滾動
int layoutScroll = Math.max(-getScrollY(), remainY);
scrollBy(0, layoutScroll);
remainY -= layoutScroll;
consumedY += layoutScroll;
}
if (mWebview != null && mWebview.canScrollDown() > 0) {
// WebView還能繼續向下滾動
int readerScroll = Math.max(-mWebview.canScrollDown(), remainY);
mWebview.scrollBy(0, readerScroll);
consumedY += readerScroll;
}
}
return consumedY;
}
複製代碼
針對此需求,已建立了一個完整的Android工程放在GitHub上: 樣例工程GitHub地址
能夠直接下載apk運行查看效果: 樣例apk下載