Android ViewDragHelper使用介绍
viewdraghelper是support.v4下提供的用于处理拖拽滑动的辅助类,查看android的drawerlayout源码,可以发现,它内部就是使用了该辅助类来处理滑动事件的.
public drawerlayout(context context, attributeset attrs, int defstyle) { super(context, attrs, defstyle); setdescendantfocusability(viewgroup.focus_after_descendants); final float density = getresources().getdisplaymetrics().density; mmindrawermargin = (int) (min_drawer_margin * density + 0.5f); final float minvel = min_fling_velocity * density; mleftcallback = new viewdragcallback(gravity.left); mrightcallback = new viewdragcallback(gravity.right); mleftdragger = viewdraghelper.create(this, touch_slop_sensitivity, mleftcallback); mleftdragger.setedgetrackingenabled(viewdraghelper.edge_left); mleftdragger.setminvelocity(minvel); mleftcallback.setdragger(mleftdragger); mrightdragger = viewdraghelper.create(this, touch_slop_sensitivity, mrightcallback); mrightdragger.setedgetrackingenabled(viewdraghelper.edge_right); mrightdragger.setminvelocity(minvel); mrightcallback.setdragger(mrightdragger); //省略 }
有了viewdraghelper,我们在写与拖拽滑动相关的自定义控件的时候就变得非常简单了,例如我们可以用来实现自定义侧滑菜单,再也不需要在ontouchevent方法里计算滑动距离来改变布局边框的位置了.
使用viewdraghelper类的大体步骤分为3步:
步骤1.在自定义的viewgroup子类下通过viewdraghelper的静态方法获取到viewdraghelper的实例引用,注意它是一个单例的.
查看源码:
/** * factory method to create a new viewdraghelper. * * @param forparent parent view to monitor * @param cb callback to provide information and receive events * @return a new viewdraghelper instance */ public static viewdraghelper create(viewgroup forparent, callback cb) { return new viewdraghelper(forparent.getcontext(), forparent, cb); }
可以发现它需要接收2个参数,参数1就是当前要使用viewdraghelper的自定义控件的引用,callback是一个回调抽象类,该回调接口是用于建立当前自定义控件与viewdraghelper沟通的桥梁,callback内定义了多个回调函数,这些回调函数涵盖了与当前自定义控件相关的是否允许拖拽,当前拖拽的view是哪一个view,拖拽的view的位置如何变化,释放的时候那个view被释放了,释放时的速度是怎么样的等等.稍后会详细介绍.
步骤2.有了viewdraghelper的引用后,我们就需要传递相关的触摸事件给viewdraghelper来帮我们处理,那么怎么传递呢?
可以通过重写onintercepttouchevent和ontouchevent这2个函数来传递,前者是用于决定是否要拦截中断事件的,后者是用于消费触摸事件的,如果前者return true则表示事件需要被拦截,那么事件就会直接回调给ontouchevent去处理,如果ontouchevent返回true,则事件被消费,返回false则向上返回它的父类调用处,如果事件在向上层层返回的过程中没有被处理的话,那么事件最终将会消失;当然,如果onintercepttouchevent返回false的话,那么事件就会继续向下传递个它的直接子view去分发处理,关于事件分发的更多理论知识,大家可以看这篇文章事件分发机制的原理总结.
实例代码:
@override public boolean onintercepttouchevent(motionevent ev) { //由viewdraghelper类来决定是否要拦截事件 return draghelper.shouldintercepttouchevent(ev); } @override public boolean ontouchevent(motionevent event) { try { //由viewdraghelper类来决定是否要处理触摸事件,这里可能有异常 draghelper.processtouchevent(event); } catch (exception e) { e.printstacktrace(); } //返回true,可以持续接收到后续事件 return true; }
步骤3:重写viewdraghelper.callback()的相关回调方法,处理事件,大体有如下方法:
下面将通过demo来分别介绍几个常用的方法,先来看看demo的整体代码,这是一个自定义的viewgroup的子类,里面有2个子控件,分别是侧边栏和主体布局
/** * created by mchenys on 2015/12/16. */ public class draglayout extends framelayout { private string tag = "draglayout"; private viewdraghelper draghelper; private linearlayout mleftcontent; //左侧面板 private linearlayout mmaincontent;//主体面板 public draglayout(context context) { this(context, null); } public draglayout(context context, attributeset attrs) { this(context, attrs, 0); } public draglayout(context context, attributeset attrs, int defstyleattr) { super(context, attrs, defstyleattr); init(); } //重写此方法,可以获取该容器下的所有的直接子view @override protected void onfinishinflate() { super.onfinishinflate(); mleftcontent = (linearlayout) getchildat(0); mmaincontent = (linearlayout) getchildat(1); } private void init() { //step1 通过viewdraghelper的单例方法获取viewdraghelper的实例 draghelper = viewdraghelper.create(this, mcallback); } //step2 传递触摸事件,需要重写onintercepttouchevent和ontouchevent @override public boolean onintercepttouchevent(motionevent ev) { //由viewdraghelper类来决定是否要拦截事件 return draghelper.shouldintercepttouchevent(ev); } @override public boolean ontouchevent(motionevent event) { try { //由viewdraghelper类来决定是否要处理触摸事件 draghelper.processtouchevent(event); } catch (exception e) { e.printstacktrace(); } //返回true,可以持续接收到后续事件 return true; } //step3 重写viewdraghelper.callback()的相关回调方法,处理事件 private viewdraghelper.callback mcallback = new viewdraghelper.callback() { /** * 1.改方法是abstract的方法,必须要实现,其返回结果决定当前child是否可以拖拽 * @param child 当前被拖拽的view * @param pointerid pointerid 区分多点触摸的id * @return true表示允许拖拽, false则不允许拖拽 ,默认返回false */ @override public boolean trycaptureview(view child, int pointerid) { log.d(tag, "trycaptureview:当前被拖拽的view:" + child); return false; } }; }
布局文件:
<?xml version="1.0" encoding="utf-8"?> <mchenys.net.csdn.blog.mytencentqq.view.draglayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/draglayout" android:layout_width="match_parent" android:layout_height="match_parent"> <!--左侧--> <linearlayout android:id="@+id/layout_left" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/holo_red_light" android:orientation="vertical"> <textview android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:text="左侧" android:textsize="30sp" /> </linearlayout> <!--主体布局--> <linearlayout android:id="@+id/layout_main" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/holo_blue_light" android:orientation="vertical"> <textview android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:text="主体" android:textsize="30sp" /> </linearlayout> </mchenys.net.csdn.blog.mytencentqq.view.draglayout>
运行效果:
打印的log
d/draglayout: trycaptureview:当前被拖拽的view:android.widget.linearlayout{32f4d44b v.e..... ........ 0,0-720,1134 #7f0c0052 app:id/layout_main} d/draglayout: trycaptureview:当前被拖拽的view:android.widget.linearlayout{32f4d44b v.e..... ........ 0,0-720,1134 #7f0c0052 app:id/layout_main} d/draglayout: trycaptureview:当前被拖拽的view:android.widget.linearlayout{32f4d44b v.e..... ........ 0,0-720,1134 #7f0c0052 app:id/layout_main} d/draglayout: trycaptureview:当前被拖拽的view:android.widget.linearlayout{32f4d44b v.e..... ........ 0,0-720,1134 #7f0c0052 app:id/layout_main}
由上面的log可以知道trycaptureview方法被执行了,但是mmaincontent却没有被拖动出来,只是为什么呢,因为trycaptureview返回了false.默认是返回false的,下面修改为mmaincontent可以拖动,mleftcontent不可以拖动:
@override public boolean trycaptureview(view child, int pointerid) { log.d(tag, "trycaptureview:当前被拖拽的view:" + child); if (child == mmaincontent) { return true; //只有主题布局可以被拖动 } return false; }
运行效果还是移动不了,这是为什么呢?
这是以因为我们还没有重写clampviewpositionhorizontal方法,下面将介绍该方法的使用
/** * 根据建议值修正将要移动到的横向位置,此时没有发生真正的移动 * @param child 当前被拖拽的view * @param left 新的建议值 * @param dx 水平位置的变化量 * @return */ @override public int clampviewpositionhorizontal(view child, int left, int dx) { log.d(tag, "clampviewpositionhorizontal:" + "旧的left坐标oldleft:" + child.getleft() + " 水平位置的变化量dx:" + dx + " 新的建议值left:" + left); return super.clampviewpositionhorizontal(child, left, dx); //父类默认返回0 }
该方法返回的是水平方向的移动建议值,该建议值等于当前的x坐标+水平方向的变化量,向右移动,偏移量为正值,向左移动则为负数.默认返回的是调用父类的重写的方法,查看源码可以发现默认返回的是0,如果建议值等于0的话,就表示水平方向不会移动.如果想要移动,我们需要返回它提供的建议值left,我们来看看运行的log:
trycaptureview:当前被拖拽的view:android.widget.linearlayout{23a3c537 v.e..... ........ 0,0-720,1134 #7f0c0052 app:id/layout_main} clampviewpositionhorizontal:旧的left坐标oldleft:0 水平位置的变化量dx:1 新的建议值left:1 clampviewpositionhorizontal:旧的left坐标oldleft:0 水平位置的变化量dx:12 新的建议值left:12 clampviewpositionhorizontal:旧的left坐标oldleft:0 水平位置的变化量dx:63 新的建议值left:63 clampviewpositionhorizontal:旧的left坐标oldleft:0 水平位置的变化量dx:53 新的建议值left:53 trycaptureview:当前被拖拽的view:android.widget.linearlayout{23a3c537 v.e..... ........ 0,0-720,1134 #7f0c0052 app:id/layout_main} clampviewpositionhorizontal:旧的left坐标oldleft:0 水平位置的变化量dx:-5 新的建议值left:-5 clampviewpositionhorizontal:旧的left坐标oldleft:0 水平位置的变化量dx:-2 新的建议值left:-2 clampviewpositionhorizontal:旧的left坐标oldleft:0 水平位置的变化量dx:-6 新的建议值left:-6 clampviewpositionhorizontal:旧的left坐标oldleft:0 水平位置的变化量dx:-11 新的建议值left:-11
由上面的log可以看出,分别是向右拖拽和向左拖拽的结果,如果我们返回了它的建议值,就可以实现水平方向的拖动了.
将clampviewpositionhorizontal的返回值修改成return left;看看运行效果:
跟我们预想的结果一样,只有主体布局可以滑动,左侧的布局不能滑动,如果想要左侧布局也可以滑动,那么只需要在trycaptureview直接返回true即可.效果如下:
同样的,如果要实现垂直方向的拖拽滚动,就需要重新下面这个方法了.
/** * 根据建议值修正将要移动到的纵向位置,此时没有发生真正的移动 * @param child 当前被拖拽的view * @param top 新的建议值 * @param dy 垂直位置的变化量 * @return */ @override public int clampviewpositionvertical(view child, int top, int dy) { log.d(tag, "clampviewpositionhorizontal:" + "旧的top坐标oldtop:" + child.gettop() + " 垂直位置的变化量dy:" + dy + " 新的建议值top:" + top); return top; }
效果如下,可以随意的滑动,实现起来是不是很简单啊
继续介绍剩下的回调方法
/** * 当capturedchild被捕获时调用 * @param capturedchild 当前被拖拽的view * @param activepointerid */ @override public void onviewcaptured(view capturedchild, int activepointerid) { log.d(tag, "onviewcaptured:当前被拖拽的view:" + capturedchild); super.onviewcaptured(capturedchild, activepointerid); }
该回调方法和trycaptureview一样都可以获取到当前被拖拽的view,不同点在于trycaptureview是可以决定哪个view是可以被拖拽滑动的,而onviewcaptured只是用来获取当前到底哪个view被正在拖拽而已.
/** * 返回拖拽的范围,不对拖拽进行真正的限制,仅仅决定了动画执行的速度 * @param child * @return 返回一个固定值 */ @override public int getviewhorizontaldragrange(view child) { int range = super.getviewhorizontaldragrange(child); log.d(tag, "被拖拽的范围getviewhorizontaldragrange:" + range); return range; }
该回调方法是用于决定水平方向的动画执行速度,相对的垂直方向肯定也会有相应的方法,没错,就是下面这个:
@override public int getviewverticaldragrange(view child) { return super.getviewverticaldragrange(child); }
那么话又说回来,我们有什么办法可以限制子view的滑动范围呢,如果范围不能很好的控制的话,那滑动也没有什么意义了.还记得上面介绍的clampviewpositionhorizontal和clampviewpositionvertical吗,分别用于设置水平方向和垂直方向的移动建议值,假设我们要限制该自定义控件的子view在水平方向移动的范围为0-屏幕宽度的0.6,那么如何控制呢.要实现这个限制,我们现在获取屏幕的宽度,由于当前的自定义控件是全屏显示的,所以直接就可以那控件的宽度来作为屏幕的宽度,那么如何获取呢?有2种方式,分别是在onmeasure和onsizechange方法里面调用getmeasuredwidth()方法获取.前者是测量完之后获取,后者是当控件的宽高发生变化后获取,例如:
@override protected void onsizechanged(int w, int h, int oldw, int oldh) { super.onsizechanged(w, h, oldw, oldh); // 当尺寸有变化的时候调用 mheight = getmeasuredheight(); mwidth = getmeasuredwidth(); // 移动的范围 mrange = (int) (mwidth * 0.6f); }
接下来,在clampviewpositionhorizontal方法内部做判断,如果当前的建议值left超过了mrange,那么返回mrange,如果小于了0,则返回0,这样一来子view的滑动范围就限定在0-mrange之间了,修改代码如下:
@override public int clampviewpositionhorizontal(view child, int left, int dx) { log.d(tag, "clampviewpositionhorizontal 建议值left:" + left + " mrange:" + mrange); if (left > mrange) { left = mrange; } else if (left < 0) { left = 0; } return left; }
如果垂直方向也想限定的话,那就修改clampviewpositionvertical返回的建议值
@override public int clampviewpositionvertical(view child, int top, int dy) { log.d(tag, "clampviewpositionvertical 建议值top:" + top + " mrange:" + mrange); if (top > mrange) { top = mrange; } else if (top < 0) { top = 0; } return top; }
来看看运行的效果:
如此一来,我们就可以随意的控制子view的拖拽滑动的范围了.那么新的问题又来的,如果现在的需求是只能mmaincontent被拖拽,mleftcontent不能被拖拽,也许你会说,这还不简单吗,直接在trycaptureview判断当前拖拽的子view是mleftcontent的话就返回false不就得了,没错,如果需求只是这样的话确实可以满足了,但是如果在加上一个条件,那就是拖拽mleftcontent的时候的效果相当于把mmaincontent拖拽了,什么意思呢,也就说现在mmaincontent已经是打开的状态了,我想通过滑动mleftcontent就能把mmaincontent滑动了.而mleftcontent还是原来的位置不动.这个要怎么实现呢?
首先可以肯定的是,trycaptureview方法必须返回true,表示mmaincontent和mleftcontent都可以被滑动,接下来要处理的就是如何在mleftcontent滑动的时候是滑动mmaincontent的.那么现在就要介绍另一个回调方法了,如下所示:
/** * 当view的位置发生变化的时候,处理要做的事情(更新状态,伴随动画,重绘界面) * 此时,view已经发生了位置的改变 * * @param changedview 改变位置的view * @param left 新的左边值 * @param top 新的上边值 * @param dx 水平方向的变化量 * @param dy 垂直方向的变化量 */ @override public void onviewpositionchanged(view changedview, int left, int top, int dx, int dy) { log.d(tag, "位置发生变化onviewpositionchanged:" + "新的左边值left: " + left + " 水平方向的变化量dx:" + dx + " 新的上边值top:" + top + " 垂直方向的变化量dy:" + dy); super.onviewpositionchanged(changedview, left, top, dx, dy); // 为了兼容低版本, 每次修改值之后, 进行重绘 invalidate(); }
该方法是随着view的位置发生变化而不断回调的.四个参数如上面的注释所述,通过该方法可以拿到当前正在拖拽滑动的view是哪个view,有了这依据之后,我们就将在mleftcontent上的滑动的水平方向和垂直方向的变化量传递给mmaincontent,这样一来,滑动mleftcontent的效果不就等于滑动mmaincontent了吗.需要注意的是,该回调方法在低版本上为了兼容还需要加上invalidate();这句代码,invalidate是用来刷新界面的,他会导致界面的重绘.
滑动mmaincontent来看看log
d/draglayout: 位置发生变化onviewpositionchanged:新的左边值left: 15 水平方向的变化量dx:15 新的上边值top:8 垂直方向的变化量dy:8
d/draglayout: 位置发生变化onviewpositionchanged:新的左边值left: 32 水平方向的变化量dx:17 新的上边值top:16 垂直方向的变化量dy:8
d/draglayout: 位置发生变化onviewpositionchanged:新的左边值left: 121 水平方向的变化量dx:89 新的上边值top:46 垂直方向的变化量dy:30
d/draglayout: 位置发生变化onviewpositionchanged:新的左边值left: 145 水平方向的变化量dx:24 新的上边值t
由log可以看出,最新的left和top值是等于上一次的位置+水平/垂直方向的变化量.这个特点有点类似建议值了.不同的是建议值发生了改变不代表view就一定已经处于滑动,除非返回的值也是建议值,但是onviewpositionchanged方法就不同了,这个方法只要一执行,就说明目标view是处于滑动状态的.
以水平方向滑动为例,垂直方向不移动,接下来就可以在onviewpositionchanged方法内做判断了,如下所示:
@override public void onviewpositionchanged(view changedview, int left, int top, int dx, int dy) { super.onviewpositionchanged(changedview, left, top, dx, dy); //获取最新的left坐标 int newleft = left; if (changedview == mleftcontent) { //如果滑动的是mleftcontent,则将其水平变化量dx传递给mmaincontent,记录在newleft中 newleft = mmaincontent.getleft() + dx; } //矫正范围 if (newleft > mrange) { newleft = mrange; } else if (newleft < 0) { newleft = 0; } //再次判断,限制mleftcontent的滑动 if (changedview == mleftcontent) { //强制将mleftcontent的位置摆会原来的位置,这里通过layout方法传入左,上,右,下坐标来实现 //当然方法不限于这一种,例如还可以通过scrollto(x,y)方法 mleftcontent.layout(0, 0, mwidth, mheight); //改变mmaincontent的位置 mmaincontent.layout(newleft, 0, newleft + mwidth, mheight); } // 为了兼容低版本, 每次修改值之后, 进行重绘 invalidate(); }
效果图:
由上面的效果图可以发现已经可以实现当手指向右滑动mleftcontent时,滑动的效果等于向右滑动mmaincontent,当同时也会发现一个问题,那就是手指在mleftcontent向左滑动的时候并没有效果,这是因为我们限制了子view的滑动范围就是0-mrange,所以,如果滑动时小于0是没有效果的.那如果我们想要实现在mleftcontent当手指有向左滑动的趋势,或者手指在mmaincontent有向左滑动的趋势时,就关闭mleftcontent,让mmaincontent自动向左滑动到x=0的位置,反之就是打开mleftcontent,让mmaincontent滑动到x=mrange的位置,这个要怎么实现呢?首先我们要能够想到的时,这个向左滑动的趋势肯定是与手指松手后相关的,那有没有一个回调方法是与手指触摸松开相关的呢?下面将介绍另一个回调方法,如下所示:
/** * 当被拖拽的view释放的时候回调,通常用于执行收尾的操作(例如执行动画) * @param releasedchild 被释放的view * @param xvel 水平方向的速度,向右释放为正值,向左为负值 * @param yvel 垂直方向的速度,向下释放为正值,向上为负值 */ @override public void onviewreleased(view releasedchild, float xvel, float yvel) { log.d(tag, "view被释放onviewreleased:" + "释放时水平速度xvel:" + xvel + " 释放时垂直速度yvel:" + yvel); super.onviewreleased(releasedchild, xvel, yvel); }
有了这个方法,我们就可以实现我们刚刚说到的效果了,首先我们来考虑下那些情况是和打开mleftcontent相关的,有2种情况:
1.当前水平方向的速度xvel=0,同时mmaincontent的x位置是大于mrange/2.0f的.
2.当前水平方向的速度xvel>0
考虑了所有打开的情况,那么剩下的情况就是关闭mleftcontent.
具体逻辑如下:
@override public void onviewreleased(view releasedchild, float xvel, float yvel) { log.d(tag, "view被释放onviewreleased:" + "释放时水平速度xvel:" + xvel + " 释放时垂直速度yvel:" + yvel); super.onviewreleased(releasedchild, xvel, yvel); if (xvel > 0 || (xvel == 0 && mmaincontent.getleft() > mrange / 2.0f)) { //打开mleftcontent,即mmaincontent的x=mrange mmaincontent.layout(mrange, 0, mrange + mwidth, mheight); } else { //关闭mleftcontent,即mmaincontent的x=0 mmaincontent.layout(0, 0, mwidth, mheight); } }
效果图:
细心的话,可以发现上面的打开和关闭动画都是瞬间完成的,看起来效果不怎么好,如何实现平滑的打开和关闭呢?
通过viewdraghelper的smoothslideviewto(view child, int finalleft, int finaltop)方法可以实现平滑的滚动目标view到指定的left或者top坐标位置,接收3个参数,参数child表示要滑动的目标view,finalleft和finaltop表示目标view最终平滑滑动到的位置.翻看源码,发现其实现原理是通过scroller对象来实现的,也就说我们还需要重写computescroll方法,不断的刷新当前界面,具体设置如下:
@override public void onviewreleased(view releasedchild, float xvel, float yvel) { log.d(tag, "view被释放onviewreleased:" + "释放时水平速度xvel:" + xvel + " 释放时垂直速度yvel:" + yvel); super.onviewreleased(releasedchild, xvel, yvel); if (xvel > 0 || (xvel == 0 && mmaincontent.getleft() > mrange / 2.0f)) { //打开mleftcontent,即mmaincontent的x=mrange if (draghelper.smoothslideviewto(mmaincontent, mrange, 0)) { // 返回true代表还没有移动到指定位置, 需要刷新界面. // 参数传this(child所在的viewgroup) viewcompat.postinvalidateonanimation(draglayout.this); } } else { //关闭mleftcontent,即mmaincontent的x=0 if (draghelper.smoothslideviewto(mmaincontent, 0, 0)) { viewcompat.postinvalidateonanimation(draglayout.this); } } @override public void computescroll() { super.computescroll(); // 2. 持续平滑动画 (高频率调用) if(draghelper.continuesettling(true)){ // 如果返回true, 动画还需要继续执行 viewcompat.postinvalidateonanimation(this); } }
效果如下:
总结
以上所述是小编给大家介绍的android viewdraghelper使用介绍,希望对大家有所帮助
推荐阅读