美文网首页
ViewDragHelper以及简单的侧滑关闭页面实现

ViewDragHelper以及简单的侧滑关闭页面实现

作者: 有点健忘 | 来源:发表于2018-07-19 17:46 被阅读8次

随便百度搜了几篇看看

https://www.jianshu.com/p/111a7bc76a0e
https://blog.csdn.net/itermeng/article/details/52159637?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io
https://blog.csdn.net/briblue/article/details/73730386
https://blog.csdn.net/coder_nice/article/details/44958341
代码如下:

import android.content.Context
import android.support.v4.widget.ViewDragHelper
import android.util.AttributeSet
import android.widget.LinearLayout
import android.view.MotionEvent
import android.view.View
import com.charliesong.demo0327.R
import kotlinx.android.synthetic.main.activity_words.*


/**
 * Created by charlie.song on 2018/4/28.
 */
class LinearLayoutDrag:LinearLayout{
    constructor(context: Context?) : super(context){
        initDrag()
    }
    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs){
        initDrag()
    }
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr){
        initDrag()
    }

    private lateinit var viewDragHelper: ViewDragHelper

    private var oldLeft=0;
    private var oldTop=0
    private fun initDrag(){
        viewDragHelper= ViewDragHelper.create(this,object : ViewDragHelper.Callback(){
            override fun tryCaptureView(child: View, pointerId: Int): Boolean {
                println("try capture  ${child}  pointer id=$pointerId")
                if(child.id== R.id.tv_word_insert){
                    oldLeft=child.left
                    oldTop=child.top
                    return true
                }
                return false
            }
            //返回值用来限制控件可以移动的范围的
            override fun clampViewPositionHorizontal(child: View?, left: Int, dx: Int): Int {
                return  left
            }

            override fun clampViewPositionVertical(child: View?, top: Int, dy: Int): Int {
                return top
            }
            override fun onViewPositionChanged(changedView: View, left: Int, top: Int, dx: Int, dy: Int) {
                super.onViewPositionChanged(changedView, left, top, dx, dy)
                println("change ==$left  $top  $dx  $dy")
            }

            override fun onViewReleased(releasedChild: View?, xvel: Float, yvel: Float) {
                super.onViewReleased(releasedChild, xvel, yvel)
                println("release=   $xvel   $yvel  $oldLeft -- $oldTop")
                //这是就是反弹回初始位置
                viewDragHelper.settleCapturedViewAt(oldLeft,oldTop)
                postInvalidate()
            }
        })
    }
    //处理是否拦截
    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        //由viewDragHelper 来判断是否应该拦截此事件
        return viewDragHelper.shouldInterceptTouchEvent(ev)
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        //将触摸事件传给viewDragHelper来解析处理
        viewDragHelper.processTouchEvent(event)
        //消费掉此事件,自己来处理
        return true
    }

    override fun computeScroll() {
        if (viewDragHelper.continueSettling(true)) {
            invalidate()
        }
    }
}

需要说明下,在onViewRelease这个方法里如果要回弹这个view
用如下的方法

//                viewDragHelper.settleCapturedViewAt(oldLeft,oldTop)
//                postInvalidate()
还需要实现下边的方法,否则无效
    override fun computeScroll() {
        if (viewDragHelper.continueSettling(true)) {
            invalidate()
        }
    }

实际操作,让一个child移动,下边的流程
首先打印下callback的日志

  1. 可以发现最先进入的是getOrderedChildIndex方法,这个方法里的index是由当前点击的view的index决定的。如果viewgroup里有4个view,当前点击的view的childindex是2的话,那么会返回3,和2,也就是在它之后的index会倒叙打印一遍。如果点在空白处或者首个view上,那么就会打印3,2,1,0
    看下draghelper的方法
    shouldInterceptTouchEvent(ev) 和processTouchEvent(event),
    里边都调用了下下代码,可以看到是从上往下找child的,所以getOrderedChildIndex会打印2次
    public View findTopChildUnder(int x, int y) {
        final int childCount = mParentView.getChildCount();
        for (int i = childCount - 1; i >= 0; i--) {
            final View child = mParentView.getChildAt(mCallback.getOrderedChildIndex(i));
            if (x >= child.getLeft() && x < child.getRight()
                    && y >= child.getTop() && y < child.getBottom()) {
                return child;
            }
        }
        return null;
    }
  1. 根据1的方法,如果你点击的位置有一个view,那么会走tryCaptureView方法,如果没有那就结束了,
    根据tryCaptureView返回true还是false,才会继续走下边的方法
  2. 如果上边你返回了true,会走onViewCaptured
  3. onViewDragStateChanged 这时候这个view的状态就成了dragging了
如果进行了移动,也就是有了action_move,那么会走5的方法,否则直接就到7了
  1. clampViewPositionHorizontal(child: View, left: Int, dx: Int)
    dx :水平方向移动的距离,left:移动后的left坐标,也就是child原来的left加上前边的dx
    需要返回一个值,也就是这个child的left的新的值,这里可以做一些限制,
    比如不让滚出左侧屏幕,就可以判断left如果小于0,直接返回0即可
    clampViewPositionVertical(child: View, top: Int, dy: Int)
    垂直方向的一个道理

  2. onViewPositionChanged 方法5返回值以后,如果left或者top发生了变化,就会走这里了

  3. onViewReleased 手指松开,释放状态,这时候可以添加条件,然后决定要做啥,比如可以让这个view回到原来的位置,或者滚动到别的地方,可以使用如下的方法

//这个方法也可以 dragHelper.smoothSlideViewTo()
dragHelper.settleCapturedViewAt(0,releasedChild.top)
viewGroup.postInvalidate()
  1. onViewDragStateChanged 滚动结束以后,状态又成0了
    流程日志如下
tryCaptureView======android.support.design.widget.AppBarLayout{574a200 V.E...... ........ 0,0-934,220 #7f080021 app:id/app_bar}======0
onViewCaptured================android.support.design.widget.AppBarLayout{574a200 V.E...... ........ 0,0-934,220 #7f080021 app:id/app_bar}======0
onViewDragStateChanged=========1
clampViewPositionVertical==============top/dy=1=1
onViewPositionChanged===============0/2/==dx/dy===0/2
clampViewPositionVertical==============top/dy=3=1
onViewPositionChanged===============0/4/==dx/dy===0/2
onViewReleased============0.0==0.0
onViewDragStateChanged=========0

其他2个方法

刚开始以为这玩意没啥用,后来搜了下https://www.jianshu.com/p/5670a67f0b19
发现这2个方法是对那些本身有触摸事件的view才起作用,比如button,checkbox等
不过这个返回值好像大于0,上边的button,checkbox就可以移动,小于等于0的不能移动,
对应2个方向,看需求决定返回值

override fun getViewHorizontalDragRange(child: View): Int

 override fun getViewVerticalDragRange(child: View): Int

具体可参考源码dragHelper.shouldInterceptTouchEvent(ev)

case MotionEvent.ACTION_MOVE: {
//checkTouchSlop方法里边调用了getViewHorizontalDragRange的方法来返回结果,如果2个方向都为0的话,返回的是false
final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy);
 if (pastSlop) {

final int hDragRange = mCallback.getViewHorizontalDragRange(toCapture);
                        final int vDragRange = mCallback.getViewVerticalDragRange(toCapture);
 if ((hDragRange == 0 || (hDragRange > 0 && newLeft == oldLeft))
                                && (vDragRange == 0 || (vDragRange > 0 && newTop == oldTop))) {
                            break;
                        }

 if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {
                        break;
                    }

这里是上边代码里使用的checkTouchSlop()

    private boolean checkTouchSlop(View child, float dx, float dy) {
        if (child == null) {
            return false;
        }
        final boolean checkHorizontal = mCallback.getViewHorizontalDragRange(child) > 0;
        final boolean checkVertical = mCallback.getViewVerticalDragRange(child) > 0;

        if (checkHorizontal && checkVertical) {
            return dx * dx + dy * dy > mTouchSlop * mTouchSlop;
        } else if (checkHorizontal) {
            return Math.abs(dx) > mTouchSlop;
        } else if (checkVertical) {
            return Math.abs(dy) > mTouchSlop;
        }
        return false;
    }

edge边界捕捉处理

  1. 监听哪个方向,上下左右,可以选
    如下,想要处理哪个方向,就在后边加上哪个,EDGE_ALL是4个方法都监听
draghelper?.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT or ViewDragHelper.EDGE_RIGHT)
  1. onEdgeTouched(edgeFlags: Int, pointerId: Int)
    我们在步骤1里设置了监听的flag以后,如果手指触摸了相应的flag,就会走这里,在这里就可以处理。
    如下,我们可以监听到左侧边界事件以后,把触摸事件交给一个child来处理。
    captureChildView 就是把某个child设置为当前正在capture的状态,之后就和前边捕获child一样了
    override fun onEdgeTouched(edgeFlags: Int, pointerId: Int) {
        super.onEdgeTouched(edgeFlags, pointerId)// 4 2 8
        if(edgeFlags==ViewDragHelper.EDGE_LEFT&&viewGroup.childCount>0){
            dragHelper.captureChildView(viewGroup.getChildAt(0),pointerId)
        }
    }

下边简单分析下各个方法的作用

如果要测试边界触摸功能,需要手动开启,4个方向自己选,或者选个all就都有了。
draghelper?.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT or ViewDragHelper.EDGE_RIGHT)

inner class CallbackBottom2 : ViewDragHelper.Callback {

        var viewGroup: ViewGroup
        var childTop: View

        constructor(viewGroup: ViewGroup) : super() {
            this.viewGroup = viewGroup
            childTop = viewGroup.getChildAt(0)
        }

        var point = Point()
        override fun tryCaptureView(child: View, pointerId: Int): Boolean {
            //返回true表示这个child允许捕获,才会有后边的操作,返回false也就没有后边的操作了,这里可以根据child来决定哪个需要移动
            println("tryCaptureView======$child======$pointerId")
            return true
        }
        //tryCaptureView返回true就会走这里,或者在edge的时候draghelper?.captureChildView里传一个child也会走这里
        override fun onViewCaptured(capturedChild: View, activePointerId: Int) {
            super.onViewCaptured(capturedChild, activePointerId)
            point.x = capturedChild.left
            point.y = capturedChild.top
            println("onViewCaptured================$capturedChild======$activePointerId")
        }
        //这个就是你如果上边返回true,那么就成了dragging状态,
        // 手指离开屏幕onViewReleased如果啥也不操作就成了idle状态了。
        //如果这时候我们settleCapturedViewAt让它回到原始位置,肯定需要时间的,这个时候的状态就是setting了。
        override fun onViewDragStateChanged(state: Int) {
            super.onViewDragStateChanged(state)
            println("onViewDragStateChanged=========$state") //STATE_IDLE STATE_DRAGGING STATE_SETTLING

        }

        //手指离开屏幕会走这里,当然前提是有captured的view
        override fun onViewReleased(releasedChild: View, xvel: Float, yvel: Float) {
            super.onViewReleased(releasedChild, xvel, yvel)
            println("onViewReleased============$xvel==$yvel")
            draghelper?.settleCapturedViewAt(point.x, point.y)
            postInvalidate()
        }

        //需要draghelper设置支持的边界才能生效draghelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT or ViewDragHelper.EDGE_RIGHT)
        override fun onEdgeTouched(edgeFlags: Int, pointerId: Int) {
            super.onEdgeTouched(edgeFlags, pointerId)//1 4 2 8
            println("onEdgeTouched====================$edgeFlags========$pointerId")
            if(edgeFlags==ViewDragHelper.EDGE_RIGHT){
                //边界触摸的时候要操作那个child就把它传进去即可
                draghelper?.captureChildView(findViewById(R.id.tv_right),pointerId)
            }
        }

        override fun onEdgeLock(edgeFlags: Int): Boolean {
            println("onEdgeLock================$edgeFlags")
            return super.onEdgeLock(edgeFlags)
        }

        //也不知道啥用,onEdgeTouched以后,这时候触摸的地方如果没有capturedview的就会走这里
        override fun onEdgeDragStarted(edgeFlags: Int, pointerId: Int) {
            super.onEdgeDragStarted(edgeFlags, pointerId)
            println("onEdgeDragStarted===============$edgeFlags=======$pointerId")
        }

        override fun getOrderedChildIndex(index: Int): Int {
            println("getOrderedChildIndex===============$index")
            return super.getOrderedChildIndex(index)
        }

        //返回0的话就不能垂直移动
        override fun getViewVerticalDragRange(child: View): Int {
            println("getViewVerticalDragRange=========$child=======${child.top}")
            return 110
        }

        override fun getViewHorizontalDragRange(child: View): Int {
            println("getViewHorizontalDragRange=========${child.left}")
            return 110
        }
        //capture view以后继续移动手指,就会走这里,返回0表示不移动,返回其他值表示view新的left位置
        override fun clampViewPositionHorizontal(child: View, left: Int, dx: Int): Int {
            println("clampViewPositionHorizontal============left/dx====$left/$dx")
//        return super.clampViewPositionHorizontal(child, left, dx)
            return if (left + dx <= 0) 0 else left + dx
        }

        //capture view以后继续移动手指,就会走这里,返回0表示不移动,返回其他值表示view新的top位置
        override fun clampViewPositionVertical(child: View, top: Int, dy: Int): Int {
            println("clampViewPositionVertical==============top/dy=$top=$dy")
            return if (top + dy < 0) 0 else top + dy
        }
        //执行了上边clamp的方法就会走这里
        override fun onViewPositionChanged(changedView: View, left: Int, top: Int, dx: Int, dy: Int) {
            super.onViewPositionChanged(changedView, left, top, dx, dy)
            println("onViewPositionChanged===============$left/$top/==dx/dy===$dx/$dy")
        }
    }

写个简单的侧滑关闭页面的

监听下edge_left事件,完事release的时候判断下,速度大于500或者当前位置大于宽度一半,就滚动到右边,完事在state事件里处理,finish掉页面。

class LeftEdgeTouchCloseLayout : FrameLayout {
    constructor(context: Context?) : super(context)
    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

    var draghelper: ViewDragHelper? = null
    private fun makesureHelper() {
        if (draghelper == null) {
            draghelper = ViewDragHelper.create(this, 1f, CallbackBottom2(this))
        }
       draghelper?.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT)
    }

    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        makesureHelper()
        return draghelper!!.shouldInterceptTouchEvent(ev)
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        makesureHelper()
        draghelper!!.processTouchEvent(event)
        return true
    }

    override fun computeScroll() {
        super.computeScroll()
        draghelper?.apply {
            if (this.continueSettling(true)) {
                postInvalidate()
            }
        }
    }

    inner class CallbackBottom2 : ViewDragHelper.Callback {
        var viewGroup: ViewGroup
        constructor(viewGroup: ViewGroup) : super() {
            this.viewGroup = viewGroup
        }
        //这个就是你如果上边返回true,那么就成了dragging状态,
        // 手指离开屏幕onViewReleased如果啥也不操作就成了idle状态了。
        //如果这时候我们settleCapturedViewAt让它回到原始位置,肯定需要时间的,这个时候的状态就是setting了。
        override fun onViewDragStateChanged(state: Int) {
            super.onViewDragStateChanged(state)
            println("onViewDragStateChanged=========$state") //STATE_IDLE STATE_DRAGGING STATE_SETTLING
            when(state){
                ViewDragHelper.STATE_IDLE->{
                    if(viewGroup.getChildAt(0).left>1){
    //如果不关闭页面的话,left应该是0
                        (viewGroup.context as Activity).finish()
                    }
                }
            }
        }

        //手指离开屏幕会走这里,当然前提是有captured的view
        override fun onViewReleased(releasedChild: View, xvel: Float, yvel: Float) {
            super.onViewReleased(releasedChild, xvel, yvel)
            println("onViewReleased============$xvel==$yvel")
            var actionX=0
            if(xvel>500||releasedChild.left>=viewGroup.width/2){
                //关闭页面
               actionX=viewGroup.width
            }
            draghelper?.settleCapturedViewAt(actionX,0)
            postInvalidate()
        }
        //需要draghelper设置支持的边界才能生效draghelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT)
        override fun onEdgeTouched(edgeFlags: Int, pointerId: Int) {
            super.onEdgeTouched(edgeFlags, pointerId)//1 4 2 8
            println("onEdgeTouched====================$edgeFlags========$pointerId")
            if(edgeFlags==ViewDragHelper.EDGE_LEFT){
                //边界触摸的时候要操作那个child就把它传进去即可
                draghelper?.captureChildView(viewGroup.getChildAt(0),pointerId)
            }
        }
        //capture view以后继续移动手指,就会走这里,返回0表示不移动,返回其他值表示view新的left位置
        override fun clampViewPositionHorizontal(child: View, left: Int, dx: Int): Int {
            println("clampViewPositionHorizontal============left/dx====$left/$dx")
//        return super.clampViewPositionHorizontal(child, left, dx)
            return if (left + dx <= 0) 0 else left + dx
        }

    }

}

之后主题弄成背景透明,要不我们滑动的时候还能能看到一个白色的背景,也就没撒用了,我们要看到的是底层activity的页面。

  <item name="android:windowIsTranslucent">true</item>
        <item name="android:windowBackground">@android:color/transparent</item>

在之后,基类里写下,替换掉原有的布局,外边包裹一层我们上边自定义的布局即可

  super.setContentView(layoutResID)
        (window.decorView as FrameLayout).apply {
            var originalView = this.getChildAt(0)
          
            originalView.setBackgroundColor(Color.WHITE)
            this.removeView(originalView)
            var addView = com.charliesong.demo0327.draghelper.LeftEdgeTouchCloseLayout(this@BaseActivity)
            addView.addView(originalView, originalView.layoutParams)
            this.addView(addView,
                    android.widget.FrameLayout.LayoutParams.MATCH_PARENT, android.widget.FrameLayout.LayoutParams.MATCH_PARENT)
        }

好了,如此,一个简单的侧滑关闭页面的功能就实现了。

开始测试,普通页面没啥问题。有布局实时发生变化的页面就不行

比如有一个页面,我写的类似弹幕那种,每隔一秒添加一个view或者删除一个view。我发现这个页面不行,滑动的时候就自动滚回去了。因为手指虽然还在屏幕上,可貌似触摸事件自动取消了,进入了onViewReleased方法了。可能那边刷新布局。

还有类似这种recyclerview不停的添加删除数据也会引起触摸事件失效的

    val viewRunnble = object : Runnable {
        override fun run() {
            adapterRV.apply {
                if (adapterRV.itemCount < 4) {
                    this.datas.add(messages[index % messages.size])//最后一个位置添加数据并notify
                    index++
                    notifyItemInserted(datas.size - 1)
                } else {
                    this.datas.removeAt(0)//删除第一条数据
                    rv_toast.adapter.notifyItemRemoved(0)
                }
            }
            handler.postDelayed(this, 1000)
        }
    }

最后的感觉就是如果你在滑动的时候,页面进行postinvalidate之类的操作,这个就不行了。
比如 view.setlayoutparams 这个也会刷新布局的,也就不行了。

ps最后的解决办法

不是很完美,不过也不像以前根本就划不动

//首先我给在基类里加的那个LeftEdgeTouchCloseLayout弄了个id 
addView.id= R.id.edgetouchid
//完事上边的runnable里判断下是否我们是否已经开始侧滑了【侧滑的话肯定left不是0了】
findViewById<ViewGroup>(R.id.edgetouchid).getChildAt(0).left>0
//如果侧滑的话我就不进行操作了,直接handler.postDelayed(this, 1000)

源码分析

简单分析下源码,就知道callback里回调的参数意义,以及啥时候调用了

  1. draghelper!!.processTouchEvent(event)

            case MotionEvent.ACTION_MOVE: {
                if (mDragState == STATE_DRAGGING) {
                    // If pointer is invalid then skip the ACTION_MOVE.
                    if (!isValidPointerForActionMove(mActivePointerId)) break;

                    final int index = ev.findPointerIndex(mActivePointerId);
                    final float x = ev.getX(index);
                    final float y = ev.getY(index);
                    final int idx = (int) (x - mLastMotionX[mActivePointerId]);
                    final int idy = (int) (y - mLastMotionY[mActivePointerId]);

                    dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy);

                    saveLastMotion(ev);
                } else {
                    // Check to see if any pointer is now over a draggable view.
                    final int pointerCount = ev.getPointerCount();
                    for (int i = 0; i < pointerCount; i++) {
                        final int pointerId = ev.getPointerId(i);

                        // If pointer is invalid then skip the ACTION_MOVE.
                        if (!isValidPointerForActionMove(pointerId)) continue;

                        final float x = ev.getX(i);
                        final float y = ev.getY(i);
                        final float dx = x - mInitialMotionX[pointerId];
                        final float dy = y - mInitialMotionY[pointerId];

                        reportNewEdgeDrags(dx, dy, pointerId);
                        if (mDragState == STATE_DRAGGING) {
                            // Callback might have started an edge drag.
                            break;
                        }

                        final View toCapture = findTopChildUnder((int) x, (int) y);
                        if (checkTouchSlop(toCapture, dx, dy)
                                && tryCaptureViewForDrag(toCapture, pointerId)) {
                            break;
                        }
                    }
                    saveLastMotion(ev);
                }
                break;
            }

dragTo :left期望值,也就是当前view的left加上手指滑动的距离,dx手指滑动的距离

    private void dragTo(int left, int top, int dx, int dy) {
        int clampedX = left;
        int clampedY = top;
        final int oldLeft = mCapturedView.getLeft();
        final int oldTop = mCapturedView.getTop();
        if (dx != 0) {
//这个clampedX一般返回参数里的left就行,也就是oldLeft +dx
            clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx);
//根据clampedX是oldLeft的差值来移动view
            ViewCompat.offsetLeftAndRight(mCapturedView, clampedX - oldLeft);
        }
        if (dy != 0) {
            clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy);
            ViewCompat.offsetTopAndBottom(mCapturedView, clampedY - oldTop);
        }

        if (dx != 0 || dy != 0) {
            final int clampedDx = clampedX - oldLeft;
            final int clampedDy = clampedY - oldTop;
        //view移动后的left,top位置,以及对应的x,y轴移动的距离
            mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY,
                    clampedDx, clampedDy);
        }
    }

相关文章

网友评论

      本文标题:ViewDragHelper以及简单的侧滑关闭页面实现

      本文链接:https://www.haomeiwen.com/subject/rmxnlftx.html