1. 程式人生 > >實現可拖拽移動的懸浮按鈕

實現可拖拽移動的懸浮按鈕

前言

  最近想要實現一個可拖拽移動的FAB按鈕,這裡記錄一下個人的思路與經驗。


如何監聽FAB按鈕的移動?

  我們可以實現View.OnTouchListener介面,在onTouch( )方法中獲取FAB按鈕移動時的位置引數。或者可以選擇複寫View自身的onTouchEvent( )方法,實現方式大同小異。


移動範圍超出螢幕怎麼辦?

  通過邏輯判斷限制FAB移動的範圍。


如何區分FAB按鈕的”拖拽移動” 和 “點選事件?”

  如果同時監聽FAB按鈕onTouch和onClick事件 , 在onTouch事件中執行拖動操作,在onClick中實現事件邏輯,但是onTouch和OnClick會有衝突。
 
  onTouch事件的返回值是boolean型別的,如果返回true ,那麼事件就會被攔截,onclick方法不會被呼叫;返回false,事件不被消費和攔截,onClick方法會同時被呼叫。
 
  策略一:

要想把OnTouch和onClick事件完全的區分,這裡的想法就是在 OnTouch中的MotionEvent.ACTION_DOWN 時,記錄下點(X1,Y1),在 MotionEvent.ACTION_UP 時,記錄下點(X2,Y2),然後比對 倆點之間的距離,如果小於一個較小數值(比如5),就認為是Click事件,onTouch中返回false,如果距離較大,可以當作onTouch事件去處理,返回true.
 
 策略二:


如何實現FAB按鈕的移動?

  通過監聽OnTouch( )方法或者覆寫onTouchEvent( )方法,我們已經可以獲取到拖拽FAB按鈕時的實時位置了,接下來我們如何表現這種位置移動的“變化“? 
  

  • 通過view.layout()方式。任何佈局上的空間都可以支援這種方式移動,上下左右引數值是相對於父viewgroup而言的。

  • 呼叫MarginLayoutParams.setMargins(),重新設定控制元件位置引數來實現控制元件移動效果。這種方式比較適合RelativeLayout、FrameLayout,AbsoluteLayout,對於LinearLayout,因為最後增加的控制元件總在最下或最右,所以達不到移動效果,TableLayout也不行。 

  • 通過view.setX() 、view.setY()方式。任意佈局上的空間都可以支援這種方式移動,引數值是指相對於自身原本位置X座標軸和Y座標軸上的偏移量。 


各種實現方式注意事項


 方案1使用View.layout()方式實現FAB按鈕的移動
 

public class FloatButton extends android.support.design.widget.FloatingActionButton implements View.OnTouchListener{

    int lastX, lastY;
    int originX, originY;
    int screenWidth ;
    int screenHeight ;
    int distance;

    public FloatButton(Context context) {
        super(context);
        getScreenWidthAndHeight(context);
        setOnTouchListener(this);
    }

    public FloatButton(Context context, AttributeSet attrs) {
        super(context, attrs);
        getScreenWidthAndHeight(context);
        setOnTouchListener(this);
    }

    public FloatButton(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        getScreenWidthAndHeight(context);
        setOnTouchListener(this);

    }


    @Override
    public boolean onTouch(View v, MotionEvent event) {
        int ea = event.getAction();
        switch (ea) {
            case MotionEvent.ACTION_DOWN:
                lastX = (int) event.getRawX();// 獲取觸控事件觸控位置的原始X座標
                lastY = (int) event.getRawY();
                originX = lastX;
                originY = lastY;
                break;
            case MotionEvent.ACTION_MOVE:
                int dx = (int) event.getRawX() - lastX;
                int dy = (int) event.getRawY() - lastY;
                int l = v.getLeft() + dx;
                int b = v.getBottom() + dy;
                int r = v.getRight() + dx;
                int t = v.getTop() + dy;
                // 下面判斷移動是否超出螢幕
                if (l < 0) {
                    l = 0;
                    r = l + v.getWidth();
                }
                if (t < 0) {
                    t = 0;
                    b = t + v.getHeight();
                }
                if (r > screenWidth) {
                    r = screenWidth;
                    l = r - v.getWidth();
                }
                if (b > screenHeight) {
                    b = screenHeight;
                    t = b - v.getHeight();
                }
                v.layout(l, t, r, b);
                lastX = (int) event.getRawX();
                lastY = (int) event.getRawY();
                v.postInvalidate();
                break;
            case MotionEvent.ACTION_UP:
                distance = (int) event.getRawX() - originX + (int)event.getRawY() - originY;

                break;
        }
        return false;

    }

    private void getScreenWidthAndHeight(Context context) {
        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics dm = new DisplayMetrics();
        wm.getDefaultDisplay().getMetrics(dm);
        screenWidth = dm.widthPixels;
        screenHeight = dm.heightPixels;

    }

}

  優劣:這種方式實現是最簡單的的,但是當同一個viewgroup中有控制元件更新(介面重新整理)時,移動的控制元件會復位,即回到一開始的位置.
  
  
  仔細觀察下圖,佈局中上方的Banner輪播圖和FAB按鈕是在同一個viewgroup內的,每當輪播圖重新整理的時候,FAB按鈕就回到了原來的位置,所以在該種情況下,不適合用於view.layout()方式實現位移。解決方法未知……
  
  
  缺陷展示效果圖:
  這裡寫圖片描述


   方案2:呼叫MarginLayoutParams.setMargins()設定View的MarginLeft以及MarginTop屬性值從而確立View移動的實時位置。

  該方案可參考:Android-滿螢幕拖動的控制元件

  注意事項:這種方式比較適合RelativeLayout、FrameLayout,AbsoluteLayout,對於LinearLayout,因為最後增加的控制元件總在最下或最右,所以達不到移動效果,TableLayout也不行。並且你在XML裡設定的一些屬性值很可能會影響到View的移動。例如:你在XML檔案裡為FAB按鈕的初始位置設定了 android:layout_marginRight=”xxx” 或者landroid:layout_alignParentBottom=”true”屬性值。


   方案3使用view.setX()、view.setY()方式實現FAB按鈕的移動
    
  程式碼來自:安卓可拖拽懸浮按鈕二

public class DragFloatActionButton extends FloatingActionButton{

    private int parentHeight;     //父容器高度
    private int parentWidth;         //父容器寬度
    private int lastX;               
    private int lastY;
    private boolean isDrag;

    public DragFloatActionButton(Context context) {
        super(context);
    }

    public DragFloatActionButton(Context context, AttributeSet attrs) {
        super(context, attrs);

    }

    public DragFloatActionButton(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

    }

   @Override
    public boolean onTouchEvent(MotionEvent event) {
        int rawX = (int) event.getRawX();
        int rawY = (int) event.getRawY();
        switch (event.getAction() & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN:
                setPressed(true);             //設定該Button狀態為摁下 Pressed狀態
                isDrag = false;               //首次摁下時設定 正在拖拽?false

                //表示子元件要自己消費這次事件,告訴父元件不要攔截(搶走)這次的事件
                 getParent().requestDisallowInterceptTouchEvent(true); 
                lastX = rawX;
                lastY = rawY;       //記錄上次首次觸碰到螢幕時Button的座標  (lastX ,lastY)
                ViewGroup parent;
                if (getParent() != null) {                          //如果有父容器
                    parent = (ViewGroup) getParent();
                    parentHeight = parent.getHeight();            //獲取容器的高度
                    parentWidth = parent.getWidth();             //獲取容器的寬度
                }
                break;


 //如果沒有父容器直接退出,不實現拖拽移動,限定button只能在有父容器的情況下實現有範圍的移動
                if (parentHeight <= 0 || parentWidth == 0) {  

                    isDrag = false;                          //不能拖拽
                    break;                                  //直接退出
                } else {
                    isDrag = true;                        //正在拖拽
                }
                int dx = rawX - lastX;                  //獲取手勢X座標軸上的實時位移量
                int dy = rawY - lastY;                 //獲取手勢Y座標軸上的實時位移量


 //這裡修復一些華為手機無法觸發點選事件

                int distance = (int) Math.sqrt(dx * dx + dy * dy);
                if (distance == 0) {
                    isDrag = false;
                    break;

 //getX()當前view距離父容器左邊緣的距離 +dx 手勢X座標實時位移量 = 意圖當前view距離父容器左邊緣的距離                             
//getY()當前view距離父容器頂部邊緣的距離 +dy 手勢Y座標實時位移量 = 意圖view距離父容器頂部邊緣的距離

                float x = getX() + dx;  
                float y = getY() + dy;                     


//檢測是否到達邊緣 左上右下   並且限制view的移動範圍為其父容器範圍
//x < 0            表示向左移動超出父容器        
//此時設定 x = 0    繼續向左拖拽懸浮按鈕不移動
//x > parentWidth-getWidth()        表示向右移動超出父容器        
//此時設定 x = parentWidth-getWidth()    
//代表往X座標軸上右方向移動的最大偏移量為  父容器寬度-自身寬度  (因為Button的右邊也不能超出螢幕所以減去button寬度)

//getY() < 0       表示當前Button超出父容器上方    ->  
//令 y = 0   回到 父容器正上方
//getY()+ getHeight > parentHeight  表示  當前Button超出父容器下方  
//令 y = 父容器高度  -  Button高度   回到父容器正下方


                x = x < 0 ? 0 : x > parentWidth - getWidth() ? parentWidth - getWidth() : x;
                y = getY() < 0 ? 0 : getY() + getHeight() > parentHeight ? parentHeight - getHeight() : y;


                setX(x);           //設定當前view距離父容器左邊緣的距離
                setY(y);          // 設定當前view距離父容器左邊緣的距離
                lastX = rawX;
                lastY = rawY;
                break;
            case MotionEvent.ACTION_UP:
                if (isDrag) {
                    setPressed(false);
                }
                break;
        }

        //如果是拖拽則消耗事件,否則正常傳遞即可。
        return isDrag || super.onTouchEvent(event);
    }

  
   優點:這裡實現了FAB按鈕的拖拽移動功能,並且不會影響為FAB按鈕設定onClick事件,並且即使同一個viewgroup中有控制元件更新(介面重新整理)時,移動的控制元件也不會復位回到一開始的位置。
  
      這裡寫圖片描述

  沒時間寫完了。。。明天繼續寫
  還有很多細節要完善。。。。。。


參考: