Reputation: 18911
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).
Upvotes: 34
Views: 40112
Reputation: 162
Floating Action button Movable everywhere in java code
flBtnCallify.setOnTouchListener(new View.OnTouchListener() {
float dX;
float dY;
float startX;
float startY;
int lastAction;
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
dX = v.getX() - event.getRawX();
dY = v.getY() - event.getRawY();
startX = event.getRawX();
startY = event.getRawY();
lastAction = MotionEvent.ACTION_DOWN;
break;
case MotionEvent.ACTION_MOVE:
v.setY(event.getRawY() + dY);
v.setX(event.getRawX() + dX);
lastAction = MotionEvent.ACTION_MOVE;
break;
case MotionEvent.ACTION_UP:
if (Math.abs(startX - event.getRawX()) < 10 && Math.abs(startY - event.getRawY()) < 10){
Toast.makeText(v.getContext(), "Clicked!", Toast.LENGTH_SHORT).show();
}
break;
default:
return false;
}
return false;
}
});
Upvotes: 0
Reputation: 1420
Floating button from function in android
private static float dX,dY;
private static float downRawX, downRawY;
private final static float CLICK_DRAG_TOLERANCE = 10;
public static void setfab(Activity activity){
FloatingActionButton fab = new FlyyFloatingButton(activity);
LinearLayout layout = new LinearLayout(activity);
LinearLayout.LayoutParams parm = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
fab.setImageResource(R.drawable.ic_rewards);
fab.setBackgroundTintList(ColorStateList.valueOf(Color.BLUE));
fab.setFocusable(true);
fab.setSize(FloatingActionButton.SIZE_AUTO);
layout.addView(fab);
View viewParent = (View) fab.getParent();
activity.addContentView(layout,parm);
fab.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent){
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(0, newX); // Don't allow the FAB past the left hand side of the parent
newX = Math.min(parentWidth - viewWidth, newX); // Don't allow the FAB past the right hand side of the parent
float newY = motionEvent.getRawY() + dY;
newY = Math.max(0, newY); // Don't allow the FAB past the top of the parent
newY = Math.min(parentHeight - viewHeight, 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 view.performClick();
}
else { // A drag
return true; // Consumed
}
}
else {
return view.onTouchEvent(motionEvent);
}
}
});
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(context, "Hello Floating Action Button", Toast.LENGTH_SHORT).show();
}
});
}
Upvotes: 0
Reputation: 39
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);
}
}
Upvotes: -1
Reputation: 1870
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;
}
}
Upvotes: 0
Reputation: 21
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;
}
Upvotes: -1
Reputation: 18911
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.
Upvotes: 78
Reputation: 101
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:
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;
}
});
Upvotes: 4
Reputation: 81
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);
}
}
Upvotes: 5
Reputation: 1
actually you can just use android.support.design.widget.CoordinatorLayout instead of Relative layout or any other layout and this will work(moving FAB)
Upvotes: 0
Reputation: 1500
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.
Upvotes: 6
Reputation: 875
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;
}
}
Upvotes: 0