AppBarLayout是material包里面提供的容器组件,可用于实现MD风格的页面滑动动效。
在开发场景中,我们经常使用CoordinatorLayout+AppBarLayout来实现页面头部的折叠和吸顶效果。在滑动过程中,View的行为随offset变化,View之间存在关联变化。
关于AppBarLayout的使用和嵌套滑动原理,可以见参考文档前三篇。
当AppBarLayout和Webview共同使用时,会出现一个问题,AppBarLayout无法折叠了,Webview只能在较小的固定区域内上下滑动,可以理解为两者上下滑动冲突了。
使用AppBarLayout,我们一般是通过addOffsetChangedListener来监听嵌套滑动的位移的,从而修改AppBarLayout的appbarState,或者做一些透明度等视觉上的修改。但在这个场景下,offset始终为0,说明滑动事件并没有那我们预期处理。
滑动冲突问题的本质,是滑动事件的分发和消费不对。
在AppBarLayout中,是通过Behavior实现嵌套滑动的:若Behavior所附属的View存在可以滑动的子View,而且正在滑动的View足够大去滑动的话,就接受嵌套滑动事件.
那么问题就变成了,我们的WebView没有实现嵌套滑动机制。
实现嵌套滑动机制,有两种方式:
方式2在实现过程中,发现在WebView的惯性滑动下,滑动到顶也无法将AppBarLayout拉下来,触顶后必须再下滑一次才能将AppBarLayout拉下来,因此处理这个问题,需要额外增加一些处理逻辑.
添加一个速度追踪器(VelocityTracker)记录滑动速度,然后在ACTION_UP触摸事件出现时,获得这个速度,在ViewConfiguration中获得最大的滑动速度,然后再调用OverScroller来计算惯性滑动的距离,并且调用View的刷新从而实现调用computeScroll方法,然后在computeScroll方法中根据OverScroller计算的应该滑动的距离滑动到指定位置.
private final int[] mScrollConsumed = new int[2];
private final int[] mScrollOffset = new int[2];
private int mLastMotionY;
private VelocityTracker mVelocityTracker;
private int mMinimumVelocity;
private int mMaximumVelocity;
private OverScroller mScroller;
private int mLastScrollerY;
private final NestedScrollingChildHelper mChildHelper;
// 初始化
public NestedScrollWebView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mChildHelper = new NestedScrollingChildHelper(this);
setNestedScrollingEnabled(true);
mScroller = new OverScroller(getContext());
final ViewConfiguration configuration = ViewConfiguration.get(getContext());
mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
}
private void initVelocityTrackerIfNotExists() {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
}
private void recycleVelocityTracker() {
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
public void fling(int velocityY) {
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH);
mScroller.fling(getScrollX(), getScrollY(), // start
0, velocityY, // velocities
0, 0, // x
Integer.MIN_VALUE, Integer.MAX_VALUE, // y
0, 0); // overscroll
mLastScrollerY = getScrollY();
ViewCompat.postInvalidateOnAnimation(this);
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
final int x = mScroller.getCurrX();
final int y = mScroller.getCurrY();
Log.d(TAG, "computeScroll: y : " + y);
int dy = y - mLastScrollerY;
if (dy != 0) {
int scrollY = getScrollY();
int dyUnConsumed = 0;
int consumedY = dy;
if (scrollY == 0) {
dyUnConsumed = dy;
consumedY = 0;
} else if (scrollY + dy < 0) {
dyUnConsumed = dy + scrollY;
consumedY = -scrollY;
}
if (!dispatchNestedScroll(0, consumedY, 0, dyUnConsumed, null,
ViewCompat.TYPE_NON_TOUCH)) {
}
}
// Finally update the scroll positions and post an invalidation
mLastScrollerY = y;
ViewCompat.postInvalidateOnAnimation(this);
} else {
// We can't scroll any more, so stop any indirect scrolling
if (hasNestedScrollingParent(ViewCompat.TYPE_NON_TOUCH)) {
stopNestedScroll(ViewCompat.TYPE_NON_TOUCH);
}
// and reset the scroller y
mLastScrollerY = 0;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
initVelocityTrackerIfNotExists();
MotionEvent vtev = MotionEvent.obtain(event);
final int actionMasked = event.getAction();
switch (actionMasked) {
case MotionEvent.ACTION_DOWN:
mLastMotionY = (int) event.getRawY();
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
mVelocityTracker.addMovement(vtev);
mScroller.computeScrollOffset();
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_UP:
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) velocityTracker.getYVelocity();
if (Math.abs(initialVelocity) > mMinimumVelocity) {
fling(-initialVelocity);
}
case MotionEvent.ACTION_CANCEL:
stopNestedScroll();
recycleVelocityTracker();
break;
case MotionEvent.ACTION_MOVE:
final int y = (int) event.getRawY();
int deltaY = mLastMotionY - y;
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
vtev.offsetLocation(0, mScrollConsumed[1]);
}
mLastMotionY = y;
int scrollY = getScrollY();
int dyUnconsumed = 0;
if (scrollY == 0) {
dyUnconsumed = deltaY;
} else if (scrollY + deltaY < 0) {
dyUnconsumed = deltaY + scrollY;
vtev.offsetLocation(0, -dyUnconsumed);
}
mVelocityTracker.addMovement(vtev);
boolean result = super.onTouchEvent(vtev);
if (dispatchNestedScroll(0, deltaY - dyUnconsumed, 0, dyUnconsumed, mScrollOffset)) {
}
return result;
default:
break;
}
return super.onTouchEvent(vtev);
}
// NestedScrollingChild 嵌套滑动接口
@Override
public void setNestedScrollingEnabled(boolean enabled) {
mChildHelper.setNestedScrollingEnabled(enabled);
}
@Override
public boolean isNestedScrollingEnabled() {
return mChildHelper.isNestedScrollingEnabled();
}
@Override
public boolean startNestedScroll(int axes) {
return mChildHelper.startNestedScroll(axes);
}
@Override
public boolean startNestedScroll(int axes, int type) {
return mChildHelper.startNestedScroll(axes, type);
}
@Override
public void stopNestedScroll() {
mChildHelper.stopNestedScroll();
}
@Override
public void stopNestedScroll(int type) {
mChildHelper.stopNestedScroll(type);
}
@Override
public boolean hasNestedScrollingParent() {
return mChildHelper.hasNestedScrollingParent();
}
@Override
public boolean hasNestedScrollingParent(int type) {
return mChildHelper.hasNestedScrollingParent(type);
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow) {
return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
offsetInWindow);
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow, int type) {
return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
offsetInWindow, type);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
int type) {
return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
}
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
}
基于AppBarLayout实现二级吸顶&踩坑记录
Android NestedScroll嵌套滑动机制解析
Android CoordinatorLayout和Behavior解析
WebView和AppBarLayout嵌套滑动联动无效分析及解决办法