Android 悬浮窗 System Alert Window
悬浮窗能显示在其他应用上方。桌面系统例如Windows,macOS,Ubuntu,打开的程序能以窗口形式显示在屏幕上。
受限于屏幕大小,安卓系统中主要使用多任务切换的方式和分屏的方式。视频播放,视频对话可能会采用悬浮窗功能(例如手Q,微信的视频通话)。应用留下一个视频(通话)窗口,用户可以返回安卓桌面,或者去其他app的界面操作。
前面我们探讨了悬浮activity的实现方式,并结合CameraX预览来实现应用内摄像头预览悬浮Activity。这些是在app内实现的悬浮activity效果。
本文我们用一个例子来展示Android悬浮窗的实现方法和注意事项。
本文例子启用了dataBinding
dataBinding {
enabled = true
}
SYSTEM_ALERT_WINDOW
权限
manifest里申明权限SYSTEM_ALERT_WINDOW
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
请求了这个权限后,app的权限管理中会有「显示悬浮窗」的权限选项。后面我们会引导用户去开启这个权限。
标题中“System Alert Window”即
SYSTEM_ALERT_WINDOW
悬浮窗的界面
准备layout文件floating_window_1.xml,它作为悬浮窗的界面。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
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">
<TextView
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" />
<TextView
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" />
<View
android:layout_width="100dp"
android:layout_height="1dp"
android:layout_margin="16dp"
android:background="#eaeaea" />
<TextView
android:id="@+id/exit_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#00897B"
android:text="关闭悬浮窗服务"
android:textColor="#FFFFFF" />
</LinearLayout>
悬浮窗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
<?xml version="1.0" encoding="utf-8"?>
<layout>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<Button
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="悬浮窗权限" />
<Button
android:id="@+id/start_btn"
style="@style/NormalBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="启动服务" />
<Button
android:id="@+id/end_btn"
style="@style/NormalBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="停止服务" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
控制
为了方便从悬浮窗出发跳回这个activity,启动模式设置为singleTop
。
<activity
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/
Android 悬浮窗 System Alert Window的更多相关文章
- Android悬浮窗及其拖动事件
主页面布局很简单,只有一个RelativelyLayout <?xml version="1.0" encoding="utf-8"?> <R ...
- Android悬浮窗实现 使用WindowManager
Android悬浮窗实现 使用WindowManager WindowManager介绍 通过Context.getSystemService(Context.WINDOW_SERVICE)可以获得 ...
- Android 悬浮窗、悬浮球开发
原文:Android 悬浮窗.悬浮球开发 1.权限管理 直接看我另外一篇博客吧,传送门: https://my.oschina.net/u/1462828/blog/1933162 2.Base类Ba ...
- Android 悬浮窗权限校验
原文:Android 悬浮窗权限校验 悬浮窗权限: <uses-permission android:name="android.permission.SYSTEM_ALERT_WIN ...
- Android 悬浮窗权限各机型各系统适配大全
这篇博客主要介绍的是 Android 主流各种机型和各种版本的悬浮窗权限适配,但是由于碎片化的问题,所以在适配方面也无法做到完全的主流机型适配,这个需要大家的一起努力,这个博客的名字永远都是一个将来时 ...
- Android 使用WindowManager实现Android悬浮窗
WindowManager介绍 通过Context.getSystemService(Context.WINDOW_SERVICE)可以获得 WindowManager对象. 每一个WindowMan ...
- Android 悬浮窗
悬浮窗是一种比较常见的需求.例如把视频通话界面缩小成一个悬浮窗,然后用户可以在其他界面上处理事情. 本文给出一个简单的悬浮窗实现.可缩小activity和还原大小.可悬浮在其他activity上.使用 ...
- Android悬浮窗注意事项
一 动画无法运行 有时候,我们对添加的悬浮窗口,做动画的时候,始终无法运行. 那么,这个时候,我们可以对要做动画的View,再添加一个parent,即容器.将要做动画的View放入容器中. 二 悬浮窗 ...
- 关于Android悬浮窗要获取按键响应的问题
要在Android中实现顶层的窗口弹出,一般都会用WindowsManager来实现,但是几乎所有的网站资源都是说弹出的悬浮窗不用接受任何按键响应. 而问题就是,我们有时候需要他响应按键,比如电视上的 ...
随机推荐
- velocity示例
创建maven项目 pom.xml <?xml version="1.0" encoding="UTF-8"?> <project xmlns ...
- 渐进式web应用 (PWA)
PWA(渐进式 Web 应用)运用现代的 Web API 以及传统的渐进式增强策略来创建跨平台 Web 应用程序. PWA的特点: Discoverable, 内容可以通过搜索引擎发现. Instal ...
- 【C/C++】n皇后问题/全排列/递归/回溯/算法笔记4.3
按常规,先说一下我自己的理解. 递归中的return常用来作为递归终止的条件,但是对于返回数值的情况,要搞明白它是怎么返回的.递归的方式就是自己调用自己,而在有返回值的函数中,上一层的函数还没执行完就 ...
- AI 2021 年度报告
建议大伙有空还是自己亲自读一下,虽然有点长,188页ppt. https://docs.google.com/presentation/d/1bwJDRC777rAf00Drthi9yT2c9b0Ma ...
- Nginx平滑升级版本
目录 一.简介 说明 环境 二.安装 三.使用验证 一.简介 说明 Nginx版本迭代迅速,新版本提供了很多功能,好在Nginx支持不停服务进行升级. 版本之间差距不要太大,不然会导致很多东西不支持 ...
- Wireshark(三):应用Wireshark IO图形工具分析数据流
原文出处: EMC中文支持论坛 基本IO Graphs: IO graphs是一个非常好用的工具.基本的Wireshark IO graph会显示抓包文件中的整体流量情况,通常是以每秒为单位(报文数或 ...
- C语言程序设计:二分查找(折半查找)
目录 C语言程序设计:二分查找(折半查找) 1.什么是二分查找 2.二分查找的优点 3.二分查找的缺点 4.二分查找原理 5.源代码实现 6.后话 C语言程序设计:二分查找(折半查找) 1.什么是二分 ...
- CF1440A Buy the String 题解
Content 有 \(t\) 组询问,每组询问给出一个长度为 \(n\) 的 \(0/1\) 串,你可以花 \(h\) 的代价把 \(0\) 修改成 \(1\) 或者把 \(1\) 修改成 \(0\ ...
- java 网络编程基础 InetAddress类;URLDecoder和URLEncoder;URL和URLConnection;多线程下载文件示例
什么是IPV4,什么是IPV6: IPv4使用32个二进制位在网络上创建单个唯一地址.IPv4地址由四个数字表示,用点分隔.每个数字都是十进制(以10为基底)表示的八位二进制(以2为基底)数字,例如: ...
- C# 使用Fluent API 创建自己的DSL
DSL(Domain Specified Language)领域专用语言是描述特定领域问题的语言,听起来很唬人,其实不是什么高深的东西.看一下下面的代码: using FlunetApiDemo; v ...