在前面的两片文章中咱们了解了 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下载