效果以下: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 來源:簡書 著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。