Android 之 ViewDragHelper详解(二)

来源:互联网 发布:java web开源报表工具 编辑:程序博客网 时间:2024/06/04 12:24
上一篇我们用到了ViewDragHelper的几个回调tryCaptureView clampViewPositionHorizontal、clampViewPositionVertical、clampViewPositionHorizontal、onEdgeDragStarted,在源码中我们又分别分析到了一些回调的时机,今天继续分析下还没有用过的回调,方便更全面的认识。

首先看下getOrderedChildIndex(),这个在哪个地方用到了呢
/**
* Find the topmost child under the given point within the parent view's coordinate system.
* The child order is determined using {@link Callback#getOrderedChildIndex(int)}.
*
* @param x X position to test in the parent's coordinate system
* @param y Y position to test in the parent's coordinate system
* @return The topmost child view under (x, y) or null if none found.
*/
public View findTopChildUnder(int x, int y) {
final int childCount = mParentView.getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
final View child = mParentView.getChildAt(mCallback.getOrderedChildIndex(i));
if (x >= child.getLeft() && x < child.getRight() &&
y >= child.getTop() && y < child.getBottom()) {
return child;
}
}
return null;
}
我们在上篇文章中,看到这个方法,但是我只是告诉大家这个方法是在找最顶层的子View,从for循环中我们看到,是从最上层View开始找的,但是呢,你也可以改变这个查找的顺序 ,怎么改呢 :mCallback.getorderedChildIndex(i) 
public int getOrderedChildIndex(int index) {
return index;
}
这个方法默认返回的就是index,因为如果不复写的话,就默认顺序找,好,既然这个我们现在再来改改
首先我们让
VDHLayout extends FrameLayout
这个布局就是重叠的,这个效果图就不用贴了,最上面的就是那个叫Drag edge的text view , 不过之前我们只对它在边界触发时候进行了调动,自然,我们在点击拖动的时候 ,它自然不会动。 那么现在, 几个TextView重叠了,如果我们点击来拖动的话是一个都不会动,why? 这个还请大家自己思考 。 那么我们就必然要打破这个默认的寻找子View的顺序
public int getOrderedChildIndex(int index) {
// TODO Auto-generated method stub
int cCount = getChildCount();
if(index == cCount - 1 ){
index = cCount - 2;
}
return index;
}
ok,这样一来,当系统找到最顶层的view的时候 ,我们改变了顺以,让它的index值减去1,这样下面的一个子View就成为了这个事件的接收者,然后就没有然后了,随意玩~


再看看onEdgeLock(),这个有点意思,这里是个误区 ,它并不像字面的意思那样,返回true就是不能从哪个边缘滑动,那它到底到底个啥意思 ,首先看我自己修改的
public int clampViewPositionVertical(View child, int top, int dy) {
return top;
}
默认返回原始的top,这样可以让它到处滚
/**
* 锁定哪个边界
*/
@Override
public boolean onEdgeLock(int edgeFlags) {
// TODO Auto-generated method stub
if(edgeFlags == ViewDragHelper.EDGE_LEFT){
return true;
}else{
return false;
}
}
从表面上看是不能从左边滑动,对不?其实不是这样 ,如果你运行下,你会看到好像没有什么叼用,我自己研究的时候 ,也是把我愣住 了,也以为是这样,我们行从源码中看看这个到底是哪里用到了
private void reportNewEdgeDrags(float dx, float dy, int pointerId) {
int dragsStarted = 0;
if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_LEFT)) {
dragsStarted |= EDGE_LEFT;
}
if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_TOP)) {
dragsStarted |= EDGE_TOP;
}
if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_RIGHT)) {
dragsStarted |= EDGE_RIGHT;
}
if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_BOTTOM)) {
dragsStarted |= EDGE_BOTTOM;
}
 
if (dragsStarted != 0) {
mEdgeDragsInProgress[pointerId] |= dragsStarted;
mCallback.onEdgeDragStarted(dragsStarted, pointerId);
}
}
 
private boolean checkNewEdgeDrag(float delta, float odelta, int pointerId, int edge) {
final float absDelta = Math.abs(delta);
final float absODelta = Math.abs(odelta);
 
if ((mInitialEdgesTouched[pointerId] & edge) != edge || (mTrackingEdges & edge) == 0 ||
(mEdgeDragsLocked[pointerId] & edge) == edge ||
(mEdgeDragsInProgress[pointerId] & edge) == edge ||
(absDelta <= mTouchSlop && absODelta <= mTouchSlop)) {
return false;
}
if (absDelta < absODelta * 0.5f && mCallback.onEdgeLock(edge)) {
mEdgeDragsLocked[pointerId] |= edge;
return false;
}
return (mEdgeDragsInProgress[pointerId] & edge) == 0 && absDelta > mTouchSlop;
}
ok,我直接给大家看个重点,在checkNewEdgeDrag的第二个if语句中,假如这个时候是EDGE_LEFT触发了,也就是从左边开始向右滑动了,我们再看看这个判断语句 ,它说,如果x方向滑动的距离 小于 y方向滑动距离的二分之一 && 回调onEdgeLock返回true,才不让滑动,否则允许滑动。 不知道大家是否看明白 了, 通俗说,如果我从左到右滑动,却出现了,y方向滑动的距离的一半比x方向滑动的距离还要大,这完全是扯谈的,所以你可以通过onEdgeLock返回true来禁止这样扯蛋的事情 ,如果你的onEdgeLock返回false(默认的就是返回false),那么这种扯蛋的事情能出现~ 现在大家知道该怎么去滑动了吧,这个只能靠大家自己领悟了~


再看看onViewReleased(),为什么看这个方法,因为接下来,我要让大家明白如何对子View进行位移的,这里涉及到了scroller的理解与应用 ,在上篇中有提到,如果不熟悉,需要先看看
protected void onLayout(boolean changed, int left, int top, int right,
int bottom) {
// TODO Auto-generated method stub
super.onLayout(changed, left, top, right, bottom);
mOrigX = (int) mReleaseView.getX();
mOrigY = (int) mReleaseView.getY();
}
这里我们定位mRelease的初始位置
/**
* 手指抬起,释放capturedChild的时候
*/
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
if(releasedChild == mReleaseView){
mDragHelper.settleCapturedViewAt(mOrigX, mOrigY);
invalidate();
}
}

我们调动了invalidate()来重绘,那么必然就会调用ViewGroup的computScroll()方法
public void computeScroll() {
if(mDragHelper.continueSettling(true)){
invalidate();
}
}
在这个方法中,我们再用mDragHelper.continueSetting(true)来不断计算位置,并重新调用invalidate()来重绘,直到不计算为止,大家有没有感觉到这就是scroller的用法 ,现在大家可以试试,是不是可以可以做到反弹的效果~

好了,我们看看这是怎么实现的
public boolean settleCapturedViewAt(int finalLeft, int finalTop) {
if (!mReleaseInProgress) {
throw new IllegalStateException("Cannot settleCapturedViewAt outside of a call to " +
"Callback#onViewReleased");
}
 
return forceSettleCapturedViewAt(finalLeft, finalTop,
(int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId),
(int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId));
}
private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) {
final int startLeft = mCapturedView.getLeft();
final int startTop = mCapturedView.getTop();
final int dx = finalLeft - startLeft;
final int dy = finalTop - startTop;
 
if (dx == 0 && dy == 0) {
// Nothing to do. Send callbacks, be done.
mScroller.abortAnimation();
setDragState(STATE_IDLE);
return false;
}
 
final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel);
mScroller.startScroll(startLeft, startTop, dx, dy, duration);
 
setDragState(STATE_SETTLING);
return true;
}
public boolean continueSettling(boolean deferCallbacks) {
if (mDragState == STATE_SETTLING) {
boolean keepGoing = mScroller.computeScrollOffset();
final int x = mScroller.getCurrX();
final int y = mScroller.getCurrY();
final int dx = x - mCapturedView.getLeft();
final int dy = y - mCapturedView.getTop();
 
if (dx != 0) {
mCapturedView.offsetLeftAndRight(dx);
}
if (dy != 0) {
mCapturedView.offsetTopAndBottom(dy);
}
 
if (dx != 0 || dy != 0) {
mCallback.onViewPositionChanged(mCapturedView, x, y, dx, dy);
}
 
if (keepGoing && x == mScroller.getFinalX() && y == mScroller.getFinalY()) {
// Close enough. The interpolator/scroller might think we're still moving
// but the user sure doesn't.
mScroller.abortAnimation();
keepGoing = false;
}
 
if (!keepGoing) {
if (deferCallbacks) {
mParentView.post(mSetIdleRunnable);
} else {
setDragState(STATE_IDLE);
}
}
}
 
return mDragState == STATE_SETTLING;
}
就是scroller的用法 ,现在值得一提的事是,在SettleCapturedViewAt的方法中,获取的x和y方向上的速度,这里传入了一个mActiviePointerId,这个到底什么用呢,这个返回的就是当前手指离开屏幕的速度 ,也就是说,手指离开屏幕的那一刻,滑动的越快,速度越大,那么在forceSettleCapturedViewAt中的duration时间就越短,这样动画的速度就越快,如果我不想这样呢
mDragHelper.smoothSlideViewTo(releasedChild, mOrigX, mOrigY)
这样就平滑的滚回去了~
至于continueSettling(true)为什么是传入true,这里就是一个handler处理message机制 ,不过这里我暂时没明白用处在哪,不过 传入true肯定不会错~


最后我们看看最后两个方法getViewHorizontalDragRange()和getViewVerticalDragRange()
修改如下
VDHLayout extends LinearLayout
<com.example.viewdraghelper_01.VDHLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
 
<Button
android:id="@+id/tv_01"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_gravity="center_horizontal"
android:layout_margin="10dp"
android:background="#44ff0000"
android:gravity="center"
android:text="Drag me" />
 
<TextView
android:id="@+id/tv_02"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_gravity="center_horizontal"
android:layout_margin="10dp"
android:background="#44ff0000"
android:gravity="center"
android:text="Drag and release me" />
 
<TextView
android:id="@+id/tv_03"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_gravity="center_horizontal"
android:layout_margin="10dp"
android:background="#44ff0000"
android:gravity="center"
android:text="Drag edge" />
 
</com.example.viewdraghelper_01.VDHLayout>
@Override
public int getViewHorizontalDragRange(View child) {
return super.getViewHorizontalDragRange(child);
}
 
@Override
public int getViewVerticalDragRange(View child) {
return super.getViewVerticalDragRange(child);
}

我们改成了button后,就不能移动移动了,通过在第一篇中的参考博客,我们可以知道View事件的传递机制,这个button可以被点击,也就是说clickable=true , 它就会消费这个事件,那么父viewGroup就不能处理这个后续的move 、up事件了,那么我们要怎么做呢
@Override
public int getViewHorizontalDragRange(View child) {
return mDragHelper.getEdgeSize();
}
 
@Override
public int getViewVerticalDragRange(View child) {
return mDragHelper.getEdgeSize();
}
其实这里只要返回大于0的数就可以(我在源码中一个判断条件 ,就是是否大于0),我们返回的触发边缘的宽度。

那么是不是觉得不合情理啊,没关系,我们来看看
在onInterceptTouchEvent中,我们用mDragHelper.shouldInterceptTouchEvent来决定是否拦截,既然这个down事件无法拦截,就拦截MOVE事件,看shouldInterceptTouchEvent中的MOVE处理
case MotionEvent.ACTION_MOVE: {
// First to cross a touch slop over a draggable view wins. Also report edge drags.
final int pointerCount = MotionEventCompat.getPointerCount(ev);
for (int i = 0; i < pointerCount; i++) {
final int pointerId = MotionEventCompat.getPointerId(ev, i);
final float x = MotionEventCompat.getX(ev, i);
final float y = MotionEventCompat.getY(ev, i);
final float dx = x - mInitialMotionX[pointerId];
final float dy = y - mInitialMotionY[pointerId];
 
final View toCapture = findTopChildUnder((int) x, (int) y);
final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy);
if (pastSlop) {
// check the callback's
// getView[Horizontal|Vertical]DragRange methods to know
// if you can move at all along an axis, then see if it
// would clamp to the same value. If you can't move at
// all in every dimension with a nonzero range, bail.
final int oldLeft = toCapture.getLeft();
final int targetLeft = oldLeft + (int) dx;
final int newLeft = mCallback.clampViewPositionHorizontal(toCapture,
targetLeft, (int) dx);
final int oldTop = toCapture.getTop();
final int targetTop = oldTop + (int) dy;
final int newTop = mCallback.clampViewPositionVertical(toCapture, targetTop,
(int) dy);
final int horizontalDragRange = mCallback.getViewHorizontalDragRange(
toCapture);
final int verticalDragRange = mCallback.getViewVerticalDragRange(toCapture);
if ((horizontalDragRange == 0 || horizontalDragRange > 0
&& newLeft == oldLeft) && (verticalDragRange == 0
|| verticalDragRange > 0 && newTop == oldTop)) {
break;
}
}
reportNewEdgeDrags(dx, dy, pointerId);
if (mDragState == STATE_DRAGGING) {
// Callback might have started an edge drag
break;
}
 
if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {
break;
}
}
saveLastMotion(ev);
break;
从上篇文章中,我们看到postSlop什么时候为true,那就是getViewHorizontalDragRange()和getViewVerticalDragRange()的返回值是>0的时候 ,现在我们正是这样做了,所以可以看最后一个if语句中的tryCaptruedViewForDrag
boolean tryCaptureViewForDrag(View toCapture, int pointerId) {
if (toCapture == mCapturedView && mActivePointerId == pointerId) {
// Already done!
return true;
}
if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) {
mActivePointerId = pointerId;
captureChildView(toCapture, pointerId);
return true;
}
return false;
}
只要我们在回调tryCaptureView中,让相应的view返回true,这样ACTION_MOVE事件就能返回true,这样就截断了事件,所以处理ACTION_MOVE事件,就是VDHLayout的onTouchEvent处理,接下来就看processTouchEvent中的ACTION_MOVE
case MotionEvent.ACTION_MOVE: {
if (mDragState == STATE_DRAGGING) {
final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
final float x = MotionEventCompat.getX(ev, index);
final float y = MotionEventCompat.getY(ev, index);
final int idx = (int) (x - mLastMotionX[mActivePointerId]);
final int idy = (int) (y - mLastMotionY[mActivePointerId]);
 
dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy);
 
saveLastMotion(ev);
} else {
// Check to see if any pointer is now over a draggable view.
final int pointerCount = MotionEventCompat.getPointerCount(ev);
for (int i = 0; i < pointerCount; i++) {
final int pointerId = MotionEventCompat.getPointerId(ev, i);
final float x = MotionEventCompat.getX(ev, i);
final float y = MotionEventCompat.getY(ev, i);
final float dx = x - mInitialMotionX[pointerId];
final float dy = y - mInitialMotionY[pointerId];
 
reportNewEdgeDrags(dx, dy, pointerId);
if (mDragState == STATE_DRAGGING) {
// Callback might have started an edge drag.
break;
}
 
final View toCapture = findTopChildUnder((int) x, (int) y);
if (checkTouchSlop(toCapture, dx, dy) &&
tryCaptureViewForDrag(toCapture, pointerId)) {
break;
}
}
saveLastMotion(ev);
}
break;
}
这里在上篇已经讲过了,不多说了。


到这里呢,所有的ViewDragHelper的知识已经讲完,可能有点地方不详细,自己多动动手,参照这些,相信你很快就能明白。
如果大家在看的同时有疑问,或者发现错误的地方,欢迎提出~



0 0