基于ViewGroup的Android可拖拽控件,同时解决和on
继承自ViewGroup的自定义拖拽控件
直接上代码:
class FloatWindow : LinearLayout {
constructor(context: Context) : super(context) {}
constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet) {}
constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int) :
super(context, attributeSet, defStyleAttr) {
}
// private var lastX: Float = 0f
// private var lastY: Float = 0f
// private var mDragging: Boolean = false
private var lastX = 0
private var lastY = 0 //手指在屏幕上的坐标
private var isDraged = false //View是否被移动过
private var isDrag = false //判断是拖动还是点击
override fun onTouchEvent(event: MotionEvent?): Boolean {
val parentRight = (this.getParent() as ViewGroup).width
val parentBottom = (this.getParent() as ViewGroup).height
val action = event!!.action
when (action) {
MotionEvent.ACTION_DOWN -> {
isDrag = false
isDraged = false
lastX = event.rawX.toInt()
lastY = event.rawY.toInt()
}
MotionEvent.ACTION_MOVE -> {
val dx = event.rawX.toInt() - lastX
val dy = event.rawY.toInt() - lastY //手指在屏幕上移动的距离
if (isDraged) {
isDrag = true //如果已经被拖动过,那么无论本次移动的距离是否为零,都判定本次事件为拖动事件
} else {
if (dx == 0 && dy == 0) {
isDraged = false //如果移动的距离为零,则认为控件没有被拖动过,灵敏度可以自己控制
} else {
isDraged = true
isDrag = true
}
}
var l: Int = this.getLeft() + dx
var b: Int = this.getBottom() + dy
var r: Int = this.getRight() + dx
var t: Int = this.getTop() + dy
if (l < 0) { //处理按钮被移动到父布局的上下左右四个边缘时的情况,防止控件被拖出父布局
l = 0
r = l + this.getWidth()
}
if (t < 0) {
t = 0
b = t + this.getHeight()
}
if (r > parentRight) {
r = parentRight
l = r - this.getWidth()
}
if (b > parentBottom) {
b = parentBottom
t = b - this.getHeight()
}
lastX = event.rawX.toInt()
lastY = event.rawY.toInt()
this.layout(l, t, r, b)
this.postInvalidate() //其他view刷新时,会导致view回到原点,可以用设置LayoutParams的方式代替
}
}
//如果没有给view设置点击事件,需返回true,否则不会响应ACTION_MOVE,导致view不会被拖动
if(isDrag){
return true
}else{
return super.onTouchEvent(event)
}
}
}
布局中引用方式如下(等同于线性布局):
<com.shanda.npc.view.FloatWindow
android:layout_width="wrap_content"
android:layout_height="wrap_content">
.
.
.
</com.shanda.npc.view.FloatWindow>
详细解读如下:
思路很简单,在move过程中重绘view。但是如果onTouchEvent方法返回true,就会消费掉本次事件,导致即使是点击事件,onClick事件也不再响应,如果返回false,那么在对控件进行拖动的同时,也会响应onClick事件,所以我们需要一个标志位,就是代码中的isDrag。
private boolean isDrag = false; //判断是拖动还是点击
但是在实现过程中,发现了另外一个问题,如果只是点击,onTouchEvent中的ACTION_MOVE也会触发,导致onClick事件不响应,所以我们需要另外一个标志位,isDraged。
private boolean isDraged = false; //View是否被移动过
下面这段代码为判断逻辑
if (isDraged){
isDrag = true; //如果已经被拖动过,那么无论本次移动的距离是否为零,都判定本次事件为拖动事件
}else{
if (dx == 0 && dy == 0){
isDraged = false; //如果移动的距离为零,则认为控件没有被拖动过,灵敏度可以自己控制
}else{
isDraged = true;
isDrag = true;
}
}
如果拖动过,那么则认为本次事件为拖动事件,不需要再判断移动距离(移动距离为0时,也会触发ACTION_MOVE),也就是说,从按下到抬起,中间的过程如果有拖动,那么之后都不再根据move的距离来判定是不是拖动事件。
相反,如果从按下之后到本次ACTION_MOVE事件触发之前,还没有拖动过,那么再根据move的距离进行判断。
这么做主要是为了应对两种情况:
1,就是上边提到的,单纯的点击事件也会触发ACTION_MOVE
2,拖动控件后不动,触发ACTION_MOVE之后,移动距离为零
最后,onTouchEvent方法,再将是否是拖动事件的标志位,也就是isDrag返回就可以了。
另外,说一下下边这个方法
v.layout(l, t, r, b);
该方法是指在父布局中的位置,也就是说
v.layout(0,0,v.getWidth(),v.getBottom());
会布局到父布局的左上角,所以,想要不被拖出父布局,那么四个参数的取值范围如下
left : 0 到 parent.getWidth() - v.getWidth()
top : 0 到 parent.getHeight() - v.getHeight()
right : v.getWidth() 到 parent.getWidth()
bootom : v.getHeight() 到 parent.getHeight()
(二)
自定义可拖拽LinearLayout(ViewGroup),防页面刷新回到原点
前段时间有需求要做一个活动,入口是一个悬浮可拖拽的按钮。如果只是一个可拖拽的View也好办,搜文章也能搜到很多自定义可拖动的View,而且项目中也有一个自定义可拖拽的ImageView。但是现在需求是这个按钮可以关闭,多加了一个关闭按钮,那只能用ViewGroup了,最后我自定义了一个LinearLayout
第一个碰到的问题,是要处在LinearLayout中的活动图片和关闭按钮,防止在移动的时候触发点击事件。根据事件分发机制我们要在onInterceptTouchEvent() 方法里做一下判断是否要进行事件拦截,如果是处在MotionEvent.ACTION_MOVE中就进行拦截
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction() & MotionEvent.ACTION_MASK;
switch (action) {
case MotionEvent.ACTION_DOWN:
mLastTouchX = ev.getX();
mLastTouchY= ev.getY();
return false;
case MotionEvent.ACTION_MOVE:
mMoveX = ev.getX();
mMoveY = ev.getY();
//移动很小的一段距离也视为点击
if(Math.abs(mMoveX - mLastTouchX) < 5 || Math.abs(mMoveY - mLastTouchY) < 5)
//不进行事件拦截
return false;
else
return true;
}
return false;
}
复制代码
第二个碰到的问题,选择在拖动自定义LinearLayout控件过程中用哪些方法来移动。因为是根据项目中可拖动自定义ImageView来做的,这个控件中是用了layout(l,t,r,b)在拖动过程中重新布局,但是有一个问题,在有banner或者下拉刷新、上拉加载的这些页面中,拖动之后只要banner进行轮播或者上拉加载数据的时候会重走onLayout(boolean changed, int l, int t, int r, int b)方法,这个时候传过来的参数是控件的起始位置,所以又回到了原点。
本来我想在onLayout()方法中用layout()方法,传的参数为移动后的位置,但是有个问题,可以点开layout(),这个方法最后走的还是onLayout()所以就造成死循环了。这是个死结,后来我还想着在onLayout()进行判断,如果是拖动后手势抬起就不再走layout(),加上判断之后果然是可行的,但是后来测试的时候我来回切Fragment回到页面中的时候死循环又开始了。。。具体什么原因我也没弄很明白。
在onLayout()方法中用layout()重新布局是不可行的了,解决这个bug的时候把同事拉过来看了一下这个问题,同事说可以用其他的方式移动,试了几种方法最后用了offsetTopAndBottom/offsetLeftAndRight完美解决。
最后一个问题,就是拖动之后,要进行吸附在左右两侧,并且控件要限制在屏幕内不要越界,这个还比较好解决的。解决这个问题前要记住getTop()、getRight()、getBottom()、getLeft()这个四个方法分别是控件顶部到屏幕顶部的距离、控件右边界到屏幕左边界的距离、控件底部到屏幕顶部的距离、控件左边界到屏幕左边界的距离。
思路就是在MotionEvent.ACTION_UP中判断,获取一下当前的相对位置,如果超过了屏幕的1/2说明吸附屏幕右侧,那么再移动控件右边界到屏幕右边界的距离,就是屏幕宽度减去getRight(),同理吸附左侧是一样的。限制超过上下边界我是在拖动后手势抬起后,把超过的那一部分再滑动回来这样解决的。或者你可以在move过程中直接限制控件不超过屏幕。完整代码如下
package com.dudou.demo;
import android.content.Context;
import android.support.annotation.Nullable;
import android.support.v4.view.MotionEventCompat;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.LinearLayout;
public class DragLinearLayout extends LinearLayout {
private float mLastTouchX = 0;
private float mLastTouchY= 0;
private float mMoveX = 0;
private float mMoveY = 0;
private float mLeft;
private float mTop;
private float mRight;
private float mBottom;
private int mDx;
private int mDy;
private boolean isLeft = false;
boolean moveRight = false;
boolean moveLeft = false;
//屏幕宽度
private static final int screenWidth = ScreenUtil.getScreenWidth();
//屏幕高度
private static final int screenHeight = ScreenUtil.getScreenHeight()
public TouchLinearLayout(Context context) {
super(context);
}
public TouchLinearLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public TouchLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction() & MotionEvent.ACTION_MASK;
switch (action) {
case MotionEvent.ACTION_DOWN:
mLastTouchX = ev.getX();
mLastTouchY= ev.getY();
return false;
case MotionEvent.ACTION_MOVE:
mMoveX = ev.getX();
mMoveY = ev.getY();
//移动很小的一段距离也视为点击
if(Math.abs(mMoveX - mLastTouchX) < 5 || Math.abs(mMoveY - mLastTouchY) < 5)
//不进行事件拦截
return false;
else
return true;
}
return false;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
switch (action) {
case MotionEvent.ACTION_DOWN: {
break;
}
case MotionEvent.ACTION_MOVE: {
moveRight = false;
moveLeft = false;
final float x = ev.getX();
final float y = ev.getY();
final float dx = x - mLastTouchX;
final float dy = y - mLastTouchY;
mLeft = getLeft() + dx;
mTop = getTop() + dy;
mRight = getRight() + dx;
mBottom = getBottom() + dy;
if(mLeft < 0){
moveLeft = true;
mLeft = 0;
mRight = mLeft + getWidth();
}
if(mRight > screenWidth){
moveRight = true;
mRight = screenWidth;
mLeft = mRight - getWidth();
}
if(mTop < 0){
mTop = 0;
mBottom = mTop + getHeight();
}
if(mBottom > screenHeight){
mBottom = screenHeight;
mTop = mBottom - getHeight();
}
mDx += dx;
mDy += dy;
offsetLeftAndRight((int)dx);
offsetTopAndBottom((int)dy);
if(moveLeft){
offsetLeftAndRight(-getLeft());
}
if(moveRight){
offsetLeftAndRight(screenWidth-getRight());
}
break;
}
case MotionEvent.ACTION_UP: {
int upX = (int) ev.getRawX();
if (upX > (screenWidth / 2)) {
isLeft = false;
offsetLeftAndRight(screenWidth-getRight());
invalidate();
} else {
isLeft = true;
offsetLeftAndRight(-getLeft());
invalidate();
}
if(getTop()<0){
mDy += -getTop();
offsetTopAndBottom(-getTop());
}
if(getBottom()>screenHeight){
mDy += screenHeight-getBottom();
offsetTopAndBottom(screenHeight-getBottom());
}
break;
}
case MotionEvent.ACTION_CANCEL: {
break;
}
case MotionEvent.ACTION_POINTER_UP: {
break;
}
}
return true;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
offsetTopAndBottom(mDy);
if(isLeft){
offsetLeftAndRight(-getLeft());
}else {
offsetLeftAndRight(screenWidth-getRight());
}
}
}
布局中引用方式:
<com.shanda.npc.view.FloatWindowNew
android:layout_width="@dimen/PX_200"
android:layout_height="@dimen/PX_258"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:layout_marginTop="@dimen/PX_48"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="16dp">
<LinearLayout
android:id="@+id/subscriber_container"
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="match_parent"/>
<ImageView
android:id="@+id/iv_video_small_mute"
android:layout_width="@dimen/PX_50"
android:layout_height="@dimen/PX_50"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentRight="true"
android:visibility="gone"
android:layout_marginBottom="@dimen/PX_20"
android:src="@mipmap/call_mute" />
</com.shanda.npc.view.FloatWindowNew>