带有悬浮窗功能的Android应用

android api29

gradle 8.9

要求

  1. 布局文件 (floating_window_layout.xml):

    • 增加、删除、关闭按钮默认隐藏。
    • 使用"开始"按钮来控制这些按钮的显示和隐藏。
  2. 服务类 (FloatingWindowService.kt):

    • 实现"开始"按钮的功能,点击时切换增加、删除、关闭按钮的可见性。
    • 处理增加、删除、关闭按钮的点击事件。
    • 使浮动窗口可拖动。
  3. 主活动 (MainActivity.kt):

    • 检查并请求悬浮窗权限。
    • 启动和停止悬浮窗服务。
  4. 清单文件 (AndroidManifest.xml):

    • 添加必要的权限声明。
floating_window_layout.xml
Kotlin 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/root_layout"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:background="#CCFFFFFF"
    android:padding="16dp">

    <Button
        android:id="@+id/start_button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="开始" />

    <LinearLayout
        android:id="@+id/control_buttons_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:visibility="gone">

        <Button
            android:id="@+id/add_button"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="增加" />

        <Button
            android:id="@+id/delete_button"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="删除" />

        <Button
            android:id="@+id/close_button"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="关闭" />
    </LinearLayout>
</LinearLayout>

FloatingWindowService.kt

Kotlin 复制代码
package com.example.application

import android.app.Service
import android.content.Intent
import android.graphics.PixelFormat
import android.os.Build
import android.os.IBinder
import android.view.Gravity
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.WindowManager
import android.widget.Button
import android.widget.LinearLayout
import android.widget.Toast

class FloatingWindowService : Service() {

    private var windowManager: WindowManager? = null
    private var floatingView: View? = null
    private var controlButtonsLayout: LinearLayout? = null

    override fun onBind(intent: Intent?): IBinder? {
        return null
    }

    override fun onCreate() {
        super.onCreate()

        // 加载浮动窗口布局
        floatingView = LayoutInflater.from(this).inflate(R.layout.floating_window_layout, null)

        // 设置浮动窗口的布局参数
        val params = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            WindowManager.LayoutParams(
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
                PixelFormat.TRANSLUCENT
            )
        } else {
            WindowManager.LayoutParams(
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.TYPE_PHONE,
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
                PixelFormat.TRANSLUCENT
            )
        }

        params.gravity = Gravity.TOP or Gravity.START
        params.x = 0
        params.y = 100

        windowManager = getSystemService(WINDOW_SERVICE) as WindowManager?
        windowManager?.addView(floatingView, params)

        // 查找按钮并设置点击监听器
        val startButton = floatingView?.findViewById<Button>(R.id.start_button)
        val addButton = floatingView?.findViewById<Button>(R.id.add_button)
        val deleteButton = floatingView?.findViewById<Button>(R.id.delete_button)
        val closeButton = floatingView?.findViewById<Button>(R.id.close_button)
        controlButtonsLayout = floatingView?.findViewById(R.id.control_buttons_layout)

        startButton?.setOnClickListener {
            if (controlButtonsLayout?.visibility == View.VISIBLE) {
                controlButtonsLayout?.visibility = View.GONE
                startButton.text = "开始"
            } else {
                controlButtonsLayout?.visibility = View.VISIBLE
                startButton.text = "收起"
            }
        }

        addButton?.setOnClickListener {
            Toast.makeText(applicationContext, "增加", Toast.LENGTH_SHORT).show()
        }

        deleteButton?.setOnClickListener {
            Toast.makeText(applicationContext, "删除", Toast.LENGTH_SHORT).show()
        }

        closeButton?.setOnClickListener {
            stopSelf()
        }

        // 使浮动窗口可拖动
        val rootLayout = floatingView?.findViewById<LinearLayout>(R.id.root_layout)
        var initialX = 0
        var initialY = 0
        var initialTouchX = 0f
        var initialTouchY = 0f

        rootLayout?.setOnTouchListener(object : View.OnTouchListener {
            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
                    }
                    MotionEvent.ACTION_MOVE -> {
                        params.x = initialX + (event.rawX - initialTouchX).toInt()
                        params.y = initialY + (event.rawY - initialTouchY).toInt()
                        windowManager?.updateViewLayout(floatingView, params)
                    }
                }
                return false
            }
        })
    }

    override fun onDestroy() {
        super.onDestroy()
        if (floatingView != null) {
            windowManager?.removeView(floatingView)
        }
    }
}

MainActivity.kt

Kotlin 复制代码
package com.example.application

import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.app.ActivityCompat
import com.example.application.ui.theme.ApplicationTheme

class MainActivity : ComponentActivity() {
    val REQUEST_CODE = 1001

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            ApplicationTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { padding ->
                    MainScreen(padding, LocalContext.current)
                }
            }
        }

        // 检查应用是否有权限显示悬浮窗
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(this)) {
            // 请求权限以显示悬浮窗
            val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName"))
            startActivityForResult(intent, REQUEST_CODE)
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == REQUEST_CODE) {
            // 检查用户是否授予了权限
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Settings.canDrawOverlays(this)) {
                // 权限已授予,可以启动服务
            } else {
                // 权限未授予,显示消息或进行其他处理
            }
        }
    }
}

@Composable
fun MainScreen(padding: PaddingValues, context: Context) {
    val isFloatingWindowRunning = remember { mutableStateOf(false) }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(padding)
            .padding(16.dp),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Greeting(name = "Android")
        Spacer(modifier = Modifier.height(16.dp))
        ToggleFloatingWindowButton(
            context = context,
            isFloatingWindowRunning = isFloatingWindowRunning.value,
            onToggle = {
                if (it) {
                    startFloatingWindow(context)
                } else {
                    stopFloatingWindow(context)
                }
                isFloatingWindowRunning.value = it
            }
        )
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Text(
        text = "你好 $name!",
        modifier = modifier
    )
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    ApplicationTheme {
        Greeting("Android")
    }
}

@Composable
fun ToggleFloatingWindowButton(
    context: Context,
    isFloatingWindowRunning: Boolean,
    onToggle: (Boolean) -> Unit
) {
    Button(onClick = {
        onToggle(!isFloatingWindowRunning)
    }) {
        Text(text = if (isFloatingWindowRunning) "停止悬浮窗" else "启动悬浮窗")
    }
}

private fun startFloatingWindow(context: Context) {
    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}"))
        ActivityCompat.startActivityForResult(context as MainActivity, intent, MainActivity().REQUEST_CODE, null)
    } else {
        val intent = Intent(context, FloatingWindowService::class.java)
        context.startService(intent)
    }
}

private fun stopFloatingWindow(context: Context) {
    val intent = Intent(context, FloatingWindowService::class.java)
    context.stopService(intent)
}

AndroidManifest.xml

Kotlin 复制代码
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

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

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Application"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:label="@string/app_name"
            android:theme="@style/Theme.Application">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service android:name=".FloatingWindowService" />
    </application>

</manifest>
复制代码
AndroidManifest.xml
Kotlin 复制代码
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

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

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Application"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:label="@string/app_name"
            android:theme="@style/Theme.Application">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service android:name=".FloatingWindowService" />
    </application>

</manifest>

实现了你所描述的功能:增加、删除、关闭按钮默认隐藏,并通过"开始"按钮来控制它们的显示和隐藏

相关推荐
凯子坚持 c8 小时前
CANN 性能剖析实战:从原始事件到交互式火焰图
windows·microsoft
消失的旧时光-19438 小时前
从 Kotlin 到 Dart:为什么 sealed 是处理「多种返回结果」的最佳方式?
android·开发语言·flutter·架构·kotlin·sealed
有位神秘人9 小时前
kotlin与Java中的单例模式总结
java·单例模式·kotlin
Jinkxs9 小时前
Gradle - 与Groovy/Kotlin DSL对比 构建脚本语言选择指南
android·开发语言·kotlin
&有梦想的咸鱼&9 小时前
Kotlin委托机制的底层实现深度解析(74)
android·开发语言·kotlin
golang学习记9 小时前
IntelliJ IDEA 2025.3 重磅发布:K2 模式全面接管 Kotlin —— 告别 K1,性能飙升 40%!
java·kotlin·intellij-idea
开开心心就好9 小时前
发票合并打印工具,多页布局设置实时预览
linux·运维·服务器·windows·pdf·harmonyos·1024程序员节
LDORntKQH9 小时前
基于深度强化学习的混合动力汽车能量管理策略 1.利用DQN算法控制电池和发动机发电机组的功率分配 2
android
獨枭9 小时前
PyCharm 跑通 SAM 全流程实战
windows
冬奇Lab9 小时前
Android 15 ServiceManager与Binder服务注册深度解析
android·源码·源码阅读