精彩案例-悬浮在桌面上的照相机

2,301 阅读5分钟
一、简介

这个案例就是在桌面上开启一个悬浮窗,悬浮窗里实时显示照相机的内容,可以自由拖动,当在非桌面状态下自动隐藏.如下图所示(): PS:gif都失真了,凑合看,实际中这个窗口是不会闪烁的

这里写图片描述 这里写图片描述

我做这个是因为公司项目里在android系统的NavigationBar里显示了行车记录仪,实时录像.我想把类似的思路分享出来.通过这个可以学习TextureView和自定义悬浮窗口的知识.

二、实现 1、显示一个悬浮窗口

在MainActivity里启动一个服务,在服务里进行悬浮窗的操作

Button btn = (Button) findViewById(R.id.btn);
        btn.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                // 启动服务,在服务里开启悬浮窗
                Intent intent = new Intent(MainActivity.this,MyService.class);
                 startService(intent);
            }
        });

新建一个MyService继承自Service,在onCreate方法里加上如下代码

//对于6.0以上的设备
        if (Build.VERSION.SDK_INT >= 23) {
            //如果支持悬浮窗功能
        if (Settings.canDrawOverlays(getApplicationContext())) {
                 showWindow();
            } else {
                //手动去开启悬浮窗
                Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                getApplicationContext().startActivity(intent);
            }
        } else {
                //6.0以下的设备直接开启
                showWindow();
        }

    }

谷歌对于6.0以上的设备,默认是把悬浮窗功能给禁了,所以需要手动去打开.我用的小米就是这样,需要手动在设置里打开显示悬浮窗的权限.

    - Settings.canDrawOverlays(context)方法是判断当前系统是否支持悬浮窗
    - Settings.ACTION_MANAGE_OVERLAY_PERMISSION 是跳转到打开悬浮窗的ACTION

以上两个都是在6.0以上的SDK里才有.

对于6.0一下的设备可以直接显示. 看下showWindow()里的代码

private void showWindow() {
        //创建MyWindow的实例
        myWindow = new MyWindow(getApplicationContext());
        //窗口管理者
        mWindowManager = (WindowManager) getSystemService(Service.WINDOW_SERVICE);
        //窗口布局参数
        Params = new WindowManager.LayoutParams();
        //布局坐标,以屏幕左上角为(0,0)
        Params.x = 0;
        Params.y = 0;

        //布局类型
        Params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT; // 系统类型

        //布局flags
        Params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; // 无焦点
        Params.flags = Params.flags | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
        Params.flags = Params.flags | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; //无限制布局 
        Params.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;

        //布局的gravity
        Params.gravity = Gravity.LEFT | Gravity.TOP;

        //布局的宽和高
        Params.width =  500;
        Params.height = 500;

        myWindow.setOnTouchListener(new OnTouchListener() {

            @Override
            public boolean onTouch(View v, MotionEvent event) {
                 switch (event.getAction()) {                

                 case MotionEvent.ACTION_MOVE:
                    //在移动时更新坐标
                    Params.x = (int) event.getRawX() - myWindow.getWidth() / 2;
                    Params.y = (int) event.getRawY() - myWindow.getHeight() / 2;
                    //更新布局位置
                    mWindowManager.updateViewLayout(myWindow, Params);

                    break;
                }
                 return false;
            }
         });

    }

首先创建了MyWindow实例,这是一个自定义布局

public class MyWindow extends LinearLayout implements SurfaceTextureListener {
     ......
    public MyWindow(Context context) {
        super(context);
        LayoutInflater.from(context).inflate(R.layout.window, this);
        this.context = context;
         initView();
    }
     ......
    }

加载布局文件进来,这个布局里放了一个Textureview和一个TextView. 创建MyWindow 实例后,获取WindowManager和布局参数LayoutParams.

   - 设置LayoutParams的坐标x,y
   - 设置LayoutParams的类型为TYPE_SYSTEM_ALERT
   - 设置LayoutParams的flags
   - 设置LayoutParams的gravity
   - 设置LayoutParams的宽和高

这样一个Window的属性设置完了. 最后设置一个触摸监听,让悬浮窗跟随手指移动.

public class MyService extends Service {
    ......
myWindow.setOnTouchListener(new OnTouchListener() {

            @Override
            public boolean onTouch(View v, MotionEvent event) {
                 switch (event.getAction()) {                

                 case MotionEvent.ACTION_MOVE:
                    Params.x = (int) event.getRawX() - myWindow.getWidth() / 2;
                    Params.y = (int) event.getRawY() - myWindow.getHeight() / 2;
                    //更新布局位置
                    mWindowManager.updateViewLayout(myWindow, Params);

                    break;
                }
                 return false;
            }
         });
         ......
         }

那么什么时候显示和隐藏悬浮窗呢? 我前面说了,会在非桌面界面隐藏该悬浮窗.在桌面界面显示悬浮窗.在这里我通过开启一个定时器每隔一秒发一个消息到Handler,在Handler里判断当前界面是否是桌面.

public class MyService extends Service {
    ......
// 定时器类
        Timer timer = new Timer();
        timer.schedule(task, 1000, 1000); // 1s后执行task,经过1s再次执行
         ......
         }
public class MyService extends Service {
    ......
//定时发送message给Handler
    TimerTask task = new TimerTask() {
        @Override
        public void run() {
            Message message = new Message();
            handler.sendMessage(message);
        }
    };
     ......
         }
public class MyService extends Service {
    ......
private Handler handler = new Handler() {
    public void handleMessage(Message msg) {

            if (isHome()) {
                // 如果回到桌面,则显示悬浮窗
                if (!myWindow.isAttachedToWindow()) {
                    mWindowManager.addView(myWindow, Params);
                }

            } else {
                // 如果在非桌面,则去掉悬浮窗
                if (myWindow.isAttachedToWindow()) {
                    mWindowManager.removeView(myWindow);
                }
            }
            super.handleMessage(msg);
        };
    };
     ......
         }

这样就完成了一个悬浮窗的显示和隐藏.进入其他应用时会隐藏该悬浮窗,回到桌面时又会自动显示出来.这里用到了两个方法.

 - 获取桌面(Launcher)的包名的方法getHomes
 - 判断当前是否是桌面的方法isHome
/**
     * @return 获取桌面(Launcher)的包名
     */
    private List getHomes() {
        List names = new ArrayList();
        PackageManager packageManager = this.getPackageManager();

        Intent intent = new Intent(Intent.ACTION_MAIN);
        intent.addCategory(Intent.CATEGORY_HOME);
        List resolveInfo = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
        for (ResolveInfo info : resolveInfo) {
            names.add(info.activityInfo.packageName);
        }
        return names;
    }
/**
     * @return 判断当前是否是桌面
     */
    public boolean isHome() {
        ActivityManager mActivityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
        List rti = mActivityManager.getRunningTasks(1);
        List strs = getHomes();
        if (strs != null && strs.size() > 0) {
            return strs.contains(rti.get(0).topActivity.getPackageName());
        } else {
            return false;
        }
    }
2、在窗口里显示照相机

这里要用到TextureView这个控件,它和SurfaceView是 "兄弟". 与SurfaceView相比,TextureView并没有创建一个单独的Surface用来绘制,这使得它可以像一般的View一样执行一些变换操作,设置透明度等。另外,Textureview必须在硬件加速开启的窗口中。对应的就是这条属性

Params.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;

以下代码是在MyWindow里实现的

private void initView() {
        //初始化
        textureView = (TextureView) findViewById(R.id.textureView);
        //设置监听
        textureView.setSurfaceTextureListener(this);
        //获取WindowManager
        mWindowManager = (WindowManager) context.getSystemService(Service.WINDOW_SERVICE);
    }

设置监听需要实现SurfaceTextureListener接口,会重载下面4个方法

 - onSurfaceTextureAvailable 可用时候执行
 - onSurfaceTextureDestroyed 销毁的时候执行
 - onSurfaceTextureSizeChanged 尺寸改变时执行
 - onSurfaceTextureUpdated 更新的时候执行
 - 

这里在onSurfaceTextureAvailable方法执行一下操作. 1.创建Camera实例(我这里用的是旧版本的Camera,已经过时了). 2.通过setPreviewTexture把内容渲染到SurfaceTexture 上 3.通过.setDisplayOrientation设置角度 这里用到了SetDegree(context)方法

private int SetDegree(MyWindow myWindow) { 
        // 获得手机的方向
        int rotation = mWindowManager.getDefaultDisplay().getRotation();
        int degree = 0;
        // 根据手机的方向计算相机预览画面应该选择的角度
        switch (rotation) {
        case Surface.ROTATION_0:
            degree = 90;
            break;
        case Surface.ROTATION_90:
            degree = 0;
            break;
        case Surface.ROTATION_180:
            degree = 270;
            break;
        case Surface.ROTATION_270:
            degree = 180;
            break;
        }
        return degree;
    }

4.通过startPreviewd开始渲染


@Override
    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {

        if (myCamera == null) {
            // 创建Camera实例
            myCamera = Camera.open();
            try {
                // 设置预览在textureView上
                myCamera.setPreviewTexture(surface);
                myCamera.setDisplayOrientation(SetDegree(MyWindow.this));

                // 开始预览
                myCamera.startPreview();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

最后onSurfaceTextureDestroyed方法里进行停止渲染和释放资源操作.

@Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
        myCamera.stopPreview(); //停止预览
        myCamera.release();     // 释放相机资源
        myCamera = null;

        return false;
    }

最后别忘了加上这三个权限:

android.permission.SYSTEM_ALERT_WINDOW

android.permission.GET_TASKS

android.permission.CAMERA

三、结束

目前掘金这个MarkDown感觉没CSDN好用,想要更好的体验,也可以移步我的博客 打开

有兴趣可以自己去丰富内容,比如加拍照的功能等等.最后附上Demo: 点击打开
密码:362r