Android 仿网易新闻上拉关闭Webview

效果如下:

《Android 仿网易新闻上拉关闭Webview》 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. 可以随惯性滑动并回弹

  1. 手势上滑及回弹。
    判断当页面滑到底部不能继续滑动的时候由本布局拦截手势, 并消费掉。 否则不了拦截。
    如何判断滑动到底部?
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);
        }
    }

至此已经实现随手势上滑并回弹,效果如下

《Android 仿网易新闻上拉关闭Webview》 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();
    }

至此实现了惯性回弹。 效果如下
《Android 仿网易新闻上拉关闭Webview》 device-2017-12-04-165546.gif

附github地址:
PullupCloseLayout

    原文作者:liu_liu_
    原文地址: https://www.jianshu.com/p/b91e5a5b90da
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞