Android NestedScrolling解決滑動衝突問題(3) - 項目實戰

實際需求

在前面的兩片文章中咱們瞭解了 NestedScroll 的相關接口及通常處理邏輯。在本篇文章中就實現一個具體的聯合滑動需求。php

Android中常常在佈局中嵌入 WebView 來展現網頁內容,並且WebView內部還有交互邏輯(滾動之類的),若是外部佈局也要處理滾動邏輯,就會有滑動衝突,這種場景在實際項目開發中很常見,例如在含有 AppBarLayoutCoordinatorLayout 中嵌入一個 WebViewWebView 底部再放一個 footer 放置收藏按鈕等,須要在向上滑動時首先保持 WebView 跟隨 AppBarLayout 滑動,在 AppBarLayout 滑出屏幕以後, WebView 全屏展現,繼續滑動 WebViewWebView 劃到底以後將 WebViewfooter 一塊兒向上繼續滑動。實際效果以下圖:java

實際滑動效果

需求解析

針對此需求,根據 CoordinatorLayoutAppBarLayout 的瞭解,咱們能夠將 WebView 放在 CoordinatorLayout 的一個子layout裏,並將該layout的 layout_behavior 設爲 appbar_scrolling_view_behavior,便可實現滑動時維持 WebViewAppBarLayout 底部並跟隨滑動直至 AppBarLayout 滑出頂部 WebView 全屏展現。android

可是如何在 WebView 全屏展現以後可以繼續滑動 WebView 內容直至不能滑動,拖動出 footer 呢。git

一種比較簡單的作法是,將 WebViewfooter 放在一個自定義的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_PARENTWebView 及height爲實際高度的 footer編程

而爲了避免影響 WebViewfooter 的點擊事件,咱們須要儘可能只攔截處理滑動相關的事件,這裏須要在自定義佈局的 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 ,這一部分須要咱們本身處理,只須要調用 WebViewscrollBy接口便可。在 WebView 沒法滑動時,咱們須要滾動整個自定義佈局,這裏也簡單,調用自定義佈局的scrollBy接口便可,它會使得 WebViewfooter 總體向上滾動。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下載

相關文章
相關標籤/搜索