Android自定义控件--- 手势检测ViewDragHelper

先不说这个我们先说下android中的拖动设置,基本思路是通过个给控件设置触摸监听加上给父布局注册onDragListener实现的这里我简单给大家写一下
首先是子类的注册触摸监听

  btnTuo.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                if (event.getAction() == MotionEvent.ACTION_DOWN) {
                    ClipData data = ClipData.newPlainText("", "");
                    View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(v);
                    v.startDrag(data, shadowBuilder, v, 0);
                    v.setVisibility(View.VISIBLE);//如果想在移动时对view做其他处理可以设置其他值 这里的效果是移动时改view还是可见的
                    width = v.getWidth();//view的宽
                    height = v.getHeight();//view的高
                    return true;
                } else {
                    return false;
                }
            }
        });

然后给父布局

 activityMain.setOnDragListener(new View.OnDragListener() {
            @Override
            public boolean onDrag(View v, DragEvent event) {
                int action = event.getAction();
                switch (action) {
                    case DragEvent.ACTION_DRAG_STARTED:
                        break;
                    case DragEvent.ACTION_DRAG_ENTERED:
                        break;
                    case DragEvent.ACTION_DRAG_EXITED:
                        break;
                    case DragEvent.ACTION_DROP://移动->手指松开
                        Button view = (Button) event.getLocalState();//获取移动后的view对象
                        RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(width, height);
                        layoutParams.setMargins((int) event.getX() - width / 2, (int) event.getY() - height / 2, 0, 0);
                        view.setLayoutParams(layoutParams);//设置view的布局参数
                        ViewGroup owner = (ViewGroup) view.getParent();
                        owner.removeView(view);//清楚该view在移动前的对象  不remove会抛异常
                        RelativeLayout container = (RelativeLayout) v;//父布局
                        container.addView(view);//向父布局中添加移动后的view
                        view.setVisibility(View.VISIBLE);
                        break;
                    case DragEvent.ACTION_DRAG_ENDED:
                        break;
                    default:
                        break;
                }
                return true;
            }
        });

这样就可以实现了,但是大家可以看到还是比较麻烦的,而且当写多个布局拖动的时候代码很容易就会过于凌乱。但是官方在v4的支持包中提供了ViewDragHelper这样一个类来帮助我们方便的编写自定义ViewGroup,用这个我们不仅可以实现拖动而且还可以处理:多手指的处理、加速度检测等等。
好了话不多说先给大家一个简单的例子(当然鸿阳大神威武了!!)
首先我们通过一个简单的例子来看看其快捷的用法,分为以下几个步骤:

  1. 创建实例
  2. 触摸相关的方法的调用
  3. ViewDragHelper.Callback实例的编写

    我们来写个自定义的viewgroup

public class VDHLayout extends LinearLayout
{
    private ViewDragHelper mDragger;

    public VDHLayout(Context context, AttributeSet attrs)
    {
        super(context, attrs);
        mDragger = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback()
        {
            @Override
            public boolean tryCaptureView(View child, int pointerId)
            {
                return true;
            }

            @Override
            public int clampViewPositionHorizontal(View child, int left, int dx)
            {
                return left;
            }

            @Override
            public int clampViewPositionVertical(View child, int top, int dy)
            {
                return top;
            }
        });
    }

   @Override
    public boolean onInterceptTouchEvent(MotionEvent event)
    {
        return mDragger.shouldInterceptTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event)
    {
        mDragger.processTouchEvent(event);
        return true;
    }
}

从上面可以看出来基本就是三个点
1.创建实例

mDragger = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback()
        {
        });

创建实例需要3个参数,第一个就是当前的ViewGroup,第二个sensitivity,主要用于设置touchSlop: (这个没太搞懂,不过暂时不影响)

helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity));

应该是传入越大,mTouchSlop的值就会越小。第三个参数就是Callback,在用户的触摸过程中会回调相关方法,在后面的三中我们会详细的讲解。

2.触摸相关方法(事件的分发)

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event)
    {
        return mDragger.shouldInterceptTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event)
    {
        mDragger.processTouchEvent(event);
        return true;
    }

这里一个是触摸事件,一个是事件拦截方法,(当你的子布局会消费掉事件的话,你会发现你的控件不会被拖动,我们就需要处理onInterceptTouchEvent了,后面我会给出方法,慢慢来)

3.实现ViewDragHelper.CallCack相关方法

new ViewDragHelper.Callback()
        {
            @Override
            public boolean tryCaptureView(View child, int pointerId)
            {
                return true;
            }

            @Override
            public int clampViewPositionHorizontal(View child, int left, int dx)
            {
                return left;
            }

            @Override
            public int clampViewPositionVertical(View child, int top, int dy)
            {
                return top;
            }
        }

这里其实还有很多,这三个是最常用的也是拖动设置中所必须的

  • tryCaptureView这个是返回来判断这个控件是否可以被拖动(true表示可以)
  • clampViewPositionHorizontal和clampViewPositionVertical是来限制拖动的范围,不设置的话默认可以随意拖动

这里大家肯定对限制拖动范围肯定迷迷糊糊的这里我给个例子说明

   @Override
            public int clampViewPositionHorizontal(View child, int left, int dx)
            {
                final int leftBound = getPaddingLeft();
                final int rightBound = getWidth() - child.getWidth() - leftBound;

                final int newLeft = Math.min(Math.max(left, leftBound), rightBound);
                Log.e("距离",newLeft+"==="+left+"==="+leftBound);
                return newLeft;
            }

首先我们要理解这个方法是如何限制的,这个方法其实是限制控件左边靠近父布局左边的位置,知道了这个就好写多了首先我们要获取靠左边距对吧 这用来做我们的最小值,然后我们要获取最大值那不就是父布局的宽度-控件的宽度-边距(这里我认为左右边距一样就这么写的 实际上应该写减去右边距getPaddingRight()),不影响。然后我们就做判断让靠左的距离在这个范围内就OK了

final int newLeft = Math.min(Math.max(left, leftBound), rightBound);

好了给出一个布局试试去

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main2"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.admin.viewdraghelper.Main2Activity">
    <com.example.admin.viewdraghelper.VDHLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <TextView
            android:layout_margin="10dp"
            android:gravity="center"
            android:layout_gravity="center"
            android:background="#44ff0000"
            android:text="我能够拖动 !"
            android:layout_width="100dp"
            android:layout_height="100dp"/>

        <TextView
            android:layout_margin="10dp"
            android:layout_gravity="center"
            android:gravity="center"
            android:background="#44303F9F"
            android:text="我能发挥到初始点!"
            android:layout_width="100dp"
            android:layout_height="100dp"/>

        <TextView
            android:layout_margin="10dp"
            android:layout_gravity="center"
            android:gravity="center"
            android:background="#44ffff00"
            android:text="我不能被拖动,但被边界滑动影响"
            android:layout_width="100dp"
            android:layout_height="100dp"/>
    </com.example.admin.viewdraghelper.VDHLayout>
</RelativeLayout>

好了简单的拖动就到此为止了,下面我们说说其他的操作

  • 边界检测、加速度检测(eg:DrawerLayout边界触发拉出)
  • 回调Drag Release(eg:DrawerLayout部分,手指抬起,自动展开/收缩)
  • 移动到某个指定的位置(eg:点击Button,展开/关闭Drawerlayout)

简单的为每个子View添加了不同的操作:

第一个View,就是演示简单的移动
第二个View,演示除了移动后,松手自动返回到原本的位置。(注意你拖动的越快,返回的越快)
第三个View,边界移动时对View进行捕获。

首先给大家说下我们禁止了第三个view’的拖动,让它仅受边界滑动的影响具体代码

第一个view:private View mDragView;
第二个view: private View mAutoBackView;
第三个view: private View mEdgeTrackerView;

   @Override
            public boolean tryCaptureView(View child, int pointerId)
            {
                //设置mEdgeTrackerView禁止直接移动
                return child == mDragView || child == mAutoBackView;
            }

然后我们获取view是在onFinishInflate方法内部

  @Override
    protected void onFinishInflate()
    {
        super.onFinishInflate();
        //按顺序获取对应的子布局
        mDragView = getChildAt(0);
        mAutoBackView = getChildAt(1);
        mEdgeTrackerView = getChildAt(2);
    }

下面说第二个效果拉动后回到原点
实际上就是用了手指释放的时候回调,在这之前我们先将第二个view的x,y存到点类去了

 private Point mAutoBackOriginPos = new Point();
     @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b)
    {
        super.onLayout(changed, l, t, r, b);

        mAutoBackOriginPos.x = mAutoBackView.getLeft();
        mAutoBackOriginPos.y = mAutoBackView.getTop();
    }

还有一个重要的方法差点漏了

  public void computeScroll()
    {
        if(mDragger.continueSettling(true))
        {
            invalidate();
        }
    }

这里mDragger.continueSettling(true)表示如果返回true,那么调用者应该call遍
继续下一帧。这里不写这个类的动效及没发执行了。

然后回调中设置回到原来的位置(invalidate()刷新界面 )

   //手指释放的时候回调
            @Override
            public void onViewReleased(View releasedChild, float xvel, float yvel)
            {
                //mAutoBackView手指释放时可以自动回去
                if (releasedChild == mAutoBackView)
                {
                    mDragger.settleCapturedViewAt(mAutoBackOriginPos.x, mAutoBackOriginPos.y);
                    invalidate();
                }
            }

下面说下第三个效果边界滑动(主要是在边界拖动时回调)

    //在边界拖动时回调
            @Override
            public void onEdgeDragStarted(int edgeFlags, int pointerId)
            {
                mDragger.captureChildView(mEdgeTrackerView, pointerId);
            }

这里需要注意的是我们要设置边界的范围

 //这里设置你在哪个方向的边界移动捕捉
        mDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_ALL);

给出改过的代码(0。0)

package com.example.admin.viewdraghelper;

import android.content.Context;
import android.graphics.Point;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;

/**
 * Created by admin on 2017/3/6.
 */

public class VDHLayout extends LinearLayout
{
    private ViewDragHelper mDragger;
    private View mDragView;
    private View mAutoBackView;
    private View mEdgeTrackerView;

    private Point mAutoBackOriginPos = new Point();
    public VDHLayout(Context context, AttributeSet attrs)
    {
        super(context, attrs);
        mDragger = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback()
        {
            @Override
            public boolean tryCaptureView(View child, int pointerId)
            {
                //设置mEdgeTrackerView禁止直接移动
                return child == mDragView || child == mAutoBackView;
            }

            @Override
            public int clampViewPositionHorizontal(View child, int left, int dx)
            {
                final int leftBound = getPaddingLeft();
                final int rightBound = getWidth() - child.getWidth() - leftBound;

                final int newLeft = Math.min(Math.max(left, leftBound), rightBound);
                Log.e("距离",newLeft+"==="+left+"==="+leftBound);
                return newLeft;
            }

            @Override
            public int clampViewPositionVertical(View child, int top, int dy)
            {
                final int topBound = getPaddingLeft();
                final int bootomBound = getHeight() - child.getHeight() - topBound;

                final int newTop = Math.min(Math.max(top, topBound), bootomBound);

                return newTop;
            }

            //手指释放的时候回调
            @Override
            public void onViewReleased(View releasedChild, float xvel, float yvel)
            {
                //mAutoBackView手指释放时可以自动回去
                if (releasedChild == mAutoBackView)
                {
                    mDragger.settleCapturedViewAt(mAutoBackOriginPos.x, mAutoBackOriginPos.y);
                    invalidate();
                }
            }

            //在边界拖动时回调
            @Override
            public void onEdgeDragStarted(int edgeFlags, int pointerId)
            {
                mDragger.captureChildView(mEdgeTrackerView, pointerId);
            }
        });
        //这里设置你在哪个方向的边界移动捕捉
        mDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_ALL);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event)
    {
        return mDragger.shouldInterceptTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event)
    {
        mDragger.processTouchEvent(event);
        return true;
    }

    @Override
    public void computeScroll()
    {
        if(mDragger.continueSettling(true))
        {
            invalidate();
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b)
    {
        super.onLayout(changed, l, t, r, b);

        mAutoBackOriginPos.x = mAutoBackView.getLeft();
        mAutoBackOriginPos.y = mAutoBackView.getTop();
    }

    @Override
    protected void onFinishInflate()
    {
        super.onFinishInflate();
        //按顺序获取对应的子布局
        mDragView = getChildAt(0);
        mAutoBackView = getChildAt(1);
        mEdgeTrackerView = getChildAt(2);
    }
}

好了大家有没有发现上面用的是textview,试试buttonn你会发现不行了。因为button消耗了事件。如果子View不消耗事件,那么整个手势(DOWN-MOVE*-UP)都是直接进入onTouchEvent,在onTouchEvent的DOWN的时候就确定了captureView。如果消耗事件,那么就会先走onInterceptTouchEvent方法,判断是否可以捕获,而在判断的过程中会去判断另外两个回调的方法:getViewHorizontalDragRange和getViewVerticalDragRange,只有这两个方法返回大于0的值才能正常的捕获。
所以重写这两方法

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

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

最后我们列一下所有的Callback方法,看看还有哪些没用过的:

  • onViewDragStateChanged
ViewDragHelper状态发生变化时回调(IDLE,DRAGGING,SETTING[自动滚动时]
  • onViewPositionChanged
当captureview的位置发生改变时回调
  • onViewCaptured
当captureview被捕获时回调
  • onViewReleased 已用 手指释放的时候回调
  • onEdgeTouched
当触摸到边界时回调。
  • onEdgeLock
true的时候会锁住当前的边界,false则unLock。
  • onEdgeDragStarted 已用 在边界拖动时回调
  • getOrderedChildIndex
改变同一个坐标(x,y)去寻找captureView位置的方法。(具体在:findTopChildUnder方法中)
  • getViewHorizontalDragRange 已用 判断横向是否可以移动
  • getViewVerticalDragRange 已用 判断纵向是否可以移动
  • tryCaptureView 已用 控件是否可拖动
  • clampViewPositionHorizontal 已用 限制横向移动位置
  • clampViewPositionVertical 已用 限制纵向移动位置
    -

好了,就说这么多了。( ⊙ o ⊙ )!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值