Android进阶之路 - 可拖拽的悬浮按钮

类似文章在CSDN上有很多,但是几经查找之后原文其实产于简书的一位作者;

综合几篇文章,在原有基础上我会尽可能全面总结一下

效果图

Android进阶之路 - 可拖拽的悬浮按钮_第1张图片

实现思路

  1. 通过重写控件的onTouchEvent方法监听触摸效果
  2. 通过View的setX()和setY()方法实现移动
  3. 使用属性动画实现边缘吸附效果
手指按下

首先是处理手指按压下的事件,这里我们把拖拽标识符设置为false并记录当前点击的屏幕坐标。然后我们在移动事件处

手指移动

这里我们把拖拽标识符设置为true,因为手指移动了。然后我们需要计算手指移动了多少偏移量

//计算手指移动了多少
int dx=rawX-lastX;
int dy=rawY-lastY;

而后的两行代码表示控件需要移动的具体距离,后面有一个简单的边缘检测计算。最终通过调用setX以及setY方法实现控件的移动

手指松开

这里如果是拖拽动作我们才需要处理自己的逻辑否则直接跳过即可。在这里我们首先恢复了按钮的按压效果,在源代码中找到setPressed(boolean)方法,这是处理按钮点击效果用的,在这里当手指松开后我们需要恢复按钮原来的效果。然后在判断控件需要往哪边吸附,吸附的过程就是做属性动画而已,原理还是不断的改变setX方法让按钮靠边移动

代码区

关于自定义的拖拽View (核心部分)

继承类(均可实现拖拽)

  • 有的继承AppCompatImageView下的ImageView
  • 有的继承FloatingActionButton(如果继承此类需要导入以下依赖)
compile 'com.android.support:design:26.1.0'
implementation 'com.android.support:appcompat-v7:26.1.0'
DragFloatActionButton (java版本)
package com.advance.yongliu.haulview;

import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.widget.ImageView;

/**
 * author  YongLiu
 * date  2019/2/22.
 * desc:
 */

@SuppressLint("AppCompatCustomView")
public class DragFloatActionButton extends ImageView {
    private int parentHeight;
    private int parentWidth;

    public DragFloatActionButtonJava(Context context) {
        super(context);
    }

    public DragFloatActionButtonJava(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

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

    private int lastX;
    private int lastY;

    private boolean isDrag;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int rawX = (int) event.getRawX();
        int rawY = (int) event.getRawY();
        switch (event.getAction() & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN:
                setPressed(true);
                isDrag = false;
                getParent().requestDisallowInterceptTouchEvent(true);
                lastX = rawX;
                lastY = rawY;
                ViewGroup parent;
                if (getParent() != null) {
                    parent = (ViewGroup) getParent();
                    parentHeight = parent.getHeight();
                    parentWidth = parent.getWidth();
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (parentHeight <= 0 || parentWidth == 0) {
                    isDrag = false;
                    break;
                } else {
                    isDrag = true;
                }
                int dx = rawX - lastX;
                int dy = rawY - lastY;
                //这里修复一些华为手机无法触发点击事件
                int distance = (int) Math.sqrt(dx * dx + dy * dy);
                if (distance == 0) {
                    isDrag = false;
                    break;
                }
                float x = getX() + dx;
                float y = getY() + dy;
                //检测是否到达边缘 左上右下
                x = x < 0 ? 0 : x > parentWidth - getWidth() ? parentWidth - getWidth() : x;
                y = getY() < 0 ? 0 : getY() + getHeight() > parentHeight ? parentHeight - getHeight() : y;
                setX(x);
                setY(y);
                lastX = rawX;
                lastY = rawY;
                Log.i("aa", "isDrag=" + isDrag + "getX=" + getX() + ";getY=" + getY() + ";parentWidth=" + parentWidth);
                break;
            case MotionEvent.ACTION_UP:
                if (!isNotDrag()) {
                    //恢复按压效果
                    setPressed(false);
                    if (rawX >= parentWidth / 2) {
                        //靠右吸附
                        animate().setInterpolator(new DecelerateInterpolator())
                                .setDuration(500)
                                .xBy(parentWidth - getWidth() - getX())
                                .start();
                    } else {
                        //靠左吸附
                        ObjectAnimator oa = ObjectAnimator.ofFloat(this, "x", getX(), 0);
                        oa.setInterpolator(new DecelerateInterpolator());
                        oa.setDuration(500);
                        oa.start();
                    }
                }
                break;
            default:
                break;
        }
        //如果是拖拽则消s耗事件,否则正常传递即可。
        return !isNotDrag() || super.onTouchEvent(event);
    }

    private boolean isNotDrag() {
        return !isDrag && (getX() == 0|| (getX() == parentWidth - getWidth()));
    }
}

使用部分
MainActivity
package com.advance.yongliu.haulview;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        DragFloatActionButtonJava mBtn = findViewById(R.id.img_btn);
        mBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(MainActivity.this,"点击了可拖拽按钮的点击事件",Toast.LENGTH_SHORT).show();
            }
        });
    }
}

MainActivity Xml



    



DragFloatActionButton (kotlin版本)
/**
 * 可拖拽的悬浮控件按钮
 * 2018-08-27
 * 直接xml布局里引用即可。
 * 要设置setOnClickListener点击事件,即可实现拖拽和点击功能。
 * 尺寸大小,样式及背景图片遵循ImageView即可。
 */
class DragFloatActionButton : ImageView {
    private var parentHeight: Int = 0
    private var parentWidth: Int = 0
 
    constructor(context: Context?) : super(context)
    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
 
    private var lastX: Int = 0
    private var lastY: Int = 0
 
    private var isDrag: Boolean = false
 
    override fun onTouchEvent(event: MotionEvent): Boolean {
        val rawX = event.rawX.toInt()
        val rawY = event.rawY.toInt()
        when (event.action and MotionEvent.ACTION_MASK) {
            MotionEvent.ACTION_DOWN -> {
                isPressed = true
                isDrag = false
                parent.requestDisallowInterceptTouchEvent(true)
                lastX = rawX
                lastY = rawY
                val parent: ViewGroup
                if (getParent() != null) {
                    parent = getParent() as ViewGroup
                    parentHeight = parent.height
                    parentWidth = parent.width
                }
            }
            MotionEvent.ACTION_MOVE -> {
                if (parentHeight <= 0 || parentWidth === 0) {
                    isDrag = false
                } else {
                    isDrag = true
                }
                val dx = rawX - lastX
                val dy = rawY - lastY
                //这里修复一些华为手机无法触发点击事件
                val distance = Math.sqrt((dx * dx + dy * dy).toDouble()).toInt()
                if (distance == 0) {
                    isDrag = false
                } else {
                    var x = x + dx
                    var y = y + dy
                    //检测是否到达边缘 左上右下
                    x = if (x < 0) 0F else if (x > parentWidth - width) (parentWidth - width).toFloat() else x
                    y = if (getY() < 0) 0F else if (getY() + height > parentHeight) (parentHeight - height).toFloat() else y
                    setX(x)
                    setY(y)
                    lastX = rawX
                    lastY = rawY
                    Log.i("aa", "isDrag=" + isDrag + "getX=" + getX() + ";getY=" + getY() + ";parentWidth=" + parentWidth)
                }
            }
            MotionEvent.ACTION_UP -> if (!isNotDrag()) {
                //恢复按压效果
                isPressed = false
                //Log.i("getX="+getX()+";screenWidthHalf="+screenWidthHalf);
                if (rawX >= parentWidth / 2) {
                    //靠右吸附
                    animate().setInterpolator(DecelerateInterpolator())
                            .setDuration(500)
                            .xBy(parentWidth - width - x)
                            .start()
                } else {
                    //靠左吸附
                    val oa = ObjectAnimator.ofFloat(this, "x", x, 0F)
                    oa.setInterpolator(DecelerateInterpolator())
                    oa.setDuration(500)
                    oa.start()
                }
            }
        }
        //如果是拖拽则消s耗事件,否则正常传递即可。
        return !isNotDrag() || super.onTouchEvent(event)
    }
 
    private fun isNotDrag(): Boolean {
        return !isDrag && (x == 0f || x == (parentWidth - width).toFloat())
    }
}

扩展部分

用WindowManager实现Android悬浮框以及拖动事件 (未进行亲自测试)
主要方法
 private void createFloatView() {
        //获取LayoutParams对象
        wmParams = new WindowManager.LayoutParams();
        //获取的是LocalWindowManager对象
        mWindowManager = this.getWindowManager();
        //设置window type
        wmParams.type = WindowManager.LayoutParams.TYPE_PHONE;
        //设置图片格式,效果为背景透明
        wmParams.format = PixelFormat.RGBA_8888;
        //设置浮动窗口不可聚焦(实现操作除浮动窗口外的其他可见窗口的操作)
        wmParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
        //调整悬浮窗显示的停靠位置为左侧置顶
        wmParams.gravity = Gravity.LEFT | Gravity.TOP;
        // 以屏幕左上角为原点,设置x、y初始值,相对于gravity
        wmParams.x = 0;
        wmParams.y = 0;
        //设置悬浮窗口长宽数据
        wmParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
        wmParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
         /*// 设置悬浮窗口长宽数据
        wmParams.width = 200;
        wmParams.height = 80;*/
 
        LayoutInflater inflater = this.getLayoutInflater();
        //获取浮动窗口视图所在布局
        mFloatLayout = (LinearLayout) inflater.inflate(R.layout.layout_item, null);
        //浮动窗口按钮
        mFloatView = mFloatLayout.findViewById(R.id.float_id);
        //添加mFloatLayout
        mWindowManager.addView(mFloatLayout, wmParams);
 
 
        //绑定触摸移动监听
        mFloatView.setOnTouchListener(new View.OnTouchListener() {
 
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                // TODO Auto-generated method stub
                wmParams.x = (int) event.getRawX() - mFloatLayout.getWidth() / 2;
                //25为状态栏高度
                wmParams.y = (int) event.getRawY() - mFloatLayout.getHeight() / 2 - 40;
                mWindowManager.updateViewLayout(mFloatLayout, wmParams);
                return false;
            }
        });
 
 
        //悬浮框设置点击监听
        mFloatView.setOnClickListener(new View.OnClickListener() {
 
            @Override
            public void onClick(View v) {
                // TODO Auto-generated method stub
                Toast.makeText(FloatWindowTest.this, "我是悬浮框", Toast.LENGTH_SHORT).show();
            }
        });
 
    }

layout_item.xml(悬浮框 )



 
    

完整代码 (原作者分别以Activity与Servier进行测试)
import android.app.Activity;
import android.graphics.PixelFormat;
import android.os.Bundle;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
 
import activity.main.qixin.com.xinqingofqian.R;
 
public class FloatWindowTest extends Activity implements View.OnClickListener {
 
    /**
     * Called when the activity is first created.
     */
    WindowManager mWindowManager;
    WindowManager.LayoutParams wmParams;
    LinearLayout mFloatLayout;
    TextView mFloatView;
    private Button start_Btn;
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.test_window_float);
        //初始化控件
        initView();
 
    }
 
    private void createFloatView() {
        //获取LayoutParams对象
        wmParams = new WindowManager.LayoutParams();
        //获取的是LocalWindowManager对象
        mWindowManager = this.getWindowManager();
        //设置window type
        wmParams.type = WindowManager.LayoutParams.TYPE_PHONE;
        //设置图片格式,效果为背景透明
        wmParams.format = PixelFormat.RGBA_8888;
        //设置浮动窗口不可聚焦(实现操作除浮动窗口外的其他可见窗口的操作)
        wmParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
        //调整悬浮窗显示的停靠位置为左侧置顶
        wmParams.gravity = Gravity.LEFT | Gravity.TOP;
        // 以屏幕左上角为原点,设置x、y初始值,相对于gravity
        wmParams.x = 0;
        wmParams.y = 0;
        //设置悬浮窗口长宽数据
        wmParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
        wmParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
         /*// 设置悬浮窗口长宽数据
        wmParams.width = 200;
        wmParams.height = 80;*/
 
        LayoutInflater inflater = this.getLayoutInflater();
        //获取浮动窗口视图所在布局
        mFloatLayout = (LinearLayout) inflater.inflate(R.layout.layout_item, null);
        //浮动窗口按钮
        mFloatView = mFloatLayout.findViewById(R.id.float_id);
        //添加mFloatLayout
        mWindowManager.addView(mFloatLayout, wmParams);
 
 
        //绑定触摸移动监听
        mFloatView.setOnTouchListener(new View.OnTouchListener() {
 
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                // TODO Auto-generated method stub
                wmParams.x = (int) event.getRawX() - mFloatLayout.getWidth() / 2;
                //25为状态栏高度
                wmParams.y = (int) event.getRawY() - mFloatLayout.getHeight() / 2 - 40;
                mWindowManager.updateViewLayout(mFloatLayout, wmParams);
                return false;
            }
        });
 
 
        //悬浮框设置点击监听
        mFloatView.setOnClickListener(new View.OnClickListener() {
 
            @Override
            public void onClick(View v) {
                // TODO Auto-generated method stub
                Toast.makeText(FloatWindowTest.this, "我是悬浮框", Toast.LENGTH_SHORT).show();
            }
        });
 
    }
 
    private void initView() {
        start_Btn = (Button) findViewById(R.id.start_id);
        start_Btn.setOnClickListener(this);
    }
 
    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.start_id:
                if (null == mWindowManager) {
                    createFloatView();
                }
                break;
 
        }
    }
 
    @Override
    public void onDestroy() {
        // TODO Auto-generated method stub
        super.onDestroy();
        if (mFloatLayout != null) {
            //移除悬浮窗口
            mWindowManager.removeView(mFloatLayout);
        }
    }
}

你可能感兴趣的:(#,自定义控件)