WindowManager(窗口管理服务)
使用 WindowManager (窗口管理服务)
WindowManager
是 Android 提供的一个系统服务类,用于管理应用的窗口(Window),包括添加、移除、更新窗口,以及控制窗口的布局、显示和行为(如悬浮窗、全屏模式)。结合 Android 动画合集的背景,WindowManager
可用于在动画场景中创建自定义悬浮窗(如悬浮按钮),或动态调整窗口属性以增强动画效果(如透明度、屏幕常亮)。本文将详细介绍 WindowManager
的功能、使用方法、权限要求及注意事项,重点展示如何创建悬浮窗并结合动画实现交互效果。
WindowManager 的作用与原理
- 作用:
WindowManager
负责管理应用的窗口层次、布局参数和显示行为,支持在屏幕上添加自定义视图(如悬浮窗、弹窗)或修改窗口属性(如全屏、屏幕常亮)。它是 Android 界面显示的核心组件,与 Activity、Dialog 和 View 密切相关。 - 原理:
WindowManager
通过 Android 的窗口管理框架与底层显示系统交互,控制窗口的 Z 轴顺序、位置、大小和标志。它支持三种窗口类型:应用窗口、子窗口和系统窗口,适用于不同场景(如普通 Activity 窗口、Toast、悬浮窗)。 - 应用场景:
- 创建悬浮窗(如聊天头像、悬浮按钮)。
- 动态调整窗口属性(如透明度、屏幕方向)。
- 结合动画(如悬浮窗的平移动画)。
- 实现屏幕常亮(替代
PowerManager.WakeLock
)。 - 显示系统级弹窗(如锁屏界面上的通知)。
权限要求
使用 WindowManager
的某些功能(如悬浮窗)需要以下权限,在 AndroidManifest.xml
中声明:
<!-- 悬浮窗权限(API 23+ 需动态请求) -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<!-- 可选:屏幕常亮 -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
- 动态权限(API 23+):
SYSTEM_ALERT_WINDOW
:显示系统级窗口(如悬浮窗)。- 使用
Settings.canDrawOverlays()
检查权限,引导用户授权。 - 普通权限:
WAKE_LOCK
:保持屏幕常亮(可选)。- 注意:API 23+(Android 6.0)对
SYSTEM_ALERT_WINDOW
要求用户通过设置手动授权;API 26+ 对后台应用创建悬浮窗有限制。
获取 WindowManager
通过 Context
获取 WindowManager
实例:
import android.content.Context;
import android.view.WindowManager;
WindowManager windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
常用功能与方法
以下是 WindowManager
的核心方法:
方法/功能 | 描述 | 权限要求 | API 级别 | 用法示例 |
---|---|---|---|---|
addView(View view, ViewGroup.LayoutParams params) | 将视图添加到窗口。 | SYSTEM_ALERT_WINDOW(悬浮窗) | 1+ | windowManager.addView(view, params); |
removeView(View view) | 移除窗口中的视图。 | 无 | 1+ | windowManager.removeView(view); |
updateViewLayout(View view, ViewGroup.LayoutParams params) | 更新窗口视图的布局参数。 | 无 | 1+ | windowManager.updateViewLayout(view, params); |
getDefaultDisplay() | 获取默认显示信息(如屏幕大小)。 | 无 | 1+ | Display display = windowManager.getDefaultDisplay(); |
addFlags(int flags) | 添加窗口标志(如屏幕常亮)。 | 无(部分需 WAKE_LOCK) | 1+ | getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); |
- 窗口类型(
WindowManager.LayoutParams.type
): - 应用窗口(1-99):如
TYPE_APPLICATION
(Activity 窗口)。 - 子窗口(1000-1999):如
TYPE_APPLICATION_ATTACHED_DIALOG
(Dialog)。 - 系统窗口(2000-2999):如
TYPE_APPLICATION_OVERLAY
(API 26+,悬浮窗)、TYPE_TOAST
。 - 常用标志(
WindowManager.LayoutParams.flags
): FLAG_NOT_FOCUSABLE
:窗口不接受输入焦点。FLAG_NOT_TOUCHABLE
:窗口不接受触摸事件。FLAG_KEEP_SCREEN_ON
:保持屏幕常亮。FLAG_LAYOUT_NO_LIMITS
:窗口可超出屏幕边界。FLAG_DIM_BEHIND
:窗口背景变暗。
完整示例(悬浮窗 + 动画)
以下是一个使用 WindowManager
创建悬浮按钮的示例,点击按钮触发缩放动画,并支持拖动。
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.Toast;
import android.animation.ObjectAnimator;
import androidx.appcompat.app.AppCompatActivity;
public class FloatingWindowActivity extends AppCompatActivity {
private static final int OVERLAY_PERMISSION_CODE = 100;
private WindowManager windowManager;
private Button floatingButton;
private WindowManager.LayoutParams params;
private ObjectAnimator scaleAnimator;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_floating_window);
Button startFloatingButton = findViewById(R.id.start_floating_button);
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
startFloatingButton.setOnClickListener(v -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(this)) {
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, OVERLAY_PERMISSION_CODE);
} else {
createFloatingWindow();
}
});
}
private void createFloatingWindow() {
// 创建悬浮按钮
floatingButton = new Button(this);
floatingButton.setText("Floating");
floatingButton.setBackgroundResource(R.drawable.ic_launcher_background);
// 设置窗口参数
params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ?
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY :
WindowManager.LayoutParams.TYPE_PHONE,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
WindowManager.LayoutParams.FORMAT_CHANGED
);
params.gravity = android.view.Gravity.TOP | android.view.Gravity.LEFT;
params.x = 0;
params.y = 100;
// 添加到窗口
windowManager.addView(floatingButton, params);
// 初始化动画
scaleAnimator = ObjectAnimator.ofFloat(floatingButton, "scaleX", 1f, 1.2f, 1f);
scaleAnimator.setDuration(200);
// 拖动处理
floatingButton.setOnTouchListener(new View.OnTouchListener() {
private int initialX, initialY;
private float initialTouchX, initialTouchY;
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
initialX = params.x;
initialY = params.y;
initialTouchX = event.getRawX();
initialTouchY = event.getRawY();
scaleAnimator.start();
return true;
case MotionEvent.ACTION_MOVE:
params.x = initialX + (int) (event.getRawX() - initialTouchX);
params.y = initialY + (int) (event.getRawY() - initialTouchY);
windowManager.updateViewLayout(floatingButton, params);
return true;
case MotionEvent.ACTION_UP:
// 点击事件
if (Math.abs(event.getRawX() - initialTouchX) < 5 && Math.abs(event.getRawY() - initialTouchY) < 5) {
Toast.makeText(FloatingWindowActivity.this, "Floating Button Clicked", Toast.LENGTH_SHORT).show();
}
return true;
}
return false;
}
});
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == OVERLAY_PERMISSION_CODE) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Settings.canDrawOverlays(this)) {
createFloatingWindow();
} else {
Toast.makeText(this, "Overlay permission denied", Toast.LENGTH_SHORT).show();
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (floatingButton != null) {
windowManager.removeView(floatingButton);
}
if (scaleAnimator != null) {
scaleAnimator.cancel();
}
}
}
- 布局 XML(res/layout/activity_floating_window.xml):
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<Button
android:id="@+id/start_floating_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Start Floating Window" />
</LinearLayout>
- AndroidManifest.xml(添加权限):
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
- 说明:
- 权限:API 23+ 动态检查
Settings.canDrawOverlays()
,引导用户授权SYSTEM_ALERT_WINDOW
。 - 悬浮窗:创建可拖动的悬浮按钮,使用
TYPE_APPLICATION_OVERLAY
(API 26+)或TYPE_PHONE
(旧版)。 - 动画:点击或拖动时触发缩放动画(
scaleX
)。 - 拖动:通过
OnTouchListener
实现悬浮窗拖动,更新LayoutParams
。 - 生命周期:
onDestroy
移除悬浮窗和取消动画。
保持屏幕常亮
通过 Window
设置屏幕常亮(替代 PowerManager.WakeLock
):
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
调整窗口透明度
动态调整 Activity 窗口透明度:
WindowManager.LayoutParams params = getWindow().getAttributes();
params.alpha = 0.8f; // 0.0f(全透明)到 1.0f(不透明)
getWindow().setAttributes(params);
优缺点
- 优点:
- 支持灵活的窗口管理(如悬浮窗、全屏)。
- 可动态调整窗口属性(如透明度、屏幕常亮)。
- 与动画结合增强交互性(如拖动、缩放)。
- 缺点:
- 悬浮窗需
SYSTEM_ALERT_WINDOW
权限,用户体验可能受限。 - API 26+ 对后台应用创建悬浮窗有限制。
- 窗口管理复杂,需正确处理生命周期。
- 替代方案:
- Dialog/AlertDialog:轻量级弹窗。
- PopupWindow:简单浮动视图。
- ViewDragHelper:Activity 内部拖动。
- Foreground Service:结合通知显示悬浮内容。
注意事项
- 权限:API 23+ 需引导用户授权
SYSTEM_ALERT_WINDOW
。 - 窗口类型:API 26+ 使用
TYPE_APPLICATION_OVERLAY
,旧版用TYPE_PHONE
或TYPE_SYSTEM_ALERT
。 - 生命周期:在
onPause
或onDestroy
移除窗口,避免内存泄漏。 - Doze 模式:API 23+ 的 Doze 模式可能限制后台窗口显示。
- 用户体验:避免悬浮窗干扰用户,遵守 Google Play 政策。
- 调试:通过 Log 检查窗口添加/移除状态,验证权限。
扩展:Jetpack Compose 中的实现
在 Jetpack Compose 中,WindowManager
可用于创建悬浮窗并结合动画:
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.Settings
import android.view.MotionEvent
import android.view.View
import android.view.WindowManager
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
@Composable
fun FloatingWindowScreen() {
val context = LocalContext.current
val windowManager = remember { context.getSystemService(Context.WINDOW_SERVICE) as WindowManager }
var floatingView by remember { mutableStateOf<View?>(null) }
var params by remember { mutableStateOf<WindowManager.LayoutParams?>(null) }
val scale by animateFloatAsState(
targetValue = if (floatingView != null) 1.2f else 1f,
animationSpec = tween(200)
)
val permissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Settings.canDrawOverlays(context)) {
createFloatingWindow(context, windowManager, { floatingView = it }, { params = it })
}
}
Column(
modifier = Modifier.fillMaxSize().padding(16.dp),
horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally
) {
Button(
onClick = {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(context)) {
val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:${context.packageName}"))
permissionLauncher.launch(intent)
} else {
createFloatingWindow(context, windowManager, { floatingView = it }, { params = it })
}
},
modifier = Modifier.scale(scale)
) {
Text("Start Floating Window")
}
}
DisposableEffect(Unit) {
onDispose {
floatingView?.let { windowManager.removeView(it) }
}
}
}
private fun createFloatingWindow(
context: Context,
windowManager: WindowManager,
setView: (View?) -> Unit,
setParams: (WindowManager.LayoutParams?) -> Unit
) {
val button = Button(context).apply {
text = "Floating"
setBackgroundColor(android.graphics.Color.BLUE)
}
val params = WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
else
WindowManager.LayoutParams.TYPE_PHONE,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
WindowManager.LayoutParams.FORMAT_CHANGED
).apply {
x = 0
y = 100
gravity = android.view.Gravity.TOP or android.view.Gravity.LEFT
}
windowManager.addView(button, params)
setView(button)
setParams(params)
button.setOnTouchListener(object : View.OnTouchListener {
private var initialX = 0
private var initialY = 0
private var initialTouchX = 0f
private var initialTouchY = 0f
override fun onTouch(v: View, event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
initialX = params.x
initialY = params.y
initialTouchX = event.rawX
initialTouchY = event.rawY
return true
}
MotionEvent.ACTION_MOVE -> {
params.x = initialX + (event.rawX - initialTouchX).toInt()
params.y = initialY + (event.rawY - initialTouchY).toInt()
windowManager.updateViewLayout(button, params)
return true
}
}
return false
}
})
}
- 说明:
- 使用
remember
缓存WindowManager
和悬浮视图。 animateFloatAsState
实现按钮缩放动画。AndroidView
集成传统 View(因 Compose 无法直接添加至WindowManager
)。rememberLauncherForActivityResult
处理悬浮窗权限。DisposableEffect
确保移除悬浮窗。
示例:结合振动
结合 Vibrator
(前文介绍)为悬浮窗添加点击振动:
floatingButton.setOnClickListener(v -> {
Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
vibrator.vibrate(VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE));
} else {
vibrator.vibrate(100);
}
scaleAnimator.start();
});
如果需要更复杂的功能(如动态调整窗口透明度、与 PowerManager
结合优化电池、或 Canvas 绘制悬浮窗动画),或想继续探讨 Android 动画合集(如过渡动画、Lottie),请告诉我!