文章的开头奉送上代码,方便对照学习。
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解析