☰
Android悬浮窗AI桌宠开发实战:融合Codex与Lottie动画
2026/10/21 16:49:38 网站建设 项目流程

说实话,我一开始对“桌宠”这类产品的态度和大多数人差不多:好看,但没什么用。直到某天脑洞一开,把Codex这种能写代码的AI装进Android手机桌面,让它以一只小精灵的形态悬浮在任何App上面,这个想法才真正“真香”了。

这个App不是简单给AI聊天机器人套个皮肤,它需要同时解决三件事:悬浮窗常驻、可交互动画、LLM异步链路。换句话说,你看到的是一个“会卖萌的终端”,但它实际上是一只藏在后台、随时响应你代码需求和日常杂事的数字室友。本文从架构设计、核心实现到踩过的坑都会过一遍,适合准备做类似悬浮窗工具、AI宠物或端侧智能助手的开发者参考。如果你只是好奇“手机桌宠到底怎么做”,这篇也能帮你把技术路线看清。

1. 项目整体设计与思路拆解

1.1 从“卖萌玩具”到“随身AI终端”的定位转变

传统的桌宠应用,无论是PC端还是手机端,核心卖点就是形象可爱、动作丰富。这类应用其实一直存在,但普遍面临一个尴尬:用户装上一周之后,新鲜感过去,基本上就再也不会打开了。桌宠的“陪伴感”不够强,因为它无法真正解决用户的实际问题——它真的只是个玩具。

如果反过来想,把一个大模型接口塞进去会怎样?用户不管是在刷购物页面、看视频,还是在地铁上临时想整理一段文本,都可以直接召唤悬浮精灵,问它:“帮我用Python写一个批量重命名文件的脚本”“这篇文章帮我总结成三句话”。它本身变成了一种“低门槛、高覆盖”的随时可用的AI工具,陪伴感和生产力同时在线。

我最后把产品定位成三句话:

  • 这是一个常驻桌面、随时召唤的AI助手入口,不是单纯的动画播放器。
  • 它的核心交互是“看哪问哪”,用户当前停留在什么页面,桌宠就围绕当前场景帮助思考。
  • 它的角色形象要足够轻量,待机状态降低存在感,呼出状态才展示细节,不让用户觉得碍眼。

这个定位决定了后面的技术选型:桌宠本身要悬浮、要全局可见,但平时又不能抢占视觉焦点。悬浮窗是绕不开的方案。

1.2 悬浮窗、小组件、通知栏气泡,为什么最终选了悬浮窗

手机端的“常驻入口”其实有几种典型形态,我做过一轮对比,也实际写了简单的Demo去验证手感:

实现方案交互能力性能开销适配成本适合场景
悬浮窗完整触摸、拖动、动画连续播放中高中桌宠、全局快捷工具
桌面小组件只能点按、信息静态刷新极低低常驻信息卡片、快捷入口
通知栏气泡点击跳转、展示能力弱低低轻提醒、待办
系统语音助手语音为主、无形象无定制性中低免手动场景

小组件的优点是省电、稳妥,但是它的交互形式太弱,做不了“宠物”该有的连续动画和手势反馈。通知栏气泡则更像工具入口,不像一个“活的东西”。

最终选了悬浮窗。从技术上看,Android从8.0开始区分了TYPE_APPLICATION_OVERLAY,权限声明规范,适配逻辑相对稳定。从体验上看,悬浮窗是唯一能全局覆盖、实时交互、又能带动画的形态。代价也很明确:权限敏感、国产ROM适配复杂、性能和电量消耗需要克制。这个代价在开发里躲不掉,只能逐个克服。

1.3 整体架构:把“显示”“交互”“智能”三层剥开

项目架构不能想着一把梭。我一开始就是把所有代码塞进一个Service里,结果改一个动画效果都要翻半天,后来才重新拆了层。

逻辑上分成四块:

  • 渲染层:负责桌宠形象展示、动画播放、气泡绘制。
  • 交互层:负责触摸事件、拖动、双击、长按菜单、语音按钮。
  • 智能层:负责与Codex类模型通信,管理上下文、流式解析、错误重试。
  • 配置层:负责角色参数、提示词模板、用户偏好设置。

实际工程结构对应的是:

app工程 - core:权限申请、前台服务、悬浮窗管理器 - widget:Lottie动画容器、气泡View、语音按钮 - llm:API网关、上下文管理、SSE流式解析 - config:角色配置、提示词模板、开关项

最核心的设计决定是:把依赖网络、解析、重试逻辑全部收敛在llm模块,widget模块永远不直接发HTTP请求。这样调试的时候,我可以脱离UI直接跑智能层单元测试,也方便以后把动画层换成其他引擎。

悬浮窗虽然看起来是“View”,但它脱胎于Service,生命周期和Activity完全无关。这就要求所有网络回调、状态切换不能在Activity里做,必须放到一个Service或者独立的仓库里管理,否则一锁屏、一切后台,整个链路就断了。

2. 核心细节解析与实操要点

2.1 悬浮窗权限、前台服务与国产ROM适配

问题永远是权限先行。Android 6.0以上,悬浮窗权限不再是一个普通运行时权限,它在“特殊应用访问权限”里单独存在。你需要检查,如果没开就用Intent引导用户跳转到系统设置页:

fun checkOverlayPermission(context: Context): Boolean { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Settings.canDrawOverlays(context) } else { true } } fun requestOverlayPermission(activity: Activity) { if (checkOverlayPermission(activity)) return val intent = Intent( Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:${activity.packageName}") ) activity.startActivity(intent) }

这只是第一步。实际跑起来后你会发现,很多国产定制ROM对这个权限还有额外限制,常见的有:

  • 在权限管理里还有“后台弹出界面”这个独立开关,不开它,悬浮窗在App退到后台后可能显示不出来。
  • 自启动管理如果不允许,服务在锁屏或一键清理后会被杀掉。
  • 电池优化白名单如果没有加入,高耗电时系统会强制把服务冻结。

所以权限引导页不能只引导一个“显示在其他应用上层”,必须把自启动、电池优化、后台弹出界面几个入口做成一键跳转的检查清单。我甚至把这类ROM的适配逻辑做成了接口,方便后来针对不同系统增加检查规则。

创建悬浮窗本身的逻辑不复杂,但有几个Flag非常关键:

val params = WindowManager.LayoutParams( WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL, PixelFormat.TRANSLUCENT ).apply { gravity = Gravity.TOP or Gravity.START x = 32 y = 200 } windowManager.addView(petView, params)

FLAG_NOT_FOCUSABLE的意思是窗口不抢占输入焦点,否则用户在任何App里输入文字时,键盘状态和焦点都会被悬浮窗打断。FLAG_NOT_TOUCH_MODAL的意思是窗口范围之外的触摸事件继续传递给下层窗口,范围之内的我们接下来自己消费。这两个Flag合在一起,才能保证悬浮窗触摸不挡下面页面,同时又能正常接受点击拖动。

提示:不要给悬浮窗设置FLAG_FULLSCREEN,也不要让它默认铺满全屏。桌宠需要的是一个“洞”式的透明窗口,窗口尺寸和动画资源尺寸保持一致就够。

2.2 桌宠动效:Lottie、VAP还是Spine

动效方案我纠结了很久。市面上桌宠动画大概有四条路:

方案资源格式优点缺点
LottieJSON + 位图资源跨端一致、可控性强、帧级控制复杂骨骼动画表现有限
VAP带透明通道视频画质上限高、大场景表现好包体积大、解码耗电、兼容性处理麻烦
Spine二进制骨骼细腻流畅、适合长篇动画运行时库定制成本高、包体不占优势
原生View动画XML/属性动画最轻量、性能最好表现力弱,不适合做“活物”

最终选了Lottie。核心原因是它支持帧级控制,我可以像控制视频一样动态切换动作片段;另一个原因是对接方便,一套JSON动画在多个机型上表现一致,不需要自己做骨骼插值。

Lottie使用上有一个要注意的点:透明背景虽然是天然支持的,但位图资源压缩时不要图省内存而强行降成RGB_565,否则透明边缘会非常脏,出现一圈黑边。比较稳妥的是ARGB_8888,再通过控制动画尺寸来控制内存占用。

桌宠需要多套动画状态,我维护了一个状态机:

  • IDLE:待机循环,动作幅度小,帧率降到20fps。
  • THINKING:思考中旋转或冒泡,等待LLM首帧返回。
  • SPEAKING:气泡打字机模式,角色嘴型或身体有节奏摆动。
  • DRAGGING:被拖拽时的悬空状态,关闭循环动作,停止背景动画。

切换动画时不要每次都重新inflate一个新的LottieAnimationView,重用同一个View,通过setMinFrame、setMaxFrame切帧,速度会快很多。资源方面用assets方式加载,动画文件控制在几百KB到1MB左右,初次加载后常驻内存。

2.3 智能层:Codex接入链路与提示词工程

Codex类模型本身是代码智能体,但手机端的App直接调用的话,要把“对话式请求”封装成结构化的消息体,同时处理好流式返回。

基本链路是:

  1. 用户语音或文本输入。
  2. 拼接系统提示词和最近N轮上下文。
  3. 发起SSE流式请求。
  4. 解析每个data事件,把增量文本交给UI层追加显示。
  5. 保存回复到上下文,控制token总量。

流式响应必须用,因为大模型返回时间普遍在几秒甚至更长,如果等全部结果返回再展示,用户会完全丧失“对话感”。用了流式之后,桌宠可以做到“边接收边说话”,体验完全不一样。

核心Kotlin示例(基于OkHttp和协程):

suspend fun streamChat(messages: List<Map<String, String>>, onDelta: (String) -> Unit) { withContext(Dispatchers.IO) { val client = OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(0, TimeUnit.SECONDS) .build() val bodyJson = """ { "model": "codex-mini", "messages": ${gson.toJson(messages)}, "temperature": 0.7, "stream": true } """.trimIndent() val request = Request.Builder() .url("https://api.example.com/chat") .header("Authorization", "Bearer $apiKey") .post(bodyJson.toRequestBody("application/json".toMediaType())) .build() client.newCall(request).execute().use { response -> val source = response.body?.source() ?: return while (!source.exhausted()) { val line = source.readUtf8Line() ?: break if (line.startsWith("data:")) { val payload = line.removePrefix("data:").trim() if (payload == "[DONE]") break val json = Json.parseToJsonElement(payload).jsonObject val delta = json["delta"]?.jsonObject?.get("content") if (delta != null) { withContext(Dispatchers.Main) { onDelta(delta.jsonPrimitive.content) } } } } } } }

注意readTimeout设置成0,读超时不能限制,否则一个正常的几十秒长回复会被客户端主动掐断。连接超时倒是要短一点,3到10秒就够,因为LLM服务通常在冷启动时需要排队,快速失败比一直吊着强。

提示词设计这个环节容易被忽视,但非常关键。我在系统提示词里写了这么一段:

你是一只住在手机里的小精灵,名字随角色切换。你对代码和脚本尤其擅长,但也会回答问题。回答风格:尽量简短,分点,如果涉及代码,直接给出可复制的代码块。如果用户当前正在看某个界面,参考定位信息辅助回答。

加上“定位信息”是我摸索出的一个小技巧。用户在悬浮窗里提问,但桌宠不知道用户在浏览什么页面。App可以通过无障碍或手动选择“当前页面标签”的方式,把“用户正在看设置页”这类上下文塞进user消息前缀,效果就像天生知道你在干嘛一样。

上下文管理也有讲究:不能无限制累积历史消息。我采用“固定窗口+字符估算”的方式,只保留最近6到8轮,超长时丢弃最老消息,并给模型塞一句“前面的内容太长了,但你已经接收过完整历史”。实测下来,回答连贯性不会受太大影响,请求体也不会撑爆网关限制。

2.4 语音识别与手势系统的几个关键选择

语音输入方面,我优先使用系统的SpeechRecognizer在线识别,因为它覆盖语种广且精度高;离线识别PocketSphinx我也测试过,识别率在中文场景下实在一般,放弃。在线识别要注意麦克风权限不能等用户点击之后在悬浮窗上直接弹申请框,否则权限弹窗很可能被悬浮窗遮挡。正确做法是:进入App主界面时就把麦克风权限申请好,把权限说明写清楚。

手势识别要注意的核心问题是“点击”和“拖动”的区分。如果用户只是指尖轻点,但系统误判成拖动,宠物会满屏乱跑。我用的策略是阈值判定:

petView.setOnTouchListener { _, event -> when (event.actionMasked) { MotionEvent.ACTION_DOWN -> { startX = event.rawX startY = event.rawY startTime = SystemClock.uptimeMillis() isDragging = false } MotionEvent.ACTION_MOVE -> { val dx = event.rawX - startX val dy = event.rawY - startY if (abs(dx) > 10.dp || abs(dy) > 10.dp) { isDragging = true } if (isDragging) { params.x = (event.rawX - petView.width / 2).toInt() params.y = (event.rawY - petView.height / 2).toInt() windowManager.updateViewLayout(petView, params) } } MotionEvent.ACTION_UP -> { if (!isDragging && SystemClock.uptimeMillis() - startTime < 500) { petViewModel.switchToThinkingAndSpeak() } } } true }

距离阈值10dp大约是用户最轻触碰的抖动范围,超过才认为是拖动。时间阈值500ms和距离阈值同时判断,避免用户长按不松手却被当成点击。

气泡View的展示也要处理一下:流式文字不能一次性setText,因为每来一个增量就setText一次会有明显的卡顿感。正确的做法是保留一个StringBuilder,增量到达后TextPaint测量当前文本宽度,超过气泡最大宽度时自动换行,然后用TextView.append追加。光标闪动用简单的属性动画控制透明度,不需要自定义复杂布局。

3. 实操过程与核心环节实现

3.1 工程搭建与模块初始化时序

工程基于Android Studio,minSdk定在24,也就是Android 7.0,覆盖绝大多数仍在活跃使用的设备。targetSdk尽量保持最新稳定版,因为应用市场对target版本有硬性要求。

模块划分严格按之前的架构来,核心依赖包括:

  • com.airbnb.android:lottie
  • com.squareup.okhttp3:okhttp
  • org.jetbrains.kotlinx:kotlinx-coroutines-android
  • com.google.code.gson

初始化顺序上,应用启动后先走一个BootReceiver或者最短启动路径:

  1. 请求必要权限(麦克风、通知、悬浮窗)。
  2. 启动ForegroundService,并创建通知渠道。
  3. Service内部初始化悬浮窗管理器和Lottie动画容器。
  4. 读取config中的角色配置和提示词模板。
  5. 通过心跳机制把桌宠View添加到WindowManager。

前台服务的通知类型我用了“ongoing”类型,内容显示“桌宠正在守护你的屏幕”,这样用户能随时感知到服务存在,也避免系统因为“无通知的长时间后台”而杀掉进程。

3.2 悬浮窗的完整增删流程

这里分享一个我犯过的低级错误:addView之后,如果没有在onDestroy里做removeView,会导致窗口泄漏,进程退出时会抛异常。因此悬浮窗的完整增删必须成对出现:

class FloatWindowManager(private val context: Context) { private val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager private var petView: PetView? = null private var params: WindowManager.LayoutParams? = null fun showPet() { if (petView != null) return petView = PetView(context).apply { setPetAnimation(R.raw.pet_idle) } params = createLayoutParams() windowManager.addView(petView, params) } fun hidePet() { petView?.let { windowManager.removeView(it) } petView = null params = null } fun updatePosition(x: Int, y: Int) { if (petView == null || params == null) return params!!.x = x params!!.y = y windowManager.updateViewLayout(petView, params) } }

这里的PetView是一个自定义FrameLayout,内部包含LottieAnimationView和气泡View。在实际项目里,我把动画状态切换、触摸回调都封装在PetView内部,WindowManager只负责View的增删和位置更新,职责更清晰。

维护悬浮窗位置时还有一个细节:需要监听屏幕宽高并做边界约束,不能让用户把宠物拖出屏幕一半,否则点不到也拽不回来。正常情况下,x范围限制在0到screenWidth - petWidth,y范围在状态栏高度到screenHeight - petHeight之间。

3.3 桌宠状态机与交互闭环

桌宠设计的核心是一个简单的状态机,状态转移逻辑:

当前状态触发事件下一状态行为
IDLE用户点击宠物THINKING启动思考动画,显示加载气泡
THINKING收到流式首帧SPEAKING切换说话动画,开始打字机
SPEAKING回复结束3秒IDLE固话气泡,缓慢淡出
任意状态用户按住拖动DRAGGING暂停动画,跟随手指
DRAGGING松手且未点击IDLE恢复待机动画

状态切换通过一个回调接口传给外部,所有状态变更集中在ViewModel里,因此在任何地方都可以方便地打印日志、上报埋点或者做调试。

完整交互闭环是:

用户看到桌宠 -> 点击或说话 -> 宠物进思考状态 -> 请求发送到Codex类模型 -> 收到首帧 -> 宠物进入说话状态,气泡逐字弹出 -> 结束后回到待机。

语音输入的部分我封装成了一个AudioInputHelper,内部使用SpeechRecognizer:

private val recognizer = SpeechRecognizer.createSpeechRecognizer(context) fun startListening(): Flow<String> = callbackFlow { val listener = object : RecognitionListener { override fun onResults(results: Bundle?) { val matches = results?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION) if (!matches.isNullOrEmpty()) { trySend(matches[0]) close() } } } recognizer.setRecognitionListener(listener) val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply { putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM) putExtra(RecognizerIntent.EXTRA_LANGUAGE, "zh-CN") } recognizer.startListening(intent) awaitClose { recognizer.destroy() } }

需要提醒的是,SpeechRecognizer是系统服务,过度频繁唤醒会导致Binder连接堆积,不要每次语音输入都新建实例,尽量复用。

3.4 耗电与内存优化:桌宠不能变成电老虎

桌宠最大的敌人不是功能,而是耗电。一开始我让Lottie动画全速跑,30fps,结果半天下来手机掉电明显。做了一轮优化后,耗电量基本可以接受,关键是这几招:

  • 空闲帧率降到15fps甚至暂停。待机状态用户一般不会盯着看,与其播放高频动画,不如降到6到10fps,进入“半睡眠”。
  • 网络请求集中控制。智能层不能每几秒就发一次心跳请求,只在用户主动提问时才建立连接。
  • 动画资源复用。不要频繁重新inflate,切换角色时只切换assets动画资源,不重建View。
  • 内存清理。WindowManager.removeView时把LottieAnimationView的bitmap缓存和回调全部释放,避免动画资源泄漏。

实测参考:一个约300KB的Lottie动画,保持悬浮状态并待机运行,App内存占用稳定在40MB到60MB之间,空闲掉电在正常使用背景下几乎感觉不到。如果你发现自己的桌宠耗电特别明显,大概率是动画帧率没有降下来。

4. 常见问题与排查技巧实录

开发期间踩了不少坑,我把高频问题整理成了速查表,方便后续排查:

问题现象常见原因解决方案
悬浮窗在某系统上自动消失省电策略或自启动被限制引导用户加白名单、允许后台弹出界面
Android 13以上没有通知弹窗缺少POST_NOTIFICATIONS权限Manifest声明并在运行时申请通知权限
Lottie动画边缘有黑边位图格式降到了RGB_565强制ARGB_8888,并压缩资源尺寸
流式文字显示卡顿主线程直接解析网络数据使用Dispatchers.IO解析,增量通过withContext回主线程
悬浮窗出现后无法点击下层页面缺少FLAG_NOT_TOUCH_MODAL检查悬浮窗WindowManager.LayoutParams的Flag
长按宠物触发语音但页面误触长按事件和点击事件冲突使用OnTouchListener自行判断时长和距离
在某些金融类App内宠物消失这些App主动调用了禁用悬浮窗的API检测包名后提示用户,这个场景无法强制解决
一键清理后服务被杀没有锁定任务卡片或加入自启动白名单前台服务+引导加白名单

4.1 适配问题:国产ROM比Android原生难缠

很多用户把App安装到手机上,发现桌宠一会儿在、一会儿不在,问题通常不在代码上,而在系统行为差异。

以某款主流国产ROM为例,悬浮窗显示需要三个开关同时满足:悬浮窗权限、后台弹出界面、自启动。而且它的电池优化策略默认把第三方应用标为“受限”,一旦用户手动查杀一次后台任务,整个应用就被冻结了。

我的经验是:做一个权限诊断页,把所有开关状态一次性读出来,用“检测到XX项未开启”这种可视化的方式引导用户。用户一般看不明白底层设置,但告诉他“你需要点一下这个按钮”,接受度高得多。

还有一点:如果App本身是工具类,建议在用户完成设置后给一个“自检成功”的反馈。这一步看着小,实际能减少很大一部分“桌宠不见了”的反馈工单。

4.2 流式响应与动画如何配合才能不崩

最开始接入流式响应的时候,我直接在OkHttp回调里更新UI,结果数据一多,主线程卡成PPT。后来做了三层处理:

  • 第一层:网络线程解析每一行data,只提取文本增量。
  • 第二层:用一个可取消的Channel缓冲增量文本。
  • 第三层:UI层通过Flow collect增量,基于TextView.append追加。

同时需要防止Activity或Service销毁后协程还在发消息,所有网络调用都绑定到当前生命周期作用域。最简单的方式是给Service的协程Job加一个取消钩子,服务销毁时统一cancel。

另外,流式过程中如果用户连续点击宠物两次,可能会堆积两个请求。我加了防抖机制:同一时间只允许一个请求在途,如果用户快速提问,新请求会覆盖旧请求并取消旧协程。

4.3 交互体验细节:宠物挡字、误触、易用性

宠物悬浮在屏幕上,不可避免会挡住内容。我的处理方案是“边缘收纳”:用户把宠物拖到屏幕边缘后,宠物可以只露出一小部分,像一只躲在墙角探头的小动物。实现方法是监听拖动结束后的坐标,如果x方向靠近左右边界,就把它吸附到边缘,只露出一半宽度。需要交互时点击边缘的“小耳朵”,宠物弹回全屏区域。

误触是另一个高频问题。桌宠位于右下角时,用户单手操作经常按到。我的做法是默认放到左侧边缘偏上位置,并支持“完全透明待机模式”。在待机时,动画播放但触摸区域缩小到宠物体积的70%左右,降低误触概率。

无障碍方面,气泡最字号默认14sp,可以跟随系统字体缩放。桌宠本体不依赖文本阅读,但气泡内容需要支持用户手动复制——长按气泡即可复制全文,这个小功能被很多用户夸过。

4.4 上架与隐私合规的几个提醒

手机桌宠类应用往往牵扯麦克风、悬浮窗、网络三个敏感权限,上架审核时容易卡在隐私声明上。我的做法是:

  • 隐私政策里明确写明麦克风最主要用于“语音输入AI对话”,不会录音存储。
  • 悬浮窗权限用途写明“用于全局悬浮显示桌宠形象和交互入口”,描述必须和实际功能一致。
  • 不要在应用描述中出现“外挂”“破解”“自动点击”等词,哪怕有相关意图也绝对不要用,否则大概率被拒。
  • 国内应用市场可能需要软著和隐私检测报告,提前准备。

实际上我做这个App之后才发现,技术不是最大的门槛,权限的解释、错误的引导、安全的边界才是。把这三块写清楚,上架审核反而会很顺利。

写在后面

这个项目我断断续续做了大概一个多月,最大的感受不是技术难,而是“分寸感”很难把握。桌宠不能太碍眼,又不能没有存在感;AI回复不能太啰嗦,又不能太敷衍;动画不能太耗电,又不能像个PPT。每一步都在做取舍。

在做完基础功能后的几天里,我每天都会把手机放桌上,时不时戳一下桌宠让它写几句代码、整理一段笔记。真正让我觉得满意的不是某个技术点,而是那种“随叫随到”的感觉——它不吵不闹地待在屏幕边缘,需要时立刻变成一个小助手。

后续我准备扩展几个方向:一是把语音交互再本地化,离线也能完成简单指令;二是支持更多角色形象,让用户能导入自己喜欢的Lottie动画;三是给桌宠加一个“快捷指令面板”,长按之后直接执行“生成日报”“写正则”“翻译”这类高频动作,减少打字成本。

如果你也在做类似的悬浮窗工具或AI桌宠,建议先想清楚一句话:用户为什么愿意让这个小东西一直留在屏幕上?回答好这个问题,技术选型和产品设计都会变得清晰很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询