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

随便百度搜了几篇看看

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 这个也会刷新布局的,也就不行了。

老的不是办法的办法

后边重写了LeftEdgeTouchCloseLayout,没有这种问题了,也就不需要特殊处理了
下边代码看看就行,用不到了

//首先我给在基类里加的那个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,view移动的时候调用的是offsetLeftAndRight 而这个方法在页面刷新的时候被还原了。
而我们刷新页面,调用了addView,removeView等,最后都会requestLayout来刷新页面。
解决:移动view不是用offsetLeftAndRight,而使用setTranslationX
dragHelper的callback方法里,系统源码里使用offsetLeftAndRight移动dx,我们接着移动-dx还原,然后调用setTranslationX来移动view

首先主题要是透明的,因为这个侧滑要看到下层的activity的,所以主题添加如下代码

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

背景透明以后,我们需要给我们的布局设置背景色,否则就看到下层了。就在我们的xml的根布局上添加个颜色,不添加也可以,工具类里默认添加了白色背景,可以修改
完整代码如下
多写了个类,方便一行代码给所有的页面添加侧滑功能
application里,第二个参数是不需要侧滑的页面,可以为null

SwipeBackUtil.init(this, arrayListOf(ActivityViewMoveTest::class.java))
//or
SwipeBackUtil.init(this)

工具SwipeBackUtil
原理很简单,里用application的registerActivityLifecycleCallbacks方法,监听activity的创建和销毁,然后修改布局
把decorView里的布局拿出来,放到我们自定义的容器里,完事把我们的容器放到decorView下。

import android.app.Activity
import android.app.Application
import android.graphics.Color
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import com.charliesong.demo0327.R

object SwipeBackUtil {

    var defaultBgColor=Color.WHITE
    fun register(activity: Activity) {
        if(excludes.contains(activity.javaClass)){
            activity.window.decorView.setBackgroundColor(defaultBgColor)
            return
        }
        (activity.window.decorView as FrameLayout).apply {
            val originalView = this.getChildAt(0)
            originalView.setBackgroundColor(defaultBgColor)
            this.removeView(originalView)
            val swipeBackView = LeftEdgeTouchCloseLayout(activity)
            swipeBackView.id = R.id.edgetouchid
            swipeBackView.addView(originalView, originalView.layoutParams)
            this.addView(swipeBackView,
                    android.widget.FrameLayout.LayoutParams.MATCH_PARENT, android.widget.FrameLayout.LayoutParams.MATCH_PARENT)
        }
    }
    val excludes= arrayListOf<Class<*>>()
    val activitys = arrayListOf<Activity>()
    fun init(app: Application,excludeClass:ArrayList<Class<*>>){
        excludes.clear()
        excludes.addAll(excludeClass)
        init(app)
    }
    fun init(app: Application) {
        activitys.clear()
        val callback = object : Application.ActivityLifecycleCallbacks {
            override fun onActivityPaused(activity: Activity?) {
            }

            override fun onActivityResumed(activity: Activity?) {
            }

            override fun onActivityStarted(activity: Activity?) {
            }

            override fun onActivityDestroyed(activity: Activity) {
                activitys.remove(activity)
                if (activitys.size == 0) {
                    app.unregisterActivityLifecycleCallbacks(this)
                }
            }

            override fun onActivitySaveInstanceState(activity: Activity?, outState: Bundle?) {
            }

            override fun onActivityStopped(activity: Activity?) {
            }

            override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
                activitys.add(activity)
                register(activity)
            }
        }
        app.registerActivityLifecycleCallbacks(callback)
    }

    fun getBackView(): View? {
        var preIndex = activitys.size - 2

        while (preIndex >= 0) {
            val activity = activitys[preIndex]
            if (activity.isFinishing) {
                preIndex--
            } else {
                return (activity.window.decorView as ViewGroup).getChildAt(0)
            }
        }
        return null
    }

    fun restorBackView() {
        getBackView()?.translationX=0f
    }
}

自定义侧滑类LeftEdgeTouchCloseLayout
代码解析在后边

import android.app.Activity
import android.content.Context
import android.support.v4.widget.ViewDragHelper
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.animation.Interpolator
import android.view.inputmethod.InputMethodManager
import android.widget.FrameLayout
import android.widget.OverScroller

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())
            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()
        if(release){
            if (scroller.computeScrollOffset()) {
                val left = scroller.currX
                getMoveView().translationX=left.toFloat()
                postInvalidateOnAnimation()
                backView?.translationX = (left - width) / factor
            }else{
                release=false
                SwipeBackUtil.restorBackView()
                if(getMoveView().translationX>10){
                    (context as Activity).onBackPressed()
                }
            }
        }

    }

    private val sInterpolator = Interpolator { t ->
        var t = t
        t -= 1.0f
        t * t * t * t * t + 1.0f
    }
    val scroller = OverScroller(context, sInterpolator)
    private fun getMoveView(): View {
        var index = 0
        if (childCount > 1) {
            index = 1
        }
        return getChildAt(index)
    }
    var backView: View? = null//底层activity的decorview
    var totolMove = 0//手指总的滑动距离
    var release=false//松开手指未true
    inner class CallbackBottom2 : ViewDragHelper.Callback {

        constructor() : super() {
        }

        override fun tryCaptureView(child: View, pointerId: Int): Boolean {
            //返回true表示这个child允许捕获,才会有后边的操作,返回false也就没有后边的操作了,这里可以根据child来决定哪个需要移动
            return false
        }

        //tryCaptureView返回true就会走这里,或者在edge的时候draghelper?.captureChildView里传一个child也会走这里
        override fun onViewCaptured(capturedChild: View, activePointerId: Int) {
            super.onViewCaptured(capturedChild, activePointerId)
            totolMove = 0
            release=false;
//            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==$xvel==$yvel=========left:${releasedChild.left}==${totolMove}")
            var actionX = 0
            if (xvel > 500 || releasedChild.translationX >= width / 2) {
                //关闭页面
                actionX = width
            }
            scroller.startScroll(totolMove,0,actionX-totolMove,0,333)
            release=true
            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_LEFT) {
                //边界触摸的时候要操作那个child就把它传进去即可
                if (backView == null) {
                    backView = SwipeBackUtil.getBackView()
                }
                draghelper?.captureChildView(getMoveView(), pointerId)
                closeKeybord(this@LeftEdgeTouchCloseLayout,context)
            }
        }

        //参数left:这个是原本的left加上dx后的值
        override fun clampViewPositionHorizontal(child: View, left: Int, dx: Int): Int {
//            println("clampViewPositionHorizontal============left/dx====$left/$dx===$child==${totolMove}")
            totolMove += dx;
            return Math.max(0, left)
        }

        //执行了上边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")
            changedView.offsetLeftAndRight(-dx)//helper里是offset dx的距离,我们这里还原回来,这个方法,布局刷新以后就还原了。
            changedView.translationX = totolMove.toFloat()//修改为移动tranxlationX,这个不受布局刷新的影响
            if (totolMove <= width) {
                backView?.translationX = (totolMove - width) / factor
            }
        }
    }

    val factor = 3f
    fun closeKeybord(v: View, mContext: Context) {
        val imm = mContext.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
        imm.hideSoftInputFromWindow(v.windowToken, 0)
    }
}

和以前的区别,有几个地方
1.增加几个变量

    var backView: View? = null//底层activity的decorview
    var totolMove = 0//手指总的滑动距离
    var release=false//松开手指未true
  1. 记录手指总的滑动距离
        override fun clampViewPositionHorizontal(child: View, left: Int, dx: Int): Int {
//            println("clampViewPositionHorizontal============left/dx====$left/$dx===$child==${totolMove}")
            totolMove += dx;
            return Math.max(0, left)
        }
  1. 修改布局的移动方式
        override fun onViewPositionChanged(changedView: View, left: Int, top: Int, dx: Int, dy: Int) {
            super.onViewPositionChanged(changedView, left, top, dx, dy)
            changedView.offsetLeftAndRight(-dx)//helper里是offset dx的距离,我们这里还原回来,这个方法,布局刷新以后就还原了。
            changedView.translationX = totolMove.toFloat()//修改为移动tranxlationX,这个不受布局刷新的影响
            if (totolMove <= width) {
                backView?.translationX = (totolMove - width) / factor
            }
        }

4.自己声明个Scroller来处理手指松开后的滑动

val scroller = OverScroller(context, sInterpolator)
        override fun onViewReleased(releasedChild: View, xvel: Float, yvel: Float) {
            super.onViewReleased(releasedChild, xvel, yvel)
            var actionX = 0
            if (xvel > 500 || releasedChild.translationX >= width / 2) {
                //关闭页面
                actionX = width
            }
//下边三行替换了dragerHelper的scroller事件。
            scroller.startScroll(totolMove,0,actionX-totolMove,0,333)
            release=true
            postInvalidate()
        }

5.处理scroller

    override fun computeScroll() {
        super.computeScroll()
        if(release){
            if (scroller.computeScrollOffset()) {
                val left = scroller.currX
                getMoveView().translationX=left.toFloat()
                postInvalidateOnAnimation()
                backView?.translationX = (left - width) / factor
            }else{
                release=false
                SwipeBackUtil.restorBackView()
                if(getMoveView().translationX>10){
                    (context as Activity).onBackPressed()
                }
            }
        }
    }

6.总结下就是
系统用的offsetLeftAndRight来移动布局,我们修改为tranxlationX。这样不受页面刷新的影响。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354

推荐阅读更多精彩内容

  • 转载请标明出处: http://blog.csdn.net/lmj623565791/article/detail...
    小红豆610阅读 402评论 0 0
  • 前言 在自定义ViewGroup中,很多效果都包含用户手指去拖动其内部的某个View(eg:侧滑菜单等),针对具体...
    浪人_天涯阅读 882评论 0 3
  • 亲爱的向日葵大小朋友们: 大家好!新的一年,新的征程已然拉开序幕,那些尚未实现的梦想和还未到达的远方,都在等待我...
    陪伴的色彩倩倩阅读 775评论 0 1
  • 一片一片,闪烁而浮泛 傍晚的微风夹带了沉坠的气息 叶子不安份的摇曳,相互倾诉 是着急着要飘落,还是恋念不舍? 南方...
    木棉禅心阅读 442评论 0 7
  • 就这样堕落 尽情享受死亡的感受 谈梦想,谈爱人 温暖后的折磨 怎能快乐?
    豆儿月阅读 146评论 0 0