小文字 吃饭,睡觉,溜狗头

Make your OverScrollLayout

img

前言

手势处理在自定义控件中随处可见,诸如下拉,侧滑,加载更多等等,本文将利用手势相关的知识,来实现一个类似微信文章中过渡滑动交互。

下滑的处理在很早之前也写过几篇分别是简单的下拉,侧滑,原理基本类似,主要依赖的是offsetTopBotom,offsetLeftRight,Scroller;

在处理OverScroll的时候需要正确决定手势是否被拦截用于,滑动视图,或者分发至系统时间响应,根据内容区视图是否能滑动也有细节上的处理,这一块可以参照SwpieRefreshLayout的处理,即提供一个检测方法canChildScroolUp,针对内容view的类型单独处理;

下面是最终实现的效果:
WebView RecyclerView

控件定义

首先考虑一下,这个效果的UI组成,当滑动到顶部时可以继续滑动,展示底部的视图;因此滑动主要解决的是视图在Y轴上的偏移:

Layout

  • 区分内容视图和背景视图 允许视图内包含一个背景View和一个内容View,为便于理解,参照FrameLayout,xml内第一个的view作为底视图,第二个作为内容视图 对子View的关系约定,可以放在合适的地方,比如inflate,或者第一次measure,layout时
if (count > 1) {
mForegroundView = getChildAt(1);
mBackgroundView = getChildAt(0);
} else {
mForegroundView = getChildAt(0);
}
  • 视图排版 初始化视图,后需要根据规范对View的measure,layout响应处理
    @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int count = getChildCount();
log("onMeasure: count=" + count);
if (count > 1) {
mForegroundView = getChildAt(1);
mBackgroundView = getChildAt(0);
} else {
mForegroundView = getChildAt(0);
}
if (mBackgroundView != null) {
measureChildWithMargins(mBackgroundView, widthMeasureSpec, 0, heightMeasureSpec, 0);
}
if (mForegroundView != null) {
measureChildWithMargins(mForegroundView, widthMeasureSpec, 0, heightMeasureSpec, 0);
mHeightLimitMax = (int) (mForegroundView.getMeasuredHeight() * mMaxOverPercent);
if (mForegroundView instanceof ScrollIntercept) {
((ScrollIntercept) mForegroundView).attach(this);
}
}
if (mForegroundView != null && mBackgroundView != null) {
mForegroundView.bringToFront();
}
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mBackgroundView != null) {
MarginLayoutParams layoutParams = (MarginLayoutParams) mBackgroundView.getLayoutParams();
int left = layoutParams.leftMargin + getPaddingLeft();
int top = layoutParams.topMargin + getPaddingTop();
int right = left + mBackgroundView.getMeasuredWidth();
int bottom = top + mBackgroundView.getMeasuredHeight();
mBackgroundView.layout(left, top, right, bottom);
log("onLayout, mBackgroudView = " + mBackgroundView.toString() +
", l=" + left + ",t=" + top + ", r=" + right + ",b=" + bottom);
}
if (mForegroundView != null) {
MarginLayoutParams layoutParams = (MarginLayoutParams) mForegroundView.getLayoutParams();
int left = layoutParams.leftMargin + getPaddingLeft();
int top = layoutParams.topMargin + getPaddingTop();
int right = left + mForegroundView.getMeasuredWidth();
int bottom = top + mForegroundView.getMeasuredHeight();
mForegroundView.layout(left, top, right, bottom);
log("onLayout, mForegroundView = " + mForegroundView.toString() +
", l=" + left + ",t=" + top + ", r=" + right + ",b=" + bottom);
}
}
  • 重载onInterceptTouchEvent 当手势在滑动且是上下滑动,超过touchslop时,开始拦截;
    @Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
log("onInterceptTouchEvent:" + ev.toString());
if (!isEnabled() || canChildScrollUp()) {
//make child layout to determine the scroll state
if (mForegroundView instanceof ScrollIntercept &&
(ev.getAction() == MotionEvent.ACTION_CANCEL || ev.getAction() == MotionEvent.ACTION_UP)) {
log("try tryScrollToTop");
((ScrollIntercept) mForegroundView).tryScrollToTop(this);
}
return false;
}
final int action = ev.getAction();
if (action != MotionEvent.ACTION_DOWN && isSliding) return true;
switch (action) {
case MotionEvent.ACTION_DOWN:
isSliding = false;
mX = ev.getX();
mY = ev.getY();
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
isSliding = false;
break;
case MotionEvent.ACTION_MOVE:
float dy = ev.getY() - mY;
log("onInterceptTouchEvent, canChildScrollUp=" + canChildScrollUp());
if (dy > mTouchSlop && !isSliding) {
mY = ev.getY();
mX = ev.getX();
isSliding = true;
log("dy=" + dy);
}
break;

}
log("onInterceptTouchEvent, isSliding=" + isSliding);
return isSliding;
}
  • 重载onTouch 这里可以开始根据move的具体值,调整view的offset
    @Override
public boolean onTouchEvent(MotionEvent event) {
log("onTouchEvent:" + event.toString());
if (!isEnabled() || canChildScrollUp()) {
// Fail fast if we're not in a state where a swipe is possible
return false;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mX = event.getX();
mY = event.getY();
isSliding = false;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
case MotionEvent.ACTION_MOVE:
float offsetX = event.getX() - mX;
float offsetY = event.getY() - mY;
log("move, offsetX=" + offsetX + ", offsetY=" + offsetY + ", mTouchSlop=" + mTouchSlop);
if (isSliding) {
if (offsetY > 0) {
scrollY((int) offsetY);
mX = event.getX();
mY = event.getY();
} else {
return false;
}
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
int currentY = mForegroundView.getTop();
int gap = (int) (mForegroundView.getHeight() * mOverPercent);
if (currentY + event.getY() - mY >= gap / 2) {
int duration = (int) (Math.abs(0 - currentY + 0.5f) / mForegroundView.getHeight() * 1000);
log("duration=" + duration);
mScroller.startScroll(0, currentY, 0, 0 - currentY, duration);
} else {
int duration = (int) (Math.abs(-gap - currentY + 0.5f) / mForegroundView.getHeight() * 1000);
log("duration=" + duration);
mScroller.startScroll(0, currentY, 0, -gap - currentY, duration);
}
isSliding = false;
invalidate();
return false;
}
return true;
}
  • 支持WebView 需要注意的是WebView在JellyBean之后内部实现有了很大变化,参考其源码实现可以证实,大量的具体逻辑都已经委托个了一个Provider实现类,这直接导致webview无法和其他view一样检测出其滑动位置;因此为了兼容WebView,需要在WebView内的OverScroll回调内手动再次处理offset,这一点实际上时参考了ChrisBean的Pull2Refresh的实现;
    @Override
public void tryScrollToTop(ScrollOver layout) {
log("tryScrollToTop:" + mScrolledY);
if (mScrolledY != 0) {
mLayout.smooth2Top();
mScrolledY = 0;
}
}

@Override
protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {
log("overScrollBy: deltaX=" + deltaX + ", deltaY=" + deltaY + ", scrollX=" + scrollX + ", scrollRangeX=" + scrollRangeX);
final boolean returnValue = super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX,
scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent);

if (mLayout != null) {
mScrolledY += deltaY;
mLayout.scrollY(-deltaY);
}
return returnValue;
}

小结

在处理手势的时候进场会遇到效果不一致,这时候通过适量的log,可以更好地帮助开发者理解问题的出处; 完整的代码可以参考在github上的地址,欢迎各种star,for,issue:

https://github.com/avenwu/overscrolllayout

参考

  • SwipeRefreshLayout
  • Action-PullToRefresh