Android 悬浮窗 System Alert Window

简介: 悬浮窗能显示在其他应用上方。桌面系统例如Windows,macOS,Ubuntu,打开的程序能以窗口形式显示在屏幕上。 受限于屏幕大小,安卓系统中主要使

悬浮窗能显示在其他应用上方。桌面系统例如Windows,macOS,Ubuntu,打开的程序能以窗口形式显示在屏幕上。

受限于屏幕大小,安卓系统中主要使用多任务切换的方式和分屏的方式。视频播放,视频对话可能会采用悬浮窗功能(例如手Q,微信的视频通话)。应用留下一个视频(通话)窗口,用户可以返回安卓桌面,或者去其他app的界面操作。

前面我们探讨了悬浮activity的实现方式,并结合CameraX预览来实现应用内摄像头预览悬浮Activity。这些是在app内实现的悬浮activity效果。

本文我们用一个例子来展示Android悬浮窗的实现方法和注意事项。

本文例子启用了dataBinding

dataBinding {

enabled = true

}

SYSTEM_ALERT_WINDOW权限

manifest里申明权限SYSTEM_ALERT_WINDOW

请求了这个权限后,app的权限管理中会有「显示悬浮窗」的权限选项。后面我们会引导用户去开启这个权限。

标题中“System Alert Window”即SYSTEM_ALERT_WINDOW

悬浮窗的界面

准备layout文件floating_window_1.xml,它作为悬浮窗的界面。

android:id="@+id/f_root"

android:layout_width="match_parent"

android:layout_height="match_parent"

android:background="#00897B"

android:gravity="center_horizontal"

android:orientation="vertical"

android:padding="4dp">

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:gravity="center_horizontal"

android:text="悬浮窗\n an.rustfisher.com "

android:textColor="#FFFFFF"

android:textSize="16sp" />

android:id="@+id/f_btn1"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_marginTop="20dp"

android:background="#00897B"

android:text="打开控制页面"

android:textColor="#FFFFFF" />

android:layout_width="100dp"

android:layout_height="1dp"

android:layout_margin="16dp"

android:background="#eaeaea" />

android:id="@+id/exit_btn"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:background="#00897B"

android:text="关闭悬浮窗服务"

android:textColor="#FFFFFF" />

悬浮窗service

新建一个Service FloatingWindowService。它来执行创建/销毁悬浮窗的操作。

完整代码如下。

// package com.rustfisher.tutorial2020.service.floating;

import android.app.Service;

import android.content.Intent;

import android.graphics.PixelFormat;

import android.os.Build;

import android.os.IBinder;

import android.util.DisplayMetrics;

import android.util.Log;

import android.view.Gravity;

import android.view.LayoutInflater;

import android.view.MotionEvent;

import android.view.View;

import android.view.ViewGroup;

import android.view.WindowManager;

import androidx.annotation.Nullable;

import com.rustfisher.tutorial2020.R;

/**

* 悬浮窗的服务

*

* @author an.rustfisher.com

* @date 2022-01-05 23:53

*/

public class FloatingWindowService extends Service {

private static final String TAG = "rfDevFloatingService";

private WindowManager windowManager;

private View floatView;

@Nullable

@Override

public IBinder onBind(Intent intent) {

return null;

}

@Override

public int onStartCommand(Intent intent, int flags, int startId) {

Log.d(TAG, "onStartCommand , " + startId);

if (floatView == null) {

Log.d(TAG, "onStartCommand: 创建悬浮窗");

initUi();

}

return super.onStartCommand(intent, flags, startId);

}

@Override

public void onDestroy() {

Log.d(TAG, "onDestroy");

super.onDestroy();

}

private void initUi() {

DisplayMetrics metrics = getApplicationContext().getResources().getDisplayMetrics();

int width = metrics.widthPixels;

int height = metrics.heightPixels;

windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);

LayoutInflater inflater = (LayoutInflater) getBaseContext().getSystemService(LAYOUT_INFLATER_SERVICE);

floatView = (ViewGroup) inflater.inflate(R.layout.floating_window_1, null);

int layoutType;

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

layoutType = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;

} else {

layoutType = WindowManager.LayoutParams.TYPE_TOAST;

}

WindowManager.LayoutParams floatLp = new WindowManager.LayoutParams(

(int) (width * (0.4f)),

(int) (height * (0.3f)),

layoutType,

WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,

PixelFormat.TRANSLUCENT

);

floatLp.gravity = Gravity.CENTER;

floatLp.x = 0;

floatLp.y = 0;

windowManager.addView(floatView, floatLp);

floatView.findViewById(R.id.f_btn1).setOnClickListener(v -> {

stopSelf();

windowManager.removeView(floatView);

Intent backToHome = new Intent(getApplicationContext(), FloatingCmdAct.class);

backToHome.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

startActivity(backToHome);

});

floatView.findViewById(R.id.exit_btn).setOnClickListener(v -> {

stopSelf();

windowManager.removeView(floatView);

});

floatView.setOnTouchListener(new View.OnTouchListener() {

final WindowManager.LayoutParams floatWindowLayoutUpdateParam = floatLp;

double x;

double y;

double px;

double py;

@Override

public boolean onTouch(View v, MotionEvent event) {

switch (event.getAction()) {

case MotionEvent.ACTION_DOWN:

x = floatWindowLayoutUpdateParam.x;

y = floatWindowLayoutUpdateParam.y;

px = event.getRawX();

py = event.getRawY();

break;

case MotionEvent.ACTION_MOVE:

floatWindowLayoutUpdateParam.x = (int) ((x + event.getRawX()) - px);

floatWindowLayoutUpdateParam.y = (int) ((y + event.getRawY()) - py);

windowManager.updateViewLayout(floatView, floatWindowLayoutUpdateParam);

break;

}

return false;

}

});

}

}

在onStartCommand方法中我们可以知道,启动这个服务时,如果没有悬浮窗floatView,则去创建一个。

WindowManager提供了与窗口管理器(window manager)沟通的接口。

显示View到窗口(屏幕)上,用的是WindowManager提供的方法addView。移除调用removeView。

layout类型需要指定

WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY

WindowManager.LayoutParams.TYPE_TOAST

LayoutInflater用来创建floatView。

floatView的OnTouchListener,用来执行拖动操作。

回到控制activity时,需要flag:Intent.FLAG_ACTIVITY_NEW_TASK,否则报错AndroidRuntimeException

android.util.AndroidRuntimeException:

Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag.

一个小问题:Service的生命周期方法运行在主线程(UI线程)上吗?

Service相关概念请参考Service综述

activity

提供一个启动服务的地方。

layout

android:layout_width="match_parent"

android:layout_height="match_parent">

android:layout_width="match_parent"

android:layout_height="match_parent"

android:gravity="center_horizontal"

android:orientation="vertical">

android:id="@+id/setting_window_btn"

style="@style/NormalBtn"

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:layout_marginTop="4dp"

android:text="悬浮窗权限" />

android:id="@+id/start_btn"

style="@style/NormalBtn"

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:layout_marginTop="4dp"

android:text="启动服务" />

android:id="@+id/end_btn"

style="@style/NormalBtn"

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:layout_marginTop="4dp"

android:text="停止服务" />

控制

为了方便从悬浮窗出发跳回这个activity,启动模式设置为singleTop。

android:name=".service.floating.FloatingCmdAct"

android:launchMode="singleTop" />

可根据具体需求选择启动模式。

以下是FloatingCmdAct的完整代码

// package com.rustfisher.tutorial2020.service.floating;

import android.content.Intent;

import android.net.Uri;

import android.os.Build;

import android.os.Bundle;

import android.provider.Settings;

import android.util.Log;

import android.widget.Toast;

import androidx.annotation.Nullable;

import androidx.appcompat.app.AppCompatActivity;

import androidx.databinding.DataBindingUtil;

import com.rustfisher.tutorial2020.R;

import com.rustfisher.tutorial2020.databinding.ActFloatingCmdBinding;

/**

* 启动服务前的界面

*

* @author an.rustfisher.com

* @date 2022-01-05 14:57

*/

public class FloatingCmdAct extends AppCompatActivity {

private static final String TAG = "rfDevFloatingCmd";

private ActFloatingCmdBinding mBinding;

@Override

protected void onCreate(@Nullable Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

mBinding = DataBindingUtil.setContentView(this, R.layout.act_floating_cmd);

mBinding.settingWindowBtn.setOnClickListener(v -> {

if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {

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

} else {

Toast.makeText(getApplicationContext(), "API < " + android.os.Build.VERSION_CODES.M, Toast.LENGTH_SHORT).show();

}

});

mBinding.startBtn.setOnClickListener(v -> {

startService(new Intent(getApplicationContext(), FloatingWindowService.class));

Toast.makeText(getApplicationContext(), "启动服务", Toast.LENGTH_SHORT).show();

});

mBinding.endBtn.setOnClickListener(v -> {

stopService(new Intent(getApplicationContext(), FloatingWindowService.class));

Toast.makeText(getApplicationContext(), "停止服务", Toast.LENGTH_SHORT).show();

});

if (!checkOverlayDisplayPermission()) {

Toast.makeText(getApplicationContext(), "请允许应用显示悬浮窗", Toast.LENGTH_SHORT).show();

}

}

@Override

protected void onNewIntent(Intent intent) {

super.onNewIntent(intent);

Log.d(TAG, "onNewIntent: 回来了");

}

private boolean checkOverlayDisplayPermission() {

// API23以后需要检查权限

if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {

return Settings.canDrawOverlays(this);

} else {

return true;

}

}

}

API23以后,需要检查是否允许显示悬浮窗。如果不允许则弹一个toast。

跳转去显示悬浮窗权限界面,用Settings.ACTION_MANAGE_OVERLAY_PERMISSION

Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName()))

代码请参考工程 https://gitee.com/rustfisher/AndroidTutorial

运行效果

红米9A(MIUI 12.5.1 Android 10)

小结

本文实现了一个简单的悬浮窗功能。有了SYSTEM_ALERT_WINDOW权限后,悬浮窗能显示在其他app上方。

添加view到窗口上,主要使用android.view.WindowManager的功能。

参考

悬浮activity的实现方式

CameraX预览

应用内摄像头预览悬浮Activity

https://www.geeksforgeeks.org/how-to-make-a-floating-window-application-in-android/