ViewDragHelper源码浅析与应用实例

2024-03-02 07:08

本文主要是介绍ViewDragHelper源码浅析与应用实例,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

ViewDragHelper源码浅析与应用实例

ViewDragHelper简介

ViewDragHelper是Google为Android开发者提供的一个强大的帮助类。使用它几乎可以完成所有和滑动拖拽相关的需求。
示例
下面让我们从源码入手,逐步掌握它的实现逻辑与使用方法。

源码浅析

该部分将首先介绍类中几个关键的域,然后介绍回调接口以及构造器,最后分析一次拖拽操作过程中所涉及的方法。

关键域
/**
* 空闲状态。
*/
public static final int STATE_IDLE = 0;/**
* 某个View正在被拖拽。
*/
public static final int STATE_DRAGGING = 1;/**
* 某个View正在被放置。
*/
public static final int STATE_SETTLING = 2;private int mDragState;

ViewDragHelper使用状态位来判断当前工作状态,有STATE_IDLE(空闲)、STATE_DRAGGING(拖拽)、STATE_SETTLING(放置)三种。这里重点说明一下拖拽与放置的区别:如果View的移动是由于用户的手指在屏幕上滑动造成的,那么当前状态属于拖拽状态;如果View的移动是由于程序内调用某些方法造成的,那么当前状态属于放置状态。

/**
* 左边界
*/
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;private int mTrackingEdges;

边界的标志位。用于后面的与边界相关的拖拽判定方法中。

private View mCapturedView;

用于记录当前被捕获的View。

private int mTouchSlop;

被判定为滑动之前可以移动的最大距离。这个值越小滑动操作越灵活。

private VelocityTracker mVelocityTracker;

ViewDragHelper使用VelocityTracker计算滑动速度,进而实现对Fling(抛动)的支持。

回调接口

初始化ViewDragHelper对象时需要传入一个ViewDragHelper.Callback对象。正如我们在许多其他场合遇到的Callback一样,它是一个需要编程人员实现的回调接口,里面包含了用于控制该ViewDragHelper行为的回调方法。由于包含的方法较多,这里只对常用到的几个方法进行介绍。

public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {}

当某个view的位置由于拖拽或放置而改变时调用。
changedView:位置改变的view;
left,top:新的X、Y的坐标;
dx,dy:X、Y方向上发生的位移。

public abstract boolean tryCaptureView(View child, int pointerId);

即将捕获一个view时调用,返回true表示允许捕获,false表示不允许捕获。
child:即将被捕获的view;
pointerId:即将捕获view的pointer的Id
注意:由于ViewDragHelper不支持同时拖拽多个view的功能,在本文其他地方将不对Pointer相关的部分进行说明。想了解多点触控的知识的话可以去搜索一些相关文章。

public void onViewReleased(View releasedChild, float xvel, float yvel) {}

当view被释放时调用。一般用于实现回弹效果。
releasedChild:被释放的view;
xvel,yvel:释放时X、Y轴上的滑动速度。

public void onEdgeTouched(int edgeFlags, int pointerId) {}

当需要追踪的某条边界被触摸到,并且当前没有子view被捕获时调用。
edgeFlags:用于判断被触摸的是哪条边;

public int clampViewPositionHorizontal(View child, int left, int dx) {return 0;}
public int clampViewPositionVertical(View child, int top, int dy) {return 0;}

在滑动时为限制view的位移量而调用。默认实现为返回0,即不可滑动。
child:正在滑动的view。
left/top:如果不加限制,view的左边缘将到达的位置。
dx/dy:如果不加限制,view将发生的位移量。

public int getViewHorizontalDragRange(View child)  {return 0;}
public int getViewVerticalDragRange(View child) {return 0;}

确定view可以滑动的范围,默认返回0。
view:目标view。

构造器

为确保不同平台版本的兼容性,ViewDragHelper不提供public的构造器,实例需要通过静态方法ViewDragHelper.create(ViewGroup forParent, Callback cb)或是ViewDragHelper.create(ViewGroup forParent, float sensitivity, Callback cb)来获取。首先看一下三参数的create()方法的实现:

public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) {
final ViewDragHelper helper = create(forParent, cb);
helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity));
return helper;
}

可以很明显地看出,两参数的create()方法是默认实现,三参数的create()方法首先调用两参数的create()方法构造一个实例,然后根据sensitivity参数修改了mTouchSlop域的值,并且sensitivity的值越大,mTouchSlop的值越小。
接下来看一下两参数的create()方法:

public static ViewDragHelper create(ViewGroup forParent, Callback cb) {
return new ViewDragHelper(forParent.getContext(), forParent, cb);
}

只有一行代码,调用构造器创建了一个实例。接下来看看构造器:

private ViewDragHelper(Context context, ViewGroup forParent, Callback cb) {//确保父view与回调接口不为nullif (forParent == null) {throw new IllegalArgumentException("Parent view may not be null");}if (cb == null) {throw new IllegalArgumentException("Callback may not be null");}mParentView = forParent;mCallback = cb;//利用ViewConfiguration获取最小滑动距离、最大抛动速度、最小抛动速度等参数final ViewConfiguration vc = ViewConfiguration.get(context);//dp与px的转换系数final float density = context.getResources().getDisplayMetrics().density;//确定边的宽度(边在判定时作为一个矩形考虑)mEdgeSize = (int) (EDGE_SIZE * density + 0.5f);mTouchSlop = vc.getScaledTouchSlop();mMaxVelocity = vc.getScaledMaximumFlingVelocity();mMinVelocity = vc.getScaledMinimumFlingVelocity();mScroller = ScrollerCompat.create(context, sInterpolator);
}

主要就是通过ViewConfiguration类获取了一些参数,并且创建了Scroller实例。创建Scroller实例时传入的sInterpolator为ViewDragHelper内部实现的一个Interpolator实例,会用在滑动动画的计算中。

一次滑动操作所涉及的方法

想要依靠ViewDragHelper实现滑动,必须首先将它所在的ViewGroup的触摸事件交给它管理。一般我们会这么实现:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {return viewDragHelper.shouldInterceptTouchEvent(ev);
}@Override
public boolean onTouchEvent(MotionEvent event) {viewDragHelper.processTouchEvent(event);return true;
}

onInterceptTouchEvent()与onTouchEvent()方法是自定义ViewGroup时非常重要的两个方法,用于控制触摸事件的分发过程。如果对View触摸事件分发不是很了解的话,可以去查查相关文章,有很多。
首先说明为什么在onTouchEvent()中返回true。ViewGroup在分发ACTION_MOVE与ACTION_UP事件时,会直接将他们交由成功处理了ACTION_DOWN事件的子view处理。因此,如果这里不返回true,就收不到后续的ACTION_MOVE与ACTION_UP事件,所有的方法也就失效了。
接下来我们重点分析一下ViewDragHelper提供的shouldInterceptTouchEvent()与processTouchEvent()两个方法。

顾名思义,shouldInterceptTouchEvent()用于判断是否需要拦截触摸事件,而processTouchEvent()用于处理触摸事件。在分析滑动逻辑的具体实现之前,首先需要对事件流进行分析。由于滑动逻辑是在processTouchEvent()中实现的(这里先记一个结论),因此必须被分发到ViewGroup的onTouchEvent()中,否则ViewDragHelper将不起作用。这里可以分为两种情况:

  1. ViewGroup的子View没能消费掉本次触摸事件,事件被分发到ViewGroup的onTouchEvent()中进行处理。
  2. ViewGroup通过onInterceptTouchEvent()拦截了本次触摸事件,事件被分发到ViewGroup的onTouchEvent()中进行处理;

首先看第一种情况。如果触摸位置的子View无法消费该事件,事件将传回ViewGroup的onTouchEvent()中,并最终在ViewDragHelper的processTouchEvent()方法中得到处理。让我们看看processTouchEvent()的实现。首先是事件类型为ACTION_DOWN时:

case MotionEvent.ACTION_DOWN: {//获取该位置的viewfinal float x = ev.getX();final float y = ev.getY();final int pointerId = ev.getPointerId(0);final View toCapture = findTopChildUnder((int) x, (int) y);//保存初始状态,这里保存了是否触摸到边的信息saveInitialMotion(x, y, pointerId);//尝试捕获该viewtryCaptureViewForDrag(toCapture, pointerId);//确认是否有需要追踪的边界被触摸到,有的话调用onEdgeTouched回调方法final int edgesTouched = mInitialEdgesTouched[pointerId];if ((edgesTouched & mTrackingEdges) != 0) {mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);}break;}

方法思路很简洁,首先根据触摸点坐标获取子view,然后尝试捕获该子view,最后尝试捕获边。很显然,这里最重要的是tryCaptureViewForDrag()方法,让我们看看它的实现:

boolean tryCaptureViewForDrag(View toCapture, int pointerId) {//如果已经捕获了该view,那么就直接返回true。if (toCapture == mCapturedView && mActivePointerId == pointerId) {// Already done!return true;}//调用callback中的tryCaptureView()方法确认是否能够捕获if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) {mActivePointerId = pointerId;//如果能够捕获,那么就捕获该viewcaptureChildView(toCapture, pointerId);return true;}//不能捕获的话返回falsereturn false;
}

这里用到了回调接口中的tryCaptureView()方法,如果没有在方法中返回true,将不会调用captureChildView()方法,捕获将失败。这告诉我们:需要在tryCaptureView()中进行判定,如果目标是我们想要拖动的view,那么就应当返回true。
然后说明一下captureChildView(View childView, int activePointerId)方法。它是ViewDragHelper的一个public方法,用于捕获一个view。它的实现很简单,就是将mCapturedView设置为childView,并将当前状态设置为STATE_DRAGGING。这里就不贴代码了。重点是,这个方法本身并不受到tryCaptureView()的限制,只要调用了就一定能捕获成功(当然是在参数合法的前提下)。正是由于这个特性,我们可以在onEdgeTouched()方法中使用它来捕获一个屏幕外的对象。本文开头的gif中的绿色方块就是这么实现的。
接下来看看事件类型为ACTION_MOVE时:

case MotionEvent.ACTION_MOVE: {//如果当前状态为正在拖拽if (mDragState == STATE_DRAGGING) {final int index = ev.findPointerIndex(mActivePointerId);//获取当前的x,y坐标final float x = ev.getX(index);final float y = ev.getY(index);//获取当前x,y坐标与上一次移动时的差值,确定本次移动的距离final int idx = (int) (x - mLastMotionX[mActivePointerId]);final int idy = (int) (y - mLastMotionY[mActivePointerId]);//根据x,y方向的位移量拖拽viewdragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy);//保存本次位移的信息saveLastMotion(ev);} else {//省略}break;
}

同样是一目了然。首先根据本次触摸事件的坐标与上次触摸事件的坐标计算位移差值,之后调用dragTo()方法移动view,最后保存信息。下面看看dragTo()的实现:

private void dragTo(int left, int top, int dx, int dy) {int clampedX = left;int clampedY = top;//此时view还未移动,因此获取的是当前的位置坐标final int oldLeft = mCapturedView.getLeft();final int oldTop = mCapturedView.getTop();//如果x方向的位移不为0if (dx != 0) {//调用callback.clampViewPositionHorizontal()方法获取处理后的left位置clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx);//计算位移量并进行移动ViewCompat.offsetLeftAndRight(mCapturedView, clampedX - oldLeft);}//如果y方向的位移不为0if (dy != 0) {//调用callback.clampViewPositionHorizontal()方法获取处理后的top位置clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy);//计算位移量并进行移动ViewCompat.offsetTopAndBottom(mCapturedView, clampedY - oldTop);}//如果进行了位移if (dx != 0 || dy != 0) {final int clampedDx = clampedX - oldLeft;final int clampedDy = clampedY - oldTop;//调用onViewPositionChanged()回调方法mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY,clampedDx, clampedDy);}
}

这里的重点在于调用回调方法clampViewPositionHorizontal(View child, int left, int dx)与clampViewPositionVertical(View child, int top, int dy)计算实际位移。如果你在这两个方法中简单地返回left/top的话,那么你手指移到哪,被拖拽的view就会跟到哪。如果你希望view只能在一定返回内滑动,可以更改实现。下面为一个示例:

@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {return left > 0 ? left : 0;
}

这样就保证了view的左边缘不会滑到屏幕外。如果想要一点拉力感,可以按一定比例缩小dx/dy,并计算出实际的left/top值。之后的事情就很简单了,根据最终位置计算出实际位移量,并调用ViewCompat.offsetLeftAndRight()与ViewCompat.offsetTopAndBottom()移动view,并调用onViewPositionChanged()回调方法。
最后看看ACTION_UP部分:

case MotionEvent.ACTION_CANCEL: {if (mDragState == STATE_DRAGGING) {dispatchViewReleased(0, 0);}cancel();break;
}

其实就是释放被捕获的view,将状态改回STATE_IDLE,并清空之前缓存的信息。dispatchViewReleased()中调用了回调方法onViewReleased()。
到这里为止,实现滑动逻辑的方法processTouchEvent()就差不多分析完了,应该还是很容易理解的。简单来讲,就是在ACTION_DOWN时捕获view,在ACTION_MOVE时拖拽view,在ACTION_UP时释放view。
读到这里,如果熟悉事件分发机制的话,应该已经会产生疑问了。上面这一套流程走下来,等于说ViewGroup处理了所有触摸事件,这在一般情况下是不可能的。如果触摸到的view能够消费掉触摸事件,那么触摸事件就不会被分发到ViewGroup的onTouchEvent()中,ViewDragHelper也就没办法对其进行处理。因此,ViewGroup需要在向子view分发触摸事件之前进行判断,并在需要时通过shouldInterceptTouchEvent()方法将其拦截。这也就是前面提到的第二种情况。下面让我们看看shouldInterceptTouchEvent()的源码,首先看一下它的返回值:

return mDragState == STATE_DRAGGING;

很简单,如果在方法调用过程中,ViewDragHelper的状态变成了STATE_DRAGGING,那么就返回true,否则返回false。由于这个方法是在ViewGroup的onInterceptTouchEvent()中作为返回值调用的,如果它返回了true,ViewGroup就会拦截下这个触摸事件,事件将交由ViewGroup的onTouchEvent()处理,否则事件将交由子view处理。
接下来看看ACTION_DOWN部分:

case MotionEvent.ACTION_DOWN: {//获取按下的位置坐标final float x = ev.getX();final float y = ev.getY();//当事件类型为ACTION_DOWN时,MotionEvent对象只会包含一个pointer的信息,因此直接通过getPointerId(0)获取当前pointer ID。final int pointerId = ev.getPointerId(0);//保存初始状态信息saveInitialMotion(x, y, pointerId);//获取该MotionEvent想要捕获的子viewfinal View toCapture = findTopChildUnder((int) x, (int) y);//***和processTouchEvent()最大的不同点。if (toCapture == mCapturedView && mDragState == STATE_SETTLING) {tryCaptureViewForDrag(toCapture, pointerId);}//获取了一开始保存的关于被触摸的边界的信息final int edgesTouched = mInitialEdgesTouched[pointerId];//确认其中是否有需要被追踪的边if ((edgesTouched & mTrackingEdges) != 0) {//如果有的话则调用onEdgeTouched()回调方法mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);}break;
}

可以发现,这部分和processTouchEvent()中的几乎一样,唯一不同的是以下部分:

if (toCapture == mCapturedView && mDragState == STATE_SETTLING) {tryCaptureViewForDrag(toCapture, pointerId);
}

和processTouchEvent()的区别在于,processTouchEvent()中没有外面那一圈if语句。由于ViewDragHelper平时处于STATE_IDLE,因此这个条件判断语句是无法通过的,里面的tryCaptureViewForDrag()也就得不到执行。之所以要这样写,是因为仅凭一个ACTION_DOWN事件,我们无法判断用户是否将要开始滑动,因此不对其进行拦截,让子view去处理。如果这里把ACTION_DOWN拦截了,那么子view所有的Touch逻辑都将失效。
接下来看看ACTION_MOVE部分:

case MotionEvent.ACTION_MOVE: {final int pointerCount = ev.getPointerCount();for (int i = 0; i < pointerCount; i++) {final int pointerId = ev.getPointerId(i);//如果pointer无效则continueif (!isValidPointerForActionMove(pointerId)) continue;//获取本次Move的信息final float x = ev.getX(i);final float y = ev.getY(i);final float dx = x - mInitialMotionX[pointerId];final float dy = y - mInitialMotionY[pointerId];final View toCapture = findTopChildUnder((int) x, (int) y);//(Step 1)判断该位置是否有子view,以及本次移动是否可以被认作滑动final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy);if (pastSlop) {//(Step 2)判断目标view有没有移动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;}}//(Step 3)尝试捕获子viewif (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {break;}}saveLastMotion(ev);break;
}

这段代码有点长,让我们分三步来看。首先看Step 1:

final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy);

前半段很简单,判断在该位置有没有找到一个子view,如果没找到,自然也就没有东西可以拖拽了。重点是后半段,让我们看看这个方法的代码:

private boolean checkTouchSlop(View child, float dx, float dy) {//child为null则直接返回falseif (child == null) {return false;}//确认能否在x,y两个方向进行拖拽final boolean checkHorizontal = mCallback.getViewHorizontalDragRange(child) > 0;final boolean checkVertical = mCallback.getViewVerticalDragRange(child) > 0;//判断位移量是否足够被认定为滑动if (checkHorizontal && checkVertical) {return dx * dx + dy * dy > mTouchSlop * mTouchSlop;} else if (checkHorizontal) {return Math.abs(dx) > mTouchSlop;} else if (checkVertical) {return Math.abs(dy) > mTouchSlop;}return false;
}

这个方法的用途是判断某个触摸事件是否能被认作滑动。方法体内调用了getViewHorizontalDragRange()与getViewVerticalDragRange()两个方法。如果其中一个方法返回0,ViewDragHelper会认为view在这个方向不能够拖动。如果两个方向都不能够拖动的话,方法会直接返回false,否则将判断位移量是否大于mTouchSlop。如果位移量大于mTouchSlop,则返回true,否则返回false。
如果子view存在,并且本次触摸事件可以被认作滑动事件,那么接下来进入Step2。Step2主要是判断一下view在本次滑动事件的作用下是否会移动,代码看似很长,实际都是前面讲解过的内容,这里也就不赘述了。
接下来看Step3:

if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {break;
}

tryCaptureViewForDrag()又出场了。它会尝试捕获该view,如果成功捕获的话,ViewDragHelper的状态会变为STATE_DRAGGING,shouldInterceptTouchEvent()就会返回true,本次滑动事件就会被拦截,交由processTouchEvent()进行处理。
最后,我们对有shouldInterceptTouchEvent()参与的滑动过程进行一下梳理:ACTION_DOWN发生时不拦截,交由子view处理;ACTION_MOVE发生时,根据滑动位移量以及子view的拖拽权限进行判断,在需要时将事件拦截,交由processTouchEvent()实现滑动;ACTION_UP发生时,如果有子view正在被拖拽,则将其释放并调用onViewReleased()回调方法。
到这里为止,ViewDragHelper的源码浅析部分基本上完成了。下面让我们看看怎么实现开头的示例程序。

应用实例

自定义的ViewGroup部分:

/*** Created by swt369 on 2017/8/18.*/public class DragGroup extends ConstraintLayout {private ViewDragHelper viewDragHelper;private TextView red;private TextView green;private TextView blue;private int mRedX;private int mRedY;private int mGreenWidth;private int mGreenY;private boolean once;public DragGroup(@NonNull Context context, @Nullable AttributeSet attrs) {super(context, attrs);viewDragHelper = ViewDragHelper.create(this, new ViewDragHelper.Callback() {//所有的子view都能够拖拽@Overridepublic boolean tryCaptureView(View child, int pointerId) {return true;}//不对水平位移进行限制@Overridepublic int clampViewPositionHorizontal(View child, int left, int dx) {return left;}//不对垂直位移进行限制@Overridepublic int clampViewPositionVertical(View child, int top, int dy) {return top;}//对于红色色块与绿色色块,调用viewDragHelper的smoothSlideViewTo()方法实现回弹。注意调用smoothSlideViewTo()后需要调用postInvalidateOnAnimation()@Overridepublic void onViewReleased(View releasedChild, float xvel, float yvel) {if(releasedChild == red){viewDragHelper.smoothSlideViewTo(releasedChild, mRedX, mRedY);ViewCompat.postInvalidateOnAnimation(DragGroup.this);}if(releasedChild == green){viewDragHelper.smoothSlideViewTo(releasedChild,-mGreenWidth,mGreenY);ViewCompat.postInvalidateOnAnimation(DragGroup.this);}}//简单实现只要返回一个正值代表可拖拽即可,具体数值很少派上用场。@Overridepublic int getViewHorizontalDragRange(View child) {return child.getWidth();}//简单实现只要返回一个正值代表可拖拽即可,具体数值很少派上用场。@Overridepublic int getViewVerticalDragRange(View child) {return child.getHeight();}//拖拽左边界时捕获绿色方块@Overridepublic void onEdgeTouched(int edgeFlags, int pointerId) {if(edgeFlags == ViewDragHelper.EDGE_LEFT){viewDragHelper.captureChildView(green,0);}}});//设置左边界是可拖拽的viewDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);}@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {return viewDragHelper.shouldInterceptTouchEvent(ev);}@Overridepublic boolean onTouchEvent(MotionEvent event) {viewDragHelper.processTouchEvent(event);return true;}@Overrideprotected void onFinishInflate() {super.onFinishInflate();red = (TextView)getChildAt(0);green = (TextView)getChildAt(1);blue = (TextView)getChildAt(2);}//获取关于色块的大小、位置的信息,并在开始时隐藏掉绿色方块@Overrideprotected void onLayout(boolean changed, int left, int top, int right, int bottom) {if(changed && !once){super.onLayout(changed, left, top, right, bottom);mRedX = red.getLeft();mRedY = red.getTop();mGreenWidth = green.getWidth();mGreenY = green.getTop();ViewCompat.offsetLeftAndRight(green,-(green.getLeft() + mGreenWidth));once = false;}}//由于smoothSlideViewTo()方法是通过Scroller实现的,需要重写computeScroll()方法手动刷帧。下面的写法几乎是通用的@Overridepublic void computeScroll() {if(viewDragHelper.continueSettling(true)){ViewCompat.postInvalidateOnAnimation(this);}}
}

接下来只要在Activity的layout文件中加入该ViewGroup,并在里面加入View即可。这里贴上加入的三个色块(注意这里将它们的clickable属性设置成了true,即可以消费触摸事件):

<TextView
    android:background="#ff0000"android:clickable="true"android:id="@+id/red"android:layout_width="100dp"android:layout_height="100dp"android:layout_marginTop="8dp"app:layout_constraintTop_toBottomOf="@+id/green"android:layout_marginLeft="8dp"app:layout_constraintLeft_toLeftOf="parent" /><TextView
    android:background="#00ff00"android:clickable="true"android:id="@+id/green"android:layout_width="100dp"android:layout_height="100dp"android:layout_marginTop="8dp"app:layout_constraintTop_toBottomOf="@+id/blue"android:layout_marginLeft="8dp"app:layout_constraintLeft_toLeftOf="parent" /><TextView
    android:background="#0000ff"android:clickable="true"android:id="@+id/blue"android:layout_width="100dp"android:layout_height="100dp"app:layout_constraintTop_toTopOf="parent"android:layout_marginTop="8dp"android:layout_marginLeft="8dp"app:layout_constraintLeft_toLeftOf="parent" />

总结

ViewDragHelper的功能的确非常强大。但是,想要用好它的话,需要对View事件分发机制有一个较为清晰的认识,还需要了解许多常用的工具类。这里再次证明了基础和知识积累的重要性。路漫漫其修远兮,吾将上下而求索。

这篇关于ViewDragHelper源码浅析与应用实例的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/765310

相关文章

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

中文分词jieba库的使用与实景应用(一)

知识星球:https://articles.zsxq.com/id_fxvgc803qmr2.html 目录 一.定义: 精确模式(默认模式): 全模式: 搜索引擎模式: paddle 模式(基于深度学习的分词模式): 二 自定义词典 三.文本解析   调整词出现的频率 四. 关键词提取 A. 基于TF-IDF算法的关键词提取 B. 基于TextRank算法的关键词提取

水位雨量在线监测系统概述及应用介绍

在当今社会,随着科技的飞速发展,各种智能监测系统已成为保障公共安全、促进资源管理和环境保护的重要工具。其中,水位雨量在线监测系统作为自然灾害预警、水资源管理及水利工程运行的关键技术,其重要性不言而喻。 一、水位雨量在线监测系统的基本原理 水位雨量在线监测系统主要由数据采集单元、数据传输网络、数据处理中心及用户终端四大部分构成,形成了一个完整的闭环系统。 数据采集单元:这是系统的“眼睛”,

csu 1446 Problem J Modified LCS (扩展欧几里得算法的简单应用)

这是一道扩展欧几里得算法的简单应用题,这题是在湖南多校训练赛中队友ac的一道题,在比赛之后请教了队友,然后自己把它a掉 这也是自己独自做扩展欧几里得算法的题目 题意:把题意转变下就变成了:求d1*x - d2*y = f2 - f1的解,很明显用exgcd来解 下面介绍一下exgcd的一些知识点:求ax + by = c的解 一、首先求ax + by = gcd(a,b)的解 这个

hdu1394(线段树点更新的应用)

题意:求一个序列经过一定的操作得到的序列的最小逆序数 这题会用到逆序数的一个性质,在0到n-1这些数字组成的乱序排列,将第一个数字A移到最后一位,得到的逆序数为res-a+(n-a-1) 知道上面的知识点后,可以用暴力来解 代码如下: #include<iostream>#include<algorithm>#include<cstring>#include<stack>#in

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

zoj3820(树的直径的应用)

题意:在一颗树上找两个点,使得所有点到选择与其更近的一个点的距离的最大值最小。 思路:如果是选择一个点的话,那么点就是直径的中点。现在考虑两个点的情况,先求树的直径,再把直径最中间的边去掉,再求剩下的两个子树中直径的中点。 代码如下: #include <stdio.h>#include <string.h>#include <algorithm>#include <map>#

【区块链 + 人才服务】可信教育区块链治理系统 | FISCO BCOS应用案例

伴随着区块链技术的不断完善,其在教育信息化中的应用也在持续发展。利用区块链数据共识、不可篡改的特性, 将与教育相关的数据要素在区块链上进行存证确权,在确保数据可信的前提下,促进教育的公平、透明、开放,为教育教学质量提升赋能,实现教育数据的安全共享、高等教育体系的智慧治理。 可信教育区块链治理系统的顶层治理架构由教育部、高校、企业、学生等多方角色共同参与建设、维护,支撑教育资源共享、教学质量评估、

【机器学习】高斯过程的基本概念和应用领域以及在python中的实例

引言 高斯过程(Gaussian Process,简称GP)是一种概率模型,用于描述一组随机变量的联合概率分布,其中任何一个有限维度的子集都具有高斯分布 文章目录 引言一、高斯过程1.1 基本定义1.1.1 随机过程1.1.2 高斯分布 1.2 高斯过程的特性1.2.1 联合高斯性1.2.2 均值函数1.2.3 协方差函数(或核函数) 1.3 核函数1.4 高斯过程回归(Gauss

AI行业应用(不定期更新)

ChatPDF 可以让你上传一个 PDF 文件,然后针对这个 PDF 进行小结和提问。你可以把各种各样你要研究的分析报告交给它,快速获取到想要知道的信息。https://www.chatpdf.com/