ViewDragHelper: ViewDragHelper的使用
一. 背景知识
2013年谷歌i/o大会上介绍了两个新的layout: SlidingPaneLayout和DrawerLayout,现在这俩个类被广泛的运用,其实研究他们的源码你会发现这两个类都运用了ViewDragHelper来处理拖动。ViewDragHelper是framework中不为人知却非常有用的一个工具。
ViewDragHelper解决了android中手势处理过于复杂的问题,在DrawerLayout出现之前,侧滑菜单都是由第三方开源代码实现的,其中著名的当属 MenuDrawer ,MenuDrawer重写onTouchEvent方法来实现侧滑效果,代码量很大,实现逻辑也需要很大的耐心才能看懂。如果每个开发人员都从这么原始的步奏开始做起,那对于安卓生态是相当不利的。所以说ViewDragHelper等的出现反映了安卓开发框架已经开始向成熟的方向迈进。
二. 解决的问题
ViewDragHelper is a utility class for writing custom ViewGroups.
It offers a number of useful operations and state tracking for allowing a user to drag and reposition views within their parent ViewGroup.
从官方注释可以看出:ViewDragHelper主要可以用来拖拽和设置ViewGroup中子View的位置。
三. 可以实现的效果
1. View的拖动效果
摘自:https://blog.csdn.net/lmj623565791/article/details/46858663
2. 仿微信语音通知的悬浮窗效果(悬浮窗可以拖动,并且在手指释放的时候,悬浮窗会自动停靠在屏幕边缘)
摘自:https://www.jianshu.com/p/a9e0a98e4d42
3. 抽屉效果
摘自:https://cloud.tencent.com/developer/article/1035828
四. 基本使用
1. ViewDragHelper的初始化
ViewDragHelper一般用在一个自定义ViewGroup的内部,比如下面自定义了一个继承于LinearLayout的DragLayout,DragLayout内部有一个子view mDragView作为成员变量:
public class DragLayout extends LinearLayout { private ViewDragHelper mDragHelper; private View mDragView; public DragLayout(Context context) { this(context, null); } public DragLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public DragLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } }
创建一个带有回调接口的 ViewDragHelper
public DragLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mDragHelper = ViewDragHelper.create(this, 1.0f, new DragHelperCallback()); }
其中1.0f是灵敏度系数,系数越大越敏感。第一个参数为this,表示要拖动子View所在的Parent View,必须为ViewGroup。
要让ViewDragHelper能够处理拖动,还需要将触摸事件传递给ViewDragHelper,这点和 Gesturedetector 是一样的:
@Override public boolean onInterceptTouchEvent(MotionEvent event) { return mDragHelper.shouldInterceptTouchEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { mDragHelper.processTouchEvent(event); return true; }
接下来,你就可以在刚才传入的回调中处理各种拖动行为了。
2. 拖动行为的处理
处理横向的拖动:
在DragHelperCallback中实现 clampViewPositionHorizontal 方法, 并且返回一个适当的数值就能实现横向拖动效果,clampViewPositionHorizontal的第二个参数是指当前拖动子view应该到达的x坐标。所以按照常理这个方法原封返回第二个参数就可以了,但为了让被拖动的view遇到边界之后就不在拖动,对返回的值做了更多的考虑。
@Override public int clampViewPositionHorizontal(View child, int left, int dx) { Log.d("DragLayout", "clampViewPositionHorizontal " + left + "," + dx); // 最小x坐标值不能小于leftBound final int leftBound = getPaddingLeft(); // 最大x坐标值不能大于rightBound final int rightBound = getWidth() - mDragView.getWidth() - getPaddingRight(); final int newLeft = Math.min(Math.max(left, leftBound), rightBound); return newLeft; }
同上,处理纵向的拖动:
在DragHelperCallback中实现clampViewPositionVertical方法,实现过程同 clampViewPositionHorizontal
@Override public int clampViewPositionVertical(@NonNull View child, int top, int dy) { Log.d("DragLayout", "top=" + top + "; dy=" + dy); // 最小 y 坐标值不能小于 topBound final int topBound = getPaddingTop(); // 最大 y 坐标值不能大于 bottomBound final int bottomBound = getHeight() - child.getHeight() - getPaddingBottom(); final int newTop = Math.min(Math.max(top, topBound), bottomBound); return newTop; }
clampViewPositionHorizontal 和 clampViewPositionVertical必须要重写,因为默认它返回的是0。事实上我们在这两个方法中所能做的事情很有限。 个人觉得这两个方法的作用就是给了我们重新定义目的坐标的机会。
完整code 参考:
activity_test_view.xml
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<?xml version="1.0" encoding="utf-8"?> <com.yongdaimi.android.androidapitest.view.DragLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center" tools:context=".ListViewActivity"> <TextView android:id="@+id/content_view" android:layout_width="100dip" android:layout_height="100dip" android:background="@android:color/holo_blue_light" android:text="内容区域" android:gravity="center" /> </com.yongdaimi.android.androidapitest.view.DragLayout>
DragLayout.java
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package com.yongdaimi.android.androidapitest.view; import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.widget.LinearLayout; import androidx.annotation.NonNull; import androidx.customview.widget.ViewDragHelper; public class DragLayout extends LinearLayout { private ViewDragHelper mDragHelper; private View mDragView; public DragLayout(Context context) { this(context, null); } public DragLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public DragLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mDragHelper = ViewDragHelper.create(this, 1.0f, new DragHelperCallback()); } private class DragHelperCallback extends ViewDragHelper.Callback { @Override public boolean tryCaptureView(@NonNull View child, int pointerId) { return true; } @Override public int clampViewPositionHorizontal(View child, int left, int dx) { Log.d("DragLayout", "clampViewPositionHorizontal " + left + "," + dx); // 最小x坐标值不能小于leftBound final int leftBound = getPaddingLeft(); // 最大x坐标值不能大于rightBound final int rightBound = getWidth() - mDragView.getWidth() - getPaddingRight(); final int newLeft = Math.min(Math.max(left, leftBound), rightBound); return newLeft; } @Override public int clampViewPositionVertical(@NonNull View child, int top, int dy) { Log.d("DragLayout", "top=" + top + "; dy=" + dy); // 最小 y 坐标值不能小于 topBound final int topBound = getPaddingTop(); // 最大 y 坐标值不能大于 bottomBound final int bottomBound = getHeight() - child.getHeight() - getPaddingBottom(); final int newTop = Math.min(Math.max(top, topBound), bottomBound); return newTop; } } @Override public boolean onInterceptTouchEvent(MotionEvent event) { return mDragHelper.shouldInterceptTouchEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { mDragHelper.processTouchEvent(event); return true; } @Override protected void onFinishInflate() { super.onFinishInflate(); mDragView = getChildAt(0); } }
五. ViewDragHelper.Callback的一些常用API的分析
我们注意到在创建ViewDragHelper时,需要传入一个Callback, 目前我们分别使用了这个Callback的 clampViewPositionHorizontal 和 clampViewPositionVertical 方法,用于实现对被拖动View的边界进行控制,实际上这个Callback大概有13个左右的方法,下面对一些常用的Callback 方法做下分析:
1. tryCaptureView(@NonNull View child, int pointerId)
这是一个抽象方法,也是Callback类中唯一 一个没有实现的抽象方法,在实例化ViewDragHelper.Callback时会强制要求我们去实现。
这个方法的作用是用于控制哪些View可以被捕获,默认值返回true, 则代表可以捕获ViewGroup中的所有View, 也就是所有的View均可以被拖动,如果只想让指定的View被拖动,则可以将指定View的返回值设置成true。例:
修改 DragLayout.java, 重写Callback下的 tryCaptureView 方法:
public class DragLayout extends LinearLayout { private ViewDragHelper mDragHelper; private View mDragView; private View mNotDragView; public DragLayout(Context context) { this(context, null); } public DragLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public DragLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mDragHelper = ViewDragHelper.create(this, 1.0f, new DragHelperCallback()); } private class DragHelperCallback extends ViewDragHelper.Callback { @Override public boolean tryCaptureView(@NonNull View child, int pointerId) { // 设置只有mDragView 可以被拖动 return child == mDragView; } @Override public int clampViewPositionHorizontal(View child, int left, int dx) { Log.d("DragLayout", "clampViewPositionHorizontal " + left + "," + dx); // 最小x坐标值不能小于leftBound final int leftBound = getPaddingLeft(); // 最大x坐标值不能大于rightBound final int rightBound = getWidth() - mDragView.getWidth() - getPaddingRight(); final int newLeft = Math.min(Math.max(left, leftBound), rightBound); return newLeft; } @Override public int clampViewPositionVertical(@NonNull View child, int top, int dy) { Log.d("DragLayout", "top=" + top + "; dy=" + dy); // 最小 y 坐标值不能小于 topBound final int topBound = getPaddingTop(); // 最大 y 坐标值不能大于 bottomBound final int bottomBound = getHeight() - child.getHeight() - getPaddingBottom(); final int newTop = Math.min(Math.max(top, topBound), bottomBound); return newTop; } } @Override public boolean onInterceptTouchEvent(MotionEvent event) { return mDragHelper.shouldInterceptTouchEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { mDragHelper.processTouchEvent(event); return true; } @Override protected void onFinishInflate() { super.onFinishInflate(); mDragView = getChildAt(0); mNotDragView = getChildAt(1); } }
同时在对应的XML文件中新增一个View:
<?xml version="1.0" encoding="utf-8"?> <com.yongdaimi.android.androidapitest.view.DragLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center" tools:context=".ListViewActivity"> <TextView android:layout_width="100dip" android:layout_height="100dip" android:background="@android:color/holo_blue_light" android:text="内容区域" android:gravity="center" /> <TextView android:layout_width="100dip" android:layout_height="100dip" android:background="@android:color/holo_green_light" android:text="内容区域1" android:gravity="center" /> </com.yongdaimi.android.androidapitest.view.DragLayout>
再次运行,发现只有第一个蓝色的方块可以滑动了。
2. onViewReleased(@NonNull View releasedChild, float xvel, float yvel)
这个方法会在View停止拖拽的时候调用,也就是手指释放的时候调用,利用这个方法可以实现回弹效果:修改DragLayout.java,重写onViewReleased方法:
package com.yongdaimi.android.androidapitest.view; import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.widget.LinearLayout; import androidx.annotation.NonNull; import androidx.customview.widget.ViewDragHelper; public class DragLayout extends LinearLayout { private ViewDragHelper mDragHelper; private View mDragView; private View mNotDragView; private View mBounceView; private int mCurrentTop; private int mCurrentLeft; public DragLayout(Context context) { this(context, null); } public DragLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public DragLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mDragHelper = ViewDragHelper.create(this, 1.0f, new DragHelperCallback()); } private class DragHelperCallback extends ViewDragHelper.Callback { @Override public boolean tryCaptureView(@NonNull View child, int pointerId) { // 设置只有mDragView 可以被拖动 return child != mNotDragView; } @Override public int clampViewPositionHorizontal(View child, int left, int dx) { Log.d("DragLayout", "clampViewPositionHorizontal " + left + "," + dx); // 最小x坐标值不能小于leftBound final int leftBound = getPaddingLeft(); // 最大x坐标值不能大于rightBound final int rightBound = getWidth() - mDragView.getWidth() - getPaddingRight(); final int newLeft = Math.min(Math.max(left, leftBound), rightBound); mCurrentLeft = newLeft; return newLeft; } @Override public int clampViewPositionVertical(@NonNull View child, int top, int dy) { Log.d("DragLayout", "top=" + top + "; dy=" + dy); // 最小 y 坐标值不能小于 topBound final int topBound = getPaddingTop(); // 最大 y 坐标值不能大于 bottomBound final int bottomBound = getHeight() - child.getHeight() - getPaddingBottom(); final int newTop = Math.min(Math.max(top, topBound), bottomBound); mCurrentTop = newTop; return newTop; } @Override public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) { super.onViewReleased(releasedChild, xvel, yvel); if (releasedChild == mBounceView) { int childWidth = releasedChild.getWidth(); int parentWidth = getWidth(); int leftBound = getPaddingLeft(); // 左边界 int rightBound = parentWidth - releasedChild.getWidth() - getPaddingRight(); // 右边界 // 如果方块的中点超过 ViewGroup 的中点时,滑动到左边缘,否则滑动到右边缘 if ((childWidth / 2 + mCurrentLeft) < parentWidth / 2) { mDragHelper.settleCapturedViewAt(leftBound, mCurrentTop); } else { mDragHelper.settleCapturedViewAt(rightBound, mCurrentTop); } invalidate(); } } } @Override public boolean onInterceptTouchEvent(MotionEvent event) { return mDragHelper.shouldInterceptTouchEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { mDragHelper.processTouchEvent(event); return true; } @Override protected void onFinishInflate() { super.onFinishInflate(); mDragView = getChildAt(0); mNotDragView = getChildAt(1); mBounceView = getChildAt(2); } @Override public void computeScroll() { super.computeScroll(); if (mDragHelper != null && mDragHelper.continueSettling(true)) { invalidate(); } } }
修改XML
<?xml version="1.0" encoding="utf-8"?> <com.yongdaimi.android.androidapitest.view.DragLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center" tools:context=".ListViewActivity"> <TextView android:layout_width="100dip" android:layout_height="100dip" android:background="@android:color/holo_blue_light" android:text="内容区域" android:gravity="center" /> <TextView android:layout_width="100dip" android:layout_height="100dip" android:background="@android:color/holo_green_light" android:text="内容区域1" android:gravity="center" /> <TextView android:layout_width="100dip" android:layout_height="100dip" android:background="@android:color/holo_orange_light" android:text="内容区域2" android:gravity="center" /> </com.yongdaimi.android.androidapitest.view.DragLayout>
运行:发现当橙色滑块在超过中轴线的位置时会自动停靠在边缘。
这里调用了 settleCapturedViewAt 方法来重新设置被捕获的View的位置,同时调用了invalidate()来要求界面进行重绘,但仅靠这个还不足,注意这个方法的注释:
If this method returns true, the caller should invoke {@link #continueSettling(boolean)} on each subsequent frame to continue the motion until it returns false. If this method returns false there is no further work to do to complete the movement.
所以还需要在 computeScroll 中调用 continueSettling 方法,continueSettling 会每次移动一定的偏移量,直到返回false。
还有一些常用的方法,这里就不具体介绍了,将要用到的时候再说。
参考链接
1. 鸿洋:Android ViewDragHelper完全解析 自定义ViewGroup神器
2. ViewDragHelper(一)— 介绍及使用(入门篇)