MIUI拖动效果

来源:互联网 发布:域名批量查询软件 编辑:程序博客网 时间:2024/04/30 17:45

先看效果图

在可以滚动的视图中,内容滚动到顶部时继续下拉,整个视图就有一个竖直方向拉伸的视差效果。滚动到底部继续上拉,也有同样的效果。

滚动视图可能是ScrollViewRecyclerView,要实现这样的效果,需要自定义并拦截Touch事件,重新处理事件逻辑。

recycleview

自定义recycleview,复写onInterceptTouchEvent方法:

@Overridepublic boolean onInterceptTouchEvent(MotionEvent event) {    int action = MotionEventCompat.getActionMasked(event);    if (isRestoring && action == MotionEvent.ACTION_DOWN) {        isRestoring = false;    }    if (!isEnabled() || isRestoring || (!isScrollToTop() && !isScrollToBottom())) {        return super.onInterceptTouchEvent(event);    }    switch (action) {        case MotionEvent.ACTION_DOWN: {            mActivePointerId = event.getPointerId(0);            isBeingDragged = false;            float initialMotionY = getMotionEventY(event);            if (initialMotionY == -1) {                return super.onInterceptTouchEvent(event);            }            mInitialMotionY = initialMotionY;            break;        }        case MotionEvent.ACTION_MOVE: {            if (mActivePointerId == MotionEvent.INVALID_POINTER_ID) {                return super.onInterceptTouchEvent(event);            }            final float y = getMotionEventY(event);            if (y == -1f) {                return super.onInterceptTouchEvent(event);            }            if (isScrollToTop() && !isScrollToBottom()) {                // 在顶部不在底部                float yDiff = y - mInitialMotionY;                if (yDiff > mTouchSlop && !isBeingDragged) {                    isBeingDragged = true;                }            } else if (!isScrollToTop() && isScrollToBottom()) {                // 在底部不在顶部                float yDiff = mInitialMotionY - y;                if (yDiff > mTouchSlop && !isBeingDragged) {                    isBeingDragged = true;                }            } else if (isScrollToTop() && isScrollToBottom()) {                // 在底部也在顶部                float yDiff = y - mInitialMotionY;                if (Math.abs(yDiff) > mTouchSlop && !isBeingDragged) {                    isBeingDragged = true;                }            } else {                // 不在底部也不在顶部                return super.onInterceptTouchEvent(event);            }            break;        }        case MotionEventCompat.ACTION_POINTER_UP:            onSecondaryPointerUp(event);            break;        case MotionEvent.ACTION_UP:        case MotionEvent.ACTION_CANCEL:            mActivePointerId = MotionEvent.INVALID_POINTER_ID;            isBeingDragged = false;            break;    }    return isBeingDragged || super.onInterceptTouchEvent(event);}

滚动RecyclerView到达顶部或者底部继续拖动时,需要拦截Touch事件。所以在MotionEvent.ACTION_MOVE时需要判断当前RecyclerView是否在顶部或者底部。需要注意的是,当RecyclerView中的item没有填充满整视图时,RecyclerView的状态既是在顶部也是在底部。

private boolean isScrollToTop() {    return !ViewCompat.canScrollVertically(this, -1);}private boolean isScrollToBottom() {    return !ViewCompat.canScrollVertically(this, 1);}

mActivePointerId表示在多点触控是当前活动手指的id,mInitialMotionY为手指按下时的Y坐标。

当达到顶部或底部继续拖动时,根据当前的位置(isScrollToTop()isScrollToBottom())和ACTION_MOVE时的移动距离yDiff来判断是否需要拦截:在顶部时向上拖动并且yDiff>mTouchSlop就需要拦截,底部时向下拖动同样yDiff>mTouchSlop也需要拦截,同时在顶部和底部时满足Math.abs(yDiff)>mTouchSlop也需要拦截。需要拦截都是在没有被拖动(!isBeingDragged)的情况下。

RecyclerViev既没有在顶部也没有在底部时,说明item滚动到中间,可以上下继续滚动,不需要拦截,交给super.onInterceptTouchEvent(event)来处理。同时其它不需要拦截的情况也都交给super来处理。

onSecondaryPointerUp(event)为当第二个手指离开屏幕是需要重新设置mActivePointerId:

private void onSecondaryPointerUp(MotionEvent event) {    final int pointerIndex = MotionEventCompat.getActionIndex(event);    final int pointerId = event.getPointerId(pointerIndex);    if (pointerId == mActivePointerId) {        int newPointerIndex = pointerIndex == 0 ? 1 : 0;        mActivePointerId = event.getPointerId(newPointerIndex);    }}

拦截到TouchEvent,在onTouchEven中处理,实现拖动视差效果:

@Overridepublic boolean onTouchEvent(MotionEvent event) {    switch (MotionEventCompat.getActionMasked(event)) {        case MotionEvent.ACTION_DOWN:            mActivePointerId = event.getPointerId(0);            isBeingDragged = false;            break;        case MotionEvent.ACTION_MOVE: {             float y = getMotionEventY(event);            if (isScrollToTop() && !isScrollToBottom()) {                // 在顶部不在底部                mDistance = y - mInitialMotionY;                if (mDistance < 0) {                    return super.onTouchEvent(event);                }                mScale = calculateRate(mDistance);                pull(mScale);            } else if (!isScrollToTop() && isScrollToBottom()) {                // 在底部不在顶部                mDistance = mInitialMotionY - y;                if (mDistance < 0) {                    return super.onTouchEvent(event);                }                mScale = calculateRate(mDistance);                push(mScale);            } else if (isScrollToTop() && isScrollToBottom()) {                // 在底部也在顶部                mDistance = y - mInitialMotionY;                if (mDistance > 0) {                    mScale = calculateRate(mDistance);                    pull(mScale);                } else {                    mScale = calculateRate(-mDistance);                    push(mScale);                }            } else {                // 不在底部也不在顶部                return super.onTouchEvent(event);            }            break;        }        case MotionEventCompat.ACTION_POINTER_DOWN:            mActivePointerId = event.getPointerId(MotionEventCompat.getActionIndex(event));            break;        case MotionEventCompat.ACTION_POINTER_UP:            onSecondaryPointerUp(event);            break;        case MotionEvent.ACTION_UP: {            if (isScrollToTop() && !isScrollToBottom()) {                animateRestore(true);            } else if (!isScrollToTop() && isScrollToBottom()) {                animateRestore(false);            } else if (isScrollToTop() && isScrollToBottom()) {                if (mDistance > 0) {                    animateRestore(true);                } else {                    animateRestore(false);                }            } else {                return super.onTouchEvent(event);            }            break;        }    }    return true;}

代码虽然有点长,但是逻辑很简单,在拦截到ACTION_MOVE事件后,同样根据顶部或底部位置以及滚动的距离mDistance来确定是否消费掉该事件。不需要消费的直接给`super.onTouchEvent(event)来处理,需要消费的话根据mDistance来计算出缩放的比例mScale,再通过pull(mScale)push(mScale)来缩放。

private float calculateRate(float distance) {    int screenHeight = getResources().getDisplayMetrics().heightPixels;    float originalDragPercent = distance / screenHeight;    float dragPercent = Math.min(1f, originalDragPercent);    float rate = 2f * dragPercent - (float) Math.pow(dragPercent, 2f);    return 1 + rate / 5f;}

mScale的计算是一个二次函数,当拖动距离越大时,mScale的变化程度越小,这样使得拖动时有一个张力效果。

private void pull(float scale) {    this.setPivotY(0);    this.setScaleY(scale);}private void push(float scale) {    this.setPivotY(this.getHeight());    this.setScaleY(scale);}

ACTION_UP时,需要将缩放的视图通过动画还原到初始状态。这里也需要判断位置,因为不同位置的的缩放中心点不一样。同时即在顶部也在底部时是根mDistance的正负值来判断拖动的方向。

private void animateRestore(final boolean isPullRestore) {    ValueAnimator animator = ValueAnimator.ofFloat(mScale, 1f);    animator.setDuration(300);    animator.setInterpolator(new DecelerateInterpolator(2f));    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {        @Override        public void onAnimationUpdate(ValueAnimator animation) {            float value = (float) animation.getAnimatedValue();            if (isPullRestore) {                pull(value);            } else {                push(value);            }        }    });    animator.addListener(new Animator.AnimatorListener() {        @Override        public void onAnimationStart(Animator animation) {            isRestoring = true;        }        @Override        public void onAnimationEnd(Animator animation) {            isRestoring = false;        }        @Override        public void onAnimationCancel(Animator animation) {        }        @Override        public void onAnimationRepeat(Animator animation) {        }    });    animator.start();}

源码地址:https://github.com/Xiemarc/DesignPatterns

原创粉丝点击