为何说Android ViewDragHelper是神器 (实战)

前言: 前面写了两篇关于ViewDragHelper的博客了,自我感觉收获了挺多知识的,光说不敲还是不行的,所以准备结合项目的例子写一篇关于VDH的实战篇。
项目需求: 做到跟ios一样的效果,滑动Activity的左边距的时候可以退出当前Activity,也就是右滑退出的效果,如果我表述不太清楚的话,可以想象下侧滑菜单,当我们隐藏菜单的时候,菜单消失,底部Activity显示,在碰到此需求的时候我第一个定位的工具就是VDH,然后就到网上搜了一下,发现在很久以前别人就写出了这么个东西,而且适配度还很高,我又何必去造轮子呢?(^__^) 嘻嘻……

先附上项目的github链接:
https://github.com/ikew0ng/SwipeBackLayout
(ps:看了下项目创建的时间,3 years ago,悲哀啊!我与世界差距太大了,不管咋滴,加油吧,骚年!!!)
先看下运行的效果:
这里写图片描述

如果对VDH的api不了解的话,可以去参考下我的前两篇博客:

思路:
1、创建一个ViewGroup,然后把Activity所在window的contentView
加入到我们创建的ViewGroup中。
2、通过VDH对自定义ViewGroup中的contentView进行边界检查、滑动
的处理。
3、根据滑动的距离添加背景、判断滑动的距离后finishActivity。

思路是很简单的,但是做起来还不是那么容易的,接下来我们一步一步实现下这个很牛掰的控件。

attrs.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="SwipeBackLayout">
        <!--可触碰边界的大小-->
        <attr name="edge_size" format="dimension"/>
        <!--滑动边界的位置为:左、右、下、全部-->
        <attr name="edge_flag" format="enum">
            <enum name="left" value="0"/>
            <enum name="right" value="1"/>
            <enum name="bottom" value="2"/>
            <enum name="all" value="3"/>
        </attr>
        <!--左边的背影-->
        <attr name="shadow_left" format="reference"/>
        <!--右边的背景-->
        <attr name="shadow_right" format="reference"/>
        <!--下边的背景-->
        <attr name="shadow_bottom" format="reference"/>
    </declare-styleable>
</resources>

SwipeBackLayout.xml:

package com.cisetech.demo.demo;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
import android.widget.FrameLayout;

import com.cisetech.demo.R;
import com.cisetech.demo.ViewDragHelper;

/**
 * author:yinqingy
 * date:2016-11-07 15:24
 * blog:http://blog.csdn.net/vv_bug
 * desc:SwipeBack
 */

public class SwipeBackLayout extends FrameLayout {
    /**
     * 从左边开始滑动
     */
    public static final int EDGE_LEFT= ViewDragHelper.EDGE_LEFT;
    /**
     * 从右边开始滑动
     */
    public static final int EDGE_RIGHT= ViewDragHelper.EDGE_RIGHT;
    /**
     * 从底部开始滑动
     */
    public static final int EDGE_BOTTOM= ViewDragHelper.EDGE_BOTTOM;
    /**
     * 所有的方向可以滑动
     */
    public static final int EDGE_ALL= EDGE_LEFT|EDGE_RIGHT|EDGE_BOTTOM;
    private static final int[]EDGE_FLAGS={
      EDGE_LEFT,EDGE_RIGHT,EDGE_BOTTOM,EDGE_ALL
    };
    /**
     * 滑动的方向
     */
    private int mEdgeFlag;
    private ViewDragHelper mDragHelper;
    private Drawable mShadowLeft;
    private Drawable mShadowRight;
    private Drawable mShadowBottom;
    public SwipeBackLayout(Context context) {
        this(context,null);
    }

    public SwipeBackLayout(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public SwipeBackLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mDragHelper=ViewDragHelper.create(this,new MyCallBack());
        obtainStyleAttrs(context,attrs,defStyleAttr);
    }

    /**
     * 获取参数属性
     * @param context
     * @param attrs
     * @param defStyleAttr
     */
    private void obtainStyleAttrs(Context context, AttributeSet attrs, int defStyleAttr) {
        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SwipeBackLayout, defStyleAttr,0);
        int count = a.getIndexCount();
        for (int i = 0; i <count; i++) {
            int attr=a.getIndex(i);
            switch (attr){
                case R.styleable.SwipeBackLayout_edge_size:
                    int edgeSize=a.getDimensionPixelSize(attr,-1);
                    if(edgeSize>0)
                    break;
                case R.styleable.SwipeBackLayout_edge_flag:
                    int mode = EDGE_FLAGS[a.getInt(R.styleable.SwipeBackLayout_edge_flag, 0)];
                    setEdgeTrackingEnabled(mode);
                    break;
                case R.styleable.SwipeBackLayout_shadow_left:
                    int shadowLeft = a.getResourceId(R.styleable.SwipeBackLayout_shadow_left,
                            R.drawable.shadow_left);
                    setShadow(shadowLeft, EDGE_LEFT);
                    break;
                case R.styleable.SwipeBackLayout_shadow_right:
                    int shadowRight = a.getResourceId(R.styleable.SwipeBackLayout_shadow_right,
                            R.drawable.shadow_right);
                    setShadow(shadowRight, EDGE_RIGHT);
                    break;
                case R.styleable.SwipeBackLayout_shadow_bottom:
                    int shadowBottom = a.getResourceId(R.styleable.SwipeBackLayout_shadow_bottom,
                            R.drawable.shadow_bottom);
                    setShadow(shadowBottom, EDGE_BOTTOM);
                    break;
            }
        }
        a.recycle();
    }

    /**
     * ViewDragHelper回调
     */
    private class MyCallBack extends ViewDragHelper.Callback{
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            return false;
        }
    }
    /**
     * 设置滑动方向
     * @param edgeFlags
     */
    public void setEdgeTrackingEnabled(int edgeFlags) {
        mEdgeFlag = edgeFlags;
        mDragHelper.setEdgeTrackingEnabled(mEdgeFlag);
    }

    /**
     * 设置边缘的背影
     * @param resId
     * @param edgeFlag
     */
    public void setShadow(int resId, int edgeFlag) {
        setShadow(getResources().getDrawable(resId), edgeFlag);
    }
    public void setShadow(Drawable shadow, int edgeFlag) {
        if ((edgeFlag & EDGE_LEFT) != 0) {
            mShadowLeft = shadow;
        } else if ((edgeFlag & EDGE_RIGHT) != 0) {
            mShadowRight = shadow;
        } else if ((edgeFlag & EDGE_BOTTOM) != 0) {
            mShadowBottom = shadow;
        }
        invalidate();
    }
}

重点说一下VDH的回调:

    /**
     * ViewDragHelper回调
     */
    private class MyCallBack extends ViewDragHelper.Callback{
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            return false;
        }
    }

1、我们不能直接拖动,当在ViewGroup边缘拖动的时候开始处理拖动。
利用mDragHelper中的isEdgeTouched方法先判断是否是边界触碰,

@Override
        public boolean tryCaptureView(View child, int pointerId) {
            //判断是否是边缘触碰
            boolean ret = mDragHelper.isEdgeTouched(mEdgeFlag, pointerId);
            if (ret) {
                //判断是哪个方向的触碰
                if (mDragHelper.isEdgeTouched(EDGE_LEFT, pointerId)) {
                    mTrackingEdge = EDGE_LEFT;
                } else if (mDragHelper.isEdgeTouched(EDGE_RIGHT, pointerId)) {
                    mTrackingEdge = EDGE_RIGHT;
                } else if (mDragHelper.isEdgeTouched(EDGE_BOTTOM, pointerId)) {
                    mTrackingEdge = EDGE_BOTTOM;
                }
            }
            //防止斜着滑动的那种情况
            boolean directionCheck = false;
            if (mEdgeFlag == EDGE_LEFT || mEdgeFlag == EDGE_RIGHT) {
                //如果是左右模式,不满足垂直可以滑动的时候
                directionCheck = !mDragHelper.checkTouchSlop(ViewDragHelper.DIRECTION_VERTICAL, pointerId);
            } else if (mEdgeFlag == EDGE_BOTTOM) {
                //如果是底部滑动的时候,不满足水平滑动的时候
                directionCheck = !mDragHelper
                        .checkTouchSlop(ViewDragHelper.DIRECTION_HORIZONTAL, pointerId);
            } else if (mEdgeFlag == EDGE_ALL) {
                directionCheck = true;
            }
            //是边缘滑动并且满足可以滑动的时候,contentview才可以拖动
            return ret & directionCheck;
        }

2、防止contentView默认具有点击事件而消费掉事件无法滑动的情况,我们要重写getViewHorizontalDragRange跟getViewVerticalDragRange。

  @Override
        public int getViewHorizontalDragRange(View child) {
            //返回值必须>0,所以只要是EDGE_LEFT或者EDGE_RIGHT模式,就返回1
            return mEdgeFlag & (EDGE_LEFT | EDGE_RIGHT);
        }

        @Override
        public int getViewVerticalDragRange(View child) {
            //返回值必须>0,所以只要是EDGE_BOTTOM,就返回1
            return mEdgeFlag & EDGE_BOTTOM;
        }

具体原因我就不去详细解析了,可以去看一下我前面两篇博客(^__^) 嘻嘻……

3、重写onViewPositionChanged,根据contentView位置变换做处理。

@Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            super.onViewPositionChanged(changedView, left, top, dx, dy);
            //获取一个滑动位置的百分比参数
            if ((mTrackingEdge & EDGE_LEFT) != 0) {
                //如果是左边滑动的情况,mScrollPercent=拖动过后的left/(contentView的宽度+左边阴影的宽度)
                mScrollPercent = Math.abs((float) left
                        / (mContentView.getWidth() + mShadowLeft.getIntrinsicWidth()));
            } else if ((mTrackingEdge & EDGE_RIGHT) != 0) {
                //如果是右边滑动的情况,获取到的left为赋值,所以需要取绝对值
                mScrollPercent = Math.abs((float) left
                        / (mContentView.getWidth() + mShadowRight.getIntrinsicWidth()));
            } else if ((mTrackingEdge & EDGE_BOTTOM) != 0) {
                //如果是底部滑动的情况,mScrollPercent=(top/(contentView的高度+底部阴影的高度))
                mScrollPercent = Math.abs((float) top
                        / (mContentView.getHeight() + mShadowBottom.getIntrinsicHeight()));
            }
            //获取拖动过后的left跟top,然后调用invalidate会重新绘制ViewGroup,在onLayout方法中重新摆放子控件位置
            mContentLeft = left;
            mContentTop = top;
            invalidate();
            /**
             * 当滑动的距离百分比》=1的时候,结束掉当前Activity
             */
            if (mScrollPercent >= 1) {
                if (!mActivity.isFinishing()) {
                    mActivity.finish();
                    mActivity.overridePendingTransition(0, 0);
                }
            }
        }

4、当手指抬起的时候,如果滑动的距离>=我们设置的Activty关闭的距离后,自动滑动退出,否则就回到初始位置。

@Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            final int childWidth = releasedChild.getWidth();
            final int childHeight = releasedChild.getHeight();

            int left = 0, top = 0;
            //当为左边滑动的时候
            if ((mTrackingEdge & EDGE_LEFT) != 0) {
                //x轴上滑动的速度>=0并且滑动的距离到达总距离自定的0.3f的时候,left即为(content的宽度+左阴影的宽度+超出滑动距离的offset)
                left = xvel > 0 || xvel == 0 && mScrollPercent > mScrollThreshold ? childWidth
                        + mShadowLeft.getIntrinsicWidth() + OVERSCROLL_DISTANCE : 0;
            } else if ((mTrackingEdge & EDGE_RIGHT) != 0) {
                left = xvel < 0 || xvel == 0 && mScrollPercent > mScrollThreshold ? -(childWidth
                        + mShadowLeft.getIntrinsicWidth() + OVERSCROLL_DISTANCE) : 0;
            } else if ((mTrackingEdge & EDGE_BOTTOM) != 0) {
                top = yvel < 0 || yvel == 0 && mScrollPercent > mScrollThreshold ? -(childHeight
                        + mShadowBottom.getIntrinsicHeight() + OVERSCROLL_DISTANCE) : 0;
            }
            //自动滚动到指定的left跟top位置
            mDragHelper.settleCapturedViewAt(left, top);
            invalidate();
        }

既然有settleCapturedViewAt方法自动回到自定位置,那么肯定会有:

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

5、最后因为有边界限制的问题,所以要重写clampViewPositionHorizontal跟clampViewPositionVertical方法

 @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            int ret = 0;
            //如果为左边界滑动,边界限制为(最小值为0,最大值为contentview的宽度)
            //如果为右边界滑动,边界限制为(最小值为-contentView的宽度,最大值为0)
            if ((mTrackingEdge & EDGE_LEFT) != 0) {
                ret = Math.min(child.getWidth(), Math.max(left, 0));
            } else if ((mTrackingEdge & EDGE_RIGHT) != 0) {
                ret = Math.min(0, Math.max(left, -child.getWidth()));
            }
            return ret;
        }

        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            int ret = 0;
            //如果为底部滑动,边界限制为(最小值为-contentView的高度,最大值为0)
            if ((mTrackingEdge & EDGE_BOTTOM) != 0) {
                ret = Math.min(0, Math.max(top, -child.getHeight()));
            }
            return ret;
        }

6、重写onLayout方法,摆置ContentView的位置:

@Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        mInLayout = true;
        if (mContentView != null)
            mContentView.layout(mContentLeft, mContentTop,
                    mContentLeft + mContentView.getMeasuredWidth(),
                    mContentTop + mContentView.getMeasuredHeight());
        mInLayout = false;
    }

6、最后把事件的拦截跟处理给VDH

@Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        if (!mEnable) {
            return false;
        }
        try {
            return mDragHelper.shouldInterceptTouchEvent(event);
        } catch (ArrayIndexOutOfBoundsException e) {
            // FIXME: handle exception
            // issues #9
            return false;
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (!mEnable) {
            return false;
        }
        mDragHelper.processTouchEvent(event);
        return true;
    }

对SwipeBackLayout简单的进行了下解析说明,具体代码,小伙伴们可以直接下载一个SwipeBackLayout,还有一些其他的功能跟封装我就不详细解析了,反正我是跟着敲了很多遍(^__^) 嘻嘻……

好啦!!VDH就暂时告一段落了,总算是解决掉了,继续努力啊!!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值