仿百度地图抽屉拖拽效果

ScrollLayout ScrollTo ScrollBy 事件传递

ScrollTo、ScrollBy说明

ScrollTo和ScrollBy滑动的是该View的显示内容(子View),并不改变该View的坐标
ScrollTo:它是滑动到指定位置
ScrollBy:它是基于当前位置的相对滑动(内部最终也会调用ScrollTo)

注意
x和y不是坐标点,是偏移量,坐标系:左正上正

演示

如果事件传递不是太清楚的同学可以先看这两篇博客
Android事件传递机制详解
super.dispatchTouchEvent(event)使用

ScrollLayout的dispatchTouchEvent各个事件

DOWN事件

case MotionEvent.ACTION_DOWN:
    dX = eX;
    lastY = dY = eY;
    isMoveValid = false;
    isEventValid = true;
    //首次进入getScrollY为0,当eY点击区域在offsetY距离下边,则进入
    //当点击距离在offsetY上边则进入else
    if (getScrollY() + eY > offsetY) {
        //当动画正在执行时,事件不往View中传递
        if (!(factor == 0 || factor == 1)) {
            isEventValid = false;
        } else {
            //当子View为非滑动事件如TextView,LinearLayoutd等会调用它们的onTouchEvent的所有事件
            //当子View为滑动事件如ListView,RecyclerView等不会调用onTouchEvent的Down事件,而是从Move事件开始调用子View
            super.dispatchTouchEvent(ev);
        }
        return true;
    }
    return false;

代码上都已经注释很清楚了,如果点击到子View上则进入if语句内,否则返回false,不处理事件

MOVE事件

case MotionEvent.ACTION_MOVE:
    //当动画正在执行时,点击滑动不起作用
    if (!isEventValid) return false;

    //下滑offset就是负数,上滑就是正数
    int offset = (int) (lastY - eY);
    lastY = eY;

    //当展开时,继续上滑则RecyclerView获取滑动事件,super.dispatchTouchEvent(ev)返回true
    //当展开时,下滑时则RecyclerView不处理滑动事件,super.dispatchTouchEvent(ev)返回false
    //当关闭时,上滑时TextView不处理滑动事件,super.dispatchTouchEvent(ev)返回false
    if ((status == STATUS_EXTEND
            || status == STATUS_CLOSE)
            && super.dispatchTouchEvent(ev)) {
        return true;
    }

    if (!isMoveValid
            && Math.abs(eY - dY) > touchSlop
            && Math.abs(eY - dY) > Math.abs(eX - dX)) {
        isMoveValid = true;
    }

    if (isMoveValid) {
    	//滑动的距离已经小于底部关闭的距离时,直接scrollTo到关闭距离
        if (getScrollY() + offset <= offsetClose) {
            scrollTo(0, offsetClose);
            status = STATUS_CLOSE;
            if (listener != null) {
                listener.onScrollChange(status);
            }
            //滑动距离大于展开距离时,直接scrollTo到展开距离
        } else if (getScrollY() + offset >= offsetExtend) {
            scrollTo(0, offsetExtend);
            status = STATUS_EXTEND;
            setRecyclerViewLastY(true);
            if (listener != null) {
                listener.onScrollChange(status);
            }
        } else {
            //偏移量坐标  左正上正
            //跟随手指慢慢移动,
            scrollBy(0, offset);
            if (listener != null) {
                if (getScrollY() > offsetB) {
                    listener.onScrollProgress(255 * getScrollY() / offsetY);
                } else {
                    listener.onScrollProgress(0);
                }
                listener.onScrollChange(STATUS_ING);
            }
        }
    }
    return true;

在Move事件里,主要就是事件的传递和拦截,通过scrollBy实现子View跟随手指移动,通过scrollTo实现到达指定位置。通过onScrollChange实现底部TextView的显隐,通过onScrollProgress实现背景色、标题栏的颜色渐变

CANCEL、UP事件

case MotionEvent.ACTION_CANCEL:
    case MotionEvent.ACTION_UP:
    	//当动画正在执行时,点击滑动不起作用
        if (!isEventValid)return false;
        if (isMoveValid
                && getScrollY() > offsetClose
                && getScrollY() < offsetExtend) {
            //通过动画的方式,实现子View到达指定位置    
            dealUp(getScrollY());
            isMoveValid = false;
            return true;
        }
        setRecyclerViewLastY(status);
        break;

在CANCEL、UP事件里,如果没有直接滑动到关闭、展开的位置,则通过dealUp方法以动画的方式把子View滚动到关闭、展开的位置,如果手机滑动到了关闭、展开的位置,则直接返回super.dispatchTouchEvent(ev)

属性动画滚动子View

animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
	    @Override
	    public void onAnimationUpdate(ValueAnimator animation) {
	        factor = (float) animation.getAnimatedValue();
	        //这里打印会先打印出:1,然后再从0-1打印,因为每次在start时,都会先调用stop
	        LogUtils.i("liuyzz--onAnimation--:", "" + factor);
	        //curY可为负数或正数
	        float scrollY = curY + (finalY - curY) * factor;
	        scrollTo(0, (int) scrollY);
	        postInvalidate();
	        if (listener != null) {
	            listener.onScrollChange(STATUS_ING);
	            //大于设置的展开距离后才设置颜色渐变
	            if (getScrollY() > offsetB) {
	                listener.onScrollProgress(255 * getScrollY() / offsetY);
	            } else {
	                listener.onScrollProgress(0);
	            }
	        }
	    }
	});

通过onAnimationUpdate更新子View的位置并设置背景色、标题栏的颜色渐变

ScrollRecyclerView的dispatchTouchEvent各个事件

DOWN事件

case MotionEvent.ACTION_DOWN:
    LogUtils.i("liuyzz:recyclerview:", "---down");
    lastY = eY;
    canScroll = true;
    super.onTouchEvent(e);
    return true;

因为是滚动列表,所以父View的dispatchTouchEvent不会调用ScrollRecyclerView的Down事件,而是从Move事件开始调用

MOVE事件

case MotionEvent.ACTION_MOVE:
    if (!canScroll) {
        return false;
    }
    //下滑负  上滑正
    int offset = (int) (lastY - eY);
    lastY = eY;
    LogUtils.i("liuyzz:recyclerview:", ""+canScroll+"-isTop:"+isTop()+"-offset:"+offset);
    canScroll = !isTop() || offset > 0;
    super.onTouchEvent(e);//如果不写,ScrollRecyclerView就不能滑动
    return canScroll;   

在Down事件中,通过isTop和offset确定move事件是否处理事件

isTop方法

private boolean isTop() {
    LayoutManager manager = getLayoutManager();
    if (manager == null
            || !(manager instanceof LinearLayoutManager)) {
        return false;
    }
    //显示区域最上面一条信息的position
    int visibleItemPosition = ((LinearLayoutManager) manager).findFirstVisibleItemPosition();
    View childView = getChildAt(0);//getChildAt(0)只能获得当前能看到的item的第一个
    LogUtils.i("liuyzz:recyclerview:", "position:"+visibleItemPosition+"-top:"+childView.getTop());
    //坐标系 下正右正
    return childView != null && visibleItemPosition <= 0 && childView.getTop() >= 0;
}

CANCEL、UP事件

case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
    canScroll = true;
    super.onTouchEvent(e);
    return true;

在CANCEL、UP事件中,只是重置状态

ScrollTextView的dispatchTouchEvent各个事件

DOWN事件

case MotionEvent.ACTION_DOWN:
    dX = eX;
    dY = eY;
    isClickValid = true;
    return true;

MOVE事件

case MotionEvent.ACTION_MOVE:
    if (isClickValid
            && (Math.abs(eY - dY) > touchSlop
            || Math.abs(eY - dY) > Math.abs(eX - dX))) {
        isClickValid = false;
    }
    return isClickValid;

在Move事件中,如果是滑动事件,则置isClickValid为false,自己不处理事件,事件会上传到ScrollLayout处理,如果不是滑动事件,则处理事件

UP事件

case MotionEvent.ACTION_CANCEL:
    //这里的目的是模拟一个点击事件
    if (isClickValid && listener != null) {
        listener.onClick(this);
    }
    return isClickValid;

在Up事件中,如果Move中不处理事件(isClickValid=false),则在Up中也不处理事件,如果Move中处理了事件,则在Up中也处理事件,并且也模拟了一个TextView的点击事件

注意
如果重写了某View的onTouchEvent方法,则该View的onClickListener就会失效

总结
代码才是最好的老师,如果您刚开始没有思路,那么看上面这些代码估计也看的云里雾里,因为这些代码只便于有思路而不知具体实现或在某个地方不是太清楚的复习使用。如果您想理清全部逻辑,还请您下载源码,慢慢嚼咽

源码下载

Demo下载

你可能感兴趣的:(算法)