PyStudio 这类 Android IDE 最容易被低估的,不是“能不能跑一条命令”,而是终端、编辑器、键盘和文件管理几条线能不能互相不添乱。键盘弹起时符号栏错位、侧滑栏误触、运行脚本后回不到编辑器、文件列表钻进空目录,这些问题单独看都小,叠在一起就会让 App 像临时 demo。

这篇更像一次 UI 复盘:哪些状态应该交给终端运行时,哪些交给页面,哪些交互要在手机上收紧。读完至少能得到一条判断线:移动端 IDE 的体验,不靠一个惊艳控件,而靠很多边界都稳定。

这张图先把终端会话、编辑器、文件系统和输入法放到同一个工作台上,后面的几个坑基本都来自这些边界被写乱。

终端不能只是一个 View

一开始终端逻辑很容易写成:

1
2
3
4
TerminalPage
-> ViewModel
-> create TerminalSession
-> attach TerminalView

这样能跑,但生命周期很脆。Activity 重建、页面切换、安装脚本运行、Python 调试会话、Alpine/proot 会话都混在 ViewModel 里,最后很难判断一个会话到底是谁拥有的。

更稳定的方向是参考 Termux 和 Visual-Code-Space:

1
2
3
4
5
6
7
8
9
TerminalService / TerminalRuntime
owns session registry
owns current session id
owns notification / foreground lifetime
exposes create / attach / write / rename / close

UI
observes state
attaches visible terminal view

也就是说,UI 只负责展示和交互,不要成为终端会话的所有者。特别是包安装和脚本运行,最好都走:

1
TerminalRuntime.runScript("Install proot", script)

让用户在终端里看到完整输出,而不是由页面状态条假装管理整个安装过程。

侧滑栏:禁止滑动打开,但允许滑动关闭

手机 IDE 的终端和编辑器都很容易误触。用户在编辑器里横向拖动、在终端里选择文本、在文件列表里滚动时,侧滑栏如果能从边缘滑出来,会非常烦。

比较舒服的规则是:

1
2
3
4
ModalNavigationDrawer(
drawerState = drawerState,
gesturesEnabled = drawerState.isOpen,
)

含义是:

  • 关闭状态下,禁止滑动展开;
  • 打开状态下,允许滑动关闭;
  • 展开入口只留给明确按钮,比如会话按钮、文件按钮、菜单按钮。

这个规则后来同时应用到终端会话侧栏、编辑器文件侧栏和 Files 页快捷路径侧栏。一个行为在多个页面一致,用户就不会每次重新猜。

Termux 风格符号栏要贴着键盘

终端里最常用的不是花哨按钮,而是 ESCTAB、方向键、CTRLALT/-| 这些输入辅助。用户明确要求过“1:1 复刻 Termux 底部工具栏”,这里的关键不是按钮长得像,而是它和键盘的关系要对。

错误体验是:

1
2
3
4
终端内容
符号栏
一大块空白
系统键盘

这通常是 imePadding()、navigation bar inset、底部栏高度重复计算导致的。正确方向是统一 bottom inset:

1
2
3
4
TerminalView
状态栏 / 统计栏
ExtraKeysView
IME

终端页的符号栏和编辑页的底部统计栏,应该用同一套键盘适配逻辑。不要一个用 imePadding(),另一个手算高度,最后空白区域会非常难排。

还有一个小细节:方向键最好直接显示 ↑ ↓ ← →,不要写 UP DOWN LEFT RIGHT。符号栏是高频输入工具,视觉识别速度比文字说明重要。

点击运行脚本的路径

我后来把“运行”当成一次明确旅程,而不是偷偷复用当前 shell。这样用户能在会话侧栏里看到它,也能复查输出。
“快速运行”按钮看似只是点一下运行 Python,实际要串起很多状态:

  1. 保存当前文件;
  2. 检查 python 命令是否存在;
  3. 不存在则引导用户安装运行时;
  4. 切换到终端 tab;
  5. 创建一个新的脚本运行会话;
  6. 显示退出码和耗时;
  7. 按返回键能回到编辑器;
  8. 根据设置决定是否自动删除这次脚本终端会话。

我后来更倾向于把它写成一个明确动作:

1
2
3
4
5
EditorRunAction
-> save file
-> RuntimeCommandChecker.require("python")
-> TerminalRuntime.runScript(title, script)
-> Navigation.select(TerminalTab)

这样“运行”不是偷偷复用当前 shell,也不是在编辑器页面塞一个输出框,而是一个可管理的终端会话。用户能从侧滑栏看到它、重命名它、关闭它。

文件侧栏不要把按钮塞进列表项

文件列表里如果每行都显示“修改 / 删除”按钮,很快会变乱,尤其在手机宽度下。更好的做法是:

  • 单击:进入文件夹或打开文件;
  • 长按:弹出操作面板;
  • 多选:顶部工具栏进入批量操作状态。

长按菜单可以是六个操作:

1
2
3
重命名    复制
剪切 删除
详细信息 多选

每项用图标 + 名称,弹窗里不需要额外“关闭”按钮。用户点空白处或返回键就能关闭。

详细信息也不要只显示静态文本。比较实用的信息包括:

  • 名称;
  • 目录;
  • 类型;
  • 大小;
  • 修改时间;
  • 权限;
  • 所有者;
  • 用户组。

名称、目录、修改时间可以点击复制值;大小显示转换后的大小,同时补充字节数;权限用类似 drwxr-xr-x (0755) 的格式。文件夹的文件数、子文件夹数可以异步统计,数字逐步跳动,既不阻塞列表,也让用户知道统计还在进行。

Files 页面和侧栏不要强行同步路径

编辑器侧栏的文件路径和独立 Files 页面路径,最好不要强行同步。

原因是两者心智不同:

  • 编辑器侧栏是“当前工作项目”的导航;
  • Files 页面是“全局文件管理器”。

如果两边路径同步,用户在 Files 页面浏览 /storage/emulated/0/Download,回到编辑器时项目侧栏也跟着跑了,体验会很奇怪。

Files 页面可以更像独立文件管理器:

  • 顶部标题显示当前路径,超出时省略开头、保留结尾;
  • 长按标题复制完整路径;
  • 点击标题可编辑并跳转;
  • 第一个列表项插入 ... 作为返回上一级;
  • 下拉到阈值后刷新,不一定要显示系统刷新圈;
  • 底部放文件操作栏;
  • 右上角设置按钮跳转设置页;
  • 侧滑栏放项目路径、根目录、内部储存等快捷路径。

全文件访问和 Android/data

文件管理器绕不开 Android 的存储权限。内部储存 /storage/emulated/0/ 需要向用户解释“所有文件访问权限”,并跳转系统授权页。

这里不要假装权限一定能拿到。逻辑要写成:

1
2
3
4
5
6
7
8
9
用户点击内部储存
-> 如果没有 MANAGE_EXTERNAL_STORAGE
弹窗解释为什么需要
跳转系统设置
回来后重新检测
-> 授权成功
解除上级目录限制
-> 授权失败
保持当前可访问范围

Android/data 更敏感,不同系统版本和厂商策略差异很大。能走 SAF 的走 SAF,能提示限制就明确提示,不要给用户一个空列表却不解释。

这类 App 的 UI 经验

移动端 IDE 要先像工具,再谈漂亮。大卡片和装饰很容易占掉本来就紧的屏幕空间,真正救体验的是稳定尺寸、清晰状态和明确反馈。

终端、文件、编辑器的底部区域最好共享一套 IME inset 处理;侧滑栏也要有一致规则,关闭时别靠误触滑开,打开后允许自然滑回去。文件操作放到长按菜单和批量模式里,比在每一行塞按钮更适合手机宽度。

运行脚本、安装依赖、打开 Alpine 这类动作,都应该落成终端会话。会话能命名、能关闭、能复查输出,用户就能理解 App 正在做什么。手机上写代码已经够局促了,工具栏、键盘、侧栏和会话管理就更应该稳一点。