先不说这个我们先说下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,用这个我们不仅可以实现拖动而且还可以处理:多手指的处理、加速度检测等等。
好了话不多说先给大家一个简单的例子(当然鸿阳大神威武了!!)
首先我们通过一个简单的例子来看看其快捷的用法,分为以下几个步骤:
- 创建实例
- 触摸相关的方法的调用
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 ⊙ )!