效果以下:git
device-2017-12-04-170020.gifgithub
分为两部分:1. View的建立。 2. 滑动事件处理web
从页面上看,主要分为上下两部分,上部为滚动的Webview,底部为拉出来的CloseView
我这里自定义了ViewGrup。初始状态Webview撑满整个屏幕,CloseView不可见,位于Webview底部。代码以下ide
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; } } } } }
此时页面布局完成。接下来第二部处理滑动事件布局
滑动事件主要处理两个状态, 1. 滑动到底部,能够随手势上滑,松手可回弹。 2. 能够随惯性滑动并回弹动画
private boolean canChildScrollUp() { // 参数为正则表明向上是否可滑动,负数则为向下, 通常用1和-1表明 return ViewCompat.canScrollVertically(mTarget, 1); }
拦截滚动事件的代码以下this
@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; }
消费手势 以下spa
@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); } }
至此已经实现随手势上滑并回弹,效果以下code
device-2017-12-04-164045.gif事件
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
做者:liu_liu_ 连接:https://www.jianshu.com/p/b91e5a5b90da 来源:简书 著做权归做者全部。商业转载请联系做者得到受权,非商业转载请注明出处。