Android – Movable/Draggable Floating Action Button (FAB)-ThrowExceptions

Exception or error:

I am using a FloatingActionButton in my app. Occasionally, it overlaps essential content, so I would like to make it so the user can drag the FAB out of the way.

No drag and drop functionality, per se, is required. It just needs to be movable. The docs do not mention this, but I’m sure I’ve seen such functionality in other apps.

Can you anyone advise / provide a code snippet on how to do it (preferably in XML).

How to solve:

Based on this answer for another SO question this is the code I have created. It seems to work nicely (with working click functionality) and isn’t dependent on the FAB’s parent layout or positioning…

package com.example;

import android.content.Context;
import android.support.design.widget.FloatingActionButton;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;

public class MovableFloatingActionButton extends FloatingActionButton implements View.OnTouchListener {

    private final static float CLICK_DRAG_TOLERANCE = 10; // Often, there will be a slight, unintentional, drag when the user taps the FAB, so we need to account for this.

    private float downRawX, downRawY;
    private float dX, dY;

    public MovableFloatingActionButton(Context context) {
        super(context);
        init();
    }

    public MovableFloatingActionButton(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public MovableFloatingActionButton(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        setOnTouchListener(this);
    }

    @Override
    public boolean onTouch(View view, MotionEvent motionEvent){

        ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams)view.getLayoutParams();

        int action = motionEvent.getAction();
        if (action == MotionEvent.ACTION_DOWN) {

            downRawX = motionEvent.getRawX();
            downRawY = motionEvent.getRawY();
            dX = view.getX() - downRawX;
            dY = view.getY() - downRawY;

            return true; // Consumed

        }
        else if (action == MotionEvent.ACTION_MOVE) {

            int viewWidth = view.getWidth();
            int viewHeight = view.getHeight();

            View viewParent = (View)view.getParent();
            int parentWidth = viewParent.getWidth();
            int parentHeight = viewParent.getHeight();

            float newX = motionEvent.getRawX() + dX;
            newX = Math.max(layoutParams.leftMargin, newX); // Don't allow the FAB past the left hand side of the parent
            newX = Math.min(parentWidth - viewWidth - layoutParams.rightMargin, newX); // Don't allow the FAB past the right hand side of the parent

            float newY = motionEvent.getRawY() + dY;
            newY = Math.max(layoutParams.topMargin, newY); // Don't allow the FAB past the top of the parent
            newY = Math.min(parentHeight - viewHeight - layoutParams.bottomMargin, newY); // Don't allow the FAB past the bottom of the parent

            view.animate()
                    .x(newX)
                    .y(newY)
                    .setDuration(0)
                    .start();

            return true; // Consumed

        }
        else if (action == MotionEvent.ACTION_UP) {

            float upRawX = motionEvent.getRawX();
            float upRawY = motionEvent.getRawY();

            float upDX = upRawX - downRawX;
            float upDY = upRawY - downRawY;

            if (Math.abs(upDX) < CLICK_DRAG_TOLERANCE && Math.abs(upDY) < CLICK_DRAG_TOLERANCE) { // A click
                return performClick();
            }
            else { // A drag
                return true; // Consumed
            }

        }
        else {
            return super.onTouchEvent(motionEvent);
        }

    }

}

And here is the XML…

    <com.example.MovableFloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="@dimen/fab_margin"
        android:src="@drawable/ic_navigate_next_white_24dp"/>

Basically, you just need to replace android.support.design.widget.FloatingActionButton with com.example.MovableFloatingActionButton in your XML.

###

Try this:

public class MainActivity extends AppCompatActivity implements View.OnTouchListener {
  float dX;
  float dY;
  int lastAction;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    final View dragView = findViewById(R.id.draggable_view);
    dragView.setOnTouchListener(this);
  }

  @Override
  public boolean onTouch(View view, MotionEvent event) {
    switch (event.getActionMasked()) {
      case MotionEvent.ACTION_DOWN:
        dX = view.getX() - event.getRawX();
        dY = view.getY() - event.getRawY();
        lastAction = MotionEvent.ACTION_DOWN;
        break;

      case MotionEvent.ACTION_MOVE:
        view.setY(event.getRawY() + dY);
        view.setX(event.getRawX() + dX);
        lastAction = MotionEvent.ACTION_MOVE;
        break;

      case MotionEvent.ACTION_UP:
        if (lastAction == MotionEvent.ACTION_DOWN)
          Toast.makeText(DraggableView.this, "Clicked!", Toast.LENGTH_SHORT).show();
        break;

      default:
        return false;
    }
    return true;
  }
}

And the XML:

<ImageButton
        android:id="@+id/draggable_view"
        android:background="@mipmap/ic_launcher"
        android:layout_gravity="bottom|right"
        android:layout_marginBottom="20dp"
        android:layout_marginEnd="20dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

You can make any View Draggable and Clickable.

###

you can try like below by just impletementing onTouch on any View,

xml

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:id="@+id/rootlayout"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</FrameLayout>

java

public class dragativity extends AppCompatActivity implements View.OnTouchListener{

    FloatingActionButton fab;

    FrameLayout rootlayout;

     int _xDelta;
     int _yDelta;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.drag);

        rootlayout = (FrameLayout) findViewById(R.id.rootlayout);

        fab = (FloatingActionButton) findViewById(R.id.fab);

        FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(150, 150);
        fab.setLayoutParams(layoutParams);
        fab.setOnTouchListener(dragativity.this);
    }

    public boolean onTouch(View view, MotionEvent event) {
        final int X = (int) event.getRawX();
        final int Y = (int) event.getRawY();
        switch (event.getAction() & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN:
                FrameLayout.LayoutParams lParams = (FrameLayout.LayoutParams) view.getLayoutParams();
                _xDelta = X - lParams.leftMargin;
                _yDelta = Y - lParams.topMargin;
                break;
            case MotionEvent.ACTION_UP:
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                break;
            case MotionEvent.ACTION_POINTER_UP:
                break;
            case MotionEvent.ACTION_MOVE:
                FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) view
                        .getLayoutParams();
                layoutParams.leftMargin = X - _xDelta;
                layoutParams.topMargin = Y - _yDelta;
                layoutParams.rightMargin = -250;
                layoutParams.bottomMargin = -250;
                view.setLayoutParams(layoutParams);
                break;
        }
        rootlayout.invalidate();
        return true;
    }


}

###

actually you can just use android.support.design.widget.CoordinatorLayout instead of Relative layout or any other layout and this will work(moving FAB)

###

Based on @ban-geoengineering answer I updated as perform ripple effect and left and right gravity like faceebook chat bubble. I created custom click listener cuz if consume touch event inside this code block, ripple effect doesnt work clearly.

    <com.sample.DraggableFloatingActionButton
    android:id="@+id/connect_to_support_fab"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom|end"
    android:layout_marginLeft="@dimen/spacing_10pt"
    android:layout_marginRight="@dimen/spacing_10pt"
    android:layout_marginBottom="@dimen/spacing_16pt"
    android:clickable="true"
    android:focusable="true"
    app:backgroundTint="@color/colorGreen"
    app:fabSize="normal"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:rippleColor="@color/colorWhite"
    app:srcCompat="@drawable/ic_live_support"
    app:tint="@color/colorWhite" />
package com.sample;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.OvershootInterpolator;

import com.google.android.material.floatingactionbutton.FloatingActionButton;

public class DraggableFloatingActionButton extends FloatingActionButton implements View.OnTouchListener {
    CustomClickListener customClickListener;

    private final static float CLICK_DRAG_TOLERANCE = 10; // Often, there will be a slight, unintentional, drag when the user taps the FAB, so we need to account for this.

    private float downRawX, downRawY;
    private float dX, dY;

    int viewWidth;
    int viewHeight;

    int parentWidth;
    int parentHeight;

    float newX;
    float newY;

    public DraggableFloatingActionButton(Context context) {
        super(context);
        init();
    }

    public DraggableFloatingActionButton(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public DraggableFloatingActionButton(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        setOnTouchListener(this);
    }

    @Override
    public boolean onTouch(View view, MotionEvent motionEvent) {

        ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) view.getLayoutParams();

        int action = motionEvent.getAction();
        if (action == MotionEvent.ACTION_DOWN) {

            downRawX = motionEvent.getRawX();
            downRawY = motionEvent.getRawY();
            dX = view.getX() - downRawX;
            dY = view.getY() - downRawY;

            return false; // not Consumed for ripple effect

        } else if (action == MotionEvent.ACTION_MOVE) {

            viewWidth = view.getWidth();
            viewHeight = view.getHeight();

            View viewParent = (View) view.getParent();
            parentWidth = viewParent.getWidth();
            parentHeight = viewParent.getHeight();

            newX = motionEvent.getRawX() + dX;
            newX = Math.max(layoutParams.leftMargin, newX); // Don't allow the FAB past the left hand side of the parent
            newX = Math.min(parentWidth - viewWidth - layoutParams.rightMargin, newX); // Don't allow the FAB past the right hand side of the parent

            newY = motionEvent.getRawY() + dY;
            newY = Math.max(layoutParams.topMargin, newY); // Don't allow the FAB past the top of the parent
            newY = Math.min(parentHeight - viewHeight - layoutParams.bottomMargin, newY); // Don't allow the FAB past the bottom of the parent

            view.animate()
                    .x(newX)
                    .y(newY)
                    .setDuration(0)
                    .start();

            return true; // Consumed

        } else if (action == MotionEvent.ACTION_UP) {

            float upRawX = motionEvent.getRawX();
            float upRawY = motionEvent.getRawY();

            float upDX = upRawX - downRawX;
            float upDY = upRawY - downRawY;

            if (newX > ((parentWidth - viewWidth - layoutParams.rightMargin) / 2)) {
                newX = parentWidth - viewWidth - layoutParams.rightMargin;
            } else {
                newX = layoutParams.leftMargin;
            }

            view.animate()
                    .x(newX)
                    .y(newY)
                    .setInterpolator(new OvershootInterpolator())
                    .setDuration(300)
                    .start();

            if (Math.abs(upDX) < CLICK_DRAG_TOLERANCE && Math.abs(upDY) < CLICK_DRAG_TOLERANCE) { // A click
                if (customClickListener != null) {
                    customClickListener.onClick(view);
                }
                return false;// not Consumed for ripple effect
            } else { // A drag
                return false; // not Consumed for ripple effect
            }

        } else {
            return super.onTouchEvent(motionEvent);
        }

    }

    public void setCustomClickListener(CustomClickListener customClickListener) {
        this.customClickListener = customClickListener;
    }

    public interface CustomClickListener {
        void onClick(View view);
    }

}

###

All proposed answers used OnTouch listener, which is not recommended by recent Android API because of the Accessibility implementations. Note also that startDrag() method is obsolete. Developers shoud use startDragAndDrop() instead.
My implementation uses OnDragListener() as follows:

  1. Define two global float variables dX and dY;
  2. Put the below snippet inside onCreatView() method, where root is root view, taken by the inflater (or any other view, which can receive Drop events);

    final FloatingActionButton fab = root.findViewById(R.id.my_fab);
    
    fab.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            // Do whatever this button will do on click event
        }
    });
    
    root.setOnDragListener(new View.OnDragListener() {
        @Override
        public boolean onDrag(View v, DragEvent event) {
            switch (event.getAction()) {
                case DragEvent.ACTION_DRAG_LOCATION:
                    dX = event.getX();
                    dY = event.getY();
                    break;
                case DragEvent.ACTION_DRAG_ENDED:
                    fab.setX(dX-fab.getWidth()/2);
                    fab.setY(dY-fab.getHeight()/2);
                    break;
            }
            return true;
        }
    });
    
    
    fab.setOnLongClickListener(new View.OnLongClickListener() {
        @Override
        public boolean onLongClick(View v) {
            View.DragShadowBuilder myShadow = new View.DragShadowBuilder(fab);
            v.startDragAndDrop(null, myShadow, null, View.DRAG_FLAG_GLOBAL);
            return true;
        }
    });
    

###

This is the listener that worked for me, with a tolerance of 70.

private class FloatingOnTouchListener implements View.OnTouchListener {
        private float x;
        private float y;
        private float nowX;
        private float nowY;
        private float downX;
        private float downY;
        private final int tolerance = 70;

        @Override
        public boolean onTouch(View view, MotionEvent event) {
            if (event.getAction() == MotionEvent.ACTION_DOWN) {
                x = (int) event.getRawX();
                y = (int) event.getRawY();
                downX = x;
                downY = y;
            } else
            if (event.getAction() == MotionEvent.ACTION_MOVE) {
                nowX = event.getRawX();
                nowY = event.getRawY();
                float movedX = nowX - x;
                float movedY = nowY - y;
                x = nowX;
                y = nowY;
                iconViewLayoutParams.x = iconViewLayoutParams.x + (int) movedX;
                iconViewLayoutParams.y = iconViewLayoutParams.y + (int) movedY;
                windowManager.updateViewLayout(view, iconViewLayoutParams);
            } else
            if (event.getAction() == MotionEvent.ACTION_UP) {
                float dx = Math.abs(nowX - downX);
                float dy = Math.abs(nowY - downY);
                if (dx < tolerance && dy < tolerance) {
                    Log.d(TAG, "clicou");
                    Log.d(TAG, "dx " + dx);
                    Log.d(TAG, "dy " + dy);
                    windowManager.removeViewImmediate(iconView);
                    windowManager.addView(displayView, layoutParams);
                } else {
                    Log.d(TAG, "dx " + dx);
                    Log.d(TAG, "dy " + dy);
                    return true;
                }
            }
            return true;
        }
    }

###

You can try this code
XML

 <com.google.android.material.floatingactionbutton.FloatingActionButton
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginEnd="8dp"
    android:layout_marginRight="8dp"
    android:id="@+id/dashboardShowActionsFab"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent" />

JAVA

fab.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View view, MotionEvent motionEvent) {


            ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) view.getLayoutParams();

            int action = motionEvent.getAction();
            if (action == MotionEvent.ACTION_DOWN) {

                downRawX = motionEvent.getRawX();
                downRawY = motionEvent.getRawY();
                dX = view.getX() - downRawX;
                dY = view.getY() - downRawY;

                return true; // Consumed

            } else if (action == MotionEvent.ACTION_MOVE) {

                int viewWidth = view.getWidth();
                int viewHeight = view.getHeight();

                View viewParent = (View) view.getParent();
                int parentWidth = viewParent.getWidth();
                int parentHeight = viewParent.getHeight();

                float newX = motionEvent.getRawX() + dX;
                newX = Math.max(layoutParams.leftMargin, newX); // Don't allow the FAB past the left hand side of the parent
                newX = Math.min(parentWidth - viewWidth - layoutParams.rightMargin, newX); // Don't allow the FAB past the right hand side of the parent

                float newY = motionEvent.getRawY() + dY;
                newY = Math.max(layoutParams.topMargin, newY); // Don't allow the FAB past the top of the parent
                newY = Math.min(parentHeight - viewHeight - layoutParams.bottomMargin, newY); // Don't allow the FAB past the bottom of the parent

                view.animate()
                        .x(newX)
                        .y(newY)
                        .setDuration(0)
                        .start();

                return true; // Consumed

            } else if (action == MotionEvent.ACTION_UP) {

                float upRawX = motionEvent.getRawX();
                float upRawY = motionEvent.getRawY();

                float upDX = upRawX - downRawX;
                float upDY = upRawY - downRawY;

                if (Math.abs(upDX) < CLICK_DRAG_TOLERANCE && Math.abs(upDY) < CLICK_DRAG_TOLERANCE) { // A click
                    // return performClick();
                    Toast.makeText(MainActivity.this, "clicked", Toast.LENGTH_SHORT).show();

                } else { // A drag
                    return true; // Consumed
                }

            } else {
                //return super.onTouchEvent(motionEvent);
            }

            return true;
        }

###

Here is a slightly updated version. It handles the ripple effect correctly, at least it did the trick for me.

public MovableFloatingActionButton(Context context) {
    super(context);
    init();
}

public MovableFloatingActionButton(Context context, AttributeSet attrs) {
    super(context, attrs);
    init();
}

public MovableFloatingActionButton(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init();
}

private void init() {
    setOnTouchListener(this);
}

@Override
public boolean onTouch(View view, MotionEvent motionEvent){
    ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams)view.getLayoutParams();

    switch (motionEvent.getActionMasked()) {
        case MotionEvent.ACTION_DOWN:
            downRawX = motionEvent.getRawX();
            downRawY = motionEvent.getRawY();
            dX = view.getX() - downRawX;
            dY = view.getY() - downRawY;
            return super.onTouchEvent(motionEvent);

        case MotionEvent.ACTION_MOVE:
            int viewWidth = view.getWidth();
            int viewHeight = view.getHeight();

            View viewParent = (View)view.getParent();
            int parentWidth = viewParent.getWidth();
            int parentHeight = viewParent.getHeight();

            float newX = motionEvent.getRawX() + dX;
            newX = Math.max(layoutParams.leftMargin, newX);
            newX = Math.min(parentWidth - viewWidth - layoutParams.rightMargin, newX);

            float newY = motionEvent.getRawY() + dY;
            newY = Math.max(layoutParams.topMargin, newY);
            newY = Math.min(parentHeight - viewHeight - layoutParams.bottomMargin, newY);

            view.animate().x(newX).y(newY).setDuration(0).start();
            setPressed(false);
            return true;

        case MotionEvent.ACTION_UP:
            final float upRawX = motionEvent.getRawX();
            final float upRawY = motionEvent.getRawY();

            final float upDX = upRawX - downRawX;
            final float upDY = upRawY - downRawY;

            final boolean isDrag = Math.abs(upDX) >= CLICK_DRAG_TOLERANCE || Math.abs(upDY) >= CLICK_DRAG_TOLERANCE;
            return isDrag || performClick();

        default:
            return super.onTouchEvent(motionEvent);

    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *