说到拖拽,Google提供了两种方式:OnDragListener
和 ViewDragHelper
,通过这两个工具我们可以非常方便的实现控件的拖拽效果。在这里不会过多的讲解两种工具的具体怎么使用,因为网上很多文章已经有讲解,还需要提出疑问:为什么拖拽工具Google要提供两种?只用一种不行吗?各自的使用场景是如何?这将是要具体分析的问题。
OnDragListener
作为一个接口,它被定义在 View
源码中:
/**
* Interface definition for a callback to be invoked when a drag is being dispatched
* to this view. The callback will be invoked before the hosting view's own
* onDrag(event) method. If the listener wants to fall back to the hosting view's
* onDrag(event) behavior, it should return 'false' from this callback.
*
*
* Developer Guides
* For a guide to implementing drag and drop features, read the
* Drag and Drop developer guide.
*
*/
public interface OnDragListener {
/**
* Called when a drag event is dispatched to a view. This allows listeners
* to get a chance to override base View behavior.
*
* @param v The View that received the drag event.
* @param event The {@link android.view.DragEvent} object for the drag event.
* @return {@code true} if the drag event was handled successfully, or {@code false}
* if the drag event was not handled. Note that {@code false} will trigger the View
* to call its {@link #onDragEvent(DragEvent) onDragEvent()} handler.
*/
boolean onDrag(View v, DragEvent event);
}
/**
* Register a drag event listener callback object for this View. The parameter is
* an implementation of {@link android.view.View.OnDragListener}. To send a drag event to a
* View, the system calls the
* {@link android.view.View.OnDragListener#onDrag(View,DragEvent)} method.
* @param l An implementation of {@link android.view.View.OnDragListener}.
*/
public void setOnDragListener(OnDragListener l) {
getListenerInfo().mOnDragListener = l;
}
该接口会回调两个参数:
View:被拖拽的View对象
DragEvent:View的拖拽状态,比较常用的有以下几种状态:
DragEvent.ACTION_DRAG_STARTED:开始拖拽,在调用 view.startDrag()
时回调
DragEvent.ACTION_DRAG_ENTERED:当拖拽触摸到了被拖拽的那个View的区域内就会回调
DragEvent.ACTION_DRAG_ENDED:已经松手结束拖拽
DragEvent.ACTION_DROP:拖拽结束松手了
OnDragListener
接口只是拖拽的回调监听,具体实现对View的拖拽,需要调用View的 startDrag()
:
/**
* @deprecated Use {@link #startDragAndDrop(ClipData, DragShadowBuilder, Object, int)
* startDragAndDrop()} for newer platform versions.
*/
@Deprecated
public final boolean startDrag(ClipData data, DragShadowBuilder shadowBuilder,
Object myLocalState, int flags) {
return startDragAndDrop(data, shadowBuilder, myLocalState, flags);
}
public final boolean startDragAndDrop(ClipData data, DragShadowBuilder shadowBuilder, Object myLocalState, int flags) {
}
startDrag()
是在API 11的时候提供,如果需要在低版本也实现 OnDragListener
的拖拽效果,可以使用 ViewCompat.startDragAndDrop()
。
startDrag()
需要传递四个参数:
ClipData:拖拽时用于传递的数据,会在 DragEvent.ACTION_DROP
拖拽结束松手时才能获取到 ClipData
的数据
DragShadowBuilder:在拖拽时生成View的半透明像素,可以观察跟随手指的拖拽状态
myLocalState:可以用它传递本地数据,监听 DragEvent.ACTION_DRAG_STARTED
、DragEvent.ACTION_DRAG_ENTERED
、DragEvent.ACTION_DRAG_ENDED
等拖拽状态时通过 DragEvent.getLocalState()
随时获取该本地数据。但需要注意的是,如果是跨进程的Activity之间通信,DragEvent.getLocalState()
会返回null
flags:控制拖拽时的操作,一般传递0即可
ClipData
和 myLocalState
是比较重要的两个参数,后续在场景分析时会详细讲解他们。
根据上面简单的说明,使用 OnDragListener
实现对View的拖拽如下:
view.startDrag(null, new DragShadowBuilder(v), v, 0);
view.setOnDragListener(new OnDragListener() {
@Override
public boolean onDrag(View v, DragEvent event) {
switch(event.getAction()) {
case DragEvent.ACTION_DRAG_STARTED:
break;
case DragEvent.ACTION_DRAG_ENTERED:
break;
case DragEvent.ACTION_DRAG_EXITED:
break;
case DragEvent.ACTION_DRAG_ENDED:
break;
case DragEvent.ACTION_DROP:
break;
}
}
});
拖拽起一个View,其他View的onDragEvent()也会接收到拖拽监听
先看一下 OnDragListener
实现的拖拽效果:
使用6个View排列成网格状,在长按的时候启动并跟随手指拖拽移动,拖拽到View的区域就开始重新排序。具体布局代码和布局如下:
<?xml version="1.0" encoding="utf-8"?>
<com.example.demo.DragListenerGridView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#EF5350" />
<View
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#9C27B0" />
<View
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#1E88E5" />
<View
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#00695C" />
<View
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#FDD835" />
<View
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#546E7A" />
</com.example.demo.DragListenerGridView>
public class DragListenerGridView extends ViewGroup {
private static final int COLUMNS = 2;
private static final int ROWS = 3;
private OnDragListener mOnDragListener = new TestDragListener();
private View mDraggedView;
private List<View> mOrderedChildren = new ArrayList<>();
public DragListenerGridView(Context context, AttributeSet attrs) {
super(context, attrs);
setChildrenDrawingOrderEnabled(true);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
// 初始化位置
mOrderedChildren.add(child);
// 长按启动拖拽
child.setOnLongClickListener(new OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
mDraggedView = v;
v.startDrag(null, new DragShadowBuilder(v), v, 0);
return false;
}
});
// 设置OnDragListener拖拽监听
child.setOnDragListener(mOnDragListener);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int specWidth = MeasureSpec.getSize(widthMeasureSpec);
int specHeight = MeasureSpec.getSize(heightMeasureSpec);
int childWidth = specWidth / COLUMNS;
int childHeight = specHeight / ROWS;
measureChildren(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));
setMeasuredDimension(specWidth, specHeight);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
int childLeft;
int childTop;
int childWidth = getWidth() / COLUMNS;
int childHeight = getHeight() / ROWS;
// 先把childView摆放在同一个位置,然后再一个个进行偏移摆放为Grid
// 这样处理主要是方便再拖拽时实现所有childView重新排列为Grid
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
childLeft = i % 2 * childWidth;
childTop = i / 2 * childHeight;
child.layout(0, 0, childWidth, childHeight);
child.setTranslationX(childLeft);
child.setTranslationY(childTop);
}
}
private class TestDragListener implements OnDragListener {
@Override
public boolean onDrag(View v, DragEvent event) {
switch (event.getAction()) {
case DragEvent.ACTION_DRAG_STARTED:
if (event.getLocalState() == v) {
// 启动拖拽时将拖拽的View隐藏
v.setVisibility(View.INVISIBLE);
}
break;
case DragEvent.ACTION_DRAG_ENTERED:
// 如果被拖拽的View进入了其他View的拖拽范围就触发重新排列
// 如果被拖拽的View是在自己的拖拽范围内,就不用重新排列
if (event.getLocalState() != v) {
sort(v);
}
break;
case DragEvent.ACTION_DRAG_EXITED:
break;
case DragEvent.ACTION_DRAG_ENDED:
if (event.getLocalState() == v) {
// 结束拖拽时将拖拽的View显示
v.setVisibility(View.VISIBLE);
}
break;
case DragEvent.ACTION_DROP:
break;
}
return true;
}
}
// 子View重新排序
private void sort(View targetView) {
int draggedIndex = -1;
int targetIndex = -1;
for (int i = 0; i< getChildCount(); i++) {
View child = mOrderedChildren.get(i);
if (targetView == child) {
targetIndex = i;
} else if (mDraggedView == child) {
draggedIndex = i;
}
}
if (targetIndex < draggedIndex) {
mOrderedChildren.remove(draggedIndex);
mOrderedChildren.add(targetIndex, mDraggedView);
} else if (targetIndex > draggedIndex) {
mOrderedChildren.remove(draggedIndex);
mOrderedChildren.add(targetIndex, mDraggedView);
}
int childLeft;
int childTop;
int childWidth = getWidth() / COLUMNS;
int childHeight = getHeight() / ROWS;
for (int i = 0; i < getChildCount(); i++) {
View child = mOrderedChildren.get(i);
childLeft = i % 2 * childWidth;
childTop = i / 2 * childHeight;
child.animate()
.translationX(childLeft)
.translationY(childTop)
.setDuration(150);
}
}
}
上面简单的示例有几个 OnDragListener
在使用上要留意的地方:
因为其他的子View也监听了 OnDragListener
,所以在回调 OnDrag
时,不仅是被拖拽的View接收到了拖拽回调,其他View同样也接收到,所以 DragEvent.ACTION_DRAG_STARTED
处理的不是该View被拖拽起来了,而是有个View被拖拽起来了
event.getLocalState() == v
和传递的View参数判断是否相同,在 v.startDrag()
方法传递了当前View对象为 myLocalState
ViewDragHelper
的使用可以分成三个步骤:
使用 ViewDragHelper.create()
创建ViewDragHelper对象
将事件拦截 onInterceptTouchEvent()
和 onTouchEvent()
交由 ViewDragHelper
接管
提供 ViewDragHelper.Callback
处理View的拖拽,ViewGroup重写 computeScroll
处理拖拽动画
首先通过 ViewDragHelper.create()
创建一个 ViewDragHelper
对象,需要传入 ViewGroup
和 ViewDragHelper.Callback
。
public static ViewDragHelper create(@NonNull ViewGroup forParent, @NonNull ViewDragHelper.Callback cb) {
return new ViewDragHelper(forParent.getContext(), forParent, cb);
}
public static ViewDragHelper create(@NonNull ViewGroup forParent, float sensitivity, @NonNull ViewDragHelper.Callback cb) {
ViewDragHelper helper = create(forParent, cb);
helper.mTouchSlop = (int)((float)helper.mTouchSlop * (1.0F / sensitivity));
return helper;
}
创建了 ViewDragHelper
后,触摸事件交由 ViewDragHelper
处理,具体实现是ViewGroup重写 onInterceptTouchEvent()
和 onTouchEvent()
将事件交由 ViewDragHelper
处理。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return viewDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
viewDragHelper.processTouchEvent(event);
return true;
}
最后要具体对View进行拖拽操作,还需要实现 ViewDragHelper.Callback
:
class DragCallback extends ViewDragHelper.Callback {
// 尝试抓住View,当手触摸到要拖拽的View时就会回调
// 返回true表示要触发拖拽,但返回true还不能够实现拖拽
// 还需要重写clampViewPositionHorizontal()或clampViewPositionVertical()
// 返回false表示不拖拽
@Override
public boolean tryCaptureView(@NonNull View view, int pointerId) {
return false;
}
// 限制View在拖拽时水平方向的偏移
// 如果只有只重写了clampViewPositionHorizontal,View的拖拽只能在水平方向移动
/**
* @param left 手指拖动View的水平距离,重写该方法返回该参数表示水平方向拖动不干预限制拖拽
*/
@Override
public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
return left;
}
// 限制View在拖拽时垂直方向的偏移
// 如果只有重写了clampViewPositionVertical,View的拖拽只能在垂直方向移动
/**
* @param top 手指拖动View的垂直距离,重写该方法返回该参数表示垂直方向拖动不干预限制拖拽
*/
@Override
public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
return top;
}
// 当View被拖拽起来的时候回调
@Override
public void onViewCaptured(@NonNull View capturedChild, int activePointerId) {
}
// 当View被移动的时候回调
@Override
public void onViewPositionChanged(@NonNull View changeView, int left, int top, int dx, int dy) {
}
// 当松手的时候会回调
@Override
public void onReleased(@NonNull View releasedChild, float xvel, float yvel) {
}
@Override
public void onViewDragStateChanged(int state) {
}
}
ViewDragHelper
和对应回调只会处理View的拖拽移动,具体被拖拽的View的移动处理是由我们控制,需要重写ViewGroup的 computeScroll()
:
@Override
public void computeScroll() {
if (viewDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
先看一下 ViewDragHelper
实现的拖拽效果:
和 OnDragListener
一样也是网格排列,不过这里为了演示方便就不实现重排序了。具体布局和代码如下:
<?xml version="1.0" encoding="utf-8"?>
<com.example.demo.DragHelperGridView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#EF5350"/>
<View
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#9C27B0"/>
<View
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#1E88E5"/>
<View
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#00695C"/>
<View
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#FDD835"/>
<View
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#546E7A"/>
</com.example.demo.DragHelperGridView>
public class DragHelperGridView extends ViewGroup {
private static final int COLUMNS = 2;
private static final int ROWS = 3;
private ViewDragHelper mViewDragHelper;
public DragHelperGridView(Context context, AttributeSet attrs) {
super(context, attrs);
mViewDragHelper = ViewDragHelper.create(this, new DragCallback());
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int specWidth = MeasureSpec.getSize(widthMeasureSpec);
int specHeight = MeasureSpec.getSize(heightMeasureSpec);
int childWidth = specWidth / COLUMNS;
int childHeight = specHeight / ROWS;
measureChildren(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));
setMeasuredDimension(specWidth, specHeight);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
int childLeft;
int childTop;
int childWidth = getWidth() / COLUMNS;
int childHeight = getHeight() / ROWS;
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
childLeft = i % 2 * childWidth;
childTop = i / 2 * childHeight;
child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mViewDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mViewDragHelper.processTouchEvent(event);
return true;
}
private class DragCallback extends ViewDragHelper.Callback {
private float captureLeft;
private float captureTop;
@Override
public boolean tryCaptureView(@NonNull View view, int pointerId) {
return true;
}
@Override
public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
return left;
}
@Override
public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
return top;
}
@Override
public void onViewCaptured(@NonNull View capturedChild, int activePointerId) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
capturedChild.setElevation(getElevation() + 1);
}
// 记录被拖拽起来时View的位置,松手时onViewRelease()返回到原始位置
captureLeft = capturedChild.getLeft();
captureTop = capturedChild.getTop();
}
@Override
public void onViewPositionChanged(@NonNull View changedView, int left, int top, int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
}
@Override
public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
// 将View移动回拖拽起来时的开始位置
// settleCapturedViewAt()可以认为只是一个计算器,不是实际执行移动的方法
mViewDragHelper.settleCapturedViewAt((int) captureLeft, (int) captureTop);
postInvalidateOnAnimation(); // 通知以动画的防止刷新返回,回调computeScroll
}
@Override
public void onViewDragStateChanged(int state) {
if (state == ViewDragHelper.STATE_IDLE) {
View capturedView = mViewDragHelper.getCapturedView();
if (capturedView != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
capturedView.setElevation(capturedView.getElevation() - 1);
}
}
}
}
}
@Override
public void computeScroll() {
if (mViewDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
}
上面讲解 OnDragListener
使用时我们发现,OnDragListener
在拖拽时并不是直接将View拖拽起来,而是生成了一个和被拖拽的View大小相同的像素进行拖拽,为什么要这样设计呢?这需要说到两个参数:ClipData
和 myLocalState
,这两个参数在调用 startDrag()
时需要我们传入:
/**
* @deprecated Use {@link #startDragAndDrop(ClipData, DragShadowBuilder, Object, int)
* startDragAndDrop()} for newer platform versions.
*/
@Deprecated
public final boolean startDrag(ClipData data, DragShadowBuilder shadowBuilder,
Object myLocalState, int flags) {
return startDragAndDrop(data, shadowBuilder, myLocalState, flags);
}
ClipData:拖拽时用于传递的数据,会在 DragEvent.ACTION_DROP
拖拽结束松手时才能获取到 ClipData
的数据
myLocalState:可以用它传递本地数据,监听 DragEvent.ACTION_DRAG_STARTED
、DragEvent.ACTION_DRAG_ENTERED
、DragEvent.ACTION_DRAG_ENDED
等拖拽状态时通过 DragEvent.getLocalState()
随时获取该本地数据
这两个参数最大的区别在于:ClipData
是可以跨进程的,而 myLocalState
不能。如果是跨进程的Activity之间通信,DragEvent.getLocalState()
会返回null。
Google将 OnDragListener
设计为拖拽时生成像素而不是直接拖拽View,假设目前有一种场景:在一个图片库app有一张图片,我们可能会希望直接将这张图片在分屏情况下拖拽到另外一个应用然后发送或上传这张图片。既然这张图片它是其他应用的,如果我们直接拖拽View,在这种跨进程的场景就会令用户感到疑惑。所以Google这种设计是合理的。
OnDragListener
更偏向于对内容数据的操作而不是对View的操作,它的拖拽是与界面无关的,不需要自定义View就可以实现拖拽,可以跨进程通信传输数据。
下面使用 OnDragListener
实现拖拽View通过 ClipData
传输数据的例子:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/iv_icon"
android:layout_width="0dp"
android:layout_height="120dp"
android:layout_weight="1"
android:contentDescription="icon"
android:src="@mipmap/ic_launcher" />
<ImageView
android:id="@+id/iv_logo"
android:layout_width="0dp"
android:layout_height="120dp"
android:layout_weight="1"
android:contentDescription="logo"
android:src="@mipmap/ic_launcher" />
</LinearLayout>
<LinearLayout
android:id="@+id/collect_layout"
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_gravity="bottom"
android:background="#78909C"
android:orientation="horizontal" />
</FrameLayout>
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.drag_to_collect);
ImageView ivIcon = findViewById(R.id.iv_icon);
ImageView ivLogo = findViewById(R.id.iv_logo);
LinearLayout collectLayout = findViewById(R.id.collect_layout);
ivIcon.setOnLongClickListener(mDragStarter);
ivLogo.setOnLongClickListener(mDragStarter);
collectLayout.setOnDragListener(mOnDragListener);
}
private View.OnLongClickListener mDragStarter = new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
ClipData imageData = ClipData.newPlainText("name", v.getContentDescription());
// DragShadowBuilder(v)就是拖拽时那个半透明的View
// OnDragListener拖拽起来的半透明View是显示在上层的,说明它是全局的,而且是跨界面跨进程的
return ViewCompat.startDragAndDrop(v, imageData, new View.DragShadowBuilder(v), v, 0);
}
};
private View.OnDragListener mOnDragListener = new CollectListener();
private class CollectListener implements View.OnDragListener {
@Override
public boolean onDrag(View v, DragEvent event) {
// event.getLocalState()和event.getClipData()都是可以装载数据的,那为什么要提供两种?
// 最主要的区别在于event.getClipData()是可以跨进程的,可以实现跨进程的拖拽获取数据
if (event.getAction() == DragEvent.ACTION_DROP) {
if (v instanceof LinearLayout) {
LinearLayout layout = (LinearLayout) v;
TextView textView = new TextView(MainActivity.this);
textView.setTextSize(16);
textView.setText(event.getClipData().getItemAt(0).getText());
layout.addView(textView);
}
}
return true;
}
}
}
ViewDragHelper
是一个工具类,使用它需要我们自定义ViewGroup,从 ViewDragHelper.create()
和后续需要 ViewDragHelper
接管触摸事件重写 onInterceptTouchEvent
和 onTouchEvent()
就能了解到:
public static ViewDragHelper create(@NonNull ViewGroup forParent, @NonNull ViewDragHelper.Callback cb) {
return new ViewDragHelper(forParent.getContext(), forParent, cb);
}
使用 ViewDragHelper
的目的在于我们想拖拽某个ViewGroup下的子View,对View的操作是限定在ViewGroup的,它与界面有关。
相比 OnDragListener
,ViewDragHelper
的使用场景会更加常见,比如Launcher桌面的上拉类似抽屉的View展示更多的应用,侧滑退出界面 SwipeBackLayout
也是使用的 ViewDragHelper
实现的。
下面使用 ViewDragHelper
简单实现一个上下拖拽View有弹性回弹的效果:
<?xml version="1.0" encoding="utf-8"?>
<com.example.demo.DragUpDownLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:id="@+id/view"
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="#388E3C"/>
</com.example.demo.DragUpDownLayout>
public class DragUpDownLayout extends FrameLayout {
private View mView;
private ViewDragHelper mViewDragHelper;
private ViewConfiguration mViewConfiguration;
public DragUpDownLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mViewDragHelper = ViewDragHelper.create(this, new DragCallback());
mViewConfiguration = ViewConfiguration.get(context);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mView = findViewById(R.id.view);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mViewDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mViewDragHelper.processTouchEvent(event);
return true;
}
private class DragCallback extends ViewDragHelper.Callback {
@Override
public boolean tryCaptureView(@NonNull View view, int i) {
return view == mView; // 触摸的是view则允许拖拽
}
@Override
public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
return top;
}
@Override
public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
// 实现回弹
if (Math.abs(yvel) > mViewConfiguration.getScaledMinimumFlingVelocity()) {
if (yvel > 0) {
mViewDragHelper.settleCapturedViewAt(0, getHeight() - releasedChild.getHeight());
} else {
mViewDragHelper.settleCapturedViewAt(0, 0);
}
} else {
if (releasedChild.getTop() < getHeight() - releasedChild.getBottom()) {
mViewDragHelper.settleCapturedViewAt(0, 0);
} else {
mViewDragHelper.settleCapturedViewAt(0, getHeight() - releasedChild.getHeight());
}
}
postInvalidateOnAnimation();
}
}
@Override
public void computeScroll() {
if (mViewDragHelper.continueSettling(true)) {
postInvalidateOnAnimation();
}
}
}
OnDragListener
在API 11时加入的,它的重点在于内容的移动而不是控件的移动,可以不需要自己自定义View,只要实现 setOnDragListener()
即可,系统会帮你生成一个可拖拽的像素,该图像像素和界面是无关的,而且可以附加拖拽时的数据,能够跨进程传数据
ViewDragHelper
是一个工具类,用户要拖动某个ViewGroup里面的某个子View时使用(tryCaptureView()
判断一个或多个子View),它需要自定义View,需要让 ViewDragHelper
接管ViewGroup的触摸事件,可以实时的拖拽移动手动修改子View的位置,使用 ViewDragHelper
是在界面的操作