Android 自定义可拖动悬浮按钮

1,258 阅读2分钟
需求:业务要求在页面中实现可以拖动并且支持自定义样式的悬浮图标如图:

微信图片_20230104172823.jpg

分析:1、支持自定义样式 2、支持拖动 3、支持拖动范围

1、支持自定义样式,我采用继承FrameLayout的方式,需要什么组件在xml中包裹即可 2、拖动在motionEvent中实现 3、拖动范围在拖动的时候动态实现

代码实现

package ***************************;

import android.content.Context;
import android.content.res.Configuration;
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.FrameLayout;

import com.cdrcbperson.mobilebank.R;


public class FloatingMagnetView extends FrameLayout {

    public static final int MARGIN_EDGE = 0;
    private float mOriginalRawX;
    private float mOriginalRawY;
    private float mOriginalX;
    private float mOriginalY;
    private MagnetViewListener mMagnetViewListener;
    private static final int TOUCH_TIME_THRESHOLD = 150;
    private long mLastTouchDownTime;
    protected MoveAnimator mMoveAnimator;
    protected int mScreenWidth;
    private int mScreenHeight;
    private int mStatusBarHeight;
    private boolean isNearestLeft = true;
    private float mPortraitY;
    private boolean dragEnable = true;
    private boolean autoMoveToEdge = true;

    private float originX;
    private float originY;

    private float moveX;
    private float moveY;
    private int scrollMarginTopDy = 0;
    private int scrollMarginBottomDy = 0;

    public void setMagnetViewListener(MagnetViewListener magnetViewListener) {
        this.mMagnetViewListener = magnetViewListener;
    }

    public FloatingMagnetView(Context context) {
        this(context, null);
    }

    public FloatingMagnetView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

    private void init() {
        mMoveAnimator = new MoveAnimator();
        mStatusBarHeight = getStatusBarHeight();
        setClickable(true);
//        updateSize();
    }

    /**
     * 设置可以拖动到上边缘的距离
     */
    public void setScrollTopMargin(int dy) {
        this.scrollMarginTopDy = dy;
    }

    /**
     * 设置可以拖动到下边缘的距离
     */
    public void setScrollBottomMargin(int dy) {
        this.scrollMarginBottomDy = dy;
    }

    /**
     * @param dragEnable 是否可拖动
     */
    public void updateDragState(boolean dragEnable) {
        this.dragEnable = dragEnable;
    }

    /**
     * @param autoMoveToEdge 是否自动到边缘
     */
    public void setAutoMoveToEdge(boolean autoMoveToEdge) {
        this.autoMoveToEdge = autoMoveToEdge;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event == null) {
            return false;
        }
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //记录下当前按下点的坐标相对父容器的位置
                originY = event.getY();
                originX = event.getX();

                dealDownEvent();
                break;
            case MotionEvent.ACTION_MOVE:
                updateViewPosition(event);
                break;
            case MotionEvent.ACTION_UP:
                clearPortraitY();
                if (autoMoveToEdge) {
                    moveToEdge();
                }
                if (isOnClickEvent()) {
                    dealClickEvent();
                } else {
                    dealUpEvent();
                }
                break;
        }
        return true;
    }

    protected void dealUpEvent() {
        if (mMagnetViewListener != null) {
            mMagnetViewListener.onUp(this);
        }
    }

    protected void dealClickEvent() {
        if (mMagnetViewListener != null) {
            mMagnetViewListener.onClick(this);
        }
    }

    protected void dealDownEvent() {
        if (mMagnetViewListener != null) {
            mMagnetViewListener.onDown(this);
        }
    }

    protected boolean isOnClickEvent() {
        return System.currentTimeMillis() - mLastTouchDownTime < TOUCH_TIME_THRESHOLD;
    }

    private void updateViewPosition(MotionEvent event) {
        //dragEnable
        if (!dragEnable) return;

//    *********************移动ViewGroup方法一 :layout *****************/
//        float y = event.getY();
//        float x = event.getX();
//
//        moveX = x - originX;
//        moveY = y - originY;
//
//        //view移动前的上下左右位置
//        float left = getLeft() + moveX;
//        float top = getTop() + moveY;
//        float right = getRight() + moveX;
//        float bottom = getBottom() + moveY;
//
//        if ((left) < 0) {
//            left = 0;
//            right = getWidth();
//        }
//
//        if (top < mStatusBarHeight) {
//            top = mStatusBarHeight;
//            bottom = getHeight()+mStatusBarHeight;
//        }
//        layout((int) (left), (int) (top), (int) (right), (int) (bottom));


//        **********************移动ViewGroup方法二:setX,setY ******************//
        //限制不可超出屏幕宽度
        float desX = mOriginalX + event.getRawX() - mOriginalRawX;
        if (desX < 0) {
            desX = MARGIN_EDGE;
        }
        if (desX > mScreenWidth) {
            desX = mScreenWidth - MARGIN_EDGE;
        }
        //这儿如果背景是正常的圆形可以不用设置,由于项目需求悬浮图标背景是不规则的拖动到左边
        //吸附的时候需要设置另外的样式,所以单独做了处理
        if (desX < mScreenWidth / 2) {
            //当拖到距离到屏幕左半部分改变位置重新设置样式
            setBackground(getResources().getDrawable(R.drawable.a_key_card_shadow_bg5_corner2));
            setPadding(30, 16, 15, 16);
        } else {
            setBackground(getResources().getDrawable(R.drawable.a_key_card_shadow_bg5_corner));
            setPadding(15, 16, 30, 16);
        }
        setX(desX);

        // 限制不可超出屏幕高度
        float desY = mOriginalY + event.getRawY() - mOriginalRawY;
        //scrollMarginTopDy 拖动到上边距最大距离
        if (desY < mStatusBarHeight + scrollMarginTopDy) {
            desY = mStatusBarHeight + scrollMarginTopDy;
        }
        //scrollMarginTopDy 拖动到上边距最大距离
        if (desY > mScreenHeight - getHeight() - scrollMarginBottomDy) {
            desY = mScreenHeight - getHeight() - scrollMarginBottomDy;
        }
        setY(desY);
    }


    private void changeOriginalTouchParams(MotionEvent event) {
        mOriginalX = getX();
        mOriginalY = getY();
        mOriginalRawX = event.getRawX();
        mOriginalRawY = event.getRawY();
        mLastTouchDownTime = System.currentTimeMillis();
    }

    protected void updateSize() {
        ViewGroup viewGroup = (ViewGroup) getParent();
        if (viewGroup != null) {
            mScreenWidth = viewGroup.getWidth() - getWidth();
            mScreenHeight = viewGroup.getHeight();
        }
//        mScreenWidth = (SystemUtils.getScreenWidth(getContext()) - this.getWidth());
//        mScreenHeight = SystemUtils.getScreenHeight(getContext());
    }

    public void moveToEdge() {
        //dragEnable
        if (!dragEnable) return;
        moveToEdge(isNearestLeft(), false);
    }

    public void moveToEdge(boolean isLeft, boolean isLandscape) {
        float moveDistance = isLeft ? MARGIN_EDGE : mScreenWidth - MARGIN_EDGE;
        float y = getY();
        if (!isLandscape && mPortraitY != 0) {
            y = mPortraitY;
            clearPortraitY();
        }
        mMoveAnimator.start(moveDistance, Math.min(Math.max(0, y), mScreenHeight - getHeight()));
    }

    private void clearPortraitY() {
        mPortraitY = 0;
    }

    protected boolean isNearestLeft() {
        int middle = mScreenWidth / 2;
        isNearestLeft = getX() < middle;
        return isNearestLeft;
    }

    public void onRemove() {
        if (mMagnetViewListener != null) {
            mMagnetViewListener.onRemove(this);
        }
    }

    //最赞的,用最基础的代码实现了动画。handler+setX()
    protected class MoveAnimator implements Runnable {
        private Handler handler = new Handler(Looper.getMainLooper());
        private float destinationX;
        private float destinationY;
        private long startingTime;

        void start(float x, float y) {
            this.destinationX = x;
            this.destinationY = y;
            startingTime = System.currentTimeMillis();
            handler.post(this);
        }

        @Override
        public void run() {
            if (getRootView() == null || getRootView().getParent() == null) {
                return;
            }
            //400ms
            float progress = Math.min(1, (System.currentTimeMillis() - startingTime) / 400f);
            float deltaX = (destinationX - getX()) * progress;
            float deltaY = (destinationY - getY()) * progress;
            move(deltaX, deltaY);
            if (progress < 1) {
                handler.post(this);
            }
        }

        private void stop() {
            handler.removeCallbacks(this);
        }
    }

    //最基础的代码实现的动画,通过handler 不断的 setX,setY来移动位置
    private void move(float deltaX, float deltaY) {
        setX(getX() + deltaX);
        setY(getY() + deltaY);
    }

    @Override
    protected void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        if (getParent() != null) {
            final boolean isLandscape = newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE;
            markPortraitY(isLandscape);
            ((ViewGroup) getParent()).post(new Runnable() {
                @Override
                public void run() {
                    updateSize();
                    moveToEdge(isNearestLeft, isLandscape);
                }
            });
        }
    }

    private void markPortraitY(boolean isLandscape) {
        if (isLandscape) {
            mPortraitY = getY();
        }
    }

    private float touchDownX;

    private void initTouchDown(MotionEvent ev) {
        changeOriginalTouchParams(ev);
        updateSize();
        mMoveAnimator.stop();
    }

    //判断是否拦截父容器
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = false;
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                intercepted = false;
                touchDownX = ev.getX();
                initTouchDown(ev);
                break;
            case MotionEvent.ACTION_MOVE:
                intercepted = Math.abs(touchDownX - ev.getX()) >= ViewConfiguration.get(getContext()).getScaledTouchSlop();
                break;
            case MotionEvent.ACTION_UP:
                intercepted = false;
                break;
        }
        return intercepted;
    }

    /**
     * 获取状态栏的高度
     *
     * @return 状态栏高度
     */
    public int getStatusBarHeight() {
        int result = 0;
        int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
        if (resourceId > 0) {
            result = getResources().getDimensionPixelSize(resourceId);
        }
        return result;
    }

    public interface MagnetViewListener {
        void onUp(FloatingMagnetView floatingMagnetView);

        void onClick(FloatingMagnetView floatingMagnetView);

        void onDown(FloatingMagnetView floatingMagnetView);

        void onRemove(FloatingMagnetView floatingMagnetView);

    }
}
在xml中使用:
<com.cdrcbperson.mobilebank.view.FloatingMagnetView
    android:id="@+id/rlRootSearch"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:layout_alignParentEnd="true"
    android:layout_marginRight="-5dp"
    android:paddingLeft="10dp"
    android:paddingRight="@dimen/dp_15"
    android:paddingTop="8dp"
    android:paddingBottom="8dp"
    android:layout_marginBottom="@dimen/dp_24"
    android:background="@drawable/a_key_card_shadow_bg5_corner">
    //这儿可以写自己需要的view 可以是任意组件
    <ImageView
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:src="@drawable/icon_search_old"/>

</com.cdrcbperson.mobilebank.view.FloatingMagnetView>
在mainactivity中使用

floatingMagnetView.post(new Runnable() {
    @Override
    public void run() {
    //这儿需要注意一下,如果需要设置按钮只能滑动到某个view的底部,需要开一个线程拿到view底部距离
    //否则由于没有渲染出来,拿到的值会为空
        floatingMagnetView.setScrollTopMargin(llLayoutTitle.getBottom()-
                ScreenUtil.getStatusBarHeight(OldMainActivity.this));
    }
});
//设置可以滑动到底部的距离
floatingMagnetView.setScrollBottomMargin(DensityUtil.dp2px(24));
floatingMagnetView.setMagnetViewListener(new FloatingMagnetView.MagnetViewListener() {
    @Override
    public void onUp(FloatingMagnetView floatingMagnetView) {

    }

    @Override
    public void onClick(FloatingMagnetView floatingMagnetView) {
        //点击事件
        Redirect.openActivity(OldMainActivity.this, ZNZSRobotChatActivity.class);
    }

    @Override
    public void onDown(FloatingMagnetView floatingMagnetView) {

    }

    @Override
    public void onRemove(FloatingMagnetView floatingMagnetView) {

    }
});