用WindowManager实现一个类似ios悬浮可拖动的虚拟导航按钮

前言:

实现这个功能,可能需要你对WindowManager有一定认识,大家可以自行去看大佬们关于WindowManager的文章。需要基本的自定义View相关的知识以及onTouch相关参数的理解。


开始操作:

既然是用WindowManager,当然开始是初始化咯:

这里比较重要是用WindowManager的add方法,添加了一个View在最顶层,这样这个View就类似悬浮在页面之上了。

/**
     * 初始化 参数
     */
    private void init(){
        STATE = STATE_NOMOR;

        mWindowManager = (WindowManager)getContext().getSystemService(Context.WINDOW_SERVICE);
        mParams = new LayoutParams(100,100,0,0, PixelFormat.TRANSPARENT);
        mParams.flags = LayoutParams.FLAG_NOT_TOUCH_MODAL | LayoutParams.FLAG_NOT_FOCUSABLE |  LayoutParams.FLAG_SHOW_WHEN_LOCKED;
        mParams.width = 100;
        mParams.height = 100;
        mParams.gravity = Gravity.LEFT | Gravity.TOP;
        mParams.x = 0;
        mParams.y = 300;
        mWindowManager.addView(this , mParams);

        screenWidth = mWindowManager.getDefaultDisplay().getWidth();
        screenHeight = mWindowManager.getDefaultDisplay().getHeight();
    }


1.自定义View绘制出模样

我们新建一个文件取名FloatingView,继承自Button,为什么不继承View,因为想着以后可以想拓展可以省很多事。具体要注意的就是自定义View的几个重要的方法需要重写:onMeasure(测量)、onLayout(布局,这个控件没用到)、onDraw(绘制):

onMeasure:这个方法里面经过测量后,你能得到测量后控件的宽高位置坐标等数据。

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        width = getWidth();
        height = getHeight();
        x = getX();
        y = getY();
        initNumber();
    }
onDraw:绘制不通状态下的控件形态(open状态目前还没开发)

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        switch (STATE){
            case STATE_MOVE:
                changeState2_MOVE();
                drawD(canvas);
                break;
            case STATE_NOMOR:
                changeState2_NOMOR();
                drawD(canvas);
                break;
            case STATE_OPEN:
                changeSate2_OPEN();
                drawOpen(canvas);
                break;
            default:
                break;
        }
    }
这里只截取了部分方法,整个类的代码在文末给出。

前面这部分整理完基本就可以看到效果如下图:



2.实现可以拖拽效果

在要使用的Activity初始化FloatingView的一些数据,为了方便,我把大部分初始化操作都放在FloatingView中,在onTouch中手指放下、移动、抬起时处理FloatingView相关的状态:

@Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_register);
        mButton = new FloatingView(this);
        mButton.setOnTouchListener(this);

    }
 @Override
    public boolean onTouch(View v, MotionEvent event) {
        float rawX = event.getRawX();
        float rawY = event.getRawY();
        int action = event.getAction();
        switch (action){
            case MotionEvent.ACTION_DOWN:
                mButton.setState(FloatingView.STATE_MOVE);
                break;
            case MotionEvent.ACTION_MOVE:
                mButton.onActionMove((int)rawX , (int)rawY);
                break;
            case MotionEvent.ACTION_UP:
                mButton.onActionUp(rawX , rawY);
                break;
            default:
                break;
        }
        return false;
    }
想要实现拖拽,最重要的就是WindowManager中的updateViewLayout()方法,调用这个方法,把当前坐标的信息给参数中的LayoutParams,就可以刷新位置了,实现了拖拽的效果,并且是在页面之上,悬浮拖动的效果就大致出来了。但是ios那个悬浮的框框还有个功能就是,不会悬浮在屏幕中间,会贴边显示,下面我们就来分析一下这个过程。

2.onTouch计算使其贴边,并且让其在获取到焦点时改变透明度

思路:我们把屏幕分成四等份如下图:


如图所示当前悬浮按钮是在左上的区域,但是如果手指滑动让其在离左边框的距离大于上边框的距离时松开手指,此时它应该自动贴边到上边框,反之就贴边左边框,其他四个区域也是类似的思路,但是在计算距离时要注意一些细节;

计算左上、右下时,可以直接使用rawX,rawY(与x,y坐标性质不同,xy是相对于父控件的位置,前者是相对于屏幕的坐标位置)和屏幕宽高比较。但是在计算左下和右上的时候,要使用rawX,rawY以及屏幕宽高混合计算。

核心算法在FloatingView中的cal方法中,我们通过计算它手指抬起时控件所属区域,并且计算应该贴在哪个边框,最终把计算控件应该贴边的位置坐标设置给LayoutParams的x,y中,调用WindowManager的updateViewLayout方法更新位置信息(为了方便我使用的是宽高比来算,这样其实不精准的):

/**
     * 计算手指停留的位置,让控件贴边
     * @param rawX 手指停留位置相对于屏幕的x轴坐标
     * @param rawY 手指停留位置相对于屏幕的y轴坐标
     */
    public void cal(float rawX , float rawY){
        //左上   &&    右下(因为两者比例都是大于1/2所以可以直接用比例来算)
        float X = rawX/screenWidth;
        float Y = rawY/screenHeight;

        //右上
        float R = screenWidth - rawX;
        //左下
        float B = screenHeight - rawY;

        if(X <= 0.5 && Y <= 0.5){//左上
            if(rawX - rawY >0){
                setParamsXY((int)rawX , 0);
            }else{
                setParamsXY(0 , (int)rawY);
            }

        }else if(X > 0.5 && Y > 0.5){//右下
            if(X - Y > 0){
                setParamsXY(screenWidth , (int)rawY);
            }else{
                setParamsXY((int)rawX , screenHeight);
            }

        }else if(X > 0.5 && Y <=0.5){//右上
            if(R - rawY > 0){
                setParamsXY((int)rawX , 0);
            }else{
                setParamsXY(screenWidth , (int)rawY);
            }
        }else{//左下
            if(rawX - B > 0){
                setParamsXY((int)rawX , screenHeight);
            }else{
                setParamsXY(0 , (int)rawY);
            }
        }
    }
其中setParamsXY方法就是设置LayoutParams的x,y,的值,让其强行改变控件位置达到贴边的效果:

/**
     * 设置控件相对于屏幕位置坐标
     * @param x x轴坐标
     * @param y y轴坐标
     */
    public void setParamsXY(int x,int y){
        mParams.x = x;
        mParams.y = y;
    }
最终效果如下GIF所示:



基本大功告成,后期要加其他功能在可以直接在上面修改!

对了还有就是在移动和手指落在控件上,会有一个背景透明度降低的过程,这样使控件看起来更亮。

在onTouch方法中设置它的不同状态即可,源码里面有这里不写了!

FloatingView的源码:

package com.xxx.your-packge-name.ui.widget;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.os.Build;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.support.v7.widget.AppCompatButton;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;


/**
 * Created by:[ Jovial 比较喜欢丶笑]
 * Created date:[ 2018/1/22 0022] 博客地址:http://my.csdn.net/caihongdao123?locationNum=0&fps=1
 * About Class:[ 制作一个类似ios可以左右悬浮的球 ]
 */

public class FloatingView extends AppCompatButton {

    /** 画笔  */
    private Paint mPaint , mPaint1 , mPaint2 , mRectPaint ;

    /** 控件 宽高  中心同心圆是根据宽高比例算出来的半径 */
    private int width , height;

    /** 控件的初始位置坐标  */
    private float x , y ;

    /** 同心圆  圆心位置  */
    private float rx , ry;

    /** 同心圆  半径  */
    private int c1,c2,c3;

    /** 控件的状态  移动&焦点(0)  静止状态(-1) 点击打开状态(2) */
    public final static int STATE_MOVE = 0;
    public final static int STATE_NOMOR = -1;
    public final static int STATE_OPEN = 2;
    private static int STATE = STATE_NOMOR;

    /** 屏幕宽高  */
    private int screenWidth;
    private int screenHeight;

    private WindowManager mWindowManager;
    /** 悬浮按钮  */
    private LayoutParams mParams;
    /** 打开状态的界面布局 */
    private LayoutParams mParamsOpen;

    public FloatingView(Context context){
        this(context, null , 0);
    }
    public FloatingView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs , 0);
    }

    public FloatingView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
//        TypedArray a = context.obtainStyledAttributes(attrs , R.styleable.CircleView);
//        mColor = a.getColor(R.styleable.CircleView_circle_color,Color.RED);
//        a.recycle();
        init();
        initPaint();
    }

    /**
     * 初始化 参数
     */
    private void init(){
        STATE = STATE_NOMOR;

        mWindowManager = (WindowManager)getContext().getSystemService(Context.WINDOW_SERVICE);
        mParams = new LayoutParams(100,100,0,0, PixelFormat.TRANSPARENT);
        mParams.flags = LayoutParams.FLAG_NOT_TOUCH_MODAL | LayoutParams.FLAG_NOT_FOCUSABLE | LayoutParams.FLAG_SHOW_WHEN_LOCKED;
        mParams.width = 100;
        mParams.height = 100;
        mParams.gravity = Gravity.LEFT | Gravity.TOP;
        mParams.x = 0;
        mParams.y = 300;
        mWindowManager.addView(this , mParams);

        screenWidth = mWindowManager.getDefaultDisplay().getWidth();
        screenHeight = mWindowManager.getDefaultDisplay().getHeight();
    }

    /**
     * 初始化状态
     */
    public void setState(int state){
        STATE = state;
    }

    /**
     * 状态类型:(-1)
     * 控件透明度增加,属于无任何状态的情况
     */
    public void changeState2_NOMOR(){
        mPaint.setAlpha(70);
        mPaint1.setAlpha(50);
        mPaint2.setAlpha(40);
        mRectPaint.setAlpha(60);
        invalidate();
    }

    /**
     * 状态类型(1)
     * 移动或者获取到焦点,透明度降低,是控件看起来颜色更亮
     */
    public void changeState2_MOVE(){
        mPaint.setAlpha(90);
        mPaint1.setAlpha(70);
        mPaint2.setAlpha(60);
        mRectPaint.setAlpha(100);
        invalidate();
    }

    /**
     * 类型状态(2)
     * 当点击打开时,调用此方法重新绘制控件
     */
    private void changeSate2_OPEN(){
    }
    /**
     * 初始化画笔
     */
    public void initPaint(){
        mPaint = new Paint();
        mPaint.setColor(Color.WHITE);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setAlpha(70);
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.FILL);

        mPaint1 = new Paint();
        mPaint1.setColor(Color.WHITE);
        mPaint1.setStrokeCap(Paint.Cap.ROUND);
        mPaint1.setAlpha(50);
        mPaint1.setAntiAlias(true);
        mPaint1.setStyle(Paint.Style.FILL);

        mPaint2 = new Paint();
        mPaint2.setColor(Color.WHITE);
        mPaint2.setStrokeCap(Paint.Cap.ROUND);
        mPaint2.setAlpha(40);
        mPaint2.setAntiAlias(true);
        mPaint2.setStyle(Paint.Style.FILL);

        mRectPaint = new Paint();
        mRectPaint.setColor(Color.BLACK);
        mRectPaint.setStrokeCap(Paint.Cap.ROUND);
        mRectPaint.setAntiAlias(true);
        mRectPaint.setAlpha(60);
        mRectPaint.setStyle(Paint.Style.FILL);


    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        width = getWidth();
        height = getHeight();
        x = getX();
        y = getY();
        initNumber();
    }

    /**
     * 计算控件绘制的参数
     */
    public void initNumber(){
        rx = x + width/2;
        ry = y + height/2;

        c1 = width/5+14;
        c2 = width/5+8;
        c3 = width/5;
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        switch (STATE){
            case STATE_MOVE:
                changeState2_MOVE();
                drawD(canvas);
                break;
            case STATE_NOMOR:
                changeState2_NOMOR();
                drawD(canvas);
                break;
            case STATE_OPEN:
                changeSate2_OPEN();
                drawOpen(canvas);
                break;
            default:
                break;
        }
    }

    /**
     * 绘制未点击状态的控件形态
     */
    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private void drawD(Canvas canvas){
        canvas.drawRoundRect(x,y,width,height,20f,20f,mRectPaint);
        canvas.drawCircle(rx,ry,c1,mPaint1);
        canvas.drawCircle(rx,ry,c2,mPaint2);
        canvas.drawCircle(rx,ry,c3,mPaint);
    }
    /**
     * 绘制点击打开状态(2)的效果
     */
    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private void drawOpen(Canvas canvas){
        canvas.drawRoundRect(x,y,screenWidth/2,screenHeight/2,20f,20f,mRectPaint);
    }
    /**
     * 计算手指停留的位置,让控件贴边
     * @param rawX 手指停留位置相对于屏幕的x轴坐标
     * @param rawY 手指停留位置相对于屏幕的y轴坐标
     */
    public void cal(float rawX , float rawY){
        //左上   &&    右下(因为两者比例都是大于1/2所以可以直接用比例来算)
        float X = rawX/screenWidth;
        float Y = rawY/screenHeight;

        //右上
        float R = screenWidth - rawX;
        //左下
        float B = screenHeight - rawY;

        if(X <= 0.5 && Y <= 0.5){//左上
            if(rawX - rawY >0){
                setParamsXY((int)rawX , 0);
            }else{
                setParamsXY(0 , (int)rawY);
            }

        }else if(X > 0.5 && Y > 0.5){//右下
            if(X - Y > 0){
                setParamsXY(screenWidth , (int)rawY);
            }else{
                setParamsXY((int)rawX , screenHeight);
            }

        }else if(X > 0.5 && Y <=0.5){//右上
            if(R - rawY > 0){
                setParamsXY((int)rawX , 0);
            }else{
                setParamsXY(screenWidth , (int)rawY);
            }
        }else{//左下
            if(rawX - B > 0){
                setParamsXY((int)rawX , screenHeight);
            }else{
                setParamsXY(0 , (int)rawY);
            }
        }
    }

    /**
     * 设置控件相对于屏幕位置坐标
     * @param x x轴坐标
     * @param y y轴坐标
     */
    public void setParamsXY(int x,int y){
        mParams.x = x;
        mParams.y = y;
    }

    /**
     * 更新控件位置,此处调用WindowManager的方法来更新
     */
    public void updateViewLayout(){
        mWindowManager.updateViewLayout(this , mParams);
    }

    /**
     * 添加onTouch监听后,手指抬起时需要调用的方法
     */
    public void onActionUp(float rawX , float rawY){
        setState(FloatingView.STATE_NOMOR);
        //计算控件距离上左下右边框的距离,让其贴边
        cal(rawX , rawY);
        updateViewLayout();
    }
    /**
     * 添加onTouch监听后,手指移动时需要调用的方法
     */
    public void onActionMove(float rawX , float rawY){
        setParamsXY((int)rawX , (int)rawY);
        updateViewLayout();
    }
}
activity调用过程:

public class RegistActivity  extends FragmentActivity implements View.OnTouchListener{

    private FloatingView mButton;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_register);
        mButton = new FloatingView(this);
        mButton.setOnTouchListener(this);

    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        float rawX = event.getRawX();
        float rawY = event.getRawY();
        int action = event.getAction();
        switch (action){
            case MotionEvent.ACTION_DOWN:
                mButton.setState(FloatingView.STATE_MOVE);
                break;
            case MotionEvent.ACTION_MOVE:
                mButton.onActionMove((int)rawX , (int)rawY);
                break;
            case MotionEvent.ACTION_UP:
                mButton.onActionUp(rawX , rawY);
                break;
            default:
                break;
        }
        return false;
    }

}

代码已提交到git上,可直接下载运行,欢迎start!-->点击打开git

-----------------------------------分割线-------------------------------------------------------

添加了open状态:


说明:

采用的是一个dialog弹窗的方式,比较简单,不过里面放置一个可以让空间以圆环形势排列的自定义CircleLayout,该自定义控件是继承自LinearLayout,具体代码已提交git上。


多读书,会发现书里面真的有很多智慧!不限于专业书,可以T向发展,让自己的爱好和知识广度加宽!



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实现拖动悬浮按钮,可以使用Android中的WindowManager类和LayoutParams类。 首先,在你的Activity或Fragment中创建一个悬浮按钮,例如: ```java Button button = new Button(this); button.setText("悬浮按钮"); ``` 然后,使用WindowManager将该按钮添加到屏幕上,并设置其LayoutParams: ```java WindowManager.LayoutParams params = new WindowManager.LayoutParams( WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSLUCENT ); params.gravity = Gravity.TOP | Gravity.START; params.x = 0; params.y = 0; WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE); windowManager.addView(button, params); ``` 这里的LayoutParams设置了按钮的宽度和高度为自适应,类型为TYPE_APPLICATION_OVERLAY(表示悬浮在其他应用程序上方),不可获得焦点,透明度为半透明。gravity设置了按钮在屏幕上的位置,x和y设置了按钮的初始位置。 接下来,可以为按钮添加一个触摸监听器,以便用户可以通过拖动来移动按钮: ```java button.setOnTouchListener(new View.OnTouchListener() { private int initialX; private int initialY; private float initialTouchX; private float initialTouchY; @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: initialX = params.x; initialY = params.y; initialTouchX = event.getRawX(); initialTouchY = event.getRawY(); return true; case MotionEvent.ACTION_UP: return true; case MotionEvent.ACTION_MOVE: params.x = initialX + (int) (event.getRawX() - initialTouchX); params.y = initialY + (int) (event.getRawY() - initialTouchY); windowManager.updateViewLayout(button, params); return true; } return false; } }); ``` 在这个触摸监听器中,我们记录了按钮的初始位置和触摸位置,并在移动事件中更新了LayoutParams中的x和y值,并通过WindowManager的updateViewLayout方法更新了按钮的位置。 这样就可以实现一个拖动悬浮按钮了。记得要在不需要的时候将其从屏幕上移除: ```java windowManager.removeView(button); ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值