使用ViewDragHelper实现可移动可点击控件
概述
上一篇文章WindowManager实现可移动可点击(可只在应用中显示)悬浮窗写了利用WindowManager实现可移动可点击的悬浮窗,这一篇主要讲的是在单个界面中利用ViewDragHelper实现可移动可点击控件。
(一)普通方法实现
在利用ViewDragHelper实现之前,我们先用普通方法实现看看,通过自定义一个父布局,在父布局中添加一个可移动的控件。
这里我创建了一个类DragLayout2继承FrameLayout:
public class DragLayout2 extends FrameLayout {
private Context mContext;
private View mDragView; //可移动控件
private float downX; //手指落下时的x坐标值
private float downY; //手指落下时的y坐标值
private OnClickListener mOnClickListener; //给可移动控件添加点击事件的回调接口
private boolean isMove = false; //判断可移动控件是否移动了
public DragLayout2(Context context) {
this(context,null);
}
public DragLayout2(Context context, AttributeSet attrs) {
super(context, attrs);
this.mContext = context;
}
}
添加可移动控件,可以在构造函数中进行:
//通过布局填充器添加
mDragView = LayoutInflater.from(mContext).inflate(R.layout.kefu, null);
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(DensityUtil.dip2px(mContext,52), DensityUtil.dip2px(mContext,52)); //设置控件宽高
this.addView(mDragView,params);
如果想要通过获取布局中子控件的方式得到mDragView,需要重写onFinishInflate(),在布局完成后得到:
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mDragView = getChildAt(1); //1是当前布局下的位置,代表第2个直接子控件
}
我这里就直接使用第一种方法了,不用再在相应的布局文件中添加。用第一种方法,我这里要让控件出来的时候就直接显示在右下角,如下图所示:
可以通过调用layout(int l, int t, int r, int b)方法设置布局:
mDragView.layout(getWidth()-mDragView.getWidth(),
getHeight()-mDragView.getHeight(),
getWidth(),
getHeight());
layout四个参数分别代表控件左边距离整个布局左边的距离,控件右边距离整个布局左边的距离,控件顶部距离整个布局顶部的距离,控件底部距离整个布局顶部的距离,参考下图:
这步设置需要放在DragLayout2执行onLayout方法之后,因为在执行该方法后才能获取到整个布局的宽高——getWidth( )和getHeight( )获得的值才不会为0。
然后最重要的就是给控件设置触摸事件了:
mDragView.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
downX = event.getX();
downY = event.getY();
isMove = false;
break;
case MotionEvent.ACTION_MOVE:
float xDistance = event.getX() - downX; //x轴移动距离
float yDistance = event.getY() - downY; //y轴移动距离
if (xDistance != 0 && yDistance != 0) {
int l = (int) (v.getLeft() + xDistance);
int r = (int) (v.getRight() + xDistance);
int t = (int) (v.getTop() + yDistance);
int b = (int) (v.getBottom() + yDistance);
if (l < 0){ //使移动不能超过左边界
l = 0;
r = mDragView.getWidth();
}
if (r > getWidth()){ //使移动不能超过右边界
r = getWidth();
l = r - mDragView.getWidth();
}
if (t < 0){ //使移动不能超过上边界
t = 0;
b = mDragView.getHeight();
}
if (b > getHeight()){ //使移动不能超过下边界
b = getHeight();
t = b - mDragView.getHeight();
}
v.layout(l, t, r, b); //设置位置
isMove = true;
}
break;
case MotionEvent.ACTION_UP:
v.layout(getWidth()-
mDragView.getWidth(),
v.getTop(),getWidth(),
v.getBottom()); //设置手指抬起时控件移到右边
if (isMove){
return true;
}
break;
}
return false;
}
});
触摸事件返回false时事件没被执行,点击事件可以被执行;返回true时说明消费了事件,点击事件就不会再被执行了。这里我定义了一个布尔变量isMove,当它移动了isMove就为true,onTouch方法就返回true,就不会触发点击事件。
然后设置一个setOnClickListener方法,让Activity可以通过调用该方法触发可移动控件的点击事件:
public void setOnClickListener(OnClickListener onClickListener) {
mOnClickListener = onClickListener;
}
mDragView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mOnClickListener.onClick(v);
}
});
最后效果如下:
(二)使用ViewDragHelper实现
使用ViewDragHelper来实现就不用自己写触摸事件了,通过在ViewDragHelper.Callback回调中重写方法就行了。
一、创建实例:
/**
* 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;
}
通过调用create(ViewGroup forParent, float sensitivity, Callback cb)方法创建实例,该方法有三个参数,第一个参数传入要监视的布局,传入this代表该布局本身;第二个参数是敏感度,1代表正常的敏感度,值越大越敏感;第三个参数是提供信息和接收事件的回调。
private ViewDragHelper mViewDragHelper; //定义一个ViewDragHelper全局变量
mViewDragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {
return child == mDragView;
}//可以判断是否要捕获当前的view,返回true时表示可以捕获
@Override //控制child在水平方向上的位置,不限制边界返回left即可
public int clampViewPositionHorizontal(View child, int left, int dx) {
if(left <0){
//限制左边界
left = 0;
}else if (left > (getMeasuredWidth() - child.getMeasuredWidth())){
//限制右边界
left = getMeasuredWidth() - child.getMeasuredWidth();
}
return left;
}
@Override //控制child在竖直方向上的位置,不限制边界返回top即可
public int clampViewPositionVertical(View child, int top, int dy) {
if(top <0){
//限制上边界
top = 0;
}else if (top > (getMeasuredHeight() - child.getMeasuredHeight())){
//限制下边界
top = getMeasuredHeight() - child.getMeasuredHeight();
}
return top;
}
@Override //不重写该方法默认返回0,返回0时若还设置了点击事件则水平方向不能移动
public int getViewHorizontalDragRange(View child) {
return getMeasuredWidth() - child.getMeasuredWidth();
}
@Override //不重写该方法默认返回0,返回0时若还设置了点击事件则竖直方向不能移动
public int getViewVerticalDragRange(View child) {
return getMeasuredHeight() - child.getMeasuredHeight();
}
@Override //处理手指释放的事件
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
if (releasedChild == mDragView){
mViewDragHelper.smoothSlideViewTo(releasedChild,getMeasuredWidth() - releasedChild.getMeasuredWidth(),releasedChild.getTop()); //平滑移动到最右边
ViewCompat.postInvalidateOnAnimation(DragLayout.this); //刷新布局
}
}
});
二、触摸、拦截事件:
callback中需要重写的方法弄好之后,需要对事件监听做相应的处理,重写onInterceptTouchEvent和onTouchEvent方法:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mViewDragHelper.shouldInterceptTouchEvent(ev); //由viewDragHelper来判断是否应该拦截此事件
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mViewDragHelper.processTouchEvent(event);
return true; //消费掉事件,让ViewDragHelper来处理
}
三、处理computeScroll():
@Override
public void computeScroll() {
super.computeScroll();
if (mViewDragHelper.continueSettling(true)){ //判断是否结束
ViewCompat.postInvalidateOnAnimation(DragLayout.this);
}
}
该方法由父级调用,以请求孩子在需要时更新mScrollX和mScrollY的值。不重写该方法,之前手指释放时的动画效果就不会执行。
以上就完成了,其中布局添加和点击事件跟用普通方法实现一样,最终效果图也差不多。
GitHub链接地址