一. 背景知识
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
<?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>