Android懸浮窗的學習

14skyang發表於2018-10-14

本文參考了董小蟲的Android懸浮窗的實現
Android Demo : 懸浮窗(支援Android7.0)
但是本文的程式碼是來自董小蟲的demo,可以支援到android 8.0。內容僅用於學習
更高階的部分功能使用看下面
Android 8.0 懸浮窗變動與用法

Android新增可拖動、點選的懸浮視窗

Android 懸浮窗基本使用

專案實戰:WindowManager中removeView的那些坑-隨心所欲removeView

懸浮窗的實現需要用到service,這樣按下Home退出到桌面的時候,懸浮窗還可以作為程式後臺,繼續顯示在螢幕上,如果不清後臺,它會一直在那裡哈。注意要在AndroidManifest.xml裡註冊service哈

我們藉助WindowManager來生成懸浮窗
WindowManager的三個最常用方法為:

addView 新增View
addView(View view, WindowManager.LayoutParams params);
View就是要新增到windowmanager中的物件,而params是視窗的設定引數,這個我們講到程式碼階段再說。

removeView 移除View removeView(View view); 從windowmanager中移除物件。

updateViewLayout重新整理View updateViewLayout(View view,
ViewGroup.LayoutParams params); 也是兩個引數,一個View一個params,參考addView。

懸浮窗的佈局就是通過addView新增、懸浮窗更改位置通過updateViewLayout進行重新整理、關閉懸浮窗時呼叫removeView。

我們需要申請許可權
懸浮窗需要在別的應用之上顯示控制元件,很顯然,這需要某些許可權才可以。
  在API Level >= 23的時候,需要在AndroidManefest.xml檔案中宣告許可權SYSTEM_ALERT_WINDOW才能在其他應用上繪製控制元件。

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

除了這個許可權外,我們還需要在系統設定裡面對本應用進行設定懸浮窗許可權。該許可權在應用中需要啟動Settings.ACTION_MANAGE_OVERLAY_PERMISSION來讓使用者手動設定許可權。

startActivityForResult(new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName())), REQUEST_CODE);

視窗型別很重要
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;
}

在Android 8.0之前,懸浮視窗設定可以為TYPE_PHONE,這種型別是用於提供使用者互動操作的非應用視窗。
  而Android 8.0對系統和API行為做了修改,如果需要實現在其他應用和視窗上方顯示提醒視窗,那麼必須為TYPE_APPLICATION_OVERLAY的型別。

具體程式碼分析
layout
這裡實現3種懸浮窗:懸浮Button(java程式碼直接實現,不寫入layout),懸浮imagView,懸浮surfaceView
activity_main.xml就在主活動排3個按鈕,點選按鈕開啟對應的懸浮窗

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_height="match_parent"
    android:layout_width="match_parent"
    android:orientation="vertical">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="start floating button"
        android:onClick="startFloatingButtonService" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="start floating image display"
        android:onClick="startFloatingImageDisplayService"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="start floating video player"
        android:onClick="startFloatingVideoService"/>

</LinearLayout>

image_display.xml是imgeView型別的懸浮窗

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <ImageView
        android:id="@+id/image_display_imageview"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>

video_display.xml是surfaceView型別的懸浮窗

<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <SurfaceView
        android:id="@+id/video_display_surfaceview"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</FrameLayout>

如果使用FrameLayout就可以在懸浮窗上面加控制元件了Ho~

在啟動服務之前,需要在活動裡先判斷一下當前是否允許開啟懸浮窗。
MainActivity.java

public class MainActivity extends AppCompatActivity {

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

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == 0) {
            if (!Settings.canDrawOverlays(this)) {
                Toast.makeText(this, "授權失敗", Toast.LENGTH_SHORT).show();
            } else {
                Toast.makeText(this, "授權成功", Toast.LENGTH_SHORT).show();
                startService(new Intent(MainActivity.this, FloatingButtonService.class));
            }
        } else if (requestCode == 1) {
            if (!Settings.canDrawOverlays(this)) {
                Toast.makeText(this, "授權失敗", Toast.LENGTH_SHORT).show();
            } else {
                Toast.makeText(this, "授權成功", Toast.LENGTH_SHORT).show();
                startService(new Intent(MainActivity.this, FloatingImageDisplayService.class));
            }
        } else if (requestCode == 2) {
            if (!Settings.canDrawOverlays(this)) {
                Toast.makeText(this, "授權失敗", Toast.LENGTH_SHORT).show();
            } else {
                Toast.makeText(this, "授權成功", Toast.LENGTH_SHORT).show();
                startService(new Intent(MainActivity.this, FloatingVideoService.class));
            }
        }
    }

    public void startFloatingButtonService(View view) {
        if (FloatingButtonService.isStarted) {
            return;
        }
        if (!Settings.canDrawOverlays(this)) {
            Toast.makeText(this, "當前無許可權,請授權", Toast.LENGTH_SHORT);
            startActivityForResult(new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName())), 0);
        } else {
            startService(new Intent(MainActivity.this, FloatingButtonService.class));
        }
    }

    public void startFloatingImageDisplayService(View view) {
        if (FloatingImageDisplayService.isStarted) {
            return;
        }
        if (!Settings.canDrawOverlays(this)) {
            Toast.makeText(this, "當前無許可權,請授權", Toast.LENGTH_SHORT);
            startActivityForResult(new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName())), 1);
        } else {
            startService(new Intent(MainActivity.this, FloatingImageDisplayService.class));
        }
    }

    public void startFloatingVideoService(View view) {
        if (FloatingVideoService.isStarted) {
            return;
        }
        if (!Settings.canDrawOverlays(this)) {
            Toast.makeText(this, "當前無許可權,請授權", Toast.LENGTH_SHORT);
            startActivityForResult(new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName())), 2);
        } else {
            startService(new Intent(MainActivity.this, FloatingVideoService.class));
        }
    }
}

懸浮Button的service
FloatingButtonService.java

public class FloatingButtonService extends Service {
    public static boolean isStarted = false;

    private WindowManager windowManager;
    private WindowManager.LayoutParams layoutParams;

    private Button button;

    @Override
    public void onCreate() {
        super.onCreate();
        isStarted = true;
        // 獲取WindowManager服務
        windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
         // 設定LayoutParam
        layoutParams = new WindowManager.LayoutParams();
        //設定type
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
        } else {
            layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
        }
        //設定效果為背景透明.
        layoutParams.format = PixelFormat.RGBA_8888;
        //設定視窗初始停靠位置.
        layoutParams.gravity = Gravity.LEFT | Gravity.TOP;
        //設定flags.不可聚焦及不可使用按鈕對懸浮窗進行操控.
        layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
        //設定懸浮視窗長寬資料.
        //注意,這裡的width和height均使用px而非dp
        //如果你想完全對應佈局設定,需要先獲取到機器的dpi
        //px與dp的換算為px = dp * (dpi / 160).
        layoutParams.width = 500;
        layoutParams.height = 100;
        layoutParams.x = 300;
        layoutParams.y = 300;
    }

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

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        showFloatingWindow();
        return super.onStartCommand(intent, flags, startId);
    }

    private void showFloatingWindow() {
        if (Settings.canDrawOverlays(this)) {
         // 新建懸浮窗控制元件
            button = new Button(getApplicationContext());
            button.setText("Floating Window");
            button.setBackgroundColor(Color.BLUE);
            // 將懸浮窗控制元件新增到WindowManager
            windowManager.addView(button, layoutParams);

            button.setOnTouchListener(new FloatingOnTouchListener());
        }
    }
    
//拖動懸浮窗
    private class FloatingOnTouchListener implements View.OnTouchListener {
        private int x;
        private int y;

        @Override
        public boolean onTouch(View view, MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    x = (int) event.getRawX();
                    y = (int) event.getRawY();
                    break;
                case MotionEvent.ACTION_MOVE:
                    int nowX = (int) event.getRawX();
                    int nowY = (int) event.getRawY();
                    int movedX = nowX - x;
                    int movedY = nowY - y;
                    x = nowX;
                    y = nowY;
                    layoutParams.x = layoutParams.x + movedX;
                    layoutParams.y = layoutParams.y + movedY;
                     // 更新懸浮窗控制元件佈局,只有呼叫了這個方法,懸浮窗的位置才會發生改變
                    windowManager.updateViewLayout(view, layoutParams);
                    break;
                default:
                    break;
            }
            return false;
        }
    }
}

format 用於設定顯示的格式。RGBA_8888是透明型,也是最常用到的。
flags 這是很重要的一個設定。FLAG_NOT_FOCUSABLE設定了不可聚焦。同時經常用的還有FLAG_WATCH_OUTSIDE_TOUCH,這個設定可以讓懸浮窗接收到外部點選事件,如果你想在之後做小懸浮窗點選變大,再點選懸浮窗之外又變回小懸浮窗。這個可以用到。多個FLAG的話可以用|來連線,如
params.flags = FLAG_NOT_FOCUSABLE | FLAG_WATCH_OUTSIDE_TOUCH
gravity 用於設定視窗的初始停靠位置。我們設定的是讓它初始在最左&最上方生成,後面兩句是定義這裡的xy值都為0。
width&height用於設定懸浮視窗的大小,建議設定成和佈局一樣大最好。小於佈局會把裡面的元件進行擠壓。

接下來是懸浮ImagView的service了,它還可以每隔兩秒更換一張圖片
FloatingImageDisplayService.java

public class FloatingImageDisplayService extends Service {
    public static boolean isStarted = false;

    private WindowManager windowManager;
    private WindowManager.LayoutParams layoutParams;

    private View displayView;

    private int[] images;
    private int imageIndex = 0;

    private Handler changeImageHandler;

    @Override
    public void onCreate() {//建立服務
        super.onCreate();
        isStarted = true;
        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.format = PixelFormat.RGBA_8888;
        layoutParams.gravity = Gravity.LEFT | Gravity.TOP;
        layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
        layoutParams.width = 500;
        layoutParams.height = 500;
        layoutParams.x = 300;
        layoutParams.y = 300;

        images = new int[] {
                R.drawable.image_01,
                R.drawable.image_02,
                R.drawable.image_03,
                R.drawable.image_04,
                R.drawable.image_05,
        };
//藉助Handler定時傳遞訊息機制來實現定時切換圖片的機制
        changeImageHandler = new Handler(this.getMainLooper(),changeImageCallback);
    }

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

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        showFloatingWindow();//啟動服務
        return super.onStartCommand(intent, flags, startId);
    }

    private void showFloatingWindow() {
        if (Settings.canDrawOverlays(this)) { //如果允許了懸浮窗
            LayoutInflater layoutInflater = LayoutInflater.from(this);//獲取LayoutInflater物件
            displayView = layoutInflater.inflate(R.layout.image_display, null);
            displayView.setOnTouchListener(new FloatingOnTouchListener());
            ImageView imageView = displayView.findViewById(R.id.image_display_imageview);//初始化第一張圖
            imageView.setImageResource(images[imageIndex]);
            windowManager.addView(displayView, layoutParams);

            changeImageHandler.sendEmptyMessageDelayed(0, 2000);//第一次延遲2秒傳送空訊息,編號為0
        }
    }

    private Handler.Callback changeImageCallback = new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            if (msg.what == 0) {//Handler接收訊息判斷
                imageIndex++;
                if (imageIndex >= 5) {
                    imageIndex = 0;
                }
                if (displayView != null) {
                    ((ImageView) displayView.findViewById(R.id.image_display_imageview)).setImageResource(images[imageIndex]);//更新UI圖
                }

                changeImageHandler.sendEmptyMessageDelayed(0, 2000);//圖換完了,再發一條訊息
            }
            return false;//否則沒有接受訊息
        }
    };
//懸浮窗拖動操作部分程式碼完全不變
    private class FloatingOnTouchListener implements View.OnTouchListener {
        private int x;
        private int y;

        @Override
        public boolean onTouch(View view, MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    x = (int) event.getRawX();
                    y = (int) event.getRawY();
                    break;
                case MotionEvent.ACTION_MOVE:
                    int nowX = (int) event.getRawX();
                    int nowY = (int) event.getRawY();
                    int movedX = nowX - x;
                    int movedY = nowY - y;
                    x = nowX;
                    y = nowY;
                    layoutParams.x = layoutParams.x + movedX;
                    layoutParams.y = layoutParams.y + movedY;
                    windowManager.updateViewLayout(view, layoutParams);
                    break;
                default:
                    break;
            }
            return false;
        }
    }
}

最後是SurfaceView的service了,我們用它來實現播放小視訊
大同小異

public class FloatingVideoService extends Service {
    public static boolean isStarted = false;

    private WindowManager windowManager;
    private WindowManager.LayoutParams layoutParams;

    private MediaPlayer mediaPlayer;
    private View displayView;

    @Override
    public void onCreate() {
        super.onCreate();
        isStarted = true;
        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.format = PixelFormat.RGBA_8888;
        layoutParams.gravity = Gravity.LEFT | Gravity.TOP;
        layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
        layoutParams.width = 800;
        layoutParams.height = 450;
        layoutParams.x = 300;
        layoutParams.y = 300;

        mediaPlayer = new MediaPlayer();//建立媒體播放器物件
    }

    @Nullable//表示可以為空
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        showFloatingWindow();
        return super.onStartCommand(intent, flags, startId);
    }

    private void showFloatingWindow() {
        if (Settings.canDrawOverlays(this)) {
            LayoutInflater layoutInflater = LayoutInflater.from(this);
            displayView = layoutInflater.inflate(R.layout.video_display, null);
            displayView.setOnTouchListener(new FloatingOnTouchListener());
            mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
            SurfaceView surfaceView = displayView.findViewById(R.id.video_display_surfaceview);
            final SurfaceHolder surfaceHolder = surfaceView.getHolder();
            surfaceHolder.addCallback(new SurfaceHolder.Callback() {
                @Override
                public void surfaceCreated(SurfaceHolder holder) {
                    mediaPlayer.setDisplay(surfaceHolder);
                }

                @Override
                public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

                }

                @Override
                public void surfaceDestroyed(SurfaceHolder holder) {

                }
            });
            mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
                @Override
                public void onPrepared(MediaPlayer mp) {
                    mediaPlayer.start();
                }
            });
            try {
                mediaPlayer.setDataSource(this, Uri.parse("https://raw.githubusercontent.com/dongzhong/ImageAndVideoStore/master/Bruno%20Mars%20-%20Treasure.mp4"));
                mediaPlayer.prepareAsync();
            }
            catch (IOException e) {
                Toast.makeText(this, "無法開啟視訊源", Toast.LENGTH_LONG).show();
            }
            windowManager.addView(displayView, layoutParams);
        }
    }
    
//拖動懸浮窗部分程式碼 完全不變
    private class FloatingOnTouchListener implements View.OnTouchListener {
        private int x;
        private int y;

        @Override
        public boolean onTouch(View view, MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    x = (int) event.getRawX();
                    y = (int) event.getRawY();
                    break;
                case MotionEvent.ACTION_MOVE:
                    int nowX = (int) event.getRawX();
                    int nowY = (int) event.getRawY();
                    int movedX = nowX - x;
                    int movedY = nowY - y;
                    x = nowX;
                    y = nowY;
                    layoutParams.x = layoutParams.x + movedX;
                    layoutParams.y = layoutParams.y + movedY;
                    windowManager.updateViewLayout(view, layoutParams);
                    break;
                default:
                    break;
            }
            return true;
        }
    }
}

相關文章