android ViewDragHelper讲解

文章的开头奉送上代码,方便对照学习。

1 前言

2013年谷歌i/o大会上介绍了两个新的layout: SlidingPaneLayout和DrawerLayout,现在这俩个类被广泛的运用, 其实研究他们的源码你会发现这两个类都运用了ViewDragHelper来处理拖动。
ViewDragHelper解决了android中手势处理过于复杂的问题,下面让我们总结一下android中都有那些处理view移动的控件.

1.1 View移动方法总结

1.layout

在自定义控件中,View绘制的一个重写方法layout(),用来设置显示的位置。所以,可以通过修改View的坐标值来改变view在父View的位置,以此可以达到移动的效果!但是缺点是只能移动指定的View,如常见的:view.layout(l,t,r,b);

2.offsetLeftAndRight /offsetTopAndBottom

非常方便的封装方法,只需提供水平、垂直方向上的偏移量,展示效果与layout()方法相同。
view.offsetLeftAndRight(offset);//同时改变left和right  view.offsetTopAndBottom(offset);//同时改变top和bottom

3.LayoutParams

此类保存了一个View的布局参数,可通过LayoutParams动态改变一个布局的位置参数,以此动态地修改布局,达到View位置移动的效果!但是在获取getLayoutParams()时,要根据该子View对应的父View布局来决定自身的LayoutParams 。所以一切的前提是:必须要有一个父View,否则无法获取LayoutParams。

LinearLayout.LayoutParamslayoutParams = (LinearLayout.LayoutParams)getLayoutParams(); 
layoutParams.leftMargin = getLeft() + dx; layoutParams.topMargin = getTop() + dy; setLayoutParams(layoutParams); 

4.scrollTo/scrollBy

通过改变scrollX和scrollY来移动,但是可以移动所有的子View。scrollTo(x,y)表示移动到一个具体的坐标点(x,y),而scrollBy(x,y)表示移动的增量为dx,dy。

注意:这里使用scrollBy(xOffset,yOffset);,你会发现并没有效果,因为以上两个方法移动的是View的content。若在ViewGroup中使用,移动的是所有子View;若在View中使用,移动的是View的内容(比如TextView)。所以,不可在view中使用以上方法! 
要想使用scrollBy,应该在View所在的ViewGroup中使用:((View)getParent()).scrollBy(offsetX, offsetY); 

5.canvas

通过改变Canvas绘制的位置来移动View的内容,用的少,一般用在自定义的View中,比如老早之前实现手写板:canvas.drawBitmap(bitmap, left, top, paint)

说完View的移动相关的属性,我们来看一下大名鼎鼎的ViewDragHelper。

2 ViewDragHelper

2.1 注意事项

要理解ViewDragHelper,我们需要掌握以下几点:

1.ViewDragHelper.Callback是连接ViewDragHelper与view之间的桥梁;只要ViewDragHelper控制的ViewGroup中View变化时ViewDragHelper.Callback就会被回调。
2.ViewDragHelper的实例是通过静态工厂方法创建的;
3.ViewDragHelper可以检测到是否触及到边缘;
4.ViewDragHelper并不是直接作用于要被拖动的View,而是使其控制的视图容器中的子View可以被拖动,如果要指定某个子view的行为,需要在Callback中实现;
5.ViewDragHelper的本质其实是分析onInterceptTouchEvent和onTouchEvent的MotionEvent参数,然后根据分析的结果去改变一个容器中被拖动子View的位置。

2.2 使用步骤

下面讲解一下ViewDragHelper实现步骤:
步骤 1.使用静态方法来构造一个ViewDragHelper,需要传入一个ViewDragHelper.Callback对象.代码如下:

   ViewDragHelper mDragger = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {
    ...
    这里面省略很多实现的方法
    ...
});

ViewDragHelper的构造方法如下:

ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb)

forParent是viewGroup,sensitivity是灵敏度1.0f是正常灵敏度,值越大,对滑动的检测就越敏感。cb是ViewDragHelper中的一个内部抽象类,也是本章重点要讲解的。

步骤2.重写onInterceptTouchEvent和onTouchEvent回调ViewDragHelper中对应方法.代码如下:

 @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        return mDragger.shouldInterceptTouchEvent(event);
    }

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

我们不在touch事件中处理而是调用ViewDragHelper的方法,我们通过在ViewDragHelper.callback中处理事件。
步骤3.在ViewDragHelper.Callback中对视图做操作.
我们通过实现Callback中的方法来对视图做操作.

2.3 方法介绍

ViewDragHelper.Callback方法介绍:

方法介绍
onViewDragStateChanged(int state)当托拽状态变化时回调,譬如动画结束后回调为STATE_IDLE等,state有三种状态,均以STATE_XXXX模式
onViewPositionChanged(View changedView, int left, int top, int dx, int dy)//当前被触摸的View位置变化时回调,changedView为位置变化的View,left/top变化时新的x左/y顶坐标,dx/dy为从旧到新的偏移量
onViewCaptured(View capturedChild, int activePointerId)//tryCaptureViewForDrag()成功捕获到子View时或者手动调用captureChildView()时回调
onViewReleased(View releasedChild, float xvel, float yvel)当子View被松手或者ACTION_CANCEL时时回调,xvel/yvel为离开屏幕时各方向每秒运动的速率,为px
onEdgeTouched(int edgeFlags, int pointerId)当触摸ACTION_DOWN或ACTION_POINTER_DOWN边沿时回调
onEdgeLock(int edgeFlags)返回true锁定edgeFlags对应的边缘,锁定后的边缘就不会回调onEdgeDragStarted()
onEdgeDragStarted(int edgeFlags, int pointerId)ACTION_MOVE触摸边缘且没有锁定边缘时触发,可在此手动调用captureChildView()触发从边缘拖动子View,有点类似略过tryCaptureView返回false响应重定向其他View的效果
getOrderedChildIndex(int index)寻找当前触摸点View时回调此方法,如果需要改变子View的倒序遍历查询顺序则可改写此方法,譬如让重叠的下层View先于上层View被捕获
getViewHorizontalDragRange(View child)返回给定子View在相应方向上可以被拖动的最远距离,默认为0,一般是可被挪动View时指定为指定View的大小等
getViewVerticalDragRange(View child)返回给定子View在相应方向上可以被拖动的最远距离,默认为0,一般是可被挪动View时指定为指定View的大小等
tryCaptureView(View child, int pointerId)传递当前触摸上的子View,如果需要当前触摸的子View进行拖拽移动就返回true,否则返回false
clampViewPositionHorizontal(View child, int left, int dx)决定要拖拽的子View在所属方向上应该移动到的位置,child为拖拽的子View,left为期望值,dx为挪动差值
clampViewPositionVertical(View child, int top, int dy)决定要拖拽的子View在所属方向上应该移动到的位置,child为拖拽的子View,left为期望值,dx为挪动差值

ViewDragHelper方法介绍:
常量:

//当前View处于空闲状态,静止
public static final int STATE_IDLE = 0;
//当前View处于托动状态中
public static final int STATE_DRAGGING = 1;
//当前View处于滚动惯性到settling坐标间的状态
public static final int STATE_SETTLING = 2;

//可托拽边缘方向常量
public static final int EDGE_LEFT = 1 << 0;
public static final int EDGE_RIGHT = 1 << 1;
public static final int EDGE_TOP = 1 << 2;
public static final int EDGE_BOTTOM = 1 << 3;
public static final int EDGE_ALL = EDGE_LEFT | EDGE_TOP | EDGE_RIGHT | EDGE_BOTTOM;
方法介绍
create(ViewGroup forParent, float sensitivity, Callback cb)构造工厂方法,sensitivity用来调节mTouchSlop的值,默认一般传递1即可,sensitivity越大,mTouchSlop越小,对滑动的检测就越敏感,譬如手指move多少才算滑动,否则忽略
setEdgeTrackingEnabled(int edgeFlags)设置允许父View的某个边缘可以用来响应托拽,相当于控制了CallBack对象的onEdgeTouched()和onEdgeDragStarted()方法是否被回调
shouldInterceptTouchEvent(MotionEvent ev),processTouchEvent(MotionEvent ev)两个传递MotionEvent的方法
captureChildView(View childView, int activePointerId)主动在父View内捕获指定的子view用于拖曳,会回调tryCaptureView()
smoothSlideViewTo(View child, int finalLeft, int finalTop)指定某个View自动滚动到指定的位置,初速度为0,可在任何地方调用,如果这个方法返回true,那么在接下来动画移动的每一帧中都会回调continueSettling(boolean)方法,直到结束
settleCapturedViewAt(int finalLeft, int finalTop)以松手前的滑动速度为初值,让捕获到的子View自动滚动到指定位置,只能在Callback的onViewReleased()中使用,如果这个方法返回true,那么在接下来动画移动的每一帧中都会回调continueSettling(boolean)方法,直到结束
flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop)以松手前的滑动速度为初值,让捕获到的子View在指定范围内fling惯性运动,只能在Callback的onViewReleased()中使用,如果这个方法返回true,那么在接下来动画移动的每一帧中都会回调continueSettling(boolean)方法,直到结束
setMinVelocity(float minVel),getMinVelocity()设置与获取最小速率,一般保持默认
getViewDragState()获取当前子View所处状态
getEdgeSize()返回可触摸反馈区域边缘大小,单位为px
getCapturedView()返回当前捕获的子View,如果没有则为null
getActivePointerId()获取当前拖曳的View的Pointer ID
getTouchSlop()获取最小触发拖曳动作的灵敏度差值,单位为px
cancel()类似ACTION_CANCEL事件的触发调运
abort()终止手势,结束动画滚动等,恢复初始STATE_IDLE状态
continueSettling(boolean deferCallbacks)在整个settle状态中,这个方法会返回true,deferCallbacks决定滑动是否Runnable推迟,一般推迟 在调用settleCapturedViewAt()、flingCapturedView()和smoothSlideViewTo()时, 需要实现mParentView的computeScroll()方法如:public void computeScroll() {if (mDragHelper.continueSettling(true)) { ViewCompat.postInvalidateOnAnimation(this); }}

3 实例讲解

先看一个效果图:
这里写图片描述
可能没有鼠标看不出效果来,view1当松开手的时候会自动回到起点,view2只有滑动边缘的时候才滑动,view3随意滑动。在下面我直接用view1、2、3代码3个子view。大家别弄混了。下面我们讲解一下代码:

3.1步骤1.自定义一个viewGroup:

这里假设大家对viewGroup的onMeasure和onLayout有一定了解,这里我就不详细介绍了,大家如果不明白可以找自定义viewGroup资料学习一下。

public class MyLayout extends ViewGroup {

    private View view1, view2, view3;

    public MyLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        view1 = getChildAt(0);
        view2 = getChildAt(1);
        view3 = getChildAt(2);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        /**
         * 获得此ViewGroup上级容器为其推荐的宽和高,以及计算模式
         */
        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
        // 计算出所有的childView的宽和高
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        for (int i = 0; i < getChildCount(); ++i) {
            final View child = getChildAt(i);
            int width = child.getMeasuredWidth();
            int height = child.getMeasuredHeight();
            child.measure(View.MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
        }
        setMeasuredDimension(sizeWidth, sizeHeight);
    }

    private int appandHeight = 0;
    private int appandWidth = 0;


    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        if (changed) {
            for (int i = 0; i < getChildCount(); i++) {
                View child = getChildAt(i);
                getChildAt(i).layout(appandWidth, appandHeight, appandWidth + child.getMeasuredWidth(), appandHeight + child.getMeasuredHeight());
                appandHeight += child.getMeasuredHeight();
                appandWidth += child.getMeasuredWidth();
            }
        }

    }

}

xml代码如下:

<?xml version="1.0" encoding="utf-8"?>
<mystudy.czh.com.myviewdraghelperstudy.MyLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/view1"
        android:text="我会自动回到起始位置"
        android:background="@android:color/holo_blue_bright"
        android:layout_width="100dp"
        android:textColor="#ffffff"
        android:gravity="center"
        android:layout_height="100dp"/>
    <TextView
        android:id="@+id/view2"
        android:text="我是边缘滑动"
        android:textColor="#ffffff"
        android:gravity="center"
        android:background="@android:color/holo_orange_light"
        android:layout_width="100dp"
        android:layout_height="100dp"/>

    <TextView
        android:id="@+id/view3"
        android:text="我可以自由滑动"
        android:textColor="#ffffff"
        android:gravity="center"
        android:background="@android:color/holo_green_dark"
        android:layout_width="100dp"
        android:layout_height="100dp"/>

</mystudy.czh.com.myviewdraghelperstudy.MyLayout>

3.2 步骤2 创建ViewDragHelper:

 public MyLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        mDragger = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {
            /**
             * 手指释放的时候
             */
            @Override
            public void onViewReleased(View releasedChild, float xvel, float yvel) {
                super.onViewReleased( releasedChild,  xvel,  yvel);
            }
            /**
             * 边缘滑动的方法
             * @param edgeFlags
             * @param pointerId
             */
            @Override
            public void onEdgeDragStarted(int edgeFlags, int pointerId) {
                super.onEdgeDragStarted( edgeFlags,  pointerId);
            }
            /**
             * 横向滑动的时候
             */
            @Override
            public int clampViewPositionHorizontal(View child, int left, int dx) {
                return super.clampViewPositionHorizontal( child,  left,  dx);
            }
            /**
             * 竖向滑动的时候
             */
            @Override
            public int clampViewPositionVertical(View child, int top, int dy) {
                    return super.clampViewPositionVertical( child,  top,  dy);
            }
            /**
             * tryCaptureView如果返回ture则表示可以捕获该view,你可以根据传入的第一个view参数决定哪些可以捕获
             */
            @Override
            public boolean tryCaptureView(View child, int pointerId) {
                return true;
            }
        });
        //设置可以边缘滑动
        mDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_ALL);
    }

3.3 步骤3 重写onInterceptTouchEvent和onTouchEvent回调ViewDragHelper中对应方法

 @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        return mDragger.shouldInterceptTouchEvent(event);
    }

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

3.4 步骤4 重写ViewDragHelper.Callback中相关方法操作视图

第一要重写的方法是tryCaptureView()方法,代码如下:

 /**
  * tryCaptureView如果返回ture则表示可以捕获该view,你可以根据传入的第一个view参数决定哪些可以捕获
 */
 @Override
 public boolean tryCaptureView(View child, int pointerId) {
        //禁止view2直接移动
        return child == view1 || child == view3;
 }

我们只允许view1和view3直接移动,因为view2要通过边缘滑动,不能直接移动。但是你要明白,这只是view1和view3有滑动权限了,但是并不代表它们二个可以滑动了,所以我们还要重写2个方法,代码如下:
第二要重写clampViewPositionHorizontal和clampViewPositionVertical方法代码如下:

 /**
 * 横向滑动的时候
 */
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
       return left;
}
/**
* 竖向滑动的时候
*/
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
       return top;
}

我们分别在这二个方法中返回当前的坐标值,现在运行一下代码,太好了,view1和view3可以滑动了!好,到现在位置view3已经达到我们的要求了,现在我们要让当松开手指的时候view1回到起点 !代码如下:
第三松开手指让view1回到起点:
首先我们需要记录view的其实坐标,这个是在onlayout中记录,onlayout方法只在开始的时候执行一次,所以能很好的记录起点坐标,代码如下:

 @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        ...
        上面还有代码,这里省略。。。
        ...
        initX = view1.getLeft();
        initY = view1.getTop();
    }

然后重写onViewReleased方法:
当松开手指的时候会回调 onViewReleased方法,所以我们需要重写:

/**
             * 手指释放的时候
             * @param releasedChild
             * @param xvel
             * @param yvel
             */
            @Override
            public void onViewReleased(View releasedChild, float xvel, float yvel) {
                if (releasedChild == view1) {
                    mDragger.settleCapturedViewAt(initX, initY);
                    invalidate();
                }
            }

我们还要重写一个方法,因为ViewDragHelper内部是用Scroller来实现的,代码如下 :

 @Override
    public void computeScroll() {
        if (mDragger.continueSettling(true)) {// 判断是否滑动结束
            invalidate();
        }
    }

运行一下看看,哎呀,view1真的能回到起点了!接下来我们实现view2的边缘滑动效果(当滑动边缘的时候,view2滑动)
第四.让view2动起来
这个也很是简单,我们只需要重写一个方法即可,代码如下:

 /**
  * 边缘滑动的方法
  */
  @Override
  public void onEdgeDragStarted(int edgeFlags, int pointerId) {
        mDragger.captureChildView(view2,pointerId);
  }
  //设置二边都可以滑动,也可以设置一边
 mDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_ALL);

运行一下看看,哎呀 ,真的有效果了!
好了到此为止,ViewDragHelper基本用法讲完是,是不是很简单!ViewDragHelper能大大简化了我们的代码复杂度,感谢google的大神们!

4 总结方法的回调顺序

shouldInterceptTouchEvent:

DOWN:
    getOrderedChildIndex(findTopChildUnder)
    ->onEdgeTouched

MOVE:
    getOrderedChildIndex(findTopChildUnder)
    ->getViewHorizontalDragRange & 
      getViewVerticalDragRange(checkTouchSlop)(MOVE中可能不止一次)
    ->clampViewPositionHorizontal&
      clampViewPositionVertical
    ->onEdgeDragStarted
    ->tryCaptureView
    ->onViewCaptured
    ->onViewDragStateChanged

processTouchEvent:

DOWN:
    getOrderedChildIndex(findTopChildUnder)
    ->tryCaptureView
    ->onViewCaptured
    ->onViewDragStateChanged
    ->onEdgeTouched
MOVE:
    ->STATE==DRAGGING:dragTo
    ->STATE!=DRAGGING:
        onEdgeDragStarted
        ->getOrderedChildIndex(findTopChildUnder)
        ->getViewHorizontalDragRange&
          getViewVerticalDragRange(checkTouchSlop)
        ->tryCaptureView
        ->onViewCaptured
        ->onViewDragStateChanged

5 结尾

文章的结尾奉送上代码,方便对照学习。
好了就讲到这里吧,下一章我们自定义一个DrawerLayout玩玩 !
在技术上我依旧是一个小渣渣,加油!勉励自己!

6 参考文档

【1】Android ViewDragHelper及移动处理总结
【2】官网
【3】【Android 一步一步教你使用ViewDragHelper】
【4】 Android应用ViewDragHelper详解及部分源码浅析
【5】Android ViewDragHelper完全解析 自定义ViewGroup神器
【6】Viewdraghelper解析

介绍:用ViewDragHelper实现的activity切换动画。运行效果:使用说明: 你可以将这个库当成view来用:将DraggerView添加到root layout,并且在里面加入两个layout。<com.github.library.DraggerView     android:layout_width="match_parent"     android:layout_height="match_parent"     dragger_layout:drag_view_id="@ id/drag_view"     dragger_layout:shadow_view_id="@ id/shadow_view"     dragger_layout:drag_position="top">       <FrameLayout           android:id="@ id/shadow_view"           android:layout_width="match_parent"           android:layout_height="match_parent"           android:background="@color/transparent"           android:visibility="invisible"/>         <LinearLayout           android:id="@ id/drag_view"           android:layout_width="match_parent"           android:layout_height="match_parent"/>   </com.github.library.DraggerView>style文件中这样设置<style name="YourTheme" parent="Theme.AppCompat.Light.DarkActionBar">     <item name="android:windowIsTranslucent">true</item>       <item name="android:windowBackground">@android:color/transparent</item>       <item name="android:windowNoTitle">true</item>       <item name="windowActionBar">false</item>       <item name="android:windowAnimationStyle">@null</item> </style>manifest中<activity     android:name="com.github.dragger.BaseActivity"     android:theme="@style/YourTheme"/>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序编织梦想

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值