如何成为自定义高手(六)滑动和拖拽

滑动

GestureDetector

GestureDetector手势检测:常用用来检测onSingleTapUp(单击),onFling(快速滑动),onScroll(拖动),onLongPress(长按),onDoubleTap(双击)

VelocityTracker

速度追踪器,就是用来计算手指的滑动速度
使用方法:

  • ACTION_DOWN 事件到来时,通过 VelocityTracker.obtain()创建⼀个实例,或者使用 velocityTracker.clear() 把之前的某个实例重置
  • 对于每个事件(包括 ACTION_DOWN 事件),使用velocityTracker.addMovement(event) 把事件添加进 VelocityTracker
  • 在需要速度的时候(例如在 ACTION_UP 中计算是否达到 fling 速度),使用velocityTracker.computeCurrentVelocity(1000, maxVelocity) 来计算实时速度,并通过getXVelocity() / getYVelocity() 来获取计算出的速度。
    方法参数中的 1000 是指的计算的时间长度,单位是 ms。例如这⾥填入 1000,那么getXVelocity() 返回的值就是每 1000ms (即⼀秒)时间内手指移动的像素数。第⼆个参数是速度上限,超过这个速度时,计算出的速度会回落到这个速度。例如这里填了 200,而实时速度是 300,那么实际的返回速度将是 200 ,maxVelocity 可以通过 viewConfiguration.getScaledMaximumFlingVelocity()来获取。
    VelocityTracker velocityTracker = VelocityTracker.obtain();
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
            velocityTracker.clear();
        }
        velocityTracker.addMovement(event);

        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:
                break;
            case MotionEvent.ACTION_UP:
                velocityTracker.computeCurrentVelocity(1000, maxVelocity);           
                break;
        }
        return true;
    }

scrollTo,scrollBy,computeScroll

scrollTo(x, y)移动的是绝对值;scrollBy(deltaX, deltaY)移动的是相对值,内部也是调用scrollTo方法。
scrollTo() 是瞬时方法,不会自动使用动画。如果要用动画,需要配合 View.computeScroll()方法。computeScroll() 在 View 重绘时被自动调用

使用OverScroller实现缓慢滑动

// onTouchEvent() 中:
overScroller.startScroll(startX, startY, dx, dy);
postInvalidateOnAnimation();
......
// onTouchEvent() 外:
@Override
public void computeScroll() {
   if (overScroller.computeScrollOffset()) { // 计算实时位置
      scrollTo(overScroller.getCurrX(),
      overScroller.getCurrY()); // 更新界⾯
      postInvalidateOnAnimation(); // 下⼀帧继续
   }
}

使用Scroller实现缓慢滑动
实现原理:startScroll记录下相关参数,invalidate导致view重绘,view的draw方法中又调用computeScroll,而computeScroll又会向Scroller获取当前scrollX和scrollY,然后通过scrollTo去实现滑动

private void smoothScrollTo(int destX, int destY){
    int scrollX = getScrollX();
    int deltaX = destX - scrollX;
    int scrollY = getScrollY();
    int deltaY = destY - scrollY;
    //1000ms内滑向destX,效果就是慢慢滑动
    mScroller.startScroll(scrollX,scrollY,deltaX,deltaY,1000);
    invalidate();
}

@Override
public void computeScroll() {
    if(mScroller.computeScrollOffset()){
        scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
        postInvalidate();
    }
    super.computeScroll();
}

简单的自定义ViewPager

通过简单的自定义ViewPager来使用上面的VelocityTracker和computeScroll等使用

public class MyViewPager extends ViewGroup {
    float downX;
    float downY;
    float downScrollX;
    boolean scrolling;
    float minVelocity;
    float maxVelocity;
    OverScroller overScroller;
    ViewConfiguration viewConfiguration;
    VelocityTracker velocityTracker = VelocityTracker.obtain();

    public MyViewPager (Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        overScroller = new OverScroller(context);
        viewConfiguration = ViewConfiguration.get(context);
        maxVelocity = viewConfiguration.getScaledMaximumFlingVelocity();
        minVelocity = viewConfiguration.getScaledMinimumFlingVelocity();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childLeft = 0;
        int childTop = 0;
        int childRight = getWidth();
        int childBottom = getHeight();
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            child.layout(childLeft, childTop, childRight, childBottom);
            childLeft += getWidth();
            childRight += getWidth();
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
            velocityTracker.clear();
        }
        velocityTracker.addMovement(ev);

        boolean result = false;
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                scrolling = false;
                downX = ev.getX();
                downY = ev.getY();
                downScrollX = getScrollX();
                break;
            case MotionEvent.ACTION_MOVE:
                float dx = downX - ev.getX();
                if (!scrolling) {
                    if (Math.abs(dx) > viewConfiguration.getScaledPagingTouchSlop()) {
                        scrolling = true;
                        getParent().requestDisallowInterceptTouchEvent(true);
                        result = true;
                    }
                }
                break;
        }
        return result;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
            velocityTracker.clear();
        }
        velocityTracker.addMovement(event);

        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                downX = event.getX();
                downY = event.getY();
                downScrollX = getScrollX();
                break;
            case MotionEvent.ACTION_MOVE:
                float dx = downX - event.getX() + downScrollX;
                if (dx > getWidth()) {
                    dx = getWidth();
                } else if (dx < 0) {
                    dx = 0;
                }
                scrollTo((int) (dx), 0);
                break;
            case MotionEvent.ACTION_UP:
                velocityTracker.computeCurrentVelocity(1000, maxVelocity);
                float vx = velocityTracker.getXVelocity();
                int scrollX = getScrollX();
                int targetPage;
                if (Math.abs(vx) < minVelocity) {
                    targetPage = scrollX > getWidth() / 2 ? 1 : 0;
                } else {
                    targetPage = vx < 0 ? 1 : 0;
                }
                int scrollDistance = targetPage == 1 ? (getWidth() - scrollX) : - scrollX;
                overScroller.startScroll(getScrollX(), 0, scrollDistance, 0);
                postInvalidateOnAnimation();
                break;
        }
        return true;
    }

    @Override
    public void computeScroll() {
        if (overScroller.computeScrollOffset()) {
            scrollTo(overScroller.getCurrX(), overScroller.getCurrY());
            postInvalidateOnAnimation();
        }
    }
}

拖拽

OnDragListener

  • 通过 startDrag() 来启动拖拽
    startDrag最后会调用startDragAndDrop,
startDragAndDrop(ClipData data, DragShadowBuilder shadowBuilder,Object myLocalState, int flags)

内部有四个参数:

  1. ClipData data
    其实就是一个封装数据的对象,通过拖放操作传递给接受者。该对象可以存放一个Item的集合,Item可以存放如下数据:
public static class Item {
        final CharSequence mText;
        final String mHtmlText;
        final Intent mIntent;
        Uri mUri;
}
  1. DragShadowBuilder shadowBuilder
    用于创建拖拽view是的阴影,也就是跟随手指移动的视图,通常直接使用默认即可生成与一个原始view相同,带有透明度的阴影
  2. Object myLocalState
    当你的拖拽行为是在同一个Activity中进行时可以传递一个任意对象,在监听中可以通过{@link android.view.DragEvent#getLocalState()}获得。如果是跨Activity拖拽中无法访问此数据,getLocalState()将返回null。
  3. int flags
    控制拖放操作的标志。因为没有标志可以设置为0,flag标志拖动是否可以跨越窗口以及一些访问权限(需要API24+)
            child.setOnLongClickListener(new OnLongClickListener() {
                @Override
                public boolean onLongClick(View v) {
                    draggedView = v;
                    v.startDrag(null, new DragShadowBuilder(v), v, 0);
                    return false;
                }
            });
            child.setOnDragListener(dragListener);
  • 用setOnDragListener() 来监听
    目标View:不是被拖拽的View,是要拖拽去哪个区域,这个区域就目标View,它要设置OnDragListener监听。
    OnDragListener 内部只有⼀个方法: onDrag()。View中onDragEvent() 方法也会收到拖拽回调(界⾯中的每个 View 都会收到)
view.setOnDragListener(new View.OnDragListener() {
            @Override
            public boolean onDrag(View v, DragEvent event) {
                //v 永远是设置该监听的view,这里即fl_blue
                String simpleName = v.getClass().getSimpleName();
                Log.w(BLUE, "view name:" + simpleName);
                //获取事件
                int action = event.getAction();
                switch (action) {
                    case DragEvent.ACTION_DRAG_STARTED:
                        Log.i(BLUE, "开始拖拽");
                        break;
                    case DragEvent.ACTION_DRAG_ENDED:
                        Log.i(BLUE, "结束拖拽");
                        break;
                    case DragEvent.ACTION_DRAG_ENTERED:
                        Log.i(BLUE, "拖拽的view进入监听的view时");
                        break;
                    case DragEvent.ACTION_DRAG_EXITED:
                        Log.i(BLUE, "拖拽的view离开监听的view时");
                        break;
                    case DragEvent.ACTION_DRAG_LOCATION:
                        float x = event.getX();
                        float y = event.getY();
                        long l = SystemClock.currentThreadTimeMillis();
                        Log.i(BLUE, "拖拽的view在监听view中的位置:x =" + x + ",y=" + y);
                        break;
                    case DragEvent.ACTION_DROP:
                        Log.i(BLUE, "释放拖拽的view");
                        break;
                }
                //是否响应拖拽事件,true响应,返回false只能接受到ACTION_DRAG_STARTED事件,后续事件不会收到
                return true;
            }
        });

ViewDragHelper

  • 需要创建⼀个 ViewDragHelper 和 Callback()
    ViewDragHelper create(ViewGroup forParent, Callback cb);一个静态的创建方法,
    参数1:出入的是相应的ViewGroup
    参数2:是一个回调Callback,在后面介绍包括其中的方法
    ViewDragHelper dragHelper;
    ViewDragHelper.Callback dragListener = new DragListener();

    public DragHelperLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        dragHelper = ViewDragHelper.create(this, dragListener);
        viewConfiguration = ViewConfiguration.get(context);
    }
  • 需要写在 ViewGroup 里面,重写 onIntercept() 和 onTouchevent()
  1. shouldInterceptTouchEvent(MotionEvent ev) 处理事件分发的(主要是将ViewGroup的事件拦截onInterceptTouchEvent,委托给ViewDragHelper进行处理)
  2. processTouchEvent(MotionEvent event) 处理相应TouchEvent的方法,这里要注意一个问题,处理相应的TouchEvent的时候要将结果返回为true,消费本次事件!否则将无法使用ViewDragHelper处理相应的拖拽事件!
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return dragHelper.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        dragHelper.processTouchEvent(event);
        return true;
    }
  • ViewDragHelper.Callback的API(也就是创建ViewDragHelper传入的回调方法)
  1. tryCaptureView(View child, int pointerId) 这是一个抽象类,必须去实现,也只有在这个方法返回true的时候下面的方法才会生效;相当于事件的开始
    参数1:捕获的View(也就是你拖动的这个View)
    参数2:这个参数我也不知道什么意思API中写的一个什么指针,这里没有到也没有注意
  2. onViewDragStateChanged(int state) 当状态改变的时候回调,返回相应的状态(这里有三种状态)
    STATE_IDLE 闲置状态
    STATE_DRAGGING 正在拖动
    STATE_SETTLING 放置到某个位置
  3. onViewPositionChanged(View changedView, int left, int top, int dx, int dy) 当你拖动的View位置发生改变的时候回调
    参数1:你当前拖动的这个View
    参数2:距离左边的距离
    参数3:距离右边的距离
    参数4:x轴的变化量
    参数5:y轴的变化量
  4. onViewCaptured(View capturedChild, int activePointerId)捕获View的时候调用的方法
    参数1:捕获的View(也就是你拖动的这个View)
    参数2:这个参数我也不知道什么意思API中写的一个什么指针,这里没有到也没有注意
  5. onViewReleased(View releasedChild, float xvel, float yvel) 当View停止拖拽的时候调用的方法,一般在这个方法中重置一些参数,相当于事件的结束
    参数1:你拖拽的这个View
    参数2:x轴的速率
    参数3:y轴的速率
  6. clampViewPositionVertical(View child, int top, int dy) 竖直拖拽的时候回调的方法
    参数1:拖拽的View
    参数2:距离顶部的距离
    参数3:变化量
    7.clampViewPositionHorizontal(View child, int left, int dx) 水平拖拽的时候回调的方法
    参数1:拖拽的View
    参数2:距离左边的距离
    参数3:变化量
public class DragHelperLayout extends FrameLayout {

    @Override
    public void computeScroll() {
        if (dragHelper.continueSettling(true)) {
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

    private class DragCallback extends ViewDragHelper.Callback {
        float capturedOldLeft;
        float capturedOldTop;

        @Override
        public boolean tryCaptureView(@NonNull View child, int pointerId) {
            return true;
        }

        @Override
        public void onViewDragStateChanged(int state) {
            if (state == ViewDragHelper.STATE_IDLE) {
                View capturedView = dragHelper.getCapturedView();
                //。。。。
            }
        }

        @Override
        public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
            return left;
        }

        @Override
        public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
            return top;
        }

        @Override
        public void onViewCaptured(@NonNull View capturedChild, int activePointerId) {
            capturedOldLeft = capturedChild.getLeft();
            capturedOldTop = capturedChild.getTop();
        }

        @Override
        public void onViewPositionChanged(@NonNull View changedView, int left, int top, int dx, int dy) {
        }

        @Override
        public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
            //放回到原来的位置m
            dragHelper.settleCapturedViewAt((int) capturedOldLeft, (int) capturedOldTop);
            postInvalidateOnAnimation();
        }
    }
}

如何成为自定义高手(一)绘制
如何成为自定义高手(二)动画
如何成为自定义高手(三)布局
如何成为自定义高手(四)触摸反馈,事件分发机制
如何成为自定义高手(五)多点触摸
如何成为自定义高手(六)滑动和拖拽
如何成为自定义高手(七)滑动冲突

你可能感兴趣的:(如何成为自定义高手(六)滑动和拖拽)