实际需求

在前面的两片文章中我们了解了 NestedScroll 的相关接口及一般处理逻辑。在本篇文章中就实现一个具体的联合滑动需求。

Android中经常在布局中嵌入 WebView 来展示网页内容,而且WebView内部还有交互逻辑(滚动之类的),如果外部布局也要处理滚动逻辑,就会有滑动冲突,这种场景在实际项目开发中很常见,例如在含有 AppBarLayoutCoordinatorLayout 中嵌入一个 WebViewWebView 底部再放一个 footer 放置收藏按钮等,需要在向上滑动时首先保持 WebView 跟随 AppBarLayout 滑动,在 AppBarLayout 滑出屏幕之后, WebView 全屏展示,继续滑动 WebViewWebView 划到底之后将 WebViewfooter 一起向上继续滑动。实际效果如下图:

需求解析

针对此需求,根据 CoordinatorLayoutAppBarLayout 的了解,我们可以将 WebView 放在 CoordinatorLayout 的一个子layout里,并将该layout的 layout_behavior 设为 appbar_scrolling_view_behavior,即可实现滑动时维持 WebViewAppBarLayout 底部并跟随滑动直至 AppBarLayout 滑出顶部 WebView 全屏展示。

但是如何在 WebView 全屏展示之后能够继续滑动 WebView 内容直至不能滑动,拖动出 footer 呢。

一种比较简单的做法是,将 WebViewfooter 放在一个自定义的layout里,编程实现WebView的内容滚动及整个布局的滚动( WebView 划到底之后滚动布局)。layout文件如下:

<?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 。如下:

private int mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();private float mLastY;private boolean mIsDraging;@Overridepublic 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;}

这样自定义布局就能够拦截到滑动事件,并能够得到每一步的滑动距离,但是如何处理这个滑动距离呢。

回顾 NestedScroll 接口的使用方式及特点,我们在自定义布局中拦截了滑动事件之后需要与外部布局联动,而发起联动及控制联动的一方是 NestedScrollingChild (后面简称NC),因此我们需要在自定义布局里实现 NestedScrollingChild 相关的接口并控制滑动逻辑,而 NestedScrollingParent (后面简称NP)是哪个布局呢, 从CoordinatorLayout的代码中我们可以得知NP就是CoordinatorLayout,它会处理AppBarLayout的滑动。

联系需求的滑动交互详情,我们在向上滑动时,首先需要滑动AppBarLayout并使 WebView 跟随滑动,这一部分CoordinatorLayout会帮我们实现,我们只需要调用dispatchNestedPreScroll通知CoordinatorLayout就行了。然后AppBarLayout滑出顶部之后,需要继续滚动 WebView ,这一部分需要我们自己处理,只需要调用 WebViewscrollBy接口即可。在 WebView 无法滑动时,我们需要滚动整个自定义布局,这里也简单,调用自定义布局的scrollBy接口即可,它会使得 WebViewfooter 整体向上滚动。

分析到这里,整个向上滑动的操作过程就已经很清楚了。而向下的过程与向上基本相同。

处理逻辑的代码如下:

@Overridepublic 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下载

更多相关文章

  1. Android本地视频播放器开发--搜索本地视频(2)
  2. android xml tools 介绍(一)
  3. Android的Adapter用法总结
  4. 关于GridView宽高的问题(转载rain的文章)
  5. Android(安卓)布局简要范例
  6. Android(安卓)组件系列-----Activity初步
  7. android 的布局单位 dip dp sp px总结
  8. android,闹钟定时功能,实现过程
  9. android的帧布局,绝对布局,相对布局,表格布局

随机推荐

  1. 【女神节有奖征文】付出行动才有所回报!
  2. MPLS系列之二:MPLS *** 静态路由、RIP、EI
  3. .net项目开发经验
  4. MPLS系列之一:Multi Protocol Label Switc
  5. TCPIP卷一(12):EIGRP的负载均衡方式 And Rou
  6. TCPIP卷一(11):EIGRP的汇总、stub、leak-map
  7. 价值
  8. TCPIP卷一(9):EIGRP的数据包格式、TLV、 三
  9. 容器?
  10. TCPIP卷一(7):Routing Information Protocol