精彩案例-懸浮在桌面上的照相機

一、簡介

這個案例就是在桌面上開啓一個懸浮窗,懸浮窗裏實時顯示照相機的內容,可以自由拖動,當在非桌面狀態下自動隱藏.如下圖所示():
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<String> getHomes() {
        List<String> names = new ArrayList<String>();
        PackageManager packageManager = this.getPackageManager();

        Intent intent = new Intent(Intent.ACTION_MAIN);
        intent.addCategory(Intent.CATEGORY_HOME);
        List<ResolveInfo> 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<RunningTaskInfo> rti = mActivityManager.getRunningTasks(1);
        List<String> 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;
    }

最後別忘了加上這三個權限:

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

三、結束

有興趣可以自己去豐富內容,比如加拍照的功能等等.最後附上Demo:
點擊打開
密碼:362r

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章