乐于分享
好东西不私藏

安卓游戏《宠物1.0》制作全流程

安卓游戏《宠物1.0》制作全流程
01概述
02部署Gradle(配置API环境)

Gradle 是 Android 官方统一的项目构建工具,你可以把它理解成「安卓项目的自动化管家」。没有它,你的 Kotlin/Java 代码、图片、动画、布局资源无法编译成手机能安装的 APK 安装包。

一般情况下,第一次打开android studio,gradle就会自动部署,但是我的API 24报错了,下载不了官网上的资源包。
将网址换成国内镜像,且注释掉哈希表后还是不行。
然后我换了一个APK版本——API 30。
这个版本的兼容性不及API24好,但是我的手机也能运行。
还是不行:

DNS 解析失败,Android Studio 无法解析 Gradle 插件仓库域名,国内网络无法直连谷歌 / Gradle 官方插件源,域名找不到服务器。

最后换成了兼容性最好的API 23,很顺利,近八分钟就下载完了。
然后:

API 23 是不是最新?

不是,而且不推荐你用 API 23

API 23 = Android 6.0,发布于 2015 年,非常老旧;

Jetpack Compose(你现在项目用的界面框架)最低兼容要求是 API 21,但 API23 缺少大量现代界面、通知权限相关 API,后续你做「读取手机消息、交互动画」会频繁出现兼容报错;

小米 13 出厂 Android13 (API33),虽然能装 API23 的 App,但高系统权限逻辑和 6.0 完全不一样,适配会很麻烦;

最新正式 Android 版本是 Android15(API35),建议改回 API24(Android7.0),兼顾兼容与功能。

我不管,我先把hello world弄出来再删。
顺利,改三行代码中的输出文本即可。

minSdk=24(Android7.0)能补齐什么

完整支持通知渠道兼容代码,在小米 13 等新手机上通知、提示音 100% 正常弹出;

完善精确闹钟 API setExactAndAllowWhileIdle,大幅降低高系统后台杀定时任务概率;

XML 矢量动画、帧动画全部 API 原生兼容,无渲染 bug;

权限细分更完善,适配安卓 10~14 的后台、定位、通知限制代码更简洁,不需要大量 if 版本判断;

兼容绝大多数现代动画、定时第三方库,不会出现依赖版本冲突

很奇妙的事情发生了,我再次安装一遍24,又能安装了。
这次直接用它原生的,直接输出hello android。

API(Application Programming Interface)应用程序编程接口,一套功能调用规则 / 函数入口,是系统、库对外开放的 “操作按钮”。

SDK(Software Development Kit)软件开发工具包,一整套开发工具合集,里面包含:全部 API 源码文档、编译工具、模拟器、资源工具、系统库文件。

Gradle 项目构建工具,自动化管理工具,负责连接 Android Studio、SDK、第三方库,完成下载、编译、打包全流程。

03改页面背景颜色
bug:已在colors.xml新增darkgreen颜色,同时在MainActivity的页面代码与预览@Preview函数中,均给Scaffold添加background(colorResource(R.color.darkgreen)),刷新 Compose 预览后,页面背景仍保持白色,未变为深绿色。

Scaffold 英文原意:脚手架、支架

在 Jetpack Compose 里:页面标准布局容器,安卓官方给 App 页面封装好的「标准页面骨架」。

04android studio接入DeepSeek api
这是DeepSeek收费标准,我充了10块钱足够我用了。
在官网进入api开放平台,创建一个key,复制密钥后回到android studio。
避坑:在创建settings时选第三方提供者,不要选本地(谷歌)。
填好信息后,apply即可。
在下方选择指定模型。
然后让人啼笑皆非的事情发生了,DeepSeek说自己不是DeepSeek。
没关系,能用就行。
这个模型不支持上传图片,然后我继续问豆包,豆包知道我的问题(改了代码,画面还是纯白),于是给了我代码:
class MainActivity : ComponentActivity() {    override funonCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        enableEdgeToEdge()        setContent {            PetTheme {                // 外层Box铺满屏幕,设置全局绿色背景                Box(                    modifier = Modifier                        .fillMaxSize()                        .background(colorResource(R.color.darkgreen))                ) {                    // Scaffold设置透明,不遮挡外层绿色                    Scaffold(                        modifier = Modifier.fillMaxSize(),                        containerColor = Color.Transparent                    ) { innerPadding ->                        Greeting(                            name = "Android",                            modifier = Modifier.padding(innerPadding)                        )                    }                }            }        }    }}@ComposablefunGreeting(name: String, modifier: Modifier = Modifier) {    Text(        text = "Hello $name!",        modifier = modifier    )}@Preview@ComposablefunGreetingPreview() {    PetTheme {        Box(            modifier = Modifier                .fillMaxSize()                .background(colorResource(R.color.darkgreen))        ) {            Scaffold(                modifier = Modifier.fillMaxSize(),                containerColor = Color.Transparent            ) { innerPadding ->                Greeting("Android", Modifier.padding(innerPadding))            }        }    }}
但是这行代码复制到studio是报错的,然后我用报错行下面的fix with ai,然后很顺利,agent立马帮我修好了,运行正常,主要就是在开头添加import。
我悟了:豆包善于理解需求制定大方向,agent善于修bug,让大方向适配现实。好了,我集齐了卧龙凤雏,未来光明了。
本次消费:
05点击方块跳跃并狗叫
(1)实现点击方块跳跃:
package com.example.petimport android.os.Bundleimport androidx.activity.ComponentActivityimport androidx.activity.compose.setContentimport androidx.activity.enableEdgeToEdgeimport androidx.compose.foundation.backgroundimport androidx.compose.foundation.clickableimport androidx.compose.foundation.layout.Boximport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.paddingimport androidx.compose.foundation.layout.sizeimport androidx.compose.material3.Scaffoldimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.graphics.Colorimport androidx.compose.ui.res.colorResourceimport androidx.compose.ui.tooling.preview.Previewimport androidx.compose.ui.unit.dpimport androidx.compose.ui.unit.Dpimport com.example.pet.ui.theme.PetThemeimport androidx.compose.animation.core.animateDpAsStateimport androidx.compose.animation.core.animateFloatAsStateimport androidx.compose.animation.core.tweenimport androidx.compose.ui.graphics.graphicsLayerclass MainActivity : ComponentActivity() {    override funonCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        enableEdgeToEdge()        setContent {            PetTheme {                // 全局绿色背景外层容器                Box(                    modifier = Modifier                        .fillMaxSize()                        .background(colorResource(id = R.color.darkgreen))                ) {                    Scaffold(                        modifier = Modifier.fillMaxSize(),                        containerColor = Color.Transparent                    ) { innerPadding ->                        // 居中橙色像素小狗组件                        Box(                            modifier = Modifier                                .fillMaxSize()                                .padding(innerPadding),                            contentAlignment = Alignment.Center                        ) {                            PixelDog()                        }                    }                }            }        }    }}/** * 橙色像素小狗 + 点击缩放交互动画 */@Composablefun PixelDog(modifier: Modifier = Modifier) {    // 控制动画触发    var jumpTrigger by remember { mutableStateOf(false) }    // Y轴偏移:向上跳跃100dp    val jumpOffset: Dp by animateDpAsState(        targetValue = if (jumpTrigger) (-200).dp else 0.dp,        animationSpec = tween(durationMillis = 400),        finishedListener = { jumpTrigger = false } // 动画结束复位    )    // 逆时针旋转360度    val rotateAngle: Float by animateFloatAsState(        targetValue = if (jumpTrigger) -360f else 0f,        animationSpec = tween(durationMillis = 400)    )    Box(        modifier = modifier            .size(120.dp)            .clickable { jumpTrigger = true } // 点击触发后空翻            .graphicsLayer {                translationY = jumpOffset.toPx()                rotationZ = rotateAngle            },        contentAlignment = Alignment.Center    ) {        // 橙色像素小狗纯色方块(无图片依赖,直接消除资源报错)        Box(            modifier = Modifier                .size(300.dp)                .background(Color(0xFFfe8862))        )    }}@Preview@Composablefun Preview() {    PetTheme {        Box(            modifier = Modifier                .fillMaxSize()                .background(colorResource(id = R.color.darkgreen))        ) {            Scaffold(                modifier = Modifier.fillMaxSize(),                containerColor = Color.Transparent            ) { innerPadding ->                Box(                    modifier = Modifier                        .fillMaxSize()                        .padding(innerPadding),                    contentAlignment = Alignment.Center                ) {                    PixelDog()                }            }        }    }}
(2)尝试ai生成狗叫:
很多开源的东西都在外网,而这两天网络一直不稳定,为了一劳永逸,我打算解决网络问题。在CSDN上找到一个人推荐的软件Watt Toolkit

https://steampp.net/

非常好用。立刻解决了进入GitHub的问题。

Mac 弹出要开机密码,是因为它需要修改系统网络代理 / Hosts 文件

但是在GitHub和replicate上都没有找合适的可以在线使用的模型。(积累本地部署Suno bark的经验,镜像、资源包都忙活完了,结果最后一运行却差强人意。

https://replicate.com

(3)所以我改为使用线上可商用的音效资源:

https://www.ear0.com

狗叫 许可:CC-BY 作者:noctaro 来源:耳聆网 https://www.ear0.com/sound/13212
(4)实现点击方块狗叫
在res的子集中新建一个文件夹,命名为raw。直接commend+c 储存在本地的wav文件,commend+v raw文件夹,就能实现wav导入安卓studio。
还是用豆包文生代码,用deepseek的API调试。
package com.example.petimport android.media.MediaPlayerimport android.os.Bundleimport androidx.activity.ComponentActivityimport androidx.activity.compose.setContentimport androidx.activity.enableEdgeToEdgeimport androidx.compose.animation.core.animateDpAsStateimport androidx.compose.animation.core.animateFloatAsStateimport androidx.compose.animation.core.tweenimport androidx.compose.foundation.backgroundimport androidx.compose.foundation.clickableimport androidx.compose.foundation.layout.Boximport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.paddingimport androidx.compose.foundation.layout.sizeimport androidx.compose.material3.Scaffoldimport androidx.compose.runtime.Composableimport androidx.compose.runtime.DisposableEffectimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.graphics.Colorimport androidx.compose.ui.graphics.graphicsLayerimport androidx.compose.ui.platform.LocalContextimport androidx.compose.ui.res.colorResourceimport androidx.compose.ui.tooling.preview.Previewimport androidx.compose.ui.unit.Dpimport androidx.compose.ui.unit.dpimport com.example.pet.ui.theme.PetThemeclass MainActivity : ComponentActivity() {    override funonCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        enableEdgeToEdge()        setContent {            PetTheme {                Box(                    modifier = Modifier                        .fillMaxSize()                        .background(colorResource(id = R.color.darkgreen))                ) {                    Scaffold(                        modifier = Modifier.fillMaxSize(),                        containerColor = Color.Transparent                    ) { innerPadding ->                        Box(                            modifier = Modifier                                .fillMaxSize()                                .padding(innerPadding),                            contentAlignment = Alignment.Center                        ) {                            PixelDog()                        }                    }                }            }        }    }}/** * 橙色像素小狗 + 点击缩放跳跃旋转 + 播放狗叫wav音频 */@Composablefun PixelDog(modifier: Modifier = Modifier) {    val context = LocalContext.current    var jumpTrigger by remember { mutableStateOf(false) }    val mediaPlayer = remember {        MediaPlayer.create(context, R.raw.dog_bark)    }    DisposableEffect(Unit) {        onDispose {            mediaPlayer.release()        }    }    val jumpOffset: Dp by animateDpAsState(        targetValue = if (jumpTrigger) (-200).dp else 0.dp,        animationSpec = tween(durationMillis = 400),        finishedListener = { jumpTrigger = false }    )    val rotateAngle: Float by animateFloatAsState(        targetValue = if (jumpTrigger) -360f else 0f,        animationSpec = tween(durationMillis = 400)    )    funonDogClick() {        jumpTrigger = true        mediaPlayer.seekTo(0)        mediaPlayer.start()    }    Box(        modifier = modifier            .size(120.dp)            .clickable { onDogClick() }            .graphicsLayer {                translationY = jumpOffset.toPx()                rotationZ = rotateAngle            },        contentAlignment = Alignment.Center    ) {        Box(            modifier = Modifier                .size(300.dp)                .background(Color(0xFFfe8862))        )    }}@Preview@Composablefun Preview() {    PetTheme {        Box(            modifier = Modifier                .fillMaxSize()                .background(colorResource(id = R.color.darkgreen))        ) {            Scaffold(                modifier = Modifier.fillMaxSize(),                containerColor = Color.Transparent            ) { innerPadding ->                Box(                    modifier = Modifier                        .fillMaxSize()                        .padding(innerPadding),                    contentAlignment = Alignment.Center                ) {                    PixelDog()                }            }        }    }}
这一套组合技非常爽,不过agent的API有报错,不是我的问题。

400 是服务端参数校验失败,代表客户端(Android Studio 里的 AI Agent 插件)发给 DeepSeek 的请求格式不符合接口最新要求;

关键词 reasoning_content:DeepSeek 近期更新了思考链(深度思考)模式的入参强制校验,旧版插件没有携带这个字段,直接被 API 拒绝返回 400。

06四个小时内点击超过10次会有宠物变形动画并打饱嗝
(1)同样,在耳聆网下载音频

小狗叫 许可:CC-BY-NC 作者:centianhua 来源:耳聆网 https://www.ear0.com/sound/38121

(2)把mp3命名为burp.mp3复制粘贴给res/raw文件夹

(3)修改代码:

package com.example.petimport android.media.MediaPlayerimport android.os.Bundleimport androidx.activity.ComponentActivityimport androidx.activity.compose.setContentimport androidx.activity.enableEdgeToEdgeimport androidx.compose.animation.core.animateDpAsStateimport androidx.compose.animation.core.animateFloatAsStateimport androidx.compose.animation.core.tweenimport androidx.compose.foundation.backgroundimport androidx.compose.foundation.clickableimport androidx.compose.foundation.layout.Boximport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.paddingimport androidx.compose.foundation.layout.sizeimport androidx.compose.runtime.Composableimport androidx.compose.runtime.DisposableEffectimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableLongStateOfimport androidx.compose.runtime.mutableIntStateOfimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.saveable.rememberSaveableimport androidx.compose.runtime.setValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.graphics.Colorimport androidx.compose.ui.graphics.graphicsLayerimport androidx.compose.ui.platform.LocalContextimport androidx.compose.ui.res.colorResourceimport androidx.compose.ui.tooling.preview.Previewimport androidx.compose.ui.unit.Dpimport androidx.compose.ui.unit.dpimport com.example.pet.ui.theme.PetThemeclass MainActivity : ComponentActivity() {    override funonCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        enableEdgeToEdge()        setContent {            PetTheme {                Box(                    modifier = Modifier                        .fillMaxSize()                        .background(colorResource(id = R.color.darkgreen))                ) {                    androidx.compose.material3.Scaffold(                        modifier = Modifier.fillMaxSize(),                        containerColor = Color.Transparent                    ) { innerPadding ->                        Box(                            modifier = Modifier                                .fillMaxSize()                                .padding(innerPadding),                            contentAlignment = Alignment.Center                        ) {                            PixelDog()                        }                    }                }            }        }    }}@Composablefun PixelDog(modifier: Modifier = Modifier) {    val context = LocalContext.current    val FOUR_HOUR_MILLIS = 4 * 60 * 60 * 1000    var firstClickTime by rememberSaveable { mutableLongStateOf(0L) }    var clickCount by rememberSaveable { mutableIntStateOf(0) }    var triggerNormalAnim by remember { mutableStateOf(false) }    var triggerBurpAnim by remember { mutableStateOf(false) }    val barkPlayer = remember { MediaPlayer.create(context, R.raw.dog_bark) }    val burpPlayer = remember { MediaPlayer.create(context, R.raw.burp) }    DisposableEffect(barkPlayer, burpPlayer) {        onDispose {            barkPlayer.release()            burpPlayer.release()        }    }    funisBurpMode()Boolean {        if (firstClickTime == 0Lreturn false        val now = System.currentTimeMillis()        val timeRange = now - firstClickTime        return timeRange <= FOUR_HOUR_MILLIS && clickCount > 10    }    val jumpOffset: Dp by animateDpAsState(        targetValue = if (triggerNormalAnim) (-200).dp else 0.dp,        animationSpec = tween(400),        finishedListener = { triggerNormalAnim = false }    )    val rotateAngle: Float by animateFloatAsState(        targetValue = if (triggerNormalAnim) -360f else 0f,        animationSpec = tween(400)    )    val scaleX by animateFloatAsState(        targetValue = if (triggerBurpAnim) 1.3f else 1f,        animationSpec = tween(450),        finishedListener = { triggerBurpAnim = false }    )    val scaleY by animateFloatAsState(        targetValue = if (triggerBurpAnim) 0.6f else 1f,        animationSpec = tween(450)    )    funhandleClick() {        val currentTime = System.currentTimeMillis()        if (firstClickTime != 0L && currentTime - firstClickTime > FOUR_HOUR_MILLIS) {            clickCount = 0            firstClickTime = 0        }        if (firstClickTime == 0L) {            firstClickTime = currentTime        }        clickCount += 1        if (isBurpMode()) {            triggerBurpAnim = true            burpPlayer.seekTo(0)            burpPlayer.start()        } else {            triggerNormalAnim = true            barkPlayer.seekTo(0)            barkPlayer.start()        }    }    Box(        modifier = modifier            .size(120.dp)            .clickable { handleClick() }            .graphicsLayer {                if (isBurpMode()) {                    this.scaleX = scaleX                    this.scaleY = scaleY                } else {                    translationY = jumpOffset.toPx()                    rotationZ = rotateAngle                }            },        contentAlignment = Alignment.Center    ) {        Box(            modifier = Modifier                .size(300.dp)                .background(Color(0xFFfe8862))        )    }}@Preview@Composablefun Preview() {    PetTheme {        Box(            modifier = Modifier                .fillMaxSize()                .background(colorResource(id = R.color.darkgreen))        ) {            androidx.compose.material3.Scaffold(                modifier = Modifier.fillMaxSize(),                containerColor = Color.Transparent            ) { innerPadding ->                Box(                    modifier = Modifier                        .fillMaxSize()                        .padding(innerPadding),                    contentAlignment = Alignment.Center                ) {                    PixelDog()                }            }        }    }}

07每天早上八点、中午十二点、晚上十点和用户打招呼,调用手机通知权限,发一条消息,分别从话术库中选择一条发送,提示音是狗叫

(1)修改 AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?><manifestxmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"package="com.example.pet">    <!-- 权限全部写在application外面,manifest内部 -->    <uses-permissionandroid:name="android.permission.POST_NOTIFICATIONS"/>    <uses-permissionandroid:name="android.permission.SCHEDULE_EXACT_ALARM"/>    <uses-permissionandroid:name="android.permission.USE_EXACT_ALARM"/>    <applicationandroid:allowBackup="true"android:dataExtractionRules="@xml/data_extraction_rules"android:fullBackupContent="@xml/backup_rules"android:icon="@drawable/ic_launcher_background"android:label="@string/app_name"android:roundIcon="@drawable/ic_launcher_background"android:supportsRtl="true"android:theme="@style/Theme.Pet">        <!-- 广播接收器必须放在application标签内部 -->        <receiverandroid:name=".NotificationReceiver"android:exported="false"/>        <activityandroid:name=".MainActivity"android:exported="true"android:label="@string/app_name"android:theme="@style/Theme.Pet">            <intent-filter>                <actionandroid:name="android.intent.action.MAIN" />                <categoryandroid:name="android.intent.category.LAUNCHER" />            </intent-filter>        </activity>    </application></manifest>

(2)新建NotificationReceiver.kt

新建于这个文件夹中,类型选择class

package com.example.petimport android.content.BroadcastReceiverimport android.content.Contextimport android.content.Intentimport android.content.pm.PackageManagerimport android.media.MediaPlayerimport androidx.core.app.NotificationCompatimport androidx.core.app.NotificationManagerCompatimport androidx.core.content.ContextCompatimport kotlin.random.Randomclass NotificationReceiver : BroadcastReceiver() {    private val greetList = listOf(        "早上好,新的一天元气满满!",        "起床啦,今天也要开开心心",        "午饭时间到,记得好好吃饭哦",        "中午歇一会,放松一下吧",        "晚上好,忙碌一天辛苦啦",        "夜深了,早点休息不要熬夜"//这些台词我后面都改成不分时间段的了    )    override funonReceive(context: Context?, intent: Intent?) {        if (context == nullreturn        val selectText = greetList[Random.nextInt(greetList.size)]        val player = MediaPlayer.create(context, R.raw.dog_bark)        player.start()        player.setOnCompletionListener {            it.release()        }        // 判断通知权限        if (ContextCompat.checkSelfPermission(                context,                android.Manifest.permission.POST_NOTIFICATIONS            ) == PackageManager.PERMISSION_GRANTED        ) {            try {                val notification = NotificationCompat.Builder(context, "pet_remind")                    .setSmallIcon(R.drawable.ic_launcher_background)                    .setContentTitle("你的小宠物")                    .setContentText(selectText)                    .setPriority(NotificationCompat.PRIORITY_HIGH)                    .build()                NotificationManagerCompat.from(context).notify(1001, notification)            } catch (e: SecurityException) {                // 用户拒绝权限时什么都不执行            }        }    }}

(3)新建ScheduleManager.kt,类型选择object

package com.example.petimport android.app.AlarmManagerimport android.app.PendingIntentimport android.content.Contextimport android.content.Intentimport java.util.*object ScheduleManager {    funsetupAllAlarm(context: Context) {        setAlarm(context, 801001)        setAlarm(context, 1201002)        setAlarm(context, 2201003)    }    private funsetAlarm(context: Context, hour: Int, minute: Int, requestCode: Int) {        val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager        val intent = Intent(context, NotificationReceiver::class.java)        val pendingIntent = PendingIntent.getBroadcast(            context, requestCode, intent,            PendingIntent.FLAG_IMMUTABLE        )        val calendar = Calendar.getInstance().apply {            timeInMillis = System.currentTimeMillis()            set(Calendar.HOUR_OF_DAY, hour)            set(Calendar.MINUTE, minute)            set(Calendar.SECOND, 0)        }        if (calendar.timeInMillis <= System.currentTimeMillis()) {            calendar.add(Calendar.DAY_OF_YEAR, 1)        }        alarmManager.setExactAndAllowWhileIdle(            AlarmManager.RTC_WAKEUP,            calendar.timeInMillis,            pendingIntent        )    }    funcancelAllAlarm(context: Context) {        val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager        listOf(100110021003).forEach { code ->            val intent = Intent(context, NotificationReceiver::class.java)            val pendingIntent = PendingIntent.getBroadcast(                context, code, intent, PendingIntent.FLAG_IMMUTABLE            )            alarmManager.cancel(pendingIntent)        }    }}

(4)在build.gradle.kts中添加依赖

dependencies {    implementation("androidx.work:work-runtime-ktx:2.10.0")}

(5)修改MainActivity.kt

package com.example.petimport android.media.MediaPlayerimport android.os.Bundleimport androidx.activity.ComponentActivityimport androidx.activity.compose.setContentimport androidx.activity.enableEdgeToEdgeimport androidx.compose.animation.core.animateDpAsStateimport androidx.compose.animation.core.animateFloatAsStateimport androidx.compose.animation.core.tweenimport androidx.compose.foundation.backgroundimport androidx.compose.foundation.clickableimport androidx.compose.foundation.layout.Boximport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.paddingimport androidx.compose.foundation.layout.sizeimport androidx.compose.runtime.Composableimport androidx.compose.runtime.DisposableEffectimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableLongStateOfimport androidx.compose.runtime.mutableIntStateOfimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.saveable.rememberSaveableimport androidx.compose.runtime.setValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.graphics.Colorimport androidx.compose.ui.graphics.graphicsLayerimport androidx.compose.ui.platform.LocalContextimport androidx.compose.ui.res.colorResourceimport androidx.compose.ui.tooling.preview.Previewimport androidx.compose.ui.unit.Dpimport androidx.compose.ui.unit.dpimport com.example.pet.ui.theme.PetThemeclass MainActivity : ComponentActivity() {    // 把权限申请的 launcher 放到方法外面,作为成员变量    private val requestPermissionLauncher =        registerForActivityResult(androidx.activity.result.contract.ActivityResultContracts.RequestPermission()) { granted ->            if (granted) {                ScheduleManager.setupAllAlarm(this)            }        }    override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        enableEdgeToEdge()        setContent {            //你的Compose页面代码不变            PetTheme {                Box(                    modifier = Modifier                        .fillMaxSize()                        .background(colorResource(id = R.color.darkgreen))                ) {                    androidx.compose.material3.Scaffold(                        modifier = Modifier.fillMaxSize(),                        containerColor = Color.Transparent                    ) { innerPadding ->                        Box(                            modifier = Modifier                                .fillMaxSize()                                .padding(innerPadding),                            contentAlignment = Alignment.Center                        ) {                            PixelDog()                        }                    }                }            }        }        requestNotificationPermission()    }    private fun requestNotificationPermission() {        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {            requestPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS)        } else {            ScheduleManager.setupAllAlarm(this)        }    }}@Composablefun PixelDog(modifier: Modifier = Modifier) {    val context = LocalContext.current    val FOUR_HOUR_MILLIS = 4 * 60 * 60 * 1000    var firstClickTime by rememberSaveable { mutableLongStateOf(0L) }    var clickCount by rememberSaveable { mutableIntStateOf(0) }    var triggerNormalAnim by remember { mutableStateOf(false) }    var triggerBurpAnim by remember { mutableStateOf(false) }    val barkPlayer = remember { MediaPlayer.create(context, R.raw.dog_bark) }    val burpPlayer = remember { MediaPlayer.create(context, R.raw.burp) }    DisposableEffect(barkPlayer, burpPlayer) {        onDispose {            barkPlayer.release()            burpPlayer.release()        }    }    fun isBurpMode(): Boolean {        if (firstClickTime == 0L) return false        val now = System.currentTimeMillis()        val timeRange = now - firstClickTime        return timeRange <= FOUR_HOUR_MILLIS && clickCount > 10    }    val jumpOffset: Dp by animateDpAsState(        targetValue = if (triggerNormalAnim) (-200).dp else 0.dp,        animationSpec = tween(400),        finishedListener = { triggerNormalAnim = false }    )    val rotateAngle: Float by animateFloatAsState(        targetValue = if (triggerNormalAnim) -360f else 0f,        animationSpec = tween(400)    )    val scaleX by animateFloatAsState(        targetValue = if (triggerBurpAnim) 1.3f else 1f,        animationSpec = tween(450),        finishedListener = { triggerBurpAnim = false }    )    val scaleY by animateFloatAsState(        targetValue = if (triggerBurpAnim) 0.6f else 1f,        animationSpec = tween(450)    )    fun handleClick() {        val currentTime = System.currentTimeMillis()        if (firstClickTime != 0L && currentTime - firstClickTime > FOUR_HOUR_MILLIS) {            clickCount = 0            firstClickTime = 0        }        if (firstClickTime == 0L) {            firstClickTime = currentTime        }        clickCount += 1        if (isBurpMode()) {            triggerBurpAnim = true            burpPlayer.seekTo(0)            burpPlayer.start()        } else {            triggerNormalAnim = true            barkPlayer.seekTo(0)            barkPlayer.start()        }    }    Box(        modifier = modifier            .size(120.dp)            .clickable { handleClick() }            .graphicsLayer {                if (isBurpMode()) {                    this.scaleX = scaleX                    this.scaleY = scaleY                } else {                    translationY = jumpOffset.toPx()                    rotationZ = rotateAngle                }            },        contentAlignment = Alignment.Center    ) {        Box(            modifier = Modifier                .size(300.dp)                .background(Color(0xFFfe8862))        )    }}@Preview@Composablefun Preview() {    PetTheme {        Box(            modifier = Modifier                .fillMaxSize()                .background(colorResource(id = R.color.darkgreen))        ) {            androidx.compose.material3.Scaffold(                modifier = Modifier.fillMaxSize(),                containerColor = Color.Transparent            ) { innerPadding ->                Box(                    modifier = Modifier                        .fillMaxSize()                        .padding(innerPadding),                    contentAlignment = Alignment.Center                ) {                    PixelDog()                }            }        }    }}

08在小米13上部署测试

(1)开启手机开发者模式,设置/我的设备/全部参数与信息,点击OS版本七次,进入开发者模式

(2)设置/更多设置/开发者选项,开启USB调试和USB安装权限

(3)数据线连接电脑和手机

(4)Android Studio识别手机,点运行

于是手机上就会多出pet应用

并且能够运行,期待到点给我发消息。

测试结果:启动时有申请通知权限的提示框,但是到点并没有弹出通知,测试安卓studio的虚拟机也是同样结果。

更改NotificationReceiver的代码:

package com.example.petimport android.content.BroadcastReceiverimport android.content.Contextimport android.content.Intentimport android.content.pm.PackageManagerimport android.media.MediaPlayerimport android.os.Buildimport androidx.core.app.NotificationCompatimport androidx.core.app.NotificationManagerCompatimport androidx.core.content.ContextCompatimport android.app.NotificationChannelimport android.app.NotificationManagerimport kotlin.random.Randomclass NotificationReceiver : BroadcastReceiver() {    private val greetList = listOf(        "早上好,新的一天元气满满!",        "起床啦,今天也要开开心心",        "午饭时间到,记得好好吃饭哦",        "中午歇一会,放松一下吧",        "晚上好,忙碌一天辛苦啦",        "夜深了,早点休息不要熬夜"    )    private val CHANNEL_ID = "pet_remind"    override funonReceive(context: Context?, intent: Intent?) {        if (context == nullreturn        // Android8.0+ 必须创建通知通道,否则通知不会弹出        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {            val channel = NotificationChannel(                CHANNEL_ID,                "宠物定时问候",                NotificationManager.IMPORTANCE_HIGH            ).apply {                setShowBadge(true)            }            val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager            notificationManager.createNotificationChannel(channel)        }        val selectText = greetList[Random.nextInt(greetList.size)]        val player = MediaPlayer.create(context, R.raw.dog_bark)        player.start()        player.setOnCompletionListener { it.release() }        if (ContextCompat.checkSelfPermission(                context,                android.Manifest.permission.POST_NOTIFICATIONS            ) == PackageManager.PERMISSION_GRANTED        ) {            try {                val notification = NotificationCompat.Builder(context, CHANNEL_ID)                    .setSmallIcon(R.drawable.ic_launcher_background)                    .setContentTitle("你的小宠物")                    .setContentText(selectText)                    .setPriority(NotificationCompat.PRIORITY_HIGH)                    .build()                NotificationManagerCompat.from(context).notify(1001, notification)            } catch (_: SecurityException) {}        }    }}

虚拟机通知成功。

重新安装pet。

但是第二次试发现只有在界面的时候才会发通知,后台无法运行。

于是在小米应用设置中修改pet应用权限:改了电池消耗和自启动。

通知功能就正常了。