基於ViewGroup的Android可拖拽控件,同時解決和onClick事件的衝突問題、邊際界線問題防止被拖出屏幕外

繼承自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>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章