Android - Movable/Draggable Floating Action Button (FAB)


Answer :

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.


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);     }  } 

Comments

Popular posts from this blog

Converting A String To Int In Groovy

"Cannot Create Cache Directory /home//.composer/cache/repo/https---packagist.org/, Or Directory Is Not Writable. Proceeding Without Cache"

Android How Can I Convert A String To A Editable