效果如下:
device-2017-12-04-170020.gif
分为两部分:1. View的创建。 2. 滑动事件处理
1. View的创建
从页面上看,主要分为上下两部分,上部为滚动的Webview,底部为拉出来的CloseView
我这里自定义了ViewGrup。初始状态Webview撑满整个屏幕,CloseView不可见,位于Webview底部。代码如下
public class PullupCloseLayout extends ViewGroup {
public final static int SIZE_DEFAULT_HEIGHT = 100;
// 手势滑动view
private View mTarget;
//底部上拉关闭view
private ViewGroup mPullUpView;
//滑动关闭页面的最大高度
private int mPullUpViewMaxHeight;
public PullupCloseLayout(Context context) {
this(context, null);
}
public PullupCloseLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context context) {
//为底部CloseView
mPullUpView = (ViewGroup) LayoutInflater.from(context).inflate(R.layout.pull_up_close, this);
final DisplayMetrics metrics = getResources().getDisplayMetrics();
mPullUpViewMaxHeight = (int) (SIZE_DEFAULT_HEIGHT * metrics.density);
}
@Override
protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
final int width = getMeasuredWidth();
final int height = getMeasuredHeight();
if (mTarget == null) {
ensureView();
}
if (mTarget == null) {
return;
}
//WebView撑满屏幕
mTarget.layout(getPaddingLeft(), getPaddingTop(), width - getPaddingRight(), height - getPaddingBottom());
//CloseView在 Webview底部
mPullUpView.layout(0, height - getPaddingBottom(), width, height - getPaddingBottom() + mPullUpView.getMeasuredHeight());
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mTarget == null) {
ensureView();
}
if (mTarget == null) {
return;
}
//设置Webview的高度撑满全屏
mTarget.measure(MeasureSpec.makeMeasureSpec(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY));
//设置CloseView 为固定高度
mPullUpView.measure(MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(mPullUpViewMaxHeight, MeasureSpec.EXACTLY));
}
//初始化内部滚动view, 参考v4 SwipRefreshLayout
private void ensureView() {
if (mTarget == null) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (!child.equals(mPullUpView)) {
mTarget = child;
break;
}
}
}
}
}
此时页面布局完成。接下来第二部处理滑动事件
2. 滑动事件处理
滑动事件主要处理两个状态, 1. 滑动到底部,可以随手势上滑,松手可回弹。 2. 可以随惯性滑动并回弹
- 手势上滑及回弹。
判断当页面滑到底部不能继续滑动的时候由本布局拦截手势, 并消费掉。 否则不了拦截。
如何判断滑动到底部?
private boolean canChildScrollUp() {
// 参数为正则代表向上是否可滑动,负数则为向下, 一般用1和-1代表
return ViewCompat.canScrollVertically(mTarget, 1);
}
拦截滚动事件的代码如下
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
if (canChildScrollUp() || action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
return false;
}
switch (action) {
case MotionEvent.ACTION_DOWN:
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
final float initialDownY = getMotionEventY(ev, mActivePointerId);
if (initialDownY == -1) {
return false;
}
//记录按下的位置
mInitialDownY = initialDownY;
break;
case MotionEvent.ACTION_MOVE:
final float y = getMotionEventY(ev, mActivePointerId);
if (y == -1) {
return false;
}
//判断滚动的距离
final float yDiff = mInitialDownY - y;
//如果滚动距离>自定义的阈值,则认为需要跟随手势滚动了,此时开始拦截。
if (yDiff > mTouchSlop && !mIsBeingDragged) {
mInitialMotionY = mInitialDownY + mTouchSlop;
mIsBeingDragged = true;
}
break;
case MotionEvent.ACTION_CANCEL:
mIsBeingDragged = false;
mActivePointerId = -1;
break;
}
return mIsBeingDragged;
}
消费手势 如下
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (canChildScrollUp()) {
return false;
}
final int action = MotionEventCompat.getActionMasked(ev);
int pointerIndex;
switch (action) {
case MotionEvent.ACTION_DOWN:
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
mIsBeingDragged = false;
break;
case MotionEvent.ACTION_MOVE: {
pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
if (pointerIndex < 0) {
return false;
}
final float y = MotionEventCompat.getY(ev, pointerIndex);
//设置滚动的阻力 0.5倍系数
final int overscrollTop = (int) ((mInitialMotionY - y) * 0.5);
if (mIsBeingDragged) {//消费滑动事件
if (overscrollTop > 0) {
moveSpinner(overscrollTop);
} else {
return false;
}
}
break;
}
case MotionEventCompat.ACTION_POINTER_DOWN: {
pointerIndex = MotionEventCompat.getActionIndex(ev);
if (pointerIndex < 0) {
Log.e(TAG, "Got ACTION_POINTER_DOWN event but have an invalid action index.");
return false;
}
mActivePointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
break;
}
case MotionEventCompat.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
Log.i(TAG, "ACTION_UP");
break;
case MotionEvent.ACTION_UP:
pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
if (pointerIndex < 0) {
return false;
}
mIsBeingDragged = false;
mActivePointerId = -1;
finishSpinner();
Log.i(TAG, "ACTION_UP");
break;
}
return true;
}
// 手势移动,滚动当前view,并切换底部关闭按钮的状态
private void moveSpinner(int overscrollTop) {
scrollBy(0, overscrollTop - getScrollY());
updatePullUpViewState();
}
//手势抬起,开始回弹动画并回调是否关闭页面
private void finishSpinner() {
if (getScrollY() > 0) {
scrollBackAnimator(getScrollY());
}
//上拉回调。
if (mPullUpListener != null) {
mPullUpListener.pullUp(mCanClose);
}
}
至此已经实现随手势上滑并回弹,效果如下
device-2017-12-04-164045.gif
- 惯性和回弹效果。
搜索了很多资料都没有特别好用的回弹效果。 这里通过WebView 的 overScrollBy 的回调方法拿到Webview滚动到底部时候可继续滚动的距离,并在此时增加一个继续滑动的动效,模拟惯性
首先要在PullupCloseLayout中拿到 该回调。 这里定义了一个监听器
public class MyWebview extends WebView {
public MyWebview(Context context) {
super(context);
}
public MyWebview(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {
if (mOverscrollListener != null) {
mOverscrollListener.overScroll(deltaX, deltaY,isTouchEvent);
}
return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent);
}
private PullUpOverScrollListerer mOverscrollListener;
public void registerOverscrollListener (PullUpOverScrollListerer listener) {
if (listener != null) {
mOverscrollListener = listener;
}
}
public void unRegisterOverscrollListener () {
mOverscrollListener = null;
}
}
这样在PullupcloseLayout中
private void ensureView() {
if (mTarget == null) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (!child.equals(mPullUpView)) {
mTarget = child;
//判断滚动的view为自己的实现了onScrollBy方法的 webview则注册该监听
if (mTarget instanceof MyWebview) {
MyWebview webView = (MyWebview) mTarget;
webView.setOverScrollMode(View.OVER_SCROLL_NEVER);//去掉滑到底部的反馈水纹
webView.registerOverscrollListener(this);
}
break;
}
}
}
}
......
@Override
public void overScroll(int deltaX, int deltaY, boolean isTouchEvent) {
if (!mIsBeingDragged && !canChildScrollUp() && deltaY > mTouchSlop && mCurrentMotionEvent != MotionEvent.ACTION_MOVE) {
//1.5 倍惯性距离, 且最大滚动距离为滑动关闭的阈值
deltaY = Math.min((int)(deltaY * 1.5), mPullUpCloseHeight);
scrollBackAnimator((int) (deltaY * 1.5));
}
}
.....
//回弹动画
private void scrollBackAnimator(final int y) {
Log.i(TAG, "scrollBackAnimator y =" + y);
if (y == 0) {
return;
}
if (mAnimator != null) {
mAnimator.cancel();
mAnimator = null;
}
mAnimator = ValueAnimator.ofFloat(0, 1);
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float f = (float) animation.getAnimatedValue();
scrollTo(0, (int) (y * (1 - f)));
}
});
//long duration = SCROLL_MAX_DURATION_MS * y / mPullUpViewMaxHeight;
mAnimator.setDuration(SCROLL_MAX_DURATION_MS);
mAnimator.start();
}
至此实现了惯性回弹。 效果如下
device-2017-12-04-165546.gif
附github地址:
PullupCloseLayout