Android - Movable/Draggable Floating Action Button (FAB)

后端 未结 9 1643
温柔的废话
温柔的废话 2020-12-28 14:47

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

相关标签:
9条回答
  • 2020-12-28 15:23

    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);
        }
    
    }
    
    0 讨论(0)
  • 2020-12-28 15:26

    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;
        }
    
    
    }
    
    0 讨论(0)
  • 2020-12-28 15:27

    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);
    
        }
    }
    
    0 讨论(0)
  • 2020-12-28 15:28

    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;
            }
        }
    
    0 讨论(0)
  • 2020-12-28 15:33

    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.

    0 讨论(0)
  • 2020-12-28 15:33

    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.

    0 讨论(0)
提交回复
热议问题