WebToApp 创建按钮全解从 12 类型应用网格到各类型创建流程的源码实现本文以 WebToApp 主界面「我的应用」页面底部的创建按钮为核心完整还原它的交互行为、12 种应用类型 3 列网格的展开机制以及点击某一类型后如何经导航路由进入各自的创建流程。读完你将理解这个入口按钮在 HomeScreen.kt 中的状态驱动实现、与 AppNavigation.kt 路由表的对应关系以及「类型专属表单 → 基本信息 → 保存」的统一创建形态。一、创建按钮的定位创建按钮位于 我的应用 主页底部是新建任何应用的唯一主入口。主界面自上而下分为四个区域顶栏标题 暗亮色/语言/搜索/更多、分类标签行、应用列表、以及最下方的创建按钮。当列表为空时空状态区还会额外提供一个创建快捷按钮直接指向网页类型创建流程方便首次上手。说明主界面布局、分类标签与应用列表的完整描述见 我的应用本文只聚焦其中「创建」这一入口。二、工作方式与交互细节文档描述的三个交互行为在源码中都有精确对应点击展开点击创建 图标后 图标会旋转按钮文字标签从「创建」切换为「关闭」。面板展开按钮上方展开一个浮层面板以3 列网格展示全部 12 种应用类型。点击收起 / 进入创建点击某个类型瓦片即关闭面板并打开该类型的创建流程再次点击按钮本身或点击某个类型都会收起面板。这些行为由HomeScreen.kt中一组Composable状态变量与动画组合实现下面逐条对应。2.1 状态变量驱动展开 / 收起按钮与面板的展开状态由一个布尔量showFabMenu控制按钮onClick直接对其取反// app/src/main/java/com/webtoapp/ui/screens/HomeScreen.kt val fabRotation by animateFloatAsState( targetValue if (showFabMenu) 135f else 0f, animationSpec com.webtoapp.ui.design.WtaMotion.settleSpring(), label fabRotation ) FilledTonalButton( onClick { showFabMenu !showFabMenu }, // 再次点击即收起 ... ) { Icon( Icons.Default.Add, Strings.btnCreate, modifier Modifier .size(20.dp) .graphicsLayer { rotationZ fabRotation } // 图标旋转 ) Text( if (showFabMenu) Strings.close else Strings.createApp, // 标签切换 style MaterialTheme.typography.labelLarge ) }这里可见「图标旋转」与「标签切换」的落地rotationZ目标值在0f与135f之间切换使 号视觉旋转成接近 × 的形态文案则在Strings.close关闭与Strings.createApp创建之间切换。2.2 面板的弹簧动画展开 / 收起的浮层用AnimatedVisibility包裹进出场动画都使用弹簧曲线让面板从按钮上方「长出来」AnimatedVisibility( visible showFabMenu, enter expandVertically( animationSpec spring(dampingRatio 0.75f, stiffness Spring.StiffnessMediumLow), expandFrom Alignment.Bottom ) fadeIn(...), exit shrinkVertically(..., shrinkTowards Alignment.Bottom) fadeOut(...) ) { EnhancedElevatedCard(modifier Modifier.fillMaxWidth().padding(bottom 8.dp)) { // 3 列网格内容 } }面板内容列设了heightIn(max 360.dp)并支持verticalScroll当类型数量增多时网格可滚动避免溢出屏幕。三、12 种应用类型的 3 列网格文档中的「类型网格」在源码里是一个长度为 12 的createActionItems列表由 12 个CreateActionItem(label, iconRes, onClick)组成。每一项绑定一个本地化文案、一个 drawable 图标以及一个导航回调。网格行第 1 列第 2 列第 3 列第 1 行网页多站点HTML第 2 行离线包前端PHP第 3 行WordPressNode.jsPython第 4 行Go媒体画廊这 12 项在HomeScreen.kt中的定义如下文案与图标均来自Strings与R.drawableval createActionItems listOf( CreateActionItem(Strings.appTypeWeb, R.drawable.ic_type_web, onCreateApp), CreateActionItem(Strings.appTypeMultiWeb, R.drawable.ic_type_multi_web, onCreateMultiWebApp), CreateActionItem(Strings.appTypeHtml, R.drawable.ic_type_html, onCreateHtmlApp), CreateActionItem(Strings.websiteOfflinePack, R.drawable.ic_type_offline_pack, onCreateOfflinePack), CreateActionItem(Strings.appTypeFrontend, R.drawable.ic_type_frontend, onCreateFrontendApp), CreateActionItem(Strings.appTypePhp, R.drawable.ic_type_php, onCreatePhpApp), CreateActionItem(Strings.appTypeWordPress, R.drawable.ic_type_wordpress, onCreateWordPressApp), CreateActionItem(Strings.appTypeNodeJs, R.drawable.ic_type_nodejs, onCreateNodeJsApp), CreateActionItem(Strings.appTypePython, R.drawable.ic_type_python, onCreatePythonApp), CreateActionItem(Strings.appTypeGo, R.drawable.ic_type_go, onCreateGoApp), CreateActionItem(Strings.createMediaApp, R.drawable.ic_type_media, onCreateMediaApp), CreateActionItem(Strings.appTypeGallery, R.drawable.ic_type_gallery, onCreateGalleryApp) )3.1 3 列布局如何生成网格并不是逐行手写而是把 12 项列表按 3 个一组chunked(3)每行用Row渲染组内不足 3 项时补占位Spacer对齐列宽createActionItems.chunked(3).forEach { rowItems - Row(modifier Modifier.fillMaxWidth(), horizontalArrangement Arrangement.spacedBy(8.dp) ) { rowItems.forEach { item - CreateActionTile( label item.label, iconRes item.iconRes, onClick { showFabMenu false // 点击类型即收起面板 item.onClick() // 进入对应创建流程 }, modifier Modifier.weight(1f) ) } repeat(3 - rowItems.size) { Spacer(modifier Modifier.weight(1f)) } } }weight(1f)让三列等宽分布repeat(3 - rowItems.size)则保证每行严格占满 3 列视觉上形成规整的 4×3 网格。3.2 瓦片的视觉与触感每个瓦片由CreateActionTile渲染一个 44.dp 的圆角图标容器 最多两行的类型标签点击时带按压缩放与触觉反馈private fun CreateActionTile(label: String, iconRes: Int, onClick: () - Unit, modifier: Modifier Modifier) { val interactionSource remember { MutableInteractionSource() } val hapticClick rememberHapticClick(onClick) // 触觉反馈 Column( modifier modifier .clip(RoundedCornerShape(16.dp)) .clickable(interactionSource interactionSource, indication com.webtoapp.ui.design.rememberWtaIndication(), onClick hapticClick) .padding(vertical 8.dp, horizontal 4.dp), ... ) { // 44.dp 图标容器 labelMedium 文案 } }四、点击类型后的导航路由每个onClick回调最终都落到 AppNavigation.kt 的Routes对象定义的路由常量上。12 种类型对应 12 个CREATE_*路由HomeScreen收到的每个onCreate*回调都会navController.navigate(...)到对应目标页应用类型路由常量导航回调网页CREATE_APPonCreateApp多站点CREATE_MULTI_WEB_APPonCreateMultiWebAppHTMLCREATE_HTML_APPonCreateHtmlApp离线包CREATE_OFFLINE_PACKonCreateOfflinePack前端CREATE_FRONTEND_APPonCreateFrontendAppPHPCREATE_PHP_APPonCreatePhpAppWordPressCREATE_WORDPRESS_APPonCreateWordPressAppNode.jsCREATE_NODEJS_APPonCreateNodeJsAppPythonCREATE_PYTHON_APPonCreatePythonAppGoCREATE_GO_APPonCreateGoApp媒体CREATE_MEDIA_APPonCreateMediaApp画廊CREATE_GALLERY_APPonCreateGalleryApp值得注意的是「网页」类型走了一条略不同的路径——它在导航前先调用viewModel.createNewApp()预创建一条记录// app/src/main/java/com/webtoapp/ui/navigation/AppNavigation.kt onCreateApp { viewModel.createNewApp() navController.navigate(Routes.CREATE_APP) }, onCreateMediaApp { navController.navigate(Routes.CREATE_MEDIA_APP) }, onCreateNodeJsApp { navController.navigate(Routes.CREATE_NODEJS_APP) }, // ...其余类型同理直接 navigate 到各自 CREATE_* 路由各路由最终映射到对应的创建页面composable例如CREATE_NODEJS_APP打开CreateNodeJsAppScreenCREATE_FRONTEND_APP打开CreateFrontendAppScreen。这些创建页在用户保存时通过onCreated回调触发 ViewModel 的save*App方法如saveNodeJsApp、saveHtmlApp、saveFrontendApp再popBackStack返回「我的应用」列表。五、统一的三段式创建形态文档指出「每种类型都遵循相同的形态」。从路由回调可归纳出这套统一的创建流程类型专属表单每种类型打开各自的表单页。网页要求填 URLNode.js 要求填项目目录与启动命令画廊要求填媒体集合与布局运行时类Node.js / PHP / Python / WordPress / Go会提供入口跳转到 Linux 环境 安装工具链。以 Node.js 为例其创建页额外接入了onOpenLinuxEnv { navController.navigate(Routes.LINUX_ENVIRONMENT) }。基本信息名称与图标。图标可来自网站 favicon 抓取、图标库AI 生成或自行上传上传需经过方形 / 自由 / 圆形三种模式的裁剪步骤也可直接从相册选择。保存应用被创建并出现在 我的应用 列表上随后可用应用的 ⋮ 菜单进入「编辑核心配置」回到类型专属表单或「编辑通用配置」共享选项。5.1 类型如何映射到数据模型从源码结构看12 种界面类型最终对应 WebApp.kt 中AppType枚举的不同取值。该枚举是 Roomweb_apps表实体的类型标识// app/src/main/java/com/webtoapp/data/model/WebApp.kt enum class AppType { WEB, IMAGE, VIDEO, HTML, GALLERY, FRONTEND, WORDPRESS, NODEJS_APP, PHP_APP, PYTHON_APP, GO_APP, MULTI_WEB; // 需要执行原生服务端运行时Node/PHP/Python/Go/WordPress的类型 val requiresProcessExec: Boolean get() this in REQUIRES_PROCESS_EXEC ... }这里有一个对创建流程有实际影响的细节requiresProcessExec标记了「从应用私有存储执行原生服务端运行时」的类型集合Node.js / PHP / Python / Go / WordPress。从源码注释可推断这类应用需要把targetSdk 28因为自targetSdk 29起平台对可写数据目录强制执行 W^X可写与可执行互斥会阻止对捆绑二进制的execve而仅依赖 WebView 的类型则可以提升targetSdk。这解释了为何运行时类应用在创建时会引导用户进入 Linux 环境 准备工具链。5.2 创建与编辑复用同一页面从AppNavigation.kt可看到创建与「编辑核心配置」复用了同一套页面组件例如CREATE_APP与EDIT_APP都渲染CreateAppScreen仅通过isEdit false / true与不同的保存回调createNewAppvseditApp区分。这意味着用户在创建按钮里选某类型进入的表单与其之后通过 ⋮ 菜单「编辑核心配置」看到的表单是同一个 UI。六、如何选择合适的类型针对 12 种类型「输入 / 输出 / 适用场景」的完整对照以及每种类型能做什么建议查阅 创建应用 与对应类型的独立文档如 网页、Node.js。概览如下类型输入输出适用场景网页一个 URLWebView APK落地页、工具、仪表盘、文档多站点多个 URL标签 / 卡片 / 信息流 / 抽屉 APK链接枢纽、门户HTML本地 HTML / zip文件协议 APK可切本地托管静态构建、离线 Web 应用离线包一个 URL抓取自包含离线 APK把网站归档前端已构建的前端项目文件协议 APK可切本地托管React、Vue、Vite 构建PHPPHP 项目APK 设备端 PHP小型 PHP 应用WordPressWordPress 站点APK PHP SQLite便携站点Node.jsNode 项目APK 设备端 NodeExpress/Fastify/Koa、APIPythonPython 项目APK 设备端 PythonFlask、Django、FastAPIGoGo 项目APK 设备端 GoGin/Echo/Fiber媒体一张图片或视频媒体播放器 APK单媒体查看器画廊一个媒体集合画廊 APK相册、作品集提示应用内置了示例项目React、Vue、Vite、Node/Express、PHP/Laravel、Python/Flask、Go/Gin、WordPress 等位于app/src/main/assets/sample_projects/可先用一个示例查看所选技术栈的可工作配置再回到创建按钮走正式流程。小结WebToApp 的「创建」按钮是一个由showFabMenu状态驱动、带弹簧展开动画的入口它用createActionItems列表 chunked(3)渲染出 12 类型 × 3 列的网格点击任一项即收起面板并经AppNavigation的 12 个CREATE_*路由进入各自创建页最终统一遵循「类型专属表单 → 基本信息 → 保存」的三段式形态。理解这套入口与路由就能把「创建按钮」这一 UI 元素与 HomeScreen.kt、AppNavigation.kt 以及 AppType 数据模型串成一条完整的实现链路便于继续深入具体某个类型的创建文档。创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考