繼承自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 //手指在屏幕上的坐標(biāo)
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 //如果已經(jīng)被拖動過撩荣,那么無論本次移動的距離是否為零,都判定本次事件為拖動事件
} else {
if (dx == 0 && dy == 0) {
isDraged = false //如果移動的距離為零饶深,則認(rèn)為控件沒有被拖動過餐曹,靈敏度可以自己控制
} 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刷新時敌厘,會導(dǎo)致view回到原點台猴,可以用設(shè)置LayoutParams的方式代替
}
}
//如果沒有給view設(shè)置點擊事件,需返回true,否則不會響應(yīng)ACTION_MOVE,導(dǎo)致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>
詳細(xì)解讀如下:
思路很簡單饱狂,在move過程中重繪view曹步。但是如果onTouchEvent方法返回true,就會消費掉本次事件休讳,導(dǎo)致即使是點擊事件讲婚,onClick事件也不再響應(yīng),如果返回false俊柔,那么在對控件進(jìn)行拖動的同時筹麸,也會響應(yīng)onClick事件,所以我們需要一個標(biāo)志位雏婶,就是代碼中的isDrag物赶。
private boolean isDrag = false; //判斷是拖動還是點擊
但是在實現(xiàn)過程中,發(fā)現(xiàn)了另外一個問題留晚,如果只是點擊酵紫,onTouchEvent中的ACTION_MOVE也會觸發(fā),導(dǎo)致onClick事件不響應(yīng)错维,所以我們需要另外一個標(biāo)志位奖地,isDraged。
private boolean isDraged = false; //View是否被移動過
下面這段代碼為判斷邏輯
if (isDraged){
isDrag = true; //如果已經(jīng)被拖動過需五,那么無論本次移動的距離是否為零鹉动,都判定本次事件為拖動事件
}else{
if (dx == 0 && dy == 0){
isDraged = false; //如果移動的距離為零轧坎,則認(rèn)為控件沒有被拖動過宏邮,靈敏度可以自己控制
}else{
isDraged = true;
isDrag = true;
}
}
如果拖動過,那么則認(rèn)為本次事件為拖動事件缸血,不需要再判斷移動距離(移動距離為0時蜜氨,也會觸發(fā)ACTION_MOVE),也就是說捎泻,從按下到抬起飒炎,中間的過程如果有拖動,那么之后都不再根據(jù)move的距離來判定是不是拖動事件笆豁。
相反郎汪,如果從按下之后到本次ACTION_MOVE事件觸發(fā)之前,還沒有拖動過闯狱,那么再根據(jù)move的距離進(jìn)行判斷煞赢。
這么做主要是為了應(yīng)對兩種情況:
1,就是上邊提到的哄孤,單純的點擊事件也會觸發(fā)ACTION_MOVE
2照筑,拖動控件后不動,觸發(fā)ACTION_MOVE之后,移動距離為零
最后凝危,onTouchEvent方法波俄,再將是否是拖動事件的標(biāo)志位,也就是isDrag返回就可以了蛾默。
另外懦铺,說一下下邊這個方法
v.layout(l, t, r, b);
該方法是指在父布局中的位置,也就是說
v.layout(0,0,v.getWidth(),v.getBottom());
會布局到父布局的左上角支鸡,所以阀趴,想要不被拖出父布局,那么四個參數(shù)的取值范圍如下
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。但是現(xiàn)在需求是這個按鈕可以關(guān)閉检碗,多加了一個關(guān)閉按鈕据块,那只能用ViewGroup了,最后我自定義了一個LinearLayout
第一個碰到的問題折剃,是要處在LinearLayout中的活動圖片和關(guān)閉按鈕另假,防止在移動的時候觸發(fā)點擊事件。根據(jù)事件分發(fā)機(jī)制我們要在onInterceptTouchEvent() 方法里做一下判斷是否要進(jìn)行事件攔截怕犁,如果是處在MotionEvent.ACTION_MOVE中就進(jìn)行攔截
@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)
//不進(jìn)行事件攔截
return false;
else
return true;
}
return false;
}
復(fù)制代碼
第二個碰到的問題边篮,選擇在拖動自定義LinearLayout控件過程中用哪些方法來移動。因為是根據(jù)項目中可拖動自定義ImageView來做的奏甫,這個控件中是用了layout(l,t,r,b)在拖動過程中重新布局戈轿,但是有一個問題,在有banner或者下拉刷新阵子、上拉加載的這些頁面中思杯,拖動之后只要banner進(jìn)行輪播或者上拉加載數(shù)據(jù)的時候會重走onLayout(boolean changed, int l, int t, int r, int b)方法,這個時候傳過來的參數(shù)是控件的起始位置挠进,所以又回到了原點色乾。
本來我想在onLayout()方法中用layout()方法,傳的參數(shù)為移動后的位置领突,但是有個問題暖璧,可以點開layout(),這個方法最后走的還是onLayout()所以就造成死循環(huán)了攘须。這是個死結(jié)漆撞,后來我還想著在onLayout()進(jìn)行判斷殴泰,如果是拖動后手勢抬起就不再走layout(),加上判斷之后果然是可行的浮驳,但是后來測試的時候我來回切Fragment回到頁面中的時候死循環(huán)又開始了悍汛。。至会。具體什么原因我也沒弄很明白离咐。
在onLayout()方法中用layout()重新布局是不可行的了,解決這個bug的時候把同事拉過來看了一下這個問題奉件,同事說可以用其他的方式移動宵蛀,試了幾種方法最后用了offsetTopAndBottom/offsetLeftAndRight完美解決。
最后一個問題县貌,就是拖動之后术陶,要進(jìn)行吸附在左右兩側(cè),并且控件要限制在屏幕內(nèi)不要越界煤痕,這個還比較好解決的梧宫。解決這個問題前要記住getTop()、getRight()摆碉、getBottom()塘匣、getLeft()這個四個方法分別是控件頂部到屏幕頂部的距離、控件右邊界到屏幕左邊界的距離巷帝、控件底部到屏幕頂部的距離忌卤、控件左邊界到屏幕左邊界的距離。
思路就是在MotionEvent.ACTION_UP中判斷楞泼,獲取一下當(dāng)前的相對位置驰徊,如果超過了屏幕的1/2說明吸附屏幕右側(cè),那么再移動控件右邊界到屏幕右邊界的距離现拒,就是屏幕寬度減去getRight()辣垒,同理吸附左側(cè)是一樣的。限制超過上下邊界我是在拖動后手勢抬起后印蔬,把超過的那一部分再滑動回來這樣解決的⊥蜒茫或者你可以在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)
//不進(jìn)行事件攔截
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>