前言
随着入Android这个坑的时间越来越长,愈加觉得深入掌握原理以及技术输出的重要性,会使用轮子和造一个好轮子还是有天壤之别的。授人以鱼不如授人以渔,将一些经验分享出来,希望能够让更多的人更加深入地理解它,并帮助到有需要的朋友。本系列分为三篇,会由浅至深地对DrageHelper 进行详细讲解。
目录
ViewDragHelper 的介绍以及初步使用请阅读这篇:
ViewDragHelper (一)- 介绍及简单用例(入门篇)
ViewDragHelper 的源码以及Callback的详情介绍请阅读这篇:
ViewDragHelper (二)- 源码及原理解读(进阶篇)
利用DrageHelper 打造仿陌陌APP视频播放页的demo请阅读这篇:
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.
DrageHelper 它是Google官方推出的手势滑动辅助类,极大程度地简化了我们对控件的手势滑动跟踪及处理。让我们能够更加便捷地开发自定义ViewGroup控件,实现拖拽以及弹性滚动等功能。事实上,官方的SlidingPaneLayout和DrawerLayout都是利用ViewDragHelper实现的。掌握它,可以一定程度地减轻我们开发工作难度以及投入精力。
使用入门示例
接下来,我们主要通过一个简单的拖拽以及回弹的demo(类似于QQ空间视频播放页),来讲解如何利用DrageHelper 打造一个 ViewGroup 控件。
QQ空间视频播放页效果图:
QZone.gif大致步骤如下:
第一步:
创建一个DraggableView类继承自ViewGroup(或者也可用 FrameLayout , RelativeLayout, LinearLayout等)。
package com.test.demo;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.RelativeLayout;
/**
* Created by 小嵩 on 2017/9/10.
*/
public class MyDraggableView extends RelativeLayout{
private ViewDragHelper viewDragHelper;
public MyDraggableView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initView();
}
private void initView() {
viewDragHelper = ViewDragHelper.create(this, 1.0f, new DraggableViewCallback(this));
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return viewDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
viewDragHelper.processTouchEvent(event);
return true;
}
}
在onInterceptTouchEvent方法中,通过viewDragHelper.shouldInterceptTouchEvent(event)来决定我们是否应该拦截当前的事件,如果返回的是True,则会触发onTouchEvent。
在onTouchEvent方法中,通过viewDragHelper.processTouchEvent(event)将事件分发给viewDragHelper。
对Android的事件分发机制若还不太理解的话,可自行查资料补一下相关知识。
第二步:
在init方法中用ViewDragHelper的静态方法实例化ViewDragHelper对象
viewDragHelper = ViewDragHelper.create(this, 1.0f, new VerticalDraggableViewCallback(this));
其中第一个参数指的当前的ViewGroup对象,第二个sensitivity参数指的是对滑动检测的灵敏度,越大越敏感,所需触发滑动的距离越小,默认传1.0f 即可。它的源码如下:
/**
* Factory method to create a new ViewDragHelper.
*
* @param forParent Parent view to monitor
* @param sensitivity Multiplier for how sensitive the helper should be about detecting
* the start of a drag. Larger values are more sensitive. 1.0f is normal.
* @param cb Callback to provide information and receive events
* @return a new ViewDragHelper instance
*/
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;
}
由: helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity)); 可以显而易见地看出,其实就是mTouchSlop 除以我们传入的sensitivity然后重新赋值。而这个mTouchSlop 是怎么来的呢? 接着看源码,发现是这一段代码进行赋值的:
final ViewConfiguration vc = ViewConfiguration.get(context);
mTouchSlop = vc.getScaledTouchSlop();
由此可见,mTouchSlop 它是获取的系统判定是否触发移动事件的阈值。即:单次移动大于这个值,才会判定是MOVE操作。
第三个参数为静态回调对象CallBack,我们接下来实现相关CallBack方法来操作拖拽的View。
第三步:
实现ViewDragHelper.Callback的相关方法。
/**
* Created by 小嵩 on 2017/8/30.
*/
public class DraggableViewCallback extends ViewDragHelper.Callback {
@Override
public boolean tryCaptureView(View child, int pointerId) {
return true;
}
}
其中,ViewDragHelper.Callback 是一个内部静态抽象类, tryCaptureView是必须实现的方法,其余是可选重写的方法,一般来说,我们重写:
clampViewPositionHorizontalView
clampViewPositionVertical
onViewReleased
onViewPositionChanged
这四个方法即可。更多方法的详情及含义,可阅读DrageHelper — 源码深入解析(第二篇)。
tryCaptureView,可用于自由判定哪个子控件可被拖拽,返回true代表可拖拽,false则禁止。
第四步:
分别在 clampViewPositionVertical 和clampViewPositionHorizontal 方法中对它的可滑动边界进行控制。left , top 分别为即将移动到的位置,比如我希望只在的水平方向移动,则进行如下处理:
/**
* 子控件水平方向位置改变时触发
*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
//屏蔽掉水平方向
return 0;
}
同时,若我们只希望子控件向下平移,则做以下处理:
/**
* 子控件竖直方向位置改变时触发
*/
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
//不能滑出顶部
return Math.max(top, 0);
}
第五步:
在onViewReleased 方法中获取移动距离,判断拖拽距离是否超过阈值。若超过阈值,则执行关闭动画,否则处理回弹,Callback完整代码如下:
package com.test.demo;
import android.support.v4.widget.ViewDragHelper;
import android.util.Log;
import android.view.View;
import android.view.ViewConfiguration;
/**
* ViewDragHelper.Callback 拖拽事件监听回调
*
* @author 小嵩
*/
class DraggableViewCallback extends ViewDragHelper.Callback {
private static final String TAG = "DraggableViewCallback";
private static float Y_MIN_VELOCITY = 300;//竖直方向关闭最小值 px
private MyDraggableView mDraggableView;
public DraggableViewCallback(MyDraggableView draggableView) {
this.mDraggableView = draggableView;
Y_MIN_VELOCITY = mDraggableView.getHeight() / 3;
}
/**
* 子控件位置改变时触发(包括X和Y轴方向)
*
* @param left position.
* @param top position.
* @param dx change in X position from the last call.
* @param dy change in Y position from the last call.
*/
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
mDraggableView.onViewPositionChanged(changedView, left, top, dx, dy);
}
/**
* 子控件竖直方向位置改变时触发
*/
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
//不能滑出顶部
return Math.max(top, 0);
}
/**
* 子控件水平方向位置改变时触发
*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
//屏蔽掉水平方向
return 0;
}
/**
* 手指松开时触发
*
* @param releasedChild the captured child view now being released.
* @param xVel X velocity of the pointer as it left the screen in pixels per second.
* @param yVel Y velocity of the pointer as it left the screen in pixels per second.
*/
@Override
public void onViewReleased(View releasedChild, float xVel, float yVel) {
super.onViewReleased(releasedChild, xVel, yVel);
Log.d(TAG, "onViewReleased");
int top = releasedChild.getTop(); //获取子控件Y值
int left = releasedChild.getLeft(); //获取子控件X值
if (Math.abs(left) <= Math.abs(top)) {//若为竖直滑动
triggerOnReleaseActionsWhileVerticalDrag(top);
}
}
@Override
public boolean tryCaptureView(View view, int pointerId) {
return true;
}
/**
* 计算竖直方向的滑动
*/
private void triggerOnReleaseActionsWhileVerticalDrag(float yVel) {
if (yVel > 0 && yVel >= Y_MIN_VELOCITY) {
mDraggableView.closedToBottom();
Log.d(TAG, "ReleaseVerticalDrag" + ", closeToBottom");
} else {
mDraggableView.onReset();
Log.d(TAG, "ReleaseVerticalDrag" + ", onReset");
}
}
}
第六步:
在自定义控件MyDraggableView中处理监听回调事件。手指松开时,会有两种情况:
1.当拖拽滑动距离未达到我们设定的值,则重置到原来位置:
public void onReset() {
Log.d(TAG, "onReset");
viewDragHelper.settleCapturedViewAt(0, 0);
ViewCompat.postInvalidateOnAnimation(this);
}
2.拖拽滑动距离超过设定的值,滑向底部关闭:
public void closedToBottom() {
Log.d(TAG, "closedToBottom");
if (viewDragHelper.smoothSlideViewTo(this, 0, getHeight())) {
ViewCompat.postInvalidateOnAnimation(this);
notifyClosedToBottomListener();
}
}
其中,我们重写了computeScroll 方法,以便在手指松开时,触发系统自动滑动。代码如下:
@Override
public void computeScroll() {
if (viewDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
computeScroll的具体原理这里就不阐述了,不懂的话可以自行Google/百度 查找 View 的computeScroll 实现原理以及源码。
到这儿我们就已经实现了拖拽下拉关闭的功能了,效果演示如下:
下拉拖拽关闭.gif类似地,如果我们需要实现向左或者向右拖拽回弹或者关闭的功能,只需要把ViewDragHelper.Callback里面clampViewPositionHorizontal以及clampViewPositionVertical方法稍加修改,然后在onViewReleased回调一下,执行viewDragHelper.smoothSlideViewTo()方法让View平顺移动到指定位置即可。具体实际情况可自行实践操作一波。
稍微修改一下代码,改成左右拖拽,代码如下:
/**
* 子控件竖直方向位置改变时触发
*/
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
// return Math.max(top, 0);//不能滑出顶部
return 0;
}
/**
* 子控件水平方向位置改变时触发
*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
// return 0;
return left;
}
/**
* 手指松开时触发
*/
@Override
public void onViewReleased(View releasedChild, float xVel, float yVel) {
super.onViewReleased(releasedChild, xVel, yVel);
Log.d(TAG, "onViewReleased");
int top = releasedChild.getTop(); //获取子控件Y值
int left = releasedChild.getLeft(); //获取子控件X值
if (Math.abs(left) <= Math.abs(top)) {//若为竖直滑动
triggerOnReleaseActionsWhileVerticalDrag(top);
} else {
triggerOnReleaseActionsWhileHorizontalDrag(left);
}
}
/**
* 计算水平方向
*/
private void triggerOnReleaseActionsWhileHorizontalDrag(int xVel) {
if (xVel > 0 && xVel >= X_MIN_VELOCITY) {
mDraggableView.closedToRight();
Log.d(TAG, "ReleaseVerticalDrag" + ", closedToRight");
} else if (xVel < 0 && Math.abs(xVel) >= X_MIN_VELOCITY) {
mDraggableView.closedToLeft();
Log.d(TAG, "ReleaseVerticalDrag" + ", closedToLeft");
} else {
mDraggableView.onReset();
Log.d(TAG, "ReleaseVerticalDrag" + ", onReset");
}
}
效果如下:
水平拖拽关闭.gif结语:
读完这篇文章之后,若觉得有哪里写的不够详细或是有更多的建议,欢迎指出~ 也非常感谢各位的支持和收藏点赞。
下一篇将围绕源码进行解析它的运行原理以及所提供的方法,文章链接:ViewDragHelper (二)- 源码及原理解读(进阶篇)
这篇文章将会详细讲解ViewDragHelper它提供的方法所代表的含义,以及实现原理等。相信读完理解这篇文章的内容之后,对ViewDragHelper的基本操作会有一个更全面的理解。
(By the way,最近工作有点忙,一篇文章躺在草稿箱N久,零零散散抽时间总算出炉了。第二篇和第三篇后续会抽空抓紧赶时间写出来)
网友评论