You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Kotlin在Android Studio开发带图库背景与音乐控制的AOD应用?

开发自定义Always On Display(AOD)应用(Kotlin + Android Studio)

一、权限配置

首先在AndroidManifest.xml中添加必要权限,确保应用能正常运行:

<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" android:minSdkVersion="33" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />

<!-- 声明前台服务 -->
<service android:name=".AodService" android:foregroundServiceType="specialUse" />

二、基础AOD界面实现

通过前台服务+悬浮窗实现锁屏状态下的AOD显示,创建AodService.kt:

class AodService : Service() {
    private lateinit var windowManager: WindowManager
    private lateinit var aodView: View
    private lateinit var sharedPrefs: SharedPreferences
    private val screenReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            when (intent?.action) {
                Intent.ACTION_SCREEN_OFF -> aodView.visibility = View.VISIBLE
                Intent.ACTION_SCREEN_ON -> aodView.visibility = View.GONE
            }
        }
    }

    override fun onCreate() {
        super.onCreate()
        sharedPrefs = getSharedPreferences("AOD_PREFS", MODE_PRIVATE)
        windowManager = getSystemService(WINDOW_SERVICE) as WindowManager

        // 加载AOD布局
        aodView = LayoutInflater.from(this).inflate(R.layout.layout_aod, null)

        // 配置悬浮窗参数(适配锁屏层级)
        val params = WindowManager.LayoutParams(
            WindowManager.LayoutParams.MATCH_PARENT,
            WindowManager.LayoutParams.MATCH_PARENT,
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
            else WindowManager.LayoutParams.TYPE_SYSTEM_ALERT,
            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                    or WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
                    or WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
                    or WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
            PixelFormat.TRANSLUCENT
        )
        windowManager.addView(aodView, params)

        // 启动前台服务(避免被系统回收)
        createNotificationChannel()
        startForeground(1, buildNotification())

        // 监听屏幕状态
        registerReceiver(screenReceiver, IntentFilter().apply {
            addAction(Intent.ACTION_SCREEN_OFF)
            addAction(Intent.ACTION_SCREEN_ON)
        })

        // 加载自定义背景
        loadCustomBackground()
        // 初始化音乐控制
        initMusicControls()
    }

    private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                "AOD_CHANNEL",
                "Always On Display",
                NotificationManager.IMPORTANCE_LOW
            )
            val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
            notificationManager.createNotificationChannel(channel)
        }
    }

    private fun buildNotification(): Notification {
        return Notification.Builder(this, "AOD_CHANNEL")
            .setContentTitle("AOD 运行中")
            .setSmallIcon(R.drawable.ic_aod)
            .build()
    }

    override fun onDestroy() {
        super.onDestroy()
        windowManager.removeView(aodView)
        unregisterReceiver(screenReceiver)
    }

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

对应的layout_aod.xml布局:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#000000">

    <!-- 自定义背景图 -->
    <ImageView
        android:id="@+id/iv_background"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="centerCrop"
        android:alpha="0.3"/>

    <!-- 音乐控制按钮组 -->
    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|center_horizontal"
        android:orientation="horizontal"
        android:padding="16dp"
        android:gap="16dp">

        <ImageButton
            android:id="@+id/btn_prev"
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:src="@drawable/ic_skip_previous"
            android:background="@null"
            android:tint="#FFFFFF"/>

        <ImageButton
            android:id="@+id/btn_play_pause"
            android:layout_width="64dp"
            android:layout_height="64dp"
            android:src="@drawable/ic_play_arrow"
            android:background="@null"
            android:tint="#FFFFFF"/>

        <ImageButton
            android:id="@+id/btn_next"
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:src="@drawable/ic_skip_next"
            android:background="@null"
            android:tint="#FFFFFF"/>
    </LinearLayout>
</FrameLayout>

三、自定义背景选择功能

在主Activity中实现图库选图逻辑,保存选中图片的Uri到SharedPreferences:

class MainActivity : AppCompatActivity() {
    private val PICK_IMAGE_REQUEST = 100
    private val OVERLAY_PERMISSION_REQUEST = 200
    private lateinit var sharedPrefs: SharedPreferences

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        sharedPrefs = getSharedPreferences("AOD_PREFS", MODE_PRIVATE)

        // 检查悬浮窗权限
        if (!Settings.canDrawOverlays(this)) {
            startActivityForResult(
                Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName")),
                OVERLAY_PERMISSION_REQUEST
            )
        }

        // 选择背景按钮
        findViewById<Button>(R.id.btn_select_bg).setOnClickListener {
            val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
            } else {
                Intent(Intent.ACTION_GET_CONTENT).setType("image/*")
            }
            startActivityForResult(intent, PICK_IMAGE_REQUEST)
        }

        // 启动/停止AOD按钮
        findViewById<Button>(R.id.btn_start_aod).setOnClickListener {
            startService(Intent(this, AodService::class.java))
        }
        findViewById<Button>(R.id.btn_stop_aod).setOnClickListener {
            stopService(Intent(this, AodService::class.java))
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        when (requestCode) {
            PICK_IMAGE_REQUEST -> {
                if (resultCode == RESULT_OK && data != null) {
                    data.data?.let { uri ->
                        sharedPrefs.edit().putString("BACKGROUND_URI", uri.toString()).apply()
                    }
                }
            }
            OVERLAY_PERMISSION_REQUEST -> {
                if (!Settings.canDrawOverlays(this)) {
                    Toast.makeText(this, "需要开启悬浮窗权限才能使用AOD", Toast.LENGTH_SHORT).show()
                }
            }
        }
    }
}

在AodService中添加加载背景的方法:

private fun loadCustomBackground() {
    val bgUriStr = sharedPrefs.getString("BACKGROUND_URI", null)
    bgUriStr?.let {
        val bgUri = Uri.parse(it)
        aodView.findViewById<ImageView>(R.id.iv_background).setImageURI(bgUri)
    }
}

四、音乐控制功能

通过MediaControllerCompat实现系统音乐播放控制,在AodService中添加:

private lateinit var mediaController: MediaControllerCompat

private fun initMusicControls() {
    try {
        // 获取活跃的媒体会话控制器
        mediaController = MediaControllerCompat.getMediaController(this)
    } catch (e: IllegalStateException) {
        // 无活跃会话时尝试初始化默认控制器
        val sessionManager = MediaSessionManager.getSessionManager(this)
        if (sessionManager.activeSessions.isNotEmpty()) {
            mediaController = sessionManager.activeSessions[0]
        } else {
            Toast.makeText(this, "未检测到活跃音乐会话", Toast.LENGTH_SHORT).show()
            return
        }
    }

    // 绑定按钮点击事件
    val playPauseBtn = aodView.findViewById<ImageButton>(R.id.btn_play_pause)
    playPauseBtn.setOnClickListener {
        if (mediaController.playbackState?.state == PlaybackStateCompat.STATE_PLAYING) {
            mediaController.transportControls.pause()
            playPauseBtn.setImageResource(R.drawable.ic_play_arrow)
        } else {
            mediaController.transportControls.play()
            playPauseBtn.setImageResource(R.drawable.ic_pause)
        }
    }

    aodView.findViewById<ImageButton>(R.id.btn_prev).setOnClickListener {
        mediaController.transportControls.skipToPrevious()
    }

    aodView.findViewById<ImageButton>(R.id.btn_next).setOnClickListener {
        mediaController.transportControls.skipToNext()
    }

    // 监听播放状态变化
    mediaController.registerCallback(object : MediaControllerCompat.Callback() {
        override fun onPlaybackStateChanged(state: PlaybackStateCompat?) {
            super.onPlaybackStateChanged(state)
            playPauseBtn.setImageResource(
                if (state?.state == PlaybackStateCompat.STATE_PLAYING) R.drawable.ic_pause else R.drawable.ic_play_arrow
            )
        }
    })
}

五、优化建议

  1. 低亮度控制:通过调整屏幕亮度降低耗电,需添加WRITE_SETTINGS权限并引导用户授权:
val settingsContentResolver = contentResolver
Settings.System.putFloat(settingsContentResolver, Settings.System.SCREEN_BRIGHTNESS, 0.1f)
  1. 触摸交互:可添加点击AOD区域唤醒屏幕的逻辑,在layout_aod.xml的根布局添加点击事件:
aodView.setOnClickListener {
    val powerManager = getSystemService(POWER_SERVICE) as PowerManager
    val wakeLock = powerManager.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP, "AOD:WakeLock")
    wakeLock.acquire(1000)
}

内容的提问来源于stack exchange,提问作者Cykonix

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.24 19:46:16