一起来看看菜鸟裹裹的悬浮按钮效果是怎么样的:
看到那个免费送电影票了么,那个就是悬浮的按钮,这是一个View,可以是Button或者其他,都可以
然后我们需要用到ViewDragHelper,就要先去了解这个Helper的简单应用
首先我们重写一个ViewGroup继承一个布局就比较简单,用起布局也方便
-
第一个,初始化ViewDragHelper,这个Helper只要使用creat()方法就可以创建了
mDragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragCallback());
创建的参数都很好理解,第一个是当前的ViewGroup,第二个是灵敏度,具体效果可以多设置值去玩玩,挺好玩的,第三个就是Helper的回调 -
第二个就是要弄清楚ViewDragHelper的回调了,我这里用到了几个回调,一一解释一下,
除了图片这些,还有一个回调,名称是tryCaptureView(View view, int pointerId)
好了,现在一个一个说明:
tryCaptureView(View view, int pointerId) 这个回调是标明哪个View可以被拖动,通过view去或者点Id去判断,返回true则标明可以拖动
onViewDragStateChange(int state) 这个回调是拖动的状态改变回调,里面有3种拖动状态
1.STATE_IDLE:视图是当前没有被拖或动画舞/吸附的结果
2.STATE_DRAGGING:当前视图正在被拖动
3.STATE_SETTLING:fling完毕后被放置到一个位置
onViewPositionChanged(View changedView, int left, int top, int dx, int dy) 这个回调是拖动后位置改变的回调
onViewCaptured(View capturedChild, int activePointerId) 这个回调是当捕获子视图以拖动或结算时调用,提供当前拖动捕获视图的指针
onViewReleased(View releasedChild, float xvel, float yvel) 这个回调是当子视图不再被主动拖动时调用,也可以认为效果与STATE_IDLE差不多
onEdgeTouched(int edgeFlags, int pointerId) 这个回调是父视图的边界被触摸的回调
onEdgeLock(int edgeFlags) 这个回调是返回父视图的边界是否被锁定,edgeFlags 就当前边界的状态,true为锁定,反之则解锁
onEdgeDragStarted(int edgeFlags, int pointerId) 这个回调是父视图边界开始拖动触发的回调,并且当前没有捕获到子视图
getOrderedChildIndex(int index) 这个回调是返回子视图当前的索引
getViewHorizontalDragRange(View child) 这个回调是返回一个可拖动的子视图的像素大小在水平范围的运动。此方法应该返回0则无法水平移动
getViewVerticalDragRange(View child) 这个回调是竖直方向,和意思是和上面一样
clampViewPositionHorizontal(View child, int left, int dx) 这个回调是限制水平拖动的子视图的运动。 默认实现不允许水平运动;扩展类必须覆盖此方法并提供水平方向限制的边界
clampViewPositionHorizontal(View child, int left, int dx) 这个回调和上面一样,不过是竖直方向的
好了,回调方法就这么多,具体怎么用还需要好好去了解一下,我这里只用到了clampViewPositionHorizontal、clampViewPositionHorizontal 、onViewReleased 还有tryCaptureView ,好了,下面开始贴代码咯:
/**
* Created by Thong on 2017/6/30.
*/
public class DragViewLayout extends RelativeLayout {
private ViewDragHelper mDragHelper;
private View dragview;
public DragViewLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
dragview = getChildAt(0);//第一个子View为悬浮按钮
}
private void init() {
mDragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragCallback());
}
}
一开始声明对象,这个不需要解释了吧,然后就是构造函数,重点是onFinishInflate()和init(),在onFinishInflate中获取悬浮的按钮,然后在init中初始化ViewDragHelper
接下来我们开始重写ViewDragHelper的CallBack:
private class ViewDragCallback extends ViewDragHelper.Callback {
/**
* 尝试捕获子view,一定要返回true
* @param view 尝试捕获的view
* @param pointerId 指示器id?
* 这里可以决定哪个子view可以拖动
*/
@Override
public boolean tryCaptureView(View view, int pointerId) {
return view == dragview;//返回如果是悬浮按钮则可以拖动,否则不能拖动
}
/**
* 处理水平方向上的拖动
* @param child 被拖动到view
* @param left 移动到达的x轴的距离
* @param dx 移动的x距离
*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
// 两个if主要是为了让view在ViewGroup里拖动,不超过Viewgroup边界
if(getPaddingLeft() > left) {
return getPaddingLeft();
}
if(getWidth() - child.getWidth() < left) {
return getWidth() - child.getWidth();
}
return left;
}
/**
* 处理竖直方向上的拖动
* @param child 被拖动到view
* @param top 移动到达的y轴的距离
* @param dy 移动的y距离
*/
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
// 两个if主要是为了让view在ViewGroup里拖动,不超过Viewgroup边界
if(getPaddingTop() > top) {
return getPaddingTop();
}
if(getHeight() - child.getHeight() < top) {
return getHeight() - child.getHeight();
}
return top;
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
if (releasedChild != null){
//计算按钮View的中间的位置
int x = releasedChild.getRight() - (releasedChild.getWidth() / 2);
//这里是屏幕的中间
int center = getResources().getDisplayMetrics().widthPixels / 2;
//当x < center的时候,说明这个悬浮按钮的中间已经在屏幕的右边了
if (x < center){
// 然后最后让按钮吸附到屏幕左边
mDragHelper.settleCapturedViewAt(0,releasedChild.getTop());
}else{
// 这里与上面相反
mDragHelper.settleCapturedViewAt(getResources().getDisplayMetrics()
.widthPixels-releasedChild.getWidth(),releasedChild.getTop());
}
//重绘当前ViewGroup
invalidate();
}
}
}
这里还要说明一下,如果这样写好了,其实还是不会吸附到边界的,因为什么呢,只要点击settleCapturedViewAt这个方法的话,会看到
OK,这个还不是主要的问题,在点击forceSettleCapturedViewAt这个方法进去look,look,然后我们会看到图中红色边框的
而且还要注意到注释的@return那里,能看到true if animation should continue through continueSettling(boolean) calls,意思就是如果返回true的话动画继续通过continueSettling调用,然后我们查看continueSettling的源码,方法如下:
看注释大概明白了这个方法是将捕获的子视图移动到当前时间的适当的位置,看到下面有用到deferCallbacks这个参数的是mParentView.post(mSetIdleRunnable);相当于就是不停的发送消息去刷新布局,当动画结束或者子视图已经移动到最终位置后,则停止刷新布局。
好了,说了这么多,那我们要怎么让子视图吸附的时候有一种回弹效果呢,只需要重写一个方法就可以了:
@Override
public void computeScroll() {
if (mDragHelper.continueSettling(true)){
invalidate();
}
}
就是这么简单,直接重写computeScroll()方法,然后再里面判断mDragHelper.continueSettling(true),至于为什么传入true呢,那么再来看看源码,其实源码都已经说了,如果要在computeScroll()调用,就应该传入true,多么简单直白T ^ T,如果这样都看不懂,那我也没办法啦。
想要用起来ViewDragHelper,还有一个重要的方法要重写的,
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_DOWN:
mDragHelper.cancel(); // 相当于调用 processTouchEvent收到ACTION_CANCEL
break;
}
/**
* 检查是否可以拦截touch事件
* 如果onInterceptTouchEvent可以return true 则这里return true
*/
return mDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
/**
* 处理拦截到的事件
* 这个方法会在返回前分发事件
*/
mDragHelper.processTouchEvent(event);
return true;
}
这两个方法必须要重写的,将TouchEvent和InterceptTouchEvent交给ViewDragHelper去处理就好了。
就这样就可以使用起来啦。
下面贴一下完整代码:
/**
* Created by Thong on 2017/6/30.
*/
public class DragViewLayout extends RelativeLayout {
private ViewDragHelper mDragHelper;
private View dragview;
public DragViewLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
dragview = getChildAt(0);//第一个子View为悬浮按钮
}
private void init() {
mDragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragCallback());
}
private class ViewDragCallback extends ViewDragHelper.Callback {
/**
* 尝试捕获子view,一定要返回true
* @param view 尝试捕获的view
* @param pointerId 指示器id?
* 这里可以决定哪个子view可以拖动
*/
@Override
public boolean tryCaptureView(View view, int pointerId) {
return view == dragview;//返回如果是悬浮按钮则可以拖动,否则不能
}
/**
* 处理水平方向上的拖动
* @param child 被拖动到view
* @param left 移动到达的x轴的距离
* @param dx 建议的移动的x距离
*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
// 两个if主要是为了让view在ViewGroup里拖动,不超过Viewgroup边界
if(getPaddingLeft() > left) {
return getPaddingLeft();
}
if(getWidth() - child.getWidth() < left) {
return getWidth() - child.getWidth();
}
return left;
}
/**
* 处理竖直方向上的拖动
* @param child 被拖动到view
* @param top 移动到达的y轴的距离
* @param dy 建议的移动的y距离
*/
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
// 两个if主要是为了让view在ViewGroup里拖动,不超过Viewgroup边界
if(getPaddingTop() > top) {
return getPaddingTop();
}
if(getHeight() - child.getHeight() < top) {
return getHeight() - child.getHeight();
}
return top;
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
//这里判断吸附的边界,当子view的中间在屏幕左边,则吸附到左边,反之则吸附到右边
if (releasedChild != null){
int x = releasedChild.getRight() - (releasedChild.getWidth() / 2);
int center = getResources().getDisplayMetrics().widthPixels / 2;
if (x < center){
mDragHelper.settleCapturedViewAt(0,releasedChild.getTop());
}else{
mDragHelper.settleCapturedViewAt(getResources().getDisplayMetrics()
.widthPixels-releasedChild.getWidth(),releasedChild.getTop());
}
//想要其作用就要调用computeScroll()
invalidate();
}
}
}
@Override
public void computeScroll() {
if (mDragHelper.continueSettling(true)){
invalidate();
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_DOWN:
mDragHelper.cancel(); // 相当于调用 processTouchEvent收到ACTION_CANCEL
break;
}
/**
* 检查是否可以拦截touch事件
*/
return mDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
/**
* 处理拦截到的事件
* 这个方法会在返回前分发事件
*/
mDragHelper.processTouchEvent(event);
return true;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
}
}
修改
昨天发了这个博客,然后我遗漏了一个问题,就是拖动是可以拖动了,但是点击事件呢?所以针对这个点击的这个问题做了一下处理:
首先,我们先去研究一下源码,这个问题涉及到事件分发,所以建议先去弄懂事件分发机制。
要想触发点击事件,那么先要写一个点击事件啦,写完了之后,那么问题就来了,点击是可以了,为什么不能拖动了呢?因为事件分发机制的判断流程是这样的:dispatchTouchEvent—>onInterceptTouchEvent—>onTouchEvent,知道了流程就简单了,因为onClick事件也是一个Touch事件,所以也会走这样的流程,所以我们在onInterceptTouchEvent中打个断点调试一下,看一下拦截触摸事件里面是怎么样做的,一步一步的解决点击事件这个问题。
一开始在碰到这个页面的时候就会触发onInterceptTouchEvent,然后在ViewGroup中的onInterceptTouchEvent的return 调用 ViewDragHelper的shouldInterceptTouchEvent,然后首先看拦截事件的ACTION_DOWN,这个DOWN事件里面并没有特别的处理,主要都是记录按下时位置,然后保存,然后是到ACTION_MOVE,这个事件里面大有文章了,一进来就开始判断DOWN事件中保存的位置是不是为空,空就直接结束,然后获取触摸点,然后获取触摸点移动的位置,并且算出移动的偏移量,然后根据当前的位置去获取这个子视图,重要的来了,checkTouchSlop这个方法是判断这个子视图在当前位置是否为空,并且检查子视图是否合理拖动,如果子视图合理拖动的话,那么这个方法会返回true然后根据触摸点的移动而移动,而判断checkTouchSlop的时候,里面就判断mCallBack.getViewHorizontalDragRange(child) > 0 和mCallback.getViewVerticalDragRange(child) > 0,那这样就很明显了嘛,因为getViewHorizontalDragRange和getViewVerticalDragRange默认都是返回0的,所以这个方法返回是false,我还是贴一下代码吧,来,走着:
贴了图片应该就清楚多了吧,里面会判断拖动的范围是否大于0,然后再下面判断是水平拖动,还是垂直拖动,还是非单项拖动,只要拖动的距离大于mTouchSlop就认为是拖动状态,mTouchSlop又是什么鬼,我们可以看一看:
final ViewConfiguration vc = ViewConfiguration.get(context);
mTouchSlop = vc.getScaledTouchSlop();
ViewDragHelper中的mTouchSlop是这样获取的,相信写过ACTION_MOVE事件的人都知道有一个系统级的判断是否移动的数值,就是这个vc.getScaledTouchSlop()了,好,OK,既然知道了是因为getViewVerticalDragRange和getViewHorizontalDragRange这两个东西造成的原因,那么我们去重写这两个,让它不默认返回0,而是有返回一定数值,那么返回的数值应该是多少呢,既然是一个方向移动范围,那么我们可以获取父布局的宽高,并且减去子视图的宽高,剩下的数值就是子视图可以移动的范围数值了,所以最终这两个方法重写应该是这样的:
@Override
public int getViewVerticalDragRange(View child) {
return dragview == child ? getMeasuredHeight() - child.getHeight() : 0;
}
@Override
public int getViewHorizontalDragRange(View child) {
return dragview == child ? getMeasuredWidth() - child.getWidth() : 0;
}
绕了这么一大圈,最终的结果只需要重写这两个方法,好坑啊,但是不否认这个Helper的确很好用,赞一个,不懂得都可以直接问我,评论或者私信都可以哦!