什么是ViewDragLayout?
他是谷歌专门为我们制作自定义控件而做的一个工具类。他提供了一些很有用的方法,主要是提供拖动和重新定位子控件的操作功能。
好了那么怎么使用呢?
首先从类说明上看,应用场景应该是我们在重写一个ViewGroup,我们这里重写一个FrameLayout:
public class MyDragLayout extends FrameLayout{}
当然少不了我们还需要重写一下构造器:
public MyDragLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init() ;
}
这里我只把这个构造器的代码写出来,因为其他的构造器都会调用此构造器实现对象状态的初始化。
这里有个init方法,这个init方法中就是使用ViewDragLayout的第一步了:
private void init() {
//创建ViewDragHelper的对象
viewDragHelper = ViewDragHelper.create(this,1.0f, new MyDragCallback()) ;
}
这里我们需要调用ViewDragHelper的工程方法来实例化对象。
工厂方法的三个参数分别是:
1.View viewGroup : 这里我填的this,当然了,看了源码就明白了,这里要填的就是我们以后要拖动的子控件的父控件。
2.float slop : 这个就是手指触控的敏感度,没有特殊要求,就直接填1.0f(google官方推荐)
3.ViewDragHelper.Callback : 名字很赤果果,就是回调函数。我们在拖动子控件时的各种状态判定,都需要通过这个回调让我们参与进来。
好了现在我们需要重写一个回调函数出来:
class MyDragCallback extends ViewDragHelper.Callback{
@Override
public boolean tryCaptureView(View child, int pointerId) {
return super.tryCaptureView(child, pointerId);
}
}
上面的回调函数是默认创建出来的样子,这里有个重写的方法叫tryCaptureView()的方法,从名字看就是尝试获取View对象。实质上也是,不过呢,这里是提供一个比较,
比较当前ViewDragHelper他找到的这个View child是否是我们需要移动的这个控件,后面的pointerId是我们多触控时,返回的触控点id。当然根据我们的直觉,我们也应该了解这里不可能return super.tryCaptureView()。一般来说现在我们也没有特别要求,所以我们来返回true。代表获取到的控件,我们都认可为可以移动的控件。所以结果就是:
/**
* 用于判断指定的参数View child是否为我们当前需要操作的控件
* */
@Override
public boolean tryCaptureView(View child, int pointerId) {
return true;
}
好了,这里其实还没有完。不过呢,我们先放这里不管他。
好了我们这就定义好了一个自定义ViewGroup(继承自FrameLayout)。那么我们就来使用一下:
在Activity的布局文件中写入:
<?xml version="1.0" encoding="utf-8"?>
<com.example.administrator.viewdraghelperdemo.view.MyDragLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv_1"
android:layout_width="100dip"
android:layout_height="100dip"
android:background="@android:color/holo_red_light"
android:text="第一个TextView" />
<TextView
android:id="@+id/tv_2"
android:layout_width="100dip"
android:layout_height="100dip"
android:background="@android:color/holo_blue_light"
android:text="第二个TextView" />
<TextView
android:id="@+id/tv_3"
android:layout_width="100dip"
android:layout_height="100dip"
android:background="@android:color/holo_green_light"
android:text="第三个TextView" />
</com.example.administrator.viewdraghelperdemo.view.MyDragLayout>
然后我们启动这个Activity,我们会发现3个TextView并不会随着我们手指的滑动而移动。
当然我们写出上面的东西后,心里应该也会觉得空荡荡的少些代码,少什么呢?
我们在ViewGroup中仅仅是创建出了ViewDragHelper的对象,还没有使用过,那么怎么使用呢?
这里其实没有使用Google提供的工具,自己去实现过子控件移动的朋友应该清楚,在我们想要移动某个控件前,我们需要去了获取当前的触控事件,并且在合理的情况下,被我们拦截使用。所以在这个ViewGroup中我们还应该实现两个与触控事件处理紧密相关的方法:onTouchEvent()和onInterceptTouchEvent()。然后我们就可以在这两个方法中处理传递来的触控事件,并且决定是否交给子控件处理。而这里我们就可以利用ViewDragHelper来处理这些操作。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean shouldIntercet = viewDragHelper.shouldInterceptTouchEvent(ev) ;
return shouldIntercet;
}
当然首先是onInterceptTouchEvent传递给子控件的拦截触控事件。这里我们可以调用shouldInterceptTouchEvent(ev) ;来判断是否拦截次事件,当然我们要注意和Button这样的流氓一起使用时,这里没有办法返回true,因为Button会吃下整个触控事件,我们的ViewGroup只能得到一次ACTION_DOWN的事件,后续的ACTION_MOVE这些没法得到,而只有一次触控事件,这里的shouldInterceptTouchEvent是无法判断出来的。(这里推荐大家看看源码,源码实现也不复杂)
@Override
public boolean onTouchEvent(MotionEvent event) {
viewDragHelper.processTouchEvent(event);
return true;
}
好了,如果我们拦截下了触控事件要处理,那么我们就需要在onTouchEvent中来处理,首先返回值设为true(因为很容易忘掉...),保证我们来处理此事件。然后如果查看了上面shouldInterceptTouchEvent()的话,应该已经知道了,这里应该要调用一个叫processTouchEvent的方法,来处理我们整个拖动控件的操作。
好了,这样我们就将触控事件接受到,并处理了。当然现在如果去运行程序,你还是会发现无法操作。因为我们还没有告知子空间应当如果被拖动。
以现在我们对ViewDragHelper的了解,我们可以知道,被拖动的动作处理是在processTouchEvent方法来处理的。所以我们可以查看他的源码,在processTouchEvent中的ACTION_MOVE中我们能轻松发现一个方法dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy),看名字就知道是这个方法再操作我们的子控件移动:
private void dragTo(int left, int top, int dx, int dy) {
int clampedX = left;
int clampedY = top;
final int oldLeft = mCapturedView.getLeft();
final int oldTop = mCapturedView.getTop();
if (dx != 0) {
clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx);
mCapturedView.offsetLeftAndRight(clampedX - oldLeft);
}
if (dy != 0) {
clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy);
mCapturedView.offsetTopAndBottom(clampedY - oldTop);
}
if (dx != 0 || dy != 0) {
final int clampedDx = clampedX - oldLeft;
final int clampedDy = clampedY - oldTop;
mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY,
clampedDx, clampedDy);
}
}
可以明确从代码中看到2个关键方法:
mCapturedView.offsetLeftAndRight(clampedX - oldLeft);
mCapturedView.offsetTopAndBottom(clampedY - oldTop);
这两个方法就是用于移动控件的位置的方法。
而这两句代码附近很明确调用了Callback的3个回调:
clampViewPositionHorizontal() : 用于获取水平移动子控件的移动后位置
clampViewPositionVertical() : 用于获取竖直移动子空间的移动后位置
onViewPositionChanged() : 用于告知父控件子控件的位移情况
知道这些之后我们就可以在MyDragCallback(我们实现的Callback实现子类)中填写上这3个回调。
/**
*横向拖动时,控件左上角x坐标结果返回。这里给出了3个参数
* View child 被操作的子控件
* int left 子控件当前坐标+手指在x方向的位移,其实就是跟随手指移动后的新坐标
* int dx 手指位移距离
* */
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return left ;
}
/**
*横向拖动时,控件左上角y坐标结果返回。这里给出了3个参数
* View child 被操作的子控件
* int top 子控件当前坐标+手指在y方向的位移,其实就是跟随手指移动后的新坐标
* int dy 手指位移距离
* */
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return top ;
}
/**
* 上面两个方法的结合体
* 但是并不会与上面两个方法冲突
* 上面两个方法主要是计算结果
* 这个方法更多是通知的效果,因为移动已经完成。
* */
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
}
现在我们运行程序,我们的控件就可以移动了。
现在梳理下,我们的学习过程我们会发现,这个类的使用非常方便:
1,通过工厂方法获取实例对象
2,通过shouldInterceptTouchEvent(ev) 拦截触控事件,并交给processTouchEvent(event);处理触控事件。
3.在回调中,确认要移动的控件对象是否正确, 计算x坐标和y坐标两个方法子控件位移的距离和新的位置。
TIPS:补充
在子控件为Button这样会吃掉我们触控事件的控件时,shouldInterceptTouchEvent(ev) 无法判断出是否需要拦截。除了暴力的在onInterceptTouchEvent中返回true而外,我们还可以借用ViewDragHelper的边界拖动帮我们搞定这个事情。
实现边界拖动的过程:
1.在创建出ViewDragHelper的对象后,调用viewDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_ALL);
tips:这个函数的参数是:EDGE_ALL所有边界,EDGE_LEFT左边界,EDGE_TOP上边界,EDGE_RIGHT右边界,EDGE_BOTTOM下边界。
2.在回调中实现几个方法:
/**边缘拖动**/
//先会通知我们触控的哪条边界
// edgeFlags = EDGE_LEFT|EDGE_TOP|EDGE_RIGHT|EDGE_BOTTOM 上下左右4边
@Override
public void onEdgeTouched(int edgeFlags, int pointerId) {
switch(edgeFlags){
case EDGE_ALL:
break ;
case EDGE_LEFT:
break ;
case EDGE_TOP:
break ;
case EDGE_RIGHT:
break ;
case EDGE_BOTTOM:
break ;
}
}
/**
* 可以用来阻止某条边不能实现边缘拖动功能
* 反回false就是可以使用,返回true就是不可以用
* */
@Override
public boolean onEdgeLock(int edgeFlags) {
return false;
}
/**
* 具体哪个控件可以被边缘拖动,那么我们就要在这里取指定了
* */
@Override
public void onEdgeDragStarted(int edgeFlags, int pointerId) {
super.onEdgeDragStarted(edgeFlags, pointerId);
viewDragHelper.captureChildView(getChildAt(1),pointerId);
}
加入上面的代码,我们就可以从边缘开始实现拖动。
后的彩蛋:
ViewDragHelper还提供了一个动画移动子控件的效果:settleCapturedViewAt(x, y),这个方法内部由Scroller实现,所以还需要在ViewGroup中实现computeScroll方法。
通常这个功能是实现松手后,控件回到原位。我们可以在Callback中找到一个onViewReleased方法来配合使用:
1,在Callback的实现类中重写方法:
/**
* 用户操作完毕时,调用,我们可以在其中使用回到指定位置的方法,让我们拖动的控件,回到某个指定坐标
* */
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
//当前被移动的控件,使用移动动画,移动到0,0点
if(viewDragHelper.settleCapturedViewAt(0, 0)){
//使用Scroller时,要调用invalidate,因为computScroll是在View.draw()中调用。要重绘才会启动。
invalidate();
}
}
2.实现computScroll,在我们的ViewGroup(自定义控件)中实现:
@Override
public void computeScroll() {
if(viewDragHelper.continueSettling(true)){
invalidate();
}
}
这样实现以后,我们就会拖动控件后,控件自动恢复到0,0坐标去。