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
Post a Comment