许多朋友在开发中,经常会碰到许多的拖放,滑动效果处理。手势的判断让人烦不胜烦,还经常出现这样那样的问题,真是心累!
然而,在Android中,Google在其中support库的v4包中,为我们提供类一个强大的类——ViewDragHelper。通过这个类,我们基本可以实现各种各样的滑动与拖放效果。下面我们就来初步的了解一下这个类的用法。
如上图,我们通过ViewDragHelper来实现。
我们自定义一个DragViewLayoutOne,它继承了一个LinearLayout,代码如下
public class DragViewLayoutOne extends LinearLayout {
private ViewDragHelper mViewDragHelper;
public DragViewLayoutOne(Context context) {
super(context);
initView();
}
public DragViewLayoutOne(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public DragViewLayoutOne(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
private void initView() {
mViewDragHelper = ViewDragHelper.create(this, callback);//通过其静态方法创建ViewDragHelper对象
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) { //重写事件拦截方法
return mViewDragHelper.shouldInterceptTouchEvent(event); //是否拦截,交由ViewDragHelper进行判断
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mViewDragHelper.processTouchEvent(event);//将事件传递给ViewDragHelper处理
return true; //消费事件,使事件处理不在向上移交
}
private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() { //ViewDragHelper的回调内部类
@Override
public boolean tryCaptureView(View child, int pointerId) { //指定可以被移动的子View
return true;
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) { //水平方向上的移动回调
return left;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) { //垂直方向上的移动回调
return top;
}
};
}
创建ViewDragHelper实例
其实DragViewLayoutOne就是一个自定义的ViewGroup,或者说是一个自定义的LinearLayout,我们通过ViewDragHelper的静态方法creat来创建它的对象,下面为creat的源代码:
public static ViewDragHelper create(ViewGroup forParent, Callback cb) {
return new ViewDragHelper(forParent.getContext(), forParent, cb);
}
public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) {
final ViewDragHelper helper = create(forParent, cb);
helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity));
return helper;
}
我们可以看到,ViewDragHelper实际上拥有两个不同参数的creat方法可以进行ViewDragHelper的实例化,不过,可以看到,三个参数的creat中,也是通过调用两个参数的creat方法获得其对象的。
参数介绍:
1、第一个参数forParent就是被监控的父视图,我们监控的是当前自定义的ViewGroup内部子View的移动,所以用this。
2、第二个参数sensitivity是用来设置mTouchSlop的,通过上面源码可以发现,sensitivity值越小,mTouchSlop越大。至于mTouchSlop是什么,可以理解为,被认为是滑动状态的最小距离。简单来说就是,我们手指从ACTION_DOWN到ACTION_MOVE时,两个点之间的距离超过mTouchSlop这个最小临界值时,才会被认为是滑动状态。用来优化用户的交互体验。
3、第三个参数cb就是ViewDragHelper的内部回调对象Callback。
处理触摸事件
@Override
public boolean onInterceptTouchEvent(MotionEvent event) { //重写事件拦截方法
return mViewDragHelper.shouldInterceptTouchEvent(event); //是否拦截,交由ViewDragHelper进行判断
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mViewDragHelper.processTouchEvent(event);//将事件传递给ViewDragHelper处理
return true; //消费事件,使事件处理不在向上移交
}
通常,我们或重写这个自定义ViewGroup的事件拦截与处理方法,在其拦截方法onInterceptTouchEvent中,通过ViewDragHelper的shouldInterceptTouchEvent(event)
对Callback的方法进行实现
private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() { //ViewDragHelper的回调内部类
@Override
public boolean tryCaptureView(View child, int pointerId) { //指定可以被移动的子View
return true;
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) { //水平方向上的移动回调
return left;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) { //垂直方向上的移动回调
return top;
}
};
clampViewPositionHorizontal和clampViewPositionVertical方法分别是设置水平和垂直方向上可移动的距离。这两个方法IDE不会为我们自动重写,但是我们根据滑动需要,必须重写其中某个或者全部重写,因为他们默认返回0,代表可滑动距离为0,不重写会发现无法实现滑动效果。
其中,left和top就是对应放下上的滑动距离,我们直接返回,就是可以滑动任意的距离。当然,这样就会是活动可以超出屏幕边界,如果想要在屏幕内左右滑动,还不超出屏幕边界,代码如下:
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) { //水平方向上的移动回调
final int leftBound = getPaddingLeft(); //左边界数值
final int rightBound = getWidth() - getChildAt(0).getWidth();//右边界数值
final int newLeft = Math.min(Math.max(left, leftBound), rightBound); //可滑动到边界数值
return newLeft;
}
最后,就是布局文件了:
到此,我们就实现了图中的滑动效果了。
然而,在ViewDragHelper.Callback中,还为我们提供了许多方便的回调方法,让我们快捷的做出更加多样的滑动拖放效果。
依然是三个view,第一个和之前一样,可以随意拖动。第二个在释放手指的时候,回到初始位置。第三个是通过滑动屏幕边缘,实现对其的拖动。
下面,我们来看看是怎么实现的。
public class DragViewLayoutTwo extends LinearLayout {
private ViewDragHelper mViewDragHelper;
private TextView dragViewOne;
private TextView dragViewTwo;
private TextView dragViewThree;
private int startLeft, startTop; //记录dragViewTwo的初始位置
public DragViewLayoutTwo(Context context) {
super(context);
initView();
}
public DragViewLayoutTwo(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public DragViewLayoutTwo(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
private void initView() {
mViewDragHelper = ViewDragHelper.create(this, callback);
mViewDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);//设置左边缘拖动模式
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
//获取其中的三个子View
dragViewOne = (TextView) getChildAt(0);
dragViewTwo = (TextView) getChildAt(1);
dragViewThree = (TextView) getChildAt(2);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
//获取dragViewTwo的初始位置
startLeft = dragViewTwo.getLeft();
startTop = dragViewTwo.getTop();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
return mViewDragHelper.shouldInterceptTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mViewDragHelper.processTouchEvent(event);
return true;
}
private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {
return child == dragViewOne || child == dragViewTwo; //当捕获的child是dragViewOne或dragViewTwo时,可以拖动
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return left;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return top;
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) { //释放捕获的view时执行
super.onViewReleased(releasedChild, xvel, yvel);
if (releasedChild == dragViewTwo) {
mViewDragHelper.smoothSlideViewTo(dragViewTwo, startLeft, startTop);//使dragViewTwo滑动到初始位置
ViewCompat.postInvalidateOnAnimation(DragViewLayoutTwo.this);//刷新页面显示
}
}
@Override
public void onEdgeDragStarted(int edgeFlags, int pointerId) { //当设置了边缘滑动后,会触发此回调方法
//滑动边缘时,捕获dragViewThree为滑动对象。此方法能够越过tryCaptureView指定滑动的View
mViewDragHelper.captureChildView(dragViewThree, pointerId);
}
};
@Override
public void computeScroll() {
if (mViewDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
}
代码部分注释已经阐述了方法的作用,我们来逐条说明一下:
首先,在实例化ViewDragHelper后,我进行了边缘拖动设置setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT)。当然,它还有包扩TOP,RIGHT,BOTTOM,ALL等方向的设置,看单词就可以理解其意思了,大家可以试试。值得一提的时,这个设置时候Callback中的回调onEdgeDragStarted一起使用的,当设置了边缘拖动后,当我触发边缘拖动的时候,就会回调这个方法。我们就是在这个方法中,通过ViewDragHelper的captureChildView方法,确定哪一个View被捕获,作为实现拖动效果的目标View。captureChildView这个方法有个特点,就是它所捕获的View,无需在tryCaptureView回调中指定,也可实现拖动。
然后,就是onViewReleased回调方法了,这个方法在我们释放捕获的View时,就会调用。由于我们例子中是释放dragViewTwo的时候,其回到初始位置,所以我们在回调中判断了被释放的View是dragViewTwo这个条件,releasedChild这个参数就是回调给我们的,指的就是被释放的子View。
我们通过ViewDragHelper的smoothSlideViewTo方法,指定某个子View滑动到指定位置,这个方法实际上是通过Scroller的startScroll方法实现的,因此,我们需要重写computeScroll方法。ViewCompat.postInvalidateOnAnimation是用来刷新界面的,其参数是要刷新的View,注意的是,其指的是父容器,而不是子View本身。而这个方法与invalidate()和postInvalidate()的区别就是,google进行了适配,保证调用刷新不会出现崩溃的现象。还有一点,是个人感觉,它的刷新更加的平滑。
至于mViewDragHelper.continueSettling(true),我们只需知道这是ViewDragHelper提供给我们,用于判断回滚是否结束的就可以了,返回true说明还没有结束,至于参数为什么设置为true。
Set this to true if you are calling this method from{@link android.view.View#computeScroll()}
这是源码的参数解释,告诉我们,如果在android.view.View下的computerScroll中调用,就设置为true。其实,看源码我们会发现,continueSettling内部也是通过Scroller的computeScrollOffset进行回滚结束状态的判断的。
OK,到这,我们就将第二套效果中用到的回调介绍完毕了。
下面,我们对一些比较容易碰到的问题进行一下说明。
有些时候,我们不仅仅要求某个View可以被拖动,它还会拥有自己的点击事件,当我们给一个dragView设置点击事件的时候,会发现,这个dragView无法被拖动了。
看过setOnclickListener源码的同学会知道,它会先将view的setClickable设置为true,其实问题就出在了这里,当一个view可以被点击的时候,ViewDragHelper将不会进行事件的拦截处理,因此最终事件会被这个可点击的View消费。
解决办法:Callback中有两个回调,他们的默认返回值为0,ViewDragHelper会对其进行验证,当捕获的view可点击,并且这两个回调大于0,才可以被拖动,因此我们需要重新这两个回调,并返回大于0的值。
@Override
public int getViewHorizontalDragRange(View child) {//水平方向的拖动
return 1;
}
@Override
public int getViewVerticalDragRange(View child) {//垂直方向的拖动
return 1;
}
下面我们来说说ViewDragHelper的三个参数的creat方法中,第二个参数的作用,上面说了,这个sensitivity是用来设置mTouchSlop的,sensitivity值越小,mTouchSlop越大。而sensitivity的默认值是1。下面我们将值设置为0.1f。
mViewDragHelper = ViewDragHelper.create(this, 0.1f, callback);
通过mViewDragHelper.getTouchSlop()我们可以知道mTouchSlop的值。下面我们看看效果:
对于第一个view,貌似没什么特别影响,拖动很顺畅。但是第二个可点击的view,我们发现,在拖动了一段距离之后,才被认定为是拖动状态,之后才有了拖动效果。
对于第三个边界拖动,也是在拖动了一段距离之后,才被认定为拖动状态,出现拖动效果。这就是所谓的被认定为拖动状态的最小滑动临界值。
Demo下载地址:点击打开链接