Android悬浮窗的简单实现

概述

原理

Android的界面绘制,都是通过 WindowManager 的服务来实现的。 WindowManager 实现了 ViewManager 接口,可以通过获取 WINDOW_SERVICE 系统服务得到。而 ViewManager 接口有 addView 方法,我们就是通过这个方法将悬浮窗控件加入到屏幕中去。

为了让悬浮窗与Activity脱离,使其在应用处于后台时悬浮窗仍然可以正常运行,使用Service来启动悬浮窗并做为其背后逻辑支撑。

权限

在 API Level >= 23 的时候,需要在AndroidManefest.xml文件中声明权限 SYSTEM_ALERT_WINDOW 才能在其他应用上绘制控件。

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

除了这个权限外,我们还需要在系统设置里面对本应用进行设置悬浮窗权限。该权限在应用中需要启动 Settings.ACTION_MANAGE_OVERLAY_PERMISSION 来让用户手动设置权限。

if (!Settings.canDrawOverlays(this)) {
    showError("当前无权限,请授权");
    startActivityForResult(new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName())), 0);
} else {
    startService(new Intent(MainActivity.this, FloatingService.class));
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == 0) {
        if (!Settings.canDrawOverlays(this)) {
            showError("授权失败");
        } else {
            showMsg("授权成功");
            startService(new Intent(MainActivity.this, FloatingService.class));
        }
    }
}

LayoutParam

WindowManager 的 addView 方法有两个参数,一个是需要加入的控件对象,另一个参数是 WindowManager.LayoutParam 对象。
  这里需要着重说明的是 LayoutParam 里的 type 变量。这个变量是用来指定窗口类型的。在设置这个变量时,需要注意一个坑,那就是需要对不同版本的Android系统进行适配。

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {
    layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
}

实例

AndroidManifest.xml

添加权限

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

添加Service组件

<service android:name=".MediaFloatService"/>

MyApplication

public class PubApplication extends Application {
    //设置一个全局变量来判断悬浮窗是否已经开启
    public static Boolean isMediaFloatShow = false;
}

MediaFloatService

public class MediaFloatService extends Service {
    private WindowManager windowManager;
    private WindowManager.LayoutParams layoutParams;
    private View mView;
    private ViewFloatMediaBinding floatView;
    private ArrayList<String> lstFilePaths;
    private int currentIndex;
    private int screenWidth;
    private int screenHeight;

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        //加载窗口布局
        initWindow(intent);
        return super.onStartCommand(intent, flags, startId);
    }
    
    @Override
    public void onDestroy() {
        //移除窗口布局
        windowManager.removeView(mView);
        super.onDestroy();
    }
}

加载窗口布局

private void initWindow(Intent intent) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        if (Settings.canDrawOverlays(this)) {
            //获取WindowManager服务
            windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);

            //取得屏幕尺寸
            DisplayMetrics dm = new DisplayMetrics();
            windowManager.getDefaultDisplay().getMetrics(dm);
            screenWidth = dm.widthPixels;
            screenHeight = dm.heightPixels;

            //设置LayoutParams
            layoutParams = new WindowManager.LayoutParams();
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
            } else {
                layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
            }
            layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_FULLSCREEN
                    | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
            layoutParams.format = PixelFormat.RGBA_8888; //背景透明效果
            layoutParams.width = 512; //悬浮窗口长宽值,单位为 px 而非 dp
            layoutParams.height = 450;
            layoutParams.gravity = 51; //想要x,y生效,一定要指定Gravity为top和left //Gravity.TOP | Gravity.LEFT
            layoutParams.x = 100; //启动位置
            layoutParams.y = 100;

            //加载悬浮窗布局
            floatView = ViewFloatMediaBinding.inflate(LayoutInflater.from(MediaFloatService.this));
            mView = floatView.getRoot();
            //mView.setAlpha((float) 0.9);

            //设定悬浮窗控件
            floatView.ivFloatMediaClose.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    MyApplication.isMediaFloatShow = false;
                    //切记添加关闭服务按钮事件,调用 stopSelf() 方法以关闭悬浮窗。
                    stopSelf();
                }
            });
            //接收传值
            Bundle bundle = intent.getExtras();
            lstFilePaths = bundle.getStringArrayList("lstFilePaths");
            currentIndex = bundle.getInt("currentIndex");
            floatView.ivFloatMediaPrev.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    //判断是否是第一个,如果是则跳到最后循环播放
                    if (currentIndex == 0) currentIndex = lstFilePaths.size() - 1;
                    else currentIndex = currentIndex - 1;
                    showImage();
                    //Glide.with(MediaFloatService.this).load(lstFilePaths.get(currentIndex)).into(floatView.ivFloatMediaShow);
                }
            });
            floatView.ivFloatMediaNext.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    //判断是否是最后一个,如果是则跳到最前循环播放
                    if (currentIndex == lstFilePaths.size() - 1) currentIndex = 0;
                    else currentIndex = currentIndex + 1;
                    showImage();
                    //Glide.with(MediaFloatService.this).load(lstFilePaths.get(currentIndex)).into(floatView.ivFloatMediaShow);
                }
            });
            BitmapFactory.Options options = getBitmapOptions(lstFilePaths.get(currentIndex));
            layoutParams.width = Math.min(options.outWidth, screenWidth); //Math.min取得两个数据中的最小值
            layoutParams.height = Math.min(options.outHeight, screenHeight);
            Glide.with(this).load(lstFilePaths.get(currentIndex)).into(floatView.ivFloatMediaShow);
            //单击事件是否显示控制按钮
            floatView.ivFloatMediaShow.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    if (floatView.grpFloatMediaControl.getVisibility() == View.INVISIBLE)
                        floatView.grpFloatMediaControl.setVisibility(View.VISIBLE);
                    else floatView.grpFloatMediaControl.setVisibility(View.INVISIBLE);
                }
            });
            
			//提交布局
            windowManager.addView(mView, layoutParams);
        }
    }
}

取得屏幕尺寸

DisplayMetrics dm = new DisplayMetrics();
windowManager.getDefaultDisplay().getMetrics(dm);
screenWidth = dm.widthPixels;
screenHeight = dm.heightPixels;

根据路径取得图片尺寸

private BitmapFactory.Options getBitmapOptions(String filepath) {
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeFile(filepath, options);
    return options;
}

变更显示图片

变更图片时悬浮窗口也要根据变更后的图片再做调整,不然窗口大小还是上一图片的尺寸比例

private void showImage() {
    BitmapFactory.Options options = getBitmapOptions(lstFilePaths.get(currentIndex));
    layoutParams.width = Math.min(options.outWidth, screenWidth); //Math.min取得两个数据中的最小值
    layoutParams.height = Math.min(options.outHeight, screenHeight);
    Glide.with(this).load(lstFilePaths.get(currentIndex)).into(floatView.ivFloatMediaShow);
    windowManager.updateViewLayout(mView, layoutParams);
}

窗口拖动与缩放

窗口拖动

floatView.ivFloatMediaShow.setOnTouchListener(new View.OnTouchListener() {
    private int dX;
    private int dY;

    @Override
    public boolean onTouch(View view, MotionEvent motionEvent) {
        switch (motionEvent.getAction()) {
            case MotionEvent.ACTION_DOWN:
                dX = (int) motionEvent.getRawX();
                dY = (int) motionEvent.getRawY();
                break;
            case MotionEvent.ACTION_MOVE:
                int nX = (int) motionEvent.getRawX();
                int nY = (int) motionEvent.getRawY();
                int cW = nX - dX;
                int cH = nY - dY;
                dX = nX;
                dY = nY;
                layoutParams.x = layoutParams.x + cW;
                layoutParams.y = layoutParams.y + cH;
                windowManager.updateViewLayout(mView, layoutParams);
                break;
            default:
                break;
        }
        return true;
    }
});

单击双击

floatView.ivFloatMediaShow.setOnTouchListener(new View.OnTouchListener() {
    private int sX;
    private int sY;

    @Override
    public boolean onTouch(View view, MotionEvent motionEvent) {
        switch (motionEvent.getAction()) {
            case MotionEvent.ACTION_DOWN:
                sX = (int) motionEvent.getRawX();
                sY = (int) motionEvent.getRawY();
                break;
            case MotionEvent.ACTION_UP:
                //如果抬起时的位置和按下时的位置大致相同视作单击事件
                int nX2 = (int) motionEvent.getRawX();
                int nY2 = (int) motionEvent.getRawY();
                int cW2 = nX2 - sX;
                int cH2 = nY2 - sY;
                //间隔值可能为负值,所以要取绝对值进行比较
                if (Math.abs(cW2) < 3 && Math.abs(cH2) < 3) view.performClick();
                break;
            default:
                break;
        }
        return true;
    }
});

双指缩放

floatView.ivFloatMediaShow.setOnTouchListener(new View.OnTouchListener() {
    private ScaleGestureDetector scaleGestureDetector = new ScaleGestureDetector(MediaFloatService.this, new myScale());

    @Override
    public boolean onTouch(View view, MotionEvent motionEvent) {
        switch (motionEvent.getAction()) {
            case MotionEvent.ACTION_DOWN:
                isP2Down = true; //双指按下
                break;
            case MotionEvent.ACTION_MOVE:
                if (motionEvent.getPointerCount() == 2) {
                    //双指缩放
                    scaleGestureDetector.onTouchEvent(motionEvent);
                }
                break;
            case MotionEvent.ACTION_UP:
                isP2Down = false; //双指抬起
                break;
            default:
                break;
        }
        return true;
    }
});
ScaleGestureDetector
private float initSapcing = 0;
private int initWidth;
private int initHeight;
private int initX;
private int initY;
private boolean isP2Down = false;

private class myScale extends ScaleGestureDetector.SimpleOnScaleGestureListener {
    @Override
    public boolean onScale(ScaleGestureDetector detector) {
//            detector.getCurrentSpan();//两点间的距离跨度
//            detector.getCurrentSpanX();//两点间的x距离
//            detector.getCurrentSpanY();//两点间的y距离
//            detector.getFocusX();       //
//            detector.getFocusY();       //
//            detector.getPreviousSpan(); //上次
//            detector.getPreviousSpanX();//上次
//            detector.getPreviousSpanY();//上次
//            detector.getEventTime();    //当前事件的事件
//            detector.getTimeDelta();    //两次事件间的时间差
//            detector.getScaleFactor();  //与上次事件相比,得到的比例因子

        if (isP2Down) {
            //双指按下时最初间距
            initSapcing = detector.getCurrentSpan();
            //双指按下时窗口大小
            initWidth = layoutParams.width;
            initHeight = layoutParams.height;
            //双指按下时窗口左顶点位置
            initX = layoutParams.x;
            initY = layoutParams.y;
            isP2Down = false;
        }
        float scale = detector.getCurrentSpan() / initSapcing; //取得缩放比
        int newWidth = (int) (initWidth * scale);
        int newHeight = (int) (initHeight * scale);
        //判断窗口缩放后是否超出屏幕大小
        if (newWidth < screenWidth && newHeight < screenHeight) {
            layoutParams.width = newWidth;
            layoutParams.height = newHeight;
            layoutParams.x = initX;
            layoutParams.y = initY;
            //缩放后图片会失真重新载入图片
            Glide.with(MediaFloatService.this)
                .load(lstFilePaths.get(currentIndex))
                .into(floatView.ivFloatMediaShow);
            //提交更新布局
            windowManager.updateViewLayout(mView, layoutParams);
        }
        return true;
        //return super.onScale(detector);
    }
}

完整代码

floatView.ivFloatMediaShow.setOnTouchListener(new View.OnTouchListener() {
    private int dX;
    private int dY;
    private int sX;
    private int sY;
    private ScaleGestureDetector scaleGestureDetector = new ScaleGestureDetector(MediaFloatService.this, new myScale());

    @Override
    public boolean onTouch(View view, MotionEvent motionEvent) {
        switch (motionEvent.getAction()) {
            case MotionEvent.ACTION_DOWN:
                dX = (int) motionEvent.getRawX();
                dY = (int) motionEvent.getRawY();
                sX = (int) motionEvent.getRawX();
                sY = (int) motionEvent.getRawY();
                isP2Down = true; //双指按下
                break;
            case MotionEvent.ACTION_MOVE:
                if (motionEvent.getPointerCount() == 1) {
                    //单指拖动
                    int nX = (int) motionEvent.getRawX();
                    int nY = (int) motionEvent.getRawY();
                    int cW = nX - dX;
                    int cH = nY - dY;
                    dX = nX;
                    dY = nY;
                    layoutParams.x = layoutParams.x + cW;
                    layoutParams.y = layoutParams.y + cH;
                    windowManager.updateViewLayout(mView, layoutParams);
                } else if (motionEvent.getPointerCount() == 2) {
                    //双指缩放
                    scaleGestureDetector.onTouchEvent(motionEvent);
                }
                break;
            case MotionEvent.ACTION_UP:
                isP2Down = false; //双指抬起
                //如果抬起时的位置和按下时的位置大致相同视作单击事件
                int nX2 = (int) motionEvent.getRawX();
                int nY2 = (int) motionEvent.getRawY();
                int cW2 = nX2 - sX;
                int cH2 = nY2 - sY;
                //间隔值可能为负值,所以要取绝对值进行比较
                if (Math.abs(cW2) < 3 && Math.abs(cH2) < 3) view.performClick();
                break;
            default:
                break;
        }
        return true;
    }
});

实例2

private WindowManager windowManager;
private WindowManager.LayoutParams layoutParams;
private View floatView;
if(Settings.canDrawOverlays(this)){
    windowManager = (WindowManager)getSystemService(WINDOW_SERVICE);
    layoutParams = new WindowManager.LayoutParams();
    
    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
        layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
    } else {
        layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
    }
    layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
    layoutParams.format = PixelFormat.RGBA_8888; //背景透明效果
    layoutParams.width = 512; //悬浮窗口长宽值,单位为 px 而非 dp
    layoutParams.height = 450;
    layoutParams.gravity = Gravity.TOP | Gravity.LEFT; //启动位置
    layoutParams.x = 100; 
    layoutParams.y = 100;
    
    floatView = LayoutInflater.from(this).inflate(R.layout.note_float_view, null);
    floatView.setAlpha((float) 0.9);
    
    windowManager.addView(floatView, layoutParams);
}

常见问题

起始位置设置无效

想要x,y生效,一定要指定Gravity为top和left,想要居中就设置为 Center

layoutParams.gravity = Gravity.TOP | Gravity.LEFT; 
layoutParams.x = 100; //启动位置
layoutParams.y = 100;

获取状态栏高度

private int statusBarHeight = -1;
//获取状态栏高度
private void getStatusBarHeight(){
    mView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
    int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
    if(resourceId > 0){
        statusBarHeight = getResources().getDimensionPixelSize(resourceId);
    }
}
posted @ 2020-12-18 16:28  曲幽  阅读(3856)  评论(0编辑  收藏  举报