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_PHONETYPE_SYSTEM_ALERT
  • 生命周期:在 onPauseonDestroy 移除窗口,避免内存泄漏。
  • 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),请告诉我!

类似文章

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注