【记录】记录点滴

【场景】学习官方文档和sample时,实验的内容以及遇到的小坑

【需求】简单实现,基于View.OnDragListener, ViewDragHelper以及GestureDetector(或OnTouchEvent,OnTouchListener)实现拖放View滑动的效果

1. View.OnDragListener

官方文档提供了示例demo,包括实现拖放,自定义拖放时阴影的样式。

如果要实现拖放功能,需要1)创建View.OnDragListener,它是拖放事件的监听器;2)某些View过ViewGroup需要监听拖放事件,并根据操作状态实现需要的效果,因此对这些View设置创建好的View.OnDragListener监听器;3)创建一个View被拖放时的影子,并调用方法表明开始拖放操作。顺序不是固定的,感觉这个顺序比较好理解。

    //自定义的拖放监听器,未实现任何操作
    class DragerListener implements View.OnDragListener {
        @Override
        public boolean onDrag(View v, DragEvent event) {
            int action = event.getAction();
            switch (event.getAction()) {
                case DragEvent.ACTION_DRAG_STARTED:
                    /** 拖拽开始时 */
                    break;
                case DragEvent.ACTION_DRAG_ENTERED:
                    /** 拖拽进入区域时 */
                    break;
                case DragEvent.ACTION_DRAG_Location:
                    /** 拖拽进入区域后,仍在区域内拖动时 */
                    break;
                case DragEvent.ACTION_DRAG_EXITED:
                    /** 离开区域时 */
                    break;
                case DragEvent.ACTION_DROP:
                    /** 在区域内放开时 */
                    break;
                case DragEvent.ACTION_DRAG_ENDED:
                    /** 结束时 */
                default:
                    break;
            }
            return true;
        }
    }

 画一个简单的图说明,假设这是个手机屏幕,黑色圆是需要我们拖拽的View,红色矩形就是需要监听拖放事件并进行响应的区域。 首先创建拖放的监听器,随后仅仅对红色矩形(实际的应用场景中可能是View,也可能是ViewGroup)设置监听器,最后设置黑色圆的拖放阴影,并在适当的场景(如touch,longclick等)下告诉系统,我们开始了拖放操作。

View.OnDragListener, ViewDragHelper, GestureDetector --- 拖放滑动

开始拖拽圆时,触发START操作

DragEvent.ACTION_DRAG_STARTED

 当拖拽的点(通常是被拖拽的View的中心点)进入到红色的矩形区域后,触发ENTERED操作

DragEvent.ACTION_DRAG_ENTERED

 当拖拽的点一直在红色区域内移动时,会不断地触发LOCATION操作

DragEvent.ACTION_DRAG_LOCATION

 如果拖拽的点仍在红色区域内时,释放了被拖拽的View,则会触发DROP。因为DROP可以理解为是与区域绑定的,所以一次DROP只会交给一个对象来处理。脑补下,如果左下角还有个蓝色区域也监听了拖放事件,但是我们在红色区域内释放了View,那么只有红色区域的监听器会触发DROP操作

DragEvent.ACTION_DROP

并且在释放后,会触发ENDED操作

DragEvent.ACTION_DRAG_ENDED

另一种情况,如果拖拽的点移动出了区域,那么会触发EXITED操作

DragEvent.ACTION_DRAG_EXITED

到这里,应该可以弄清楚监听器的各操作类型的触发条件了。实现简单的拖拽

按照之前的描述,先自定义监听器,处理各种类型的操作

class DragListener implements View.OnDragListener {

        @Override
        public boolean onDrag(View v, DragEvent event) {
            int action = event.getAction();
            switch (event.getAction()) {
                case DragEvent.ACTION_DRAG_STARTED:
                    break;
                case DragEvent.ACTION_DRAG_ENTERED:
                    Log.e("lxy", "ACTION_DRAG_ENTERED");
                    //这个v就是监听拖拽事件的View,对照上面的图就是红色矩形区域
                    //拖拽进入区域后,变成蓝色背景
                    v.setBackgroundColor(Color.BLUE);
                    break;
                case DragEvent.ACTION_DRAG_LOCATION:
                    Log.e("lxy", "ACTION_DRAG_LOCATION");
                    break;
                case DragEvent.ACTION_DRAG_EXITED:
                    Log.e("lxy", "ACTION_DRAG_EXITED");
                    //拖拽出区域后,恢复成红色背景
                    v.setBackgroundColor(Color.RED);
                    break;
                case DragEvent.ACTION_DROP:
                    //释放后,v变成灰色背景
                    //也可以根据需求,做处理,比如“展示拖拽进来的图片”
                    v.setBackgroundColor(Color.GRAY);
                    break;
                case DragEvent.ACTION_DRAG_ENDED:
                default:
                    break;
            }
            return true;
        }
    }

随后,对View设置监听器

//layout1对应为上述内容中的红色区域
FrameLayout layout1 = findViewById(R.id.layout1);
layout1.setOnDragListener(mDragListen);

 最后创建默认的拖拽阴影,并在适当的情况下告诉系统我们开始拖拽了

    mDrag1.setOnLongClickListener(v -> {
            //创建拖拽阴影
            View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(mDrag1);
            //告诉系统开始拖拽了
            v.startDrag(null, shadowBuilder, v, 0);
            return true;
        });

 至此,就可以简单实现拖放功能了,这里去除了其他博客中都会有的ClipData,因为我没有要传的数据。

不过这种实现还是有些局限,比如我们拖拽的是生成的阴影,拖拽过程中View本身不会移动,释放后View也不会改变位置。

附上另一个demo的截图来说明,拖拽第一个View时,以及执行DROP后。

View.OnDragListener, ViewDragHelper, GestureDetector --- 拖放滑动                        View.OnDragListener, ViewDragHelper, GestureDetector --- 拖放滑动

如果希望改变View改变位置,觉得(因为没试过,假如有坑呢)可以让父布局监听拖拽事件,并在LOCATION或DROP/ENDED这些操作中来改变View的位置等等。

PS:如果希望修改拖拽的阴影,可以参考官方的示例,自定义个View.DragShadowBuilder

private static class MyDragShadowBuilder extends View.DragShadowBuilder {
 
// The drag shadow image, defined as a drawable thing
private static Drawable shadow;
 
    // Defines the constructor for myDragShadowBuilder
    public MyDragShadowBuilder(View v) {
 
        // Stores the View parameter passed to myDragShadowBuilder.
        super(v);
 
        // Creates a draggable image that will fill the Canvas provided by the system.
        shadow = new ColorDrawable(Color.LTGRAY);
    }
 
    // Defines a callback that sends the drag shadow dimensions and touch point back to the
    // system.
    @Override
    public void onProvideShadowMetrics (Point size, Point touch)
        // Defines local variables
        private int width, height;
 
        // Sets the width of the shadow to half the width of the original View
        width = getView().getWidth() / 2;
 
        // Sets the height of the shadow to half the height of the original View
        height = getView().getHeight() / 2;
 
        // The drag shadow is a ColorDrawable. This sets its dimensions to be the same as the
        // Canvas that the system will provide. As a result, the drag shadow will fill the
        // Canvas.
        shadow.setBounds(0, 0, width, height);
 
        // Sets the size parameter's width and height values. These get back to the system
        // through the size parameter.
        size.set(width, height);
 
        // Sets the touch point's position to be in the middle of the drag shadow
        touch.set(width / 2, height / 2);
    }
 
    // Defines a callback that draws the drag shadow in a Canvas that the system constructs
    // from the dimensions passed in onProvideShadowMetrics().
    @Override
    public void onDrawShadow(Canvas canvas) {
 
        // Draws the ColorDrawable in the Canvas passed in from the system.
        shadow.draw(canvas);
    }
}

2. ViewDragHelper

另一中简单实现拖拽的方式就是借助于ViewDragHelper,为了研究OnDragListener而查看官方sample时,竟发现sample是用这个实现的。

首先ViewDragHelper必须结合ViewGroup使用。1)自定义ViewGroup,创建ViewDragHelper;2)ViewGroup中让ViewDragHelper来处理事件拦截(onInterceptTouchEvent)和事件消费(onTouchEvent)。

首先,创建ViewDragHelper

//this是ViewGroup,1f代表灵敏度,越大越灵敏,DragCallback是ViewDragHelper.Callback,提供触摸过程中回调的相关方法
mViewDragHelper = ViewDragHelper.create(this, 1f, new DragCallback());

 然后让ViewDragHelper来处理事件拦截和事件消费

 @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int action = ev.getActionMasked();
        if (action == MotionEvent.ACTION_CANCEL
                || action == MotionEvent.ACTION_UP) {
            mViewDragHelper.cancel();
            return false;
        }
        return mViewDragHelper.shouldInterceptTouchEvent(ev);
    }
@Override
    public boolean onTouchEvent(MotionEvent event) {
        mViewDragHelper.processTouchEvent(event);
        return true;
    }

 ViewDragHelper的使用只需要这几步,很简单。接下来,分析记录下之前的DragCallback。DragCallback继承了ViewDrag.Callback,主要包含以下几个回调方法,先学习最简单和常用的几个,写了注释,剩下的再补充

//child水平方向上的坐标,left是child要移动过去的位置,dx是相对于上次的偏移量
int clampViewPositionHorizontal(View child, int left, int dx)

//child垂直方向上的坐标,top是child要移动过去的位置,dy是相对于上次的偏移量
int clampViewPositionVertical(View child, int top, int dy)

int getOrderedChildIndex(int index)

int getViewHorizontalDragRange(View child)

int getViewVerticalDragRange(View child)

void onEdgeDragStarted(int edgeFlags, int pointerId)

boolean onEdgeLock(int edgeFlags)

void onEdgeTouched(int edgeFlags, int pointerId)

//捕获时,即tryCaptureView返回true时触发
void onViewCaptured(View capturedChild, int activePointerId)

void onViewDragStateChanged(int state)

void onViewPositionChanged(View changedView, int left, int top, int dx, int dy)

//释放时触发
void onViewReleased(View releasedChild, float xvel, float yvel)

//能否捕获child,如果能捕获(返回true),才可以执行后续的拖拽
abstract boolean tryCaptureView(View child, int pointerId)

那么基于tryCaptureView,clampViewPositionHorizontal,clampViewPositionVertical,onViewCaptured以及onViewReleased就可以简单实现拖拽效果了。如下,自定义的DragCallback最后长这样,参考Google sample

    dragViews = new ArrayList<View>();
    public void addDragView(View view){
        dragViews.add(view);
    }


    class DragCallback extends ViewDragHelper.Callback{
        @Override
        public boolean tryCaptureView(@NonNull View child, int pointerId) {
            //可以拖拽的View,事先会通过addDragView方法添加到dragViews中
            if(dragViews.contains(child)){
                return true;
            }
            return false;
        }

        @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) {
            super.onViewCaptured(capturedChild, activePointerId);
            //捕获时,可以做些处理
        }

        @Override
        public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
            //释放时,可以做些处理,如下做了释放后回弹至某个位置的处理
            //这两个方法实际是基于Scroller实现的,最终会调用startScroll方法,所以回弹效果要结合computeScroll()来实现
            //如果不做处理,释放后View会停留在释放的位置
//            mViewDragHelper.settleCapturedViewAt(0, 0);
            mViewDragHelper.smoothSlideViewTo(releasedChild, 0, 0);
            invalidate();
        }
    }


    @Override
    public void computeScroll() {
        if(mViewDragHelper.continueSettling(true)){
            invalidate();
        }
    }

 在onViewReleased中,有settleCapturedViewAt和smoothSlideviewTo两个方法,他们的区别在于回弹时的起始速度。这里还必须使用invalidate()之类的强制重绘方法,用来触发computeScroll

相较于OnDragListener,ViewDragHelper更简单,更适用于单纯的拖拽。

 参考了

 https://blog.csdn.net/briblue/article/details/73730386

 https://www.cnblogs.com/liemng/p/4997427.html

3. GestureDetector(或OnTouchEvent,OnTouchListener)

其实,想到拖拽,我们一般第一反应就是重写Touch相关的处理方法,比如对View设置setOnTouchListener,重写ViewGroup中的OnTouchEvent等。所以借这个机会,简单记录下GestureDetector。

使用的话,包括:1)创建手势监听器;2)创建手势类;3)让手势类处理Touch事件。

这个是第一次写的代码,貌似逻辑很正确,distanceX与distanceY是最近两次Touch的差值,可以利用getX与getY验证,lastX - e2.getX的值一致的。

mGestureDetector = new GestureDetector(this, new DragGestureListener);

//为了看着舒服,去除了部分必要的方法
class DragGestureListener extends GestureDetector.OnGestureListener{
    @Override
            public boolean onDown(MotionEvent e) {

                return true;
            }

            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
                if(Math.abs(distanceX) > ViewConfiguration.getWindowTouchSlop() 
                    || Math.abs(distanceY) > ViewConfiguration.getWindowTouchSlop()){
                    ViewCompat.offsetLeftAndRight(mDrag3, (int) -distanceX);
                    ViewCompat.offsetTopAndBottom(mDrag3, (int) -distanceY);
                }
                return true;
            }


            @Override
            public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
                return false;
            }
}

 情况一:对View使用

用它来处理View的Touch事件。但是!拖动的过程中抖动很大(单向滑动时,distanceX与distanceY的值抖动),不顺畅。

//Activity中,对View设置
mDrag3.setOnTouchListener((v, event) -> {
    mGestureDetector.onTouchEvent(event);
    return true;
})

情况二:在ViewGroup中使用

简单修改onScroll后,将这部分代码放到ViewGroup中,就不会发生抖动的问题,虽然还是有点顿顿的(把TouchSlop的判断去掉就好了)。

    //自定义ViewGrup中,修改
    //修改监听器的onScroll,拖拽的是ViewGroup中的子View,dragV
    ...
    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        if(Math.abs(distanceX) > ViewConfiguration.getWindowTouchSlop() 
            || Math.abs(distanceY) > ViewConfiguration.getWindowTouchSlop()){
            ViewCompat.offsetLeftAndRight(dragV, (int) -distanceX);
            ViewCompat.offsetTopAndBottom(dragV, (int) -distanceY);
        }
        return true;
    }
    ...


    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
       return true;
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mGestureDetector.onTouchEvent(event);
        return true;
    }

GestureDetectore的内部源码还没有研究,针对于第一种情况,先只重写Touch事件消费来实现试试,利用getX与getY方法计算得到move的dx距离后,会发生抖动,那么用getRawX与getRawY计算得到dx后就不会。原来是这样,忘记了getX与getRawX的区别,他们的参考点是不同。借用其他博客的一张图,

View.OnDragListener, ViewDragHelper, GestureDetector --- 拖放滑动

对View使用时,View自身会移动,也就是说getX方法的参考点也发生了移动,那么dx当然会产生抖动的情况。虽然还没看GestureDetectore的相关源码,但是估计onScroll中的distanceX,distanceY是基于getX,getY方法计算得到的,然后通过getRawX验证下了猜测,这里就不粘贴内容了。想想,对于情况一,我的使用思路可能就存在问题。

PS,示例想简单点,所以很多内容需要根据实际的需求来完善,如,应该完善事件拦截onInterceptTouchEvent,判断哪些情况应该拦截;判断Down事件是否落在子View上;限制拖拽的边界等

参考

https://blog.csdn.net/dmk877/article/details/51550031

 

相关文章:

  • 2022-12-23
  • 2022-12-23
  • 2022-12-23
  • 2022-12-23
  • 2022-12-23
  • 2022-12-23
  • 2022-12-23
  • 2022-12-23
猜你喜欢
  • 2021-09-10
  • 2021-08-31
  • 2022-12-23
  • 2021-07-04
  • 2022-02-17
  • 2021-11-22
相关资源
相似解决方案