Android 仿網易新聞上拉關閉Webview

效果以下:git

 

device-2017-12-04-170020.gifgithub

分爲兩部分:1. View的建立。 2. 滑動事件處理web

1. View的建立

從頁面上看,主要分爲上下兩部分,上部爲滾動的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;
                }
            }
        }
    }
}

此時頁面佈局完成。接下來第二部處理滑動事件佈局

2. 滑動事件處理

滑動事件主要處理兩個狀態, 1. 滑動到底部,能夠隨手勢上滑,鬆手可回彈。 2. 能夠隨慣性滑動並回彈動畫

  1. 手勢上滑及回彈。
    判斷當頁面滑到底部不能繼續滑動的時候由本佈局攔截手勢, 並消費掉。 不然不了攔截。
    如何判斷滑動到底部?

 

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事件

  1. 慣性和回彈效果。
    搜索了不少資料都沒有特別好用的回彈效果。 這裏經過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

做者:liu_liu_ 連接:https://www.jianshu.com/p/b91e5a5b90da 來源:簡書 著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。

相關文章
相關標籤/搜索