Android自定义ViewGroup神器-ViewDragHelper

投稿作者:zhuhf 原文链接:http://www.jianshu.com/p/111a7bc76a0e 特别声明:本文为zhuhf原创并授权发布,未经原作者允许请勿转载,转载请联系原作者。

一、概述

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.

这是官方的解释:在自定义ViewGroup时,ViewDragHelper可以用来拖拽和设置子View的位置(在ViewGroup范围内)。另外,还提供了一系列的方法和状态跟踪。

可见,在自定义ViewGroup时,ViewDragHelper一般用来处理子View的位置移动。

二、入门示例

效果很简单,屏幕中间有两个TextView,位置随着我们的手指不断移动。

传统方式实现:一般需要重写onInterceptTouchEvent和onTouchEvent这两个方法,写好这两个方法不是一件容易的事情,需要自己去处理:事件冲突、加速检测等。

ViewDragHelper简化了很多工作,让我们更加关注“业务”的需求,实现步骤如下:

  • 创建ViewDragHelper实例
  • 处理ViewGroup的触摸事件
  • ViewDragHelper.Callback的编写

(一) 自定义ViewGroup

VDHLinearLayout的代码还是非常简单的,主要是分为以下三个步骤:

  • 创建ViewDragHelper实例 dragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {}); 创建需要三个参数,第一个为当前的ViewGroup,第二个为sensitivity,主要用于设置touchSlop: helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity)); 传入越大,touchSlop就越小。第三个参数就是ViewDragHelper.Callback,触摸过程中会回调相关方法。
  • 实现ViewDragHelper.Callback相关方法
  • tryCaptureView:如果返回true表示捕获相关View,你可以根据第一个参数child决定捕获哪个View。
  • clampViewPositionVertical:计算child垂直方向的位置,top表示y轴坐标(相对于ViewGroup),默认返回0(如果不复写该方法)。这里,你可以控制垂直方向可移动的范围。
  • clampViewPositionHorizontal:与clampViewPositionVertical类似,只不过是控制水平方向的位置。 比如效果图中,“拖拽2”明显超过屏幕范围了,你可以这样控制:
  • @Override public int clampViewPositionHorizontal(View child, int left, int dx) { if (left > getWidth() - child.getMeasuredWidth()) // 右侧边界 { left = getWidth() - child.getMeasuredWidth(); } else if (left < 0) // 左侧边界 { left = 0; } return left; }
  • 处理ViewGroup触摸事件

onInterceptTouchEvent直接交给dragHelper.shouldInterceptTouchEvent去处理,onTouchEvent通过dragHelper.processTouchEvent来处理。 如果你希望拖拽的子View是不可点击的,可以不重写onInterceptTouchEvent方法,后面我们会介绍为什么。

(二) 布局文件

布局很简单,自定义的ViewGroup包含两个TextView。

三、更多用法

ViewDragHelper不仅仅能够让子View跟随我们的手指移动,还能实现以下功能:

  • 边界触摸检测
  • Drag释放回调
  • 移动到某个指定位置

我么改造下上面的例子,效果图如下:

第一个View,可以随意被拖动位置 第二个View,只能从ViewGroup左侧拖动 第三个View,拖动释放之后会回到原始位置

修改后的ViewGroup代码如下:

  1. tryCaptureView方法,我们只捕获第一个和第三个View,分别是dragView和autoBackView。
  2. 使用dragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT)设置ViewGroup左边缘可以被拖拽,同时在ViewDragHelper.Callback的onEdgeDragStarted方法中,使用dragHelper.captureChildView主动去捕获第二个View:edgeDragView。 虽然在tryCaptureView方法中我们并未捕获edgeDragView,但dragHelper.captureChildView可以绕过该方法,详见官方解释: Capture a specific child view for dragging within the parent. The callback will be notified but {@link Callback#tryCaptureView(android.view.View, int)} will not be asked permission to capture this view.
  3. onViewReleased方法会在被捕获的子View释放之后调用,我们判断释放的View:releasedChild是autoBackView,使用dragHelper.settleCapturedViewAt方法设置autoBackView的位置为它的初始位置。 注意,此方法内部是通过Scroller实现的,所以我们需要使用invalidate来刷新,同时需要重写computeScroll方法:
    @Overridepublic void computeScroll() {
       if (dragHelper.continueSettling(true))
       {
          invalidate();
       }
    }

dragHelper.continueSettling方法是用来判断当前被捕获的子View是否还需要继续移动,类似Scroller的computeScrollOffset方法一样,我们需要在返回true的时候使用invalidate刷新。

至此,我么已经介绍了ViewDragHelper以及ViewDragHelper.Callback的多数用法。

还记得前面我们留下的一个问题吗?

“如果你希望拖拽的子View是不可点击的,可以不重写onInterceptTouchEvent方法,后面我们会介绍为什么。”

我们尝试将TextView设置成clickable=true,你会发现原本可以被拖拽的View都不动了。我们思考下,这是为什么呢?

原因在于:

由于子View是可被点击的,那么会触发ViewGroup的onInterceptTouchEvent方法。默认情况下,事件会被子View消耗掉,这显然是有问题的,因为这样ViewGroup的onTouch方法就不会被调用,而onTouch方法中正是我们的关键方法:dragHelper.processTouchEvent。

既然我们找到原因了,有人说:你不能在onInterceptTouchEvent直接返回true吗?为啥还要用dragHelper.shouldInterceptTouchEvent(ev)的返回值啊???

确实,如果你直接返回true,会发现一切都能正常工作了。

这里我们需要解释下:

打个比方,如果你的ViewGroup中有另外一个Button(或者任何可点击的View),但是它不在ViewDragHelper的处理范围内,你可能需要监听它的onClick事件,如果直接返回true,你会发现onClick事件不会被触发了。

纳尼,为啥呢?因为ViewGroup拦截了它的事件了啊。。。好吧,我们还是老实这样写吧:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    return dragHelper.shouldInterceptTouchEvent(ev);
}

你迫不及待的运行修改之后的代码。咦?为啥还是不能拖拽。。。 此时,遇到这种情况,我一般是查看下dragHelper.shouldInterceptTouchEvent的源码(此处省略了部分不相关的代码):

shouldInterceptTouchEvent返回true的条件是mDragState == STATE_DRAGGING,然而mDragState是在tryCaptureViewForDrag方法中被设置为STATE_DRAGGING的。

所以,如果horizontalDragRange == 0 && verticalDragRange == 0这个条件一直为true的话,tryCaptureViewForDrag方法就得不到调用了。

而horizontalDragRange和verticalDragRange分别是Callback的getViewHorizontalDragRange和getViewVerticalDragRange方法返回的值,这两个方法默认情况下都返回0。

  • getViewHorizontalDragRange,返回子View水平方向可以被拖拽的范围
  • getViewVerticalDragRange,返回子View垂直方向可以被拖拽的范围

我们尝试重写这两个方法:

@Override
public int getViewVerticalDragRange(View child) {
   return getMeasuredHeight() - child.getMeasuredHeight();
}

@Override
public int getViewHorizontalDragRange(View child) {
   return getMeasuredWidth() - child.getMeasuredWidth();
}

再次运行下,你会发现TextView设置clickable=true之后也可以被拖拽了。

至此,ViewDragHelper的基本使用方式我们已经介绍完了。详细的代码可以查看文章最后的源码,另外,源码中还实现了一个比较常用的效果:

本文源码:https://github.com/hiphonezhu/Android-Demos/tree/master/ViewDragHelperDemo

原文发布于微信公众号 - 非著名程序员(non-famous-coder)

原文发表时间:2016-10-27

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏jianhuicode

如何使用融云地图,文件等插件--融云 Android SDK 2.8.0+ Extension 开发文档

转载自融云 Android SDK 2.8.0+ Extension 开发文档 融云 SDK 2.8.0 后对 会话界面输入区域、+号扩展区域、语音消息、Emo...

347100
来自专栏向治洪

Android Remote Views

听名字就可以看出,remote views是一种远程view,感觉有点像远程service,其实remote views是view的一个结构,他可以在其他的进程...

20470
来自专栏Java成神之路

GEF入门实例_总结_05_显示一个空白编辑器

在第三节( GEF入门实例_总结_03_显示菜单和工具栏  ),我们创建了菜单和工具栏。

10330
来自专栏developerHaoz 的安卓之旅

Android Volley 源码解析(三),图片加载的实现

在上一篇文章中,我们一起深入探究了 Volley 的缓存机制,通过源码分析对缓存的工作原理进行了了解,这篇文章将带大家一起探究「Volley 图片加载的实现」,...

10320
来自专栏指尖下的Android

菜鸡的MVP架构漫谈

相信大家在网上看过关于MVP架构的博客数不胜数,至于MVP到底是什么,也不需要我再从百度百科复制一遍了,通俗的说MVP就是解决Model和View的耦合,没有使...

10020
来自专栏漏斗社区

天空飘来五字:Android逆向smali

本期,我们将继续Android逆向动态分析之smali篇。内容包括smali语言介绍与动态调试。

13320
来自专栏向治洪

HTML中的javascript交互

在Android开发中,越来越多的商业项目使用了Android原生控件与WebView进行混合开发,当然不仅仅就是显示一个WebView那么简单,有时候还需要...

23050
来自专栏Samego开发资源

简单快捷的退出APP应用

19070
来自专栏郭霖

Android ListView异步加载图片乱序问题,原因分析及解决方案

在Android所有系统自带的控件当中,ListView这个控件算是用法比较复杂的了,关键是用法复杂也就算了,它还经常会出现一些稀奇古怪的问题,让人非常头疼。比...

421100
来自专栏向治洪

Universal Image Loader for Android 使用实例

<span style="white-space:pre">      </span>// 1.获取ImageLoader实例         ImageLo...

244100

扫码关注云+社区

领取腾讯云代金券