☰
plain-ui 指南:PlainApp 的 Compose Multiplatform 共享 UI 组件库
2026/10/8 1:32:15 网站建设 项目流程
  • 移动开发
  • 后端
  • 即时通讯
  • 音视频

【免费下载链接】plain-app

🔥 PlainApp is an open-source app that lets you securely manage your phone from a web browser. Access files, media, contacts, SMS, calls, and more through a simple, easy-to-use interface on your desktop.

项目地址:https://gitcode.com/gh_mirrors/pl/plain-app
点击查看免费下载

导读

plain-ui 是 PlainApp 系列应用(Android、iOS 与 Web)共同复用的 Compose Multiplatform UI 组件库,它把PScaffold、PTopAppBar、快速滚动条、下拉刷新、拖拽选择等高频界面能力收敛到com.ismartcoding.plain.ui.base,并内置了可处理大文件的CodeEditor编辑器及其文档、语法高亮、搜索、编辑历史引擎,以及com.ismartcoding.plain.ui.scanner下的QrCodeScanner扫码控件。阅读本文后,你将掌握如何通过 Maven 坐标引入该库、按包名使用各组件、理解扫码控件与宿主应用之间的回调契约,以及编辑器引擎的底层工作原理。

一、库定位:跨平台复用的 UI 原语

plain-ui 被定义为 "Reusable Compose Multiplatform UI primitives shared by Plain applications",即 Plain 系列应用共享的可复用 Compose Multiplatform UI 原语。它在不承载具体业务逻辑的前提下,把 Plain 应用日常页面中最常见、最容易重复书写的界面结构提炼为统一组件,让各端保持一致的视觉与交互体验。

从目录结构(plain-ui/src/commonMain/kotlin/com/ismartcoding/plain/ui)可以清楚看到库的分层组织:

  • base:通用基础组件(PScaffold、PTopAppBar等)以及fastscroll、pullrefresh、dragselect三个能力子包;
  • components/codeeditor:可复用的大文件代码编辑器,含独立的engine引擎;
  • scanner:QrCodeScanner扫码控件及配套组件;
  • theme:主题、颜色、形状、排版定义。

这套结构把"页面骨架"与"重型能力"分离:轻量组件随取随用,重量级编辑器与扫码器则独立成包,按需引入。

二、Maven 坐标与版本获取

依赖坐标

plain-ui 的 Maven 坐标是com.ismartcoding:plain-ui。在 Gradle 项目中按如下方式声明依赖(示例版本为 0.4.0):

dependencies { implementation("com.ismartcoding:plain-ui:0.4.0") }

发布仓库与版本来源

版本发布到专用 Maven 仓库https://plainhub.github.io/plain-app/maven,该仓库托管在独立的maven分支上(该分支同时承载 policy 与 terms 页面)。每次发布通过两种方式触发:

  • 打上plain-ui-v<version>形式的 git tag;
  • 或运行仓库中的Publish plain-ui工作流(即 .github/workflows/publish-plain-ui.yml)。

README 还指出,PlainRouter Android 客户端已经在使用与plain-common相同的 Maven 仓库地址,也就是说同一发布通道同时服务于 plain-common 与 plain-ui 两个库,接入方只需配置一次仓库地址即可消费全部 Plain 共享库。

使用前提:该仓库地址是发布 plain-ui 的专用仓库,实际引入时需在repositories中声明这一 Maven 仓库,并选取当前已发布的具体版本号(README 示例为 0.4.0)。

三、从 base 包使用共享组件

README 建议从com.ismartcoding.plain.ui.base使用共享组件,并从fastscroll、pullrefresh、dragselect三个子包取用能力组件。

3.1 PScaffold:页面骨架与系统栏内边距处理

PScaffold是 Material3Scaffold的轻量封装,位于 PScaffold.kt,签名如下:

@Composable fun PScaffold( modifier: Modifier = Modifier, containerColor: Color = MaterialTheme.colorScheme.background, topBar: @Composable () -> Unit = {}, bottomBar: (@Composable () -> Unit)? = null, floatingActionButton: (@Composable () -> Unit)? = null, content: @Composable (PaddingValues) -> Unit = {}, )

其核心价值在于源码注释所描述的细节:它在内容区域居中应用水平方向的系统栏 inset(calculateStartPadding/calculateEndPadding),这样各页面在横屏、三键导航栏遮挡内容时无需各自处理水平边距,只需通过传入的PaddingValues处理顶部和底部的 inset 即可(水平方向已归零)。这是把"易踩坑"的系统栏适配统一收敛到骨架层的典型设计。

3.2 PTopAppBar:导航回调与主题切换适配

PTopAppBar位于 PTopAppBar.kt,接受onNavigateBack与navigationIcon两个回调:

@Composable fun PTopAppBar( onNavigateBack: (() -> Unit)? = null, modifier: Modifier = Modifier, navigationIcon: (@Composable () -> Unit)? = null, title: String, subtitle: String = "", titleTrailing: (@Composable () -> Unit)? = null, containerColor: Color? = null, subtitleColor: Color? = null, actions: (@Composable RowScope.() -> Unit)? = null, scrollBehavior: TopAppBarScrollBehavior? = null, )

值得注意的工程细节:

  • 导航图标策略:navigationIcon != null时优先使用自定义图标;否则若提供了onNavigateBack,自动渲染内置的返回箭头(BackIcon,使用资源中的arrow_left图标与back文案,见 PTopAppBar.kt)。两个都为空则不显示导航区。
  • 副标题模式:subtitle非空时,标题切换为"主标题 + 副标题"的两行排版;为空则单行标题。
  • 主题切换适配:源码注释明确解释了为何在外层套一个即时着色的Surface、而内部TopAppBar保持透明——Material3 的TopAppBar会内部动画化容器颜色,导致主题切换时比页面背景慢半拍;用即时Surface兜底可让两者同步变化。

另外 README 特别提到"下拉刷新的文案随库资源发布"(Pull refresh strings ship with the library resources),即pullrefresh子包内的EllipseRefreshContent、LoadMoreRefreshContent等组件所需的提示文案已内置于库的 Compose 资源中,使用方无需再自行翻译维护。

3.3 fastscroll:懒加载列表/网格的快速滚动条

fastscroll子包为LazyColumn、LazyVerticalGrid与普通滚动状态提供滚动条。以 LazyColumnScrollbar.kt 为例:

@Composable fun LazyColumnScrollbar( state: LazyListState, modifier: Modifier = Modifier, settings: ScrollbarSettings = ScrollbarSettings.Default, indicatorContent: (@Composable (index: Int, isThumbSelected: Boolean) -> Unit)? = null, content: @Composable () -> Unit, )
  • 通过ScrollbarSettings控制启用状态(enabled)、滑块最小长度(thumbMinLength)、是否常显(alwaysShowScrollbar)与选择模式(selectionMode);
  • 底层controller包中提供了rememberLazyListStateController、rememberLazyGridStateController、rememberScrollStateController三类控制器(见 controller 目录),分别适配列表、网格与普通滚动;
  • 滚动条布局逻辑封装在foundation包(ScrollbarLayoutSettings、VerticalScrollbarLayout等),可搭配 Material3 的Scaffold的scrollBehavior使用,实现"滚动即隐藏/淡出"的现代移动端交互。

3.4 pullrefresh:下拉刷新与加载更多

PullToRefresh是入口封装(PullToRefresh.kt),核心参数包括:

  • refreshLayoutState: RefreshLayoutState:刷新状态对象;
  • userEnable: Boolean = true:是否允许用户手势触发;
  • refreshContent:刷新指示器内容,默认为PullToRefreshContent;
  • content:被包裹的列表内容。

同一子包还提供EllipseRefreshContent(椭圆动画刷新指示)、LoadMoreRefreshContent(加载更多指示)、ComposePosition等,RefreshLayoutNestedScrollConnection负责把刷新手势接入 Compose 的嵌套滚动体系,保证与LazyColumn等可滚动容器的协同。

3.5 dragselect:拖拽框选与全选管理

DragSelectState(DragSelectState.kt)是拖拽选择的状态核心,它持有selectedIds、selectMode、dragState,并基于 README 提到的Identifiable契约工作——该契约来自plain-common,定义极其精简:

interface Identifiable { val id: String }

见 Identifiable.kt。任意数据模型只需实现id属性即可接入toggleSelectAll(allItems: List<Identifiable>)、isAllSelected等批量操作。子包内还提供GridDragSelect、ListDragSelect两种实现,分别适配网格与列表布局,并支持拖拽过程中的自动滚动(autoScrollSpeed)。

四、QrCodeScanner:扫码控件与宿主回调契约

QrCodeScanner位于com.ismartcoding.plain.ui.scanner,是"可复用的扫码 UI + 宿主业务回调"的典型组合。README 明确指出:应用方保留扫码页的 TopBar 与结果弹层(result sheet),扫码控件接收宿主的回调,负责图像选择、扫码结果与特定应用逻辑的处理。

4.1 完整参数清单

@Composable fun QrCodeScanner( cameraPermissionGranted: Boolean, multipleCodesHint: String, imagePickerDescription: String, closeRequest: Int, onCloseActionVisibilityChanged: (Boolean) -> Unit, pickImage: ((String) -> Unit) -> Unit, onScanResult: (String, () -> Unit) -> Unit, handleSpecialCode: (String, () -> Unit) -> Boolean, showNoCodeFound: () -> Unit, showImageLoading: () -> Unit, hideImageLoading: () -> Unit, modifier: Modifier = Modifier, )

各回调的职责:

参数类型职责
cameraPermissionGrantedBoolean相机权限是否已授予,决定是否渲染相机预览
multipleCodesHintString同时识别到多个码时展示的提示文案
imagePickerDescriptionString相册取图按钮的无障碍描述
closeRequestInt宿主发来的"关闭/重置"请求信号(增量计数触发)
onCloseActionVisibilityChanged(Boolean) -> Unit通知宿主"关闭按钮"何时该显示/隐藏(如冻结多码或展示图片选择器时)
pickImage((String) -> Unit) -> Unit宿主提供的取图入口,回调内将图片 URI 回传给控件
onScanResult(String, () -> Unit) -> Unit扫码结果回调,第二个参数是控件提供的"完成/继续"函数
handleSpecialCode(String, () -> Unit) -> Boolean处理应用特有码(如配对码);返回 true 表示已消费,不再走通用结果流程
showNoCodeFound/showImageLoading/hideImageLoading无参回调图片识别无结果、加载中、加载完成的状态通知

4.2 识别流程与防误报机制

扫码逻辑在 QrCodeScanner.kt 的onFrameCodes中体现为"逐帧确认"流程:

  1. 每帧的识别结果先交给ScanCodeTracker去重确认(见 ScanCodeTracker.kt):同一码需在CONFIRM_FRAMES = 2帧内连续出现才被确认,允许MISSED_FRAMES_TOLERANCE = 1帧的丢失,从而保证单帧误识别永远不会直接冒给用户;标签顺序按首次出现排序,保持稳定。
  2. 确认结果若同时出现 ≥2 个码,则冻结画面并渲染可点击的码标签(ScanCodeTags),配合半透明遮罩与底部提示;用户点选某个码后进入对应处理。
  3. 若确认出恰好 1 个码,先经ScanAutoOpenPolicy(ScanAutoOpenPolicy.kt)裁决是否立即自动弹出结果:当画面中还有其他"未确认"的码时,最多等待PENDING_FRAME_CAP = 3帧,避免"多码场景中先确认的那个码抢先弹出结果弹层"。
  4. 结果弹出后,通过onScanResult(text) { resumeIfIdle() }与宿主协作:宿主处理完业务后调用回调函数恢复扫描。

4.3 相册取图识别

右下角的圆形取图按钮触发pickImage宿主回调,拿到 URI 后控件内部用rememberQrImageDecoder()(一个expect/actual平台解码器,见 Scan.kt)解码图片:无结果时通知showNoCodeFound;单个码直接进入结果处理;多个码则切换到ScanImageCodePicker图片内点选界面。

4.4 expect/actual 平台抽象

ScanCameraView与rememberQrImageDecoder都是expect声明,由各平台提供actual实现(相机预览、图像解码分别依赖各平台的相机与图像框架)。这意味着扫码控件的交互逻辑、状态机、UI 全部跨平台共享,只有相机/解码这类平台能力需要分端实现。

五、CodeEditor:大文件代码编辑器引擎

CodeEditor位于com.ismartcoding.plain.ui.components.codeeditor,专为大文件场景设计,其能力由独立的engine包支撑。

5.1 组件与控制器架构

  • CodeEditor(CodeEditor.kt)是公开入口,接收EditorController渲染:搜索栏(按需)、编辑视口 + 输入层 + 选择工具栏、状态栏三部分纵向组合;
  • EditorController(EditorController.kt)是状态中枢,持有文档、撤销历史、搜索会话与滚动/光标/选区状态,并通过mutableStateOf暴露loadState、docVersion、wrapContent、readOnly、fontSizeSp、isDirty、canUndo/canRedo、searchVisible等可观察状态。

从EditorController的注释可以看到线程策略:重活(索引构建、搜索、高亮)跑在Dispatchers.Default,编辑应用在主线程,从而保证大文件打开与编辑时界面不卡顿。

5.2 engine 引擎组件

engine包各模块分工(engine 目录):

模块职责
LineVectorDocument按行组织的向量文档模型,支撑大文件
LineIndexBuilder行索引构建器(大文件加载阶段)
EditHistory编辑历史(撤销/重做),通过injectClock(fileIO::nowMillis)注入时钟
HighlightEngine+RegexLexer语法高亮引擎,按正则词法规则着色,支持多种语言(Languages)
SearchEngine搜索会话,支持大小写与正则模式,产出SearchMatch列表
EncodingProbe/DetectedEncoding文件编码探测
ByteSource字节源抽象
HorizontalPan/IntList/VisualLineMapper水平平移、性能数据结构、可视行映射

5.3 视口渲染与异步高亮

视口(EditorViewport,见 CodeEditor.kt)的关键实现点:

  • 等宽字体网格排版:charWidthPx、lineHeightPx依据字号动态计算,行高为字号的 1.5 倍;
  • 行号槽宽度按controller.gutterDigits() * 9 + 16.dp动态计算;
  • 异步高亮:snapshotFlow监听当前可见区间的firstVisibleItemIndex与可见行数,切到Dispatchers.Default计算first..(first + n + 8)范围的高亮——只对可见区域加少量预取行做高亮,配合LazyColumn虚拟化,是大文件流畅滚动的关键;
  • 水平平移(HorizontalPan)以"视口宽度减行号槽宽度"为边界,保证完全平移后的行不会越过屏幕边缘(syncPan逻辑见 EditorController.kt)。

5.4 打开流程

EditorController.open(filePath, gotoEnd)(EditorController.kt)的流程为:按路径经Languages.pathToLanguageId推断语言 → 在Dispatchers.Default上loadDocument→ 状态流转为Loading(progress) → Ready,失败则进入Error(message)。EditorLoadState密封类(Idle/Loading/Ready/Error)将打开进度暴露给 UI(见 EditorController.kt)。

六、在应用中组合使用

综合 README 的包路径指引与上述源码结构,接入 plain-ui 的推荐方式是:

  1. 声明依赖并配置 Maven 仓库(见第二节);
  2. 页面骨架:用PScaffold+PTopAppBar(传onNavigateBack或自定义navigationIcon)搭出统一框架;
  3. 列表页:LazyColumnScrollbar/LazyVerticalGridScrollbar提供快速滚动,PullToRefresh提供下拉刷新,DragSelectState+ListDragSelect/GridDragSelect提供长按拖拽多选;
  4. 重型能力按需取用:QrCodeScanner(com.ismartcoding.plain.ui.scanner)负责扫码 UI 与回调协作,宿主自持 TopBar 与结果弹层;CodeEditor(com.ismartcoding.plain.ui.components.codeeditor)负责大文件查看/编辑;
  5. 主题统一:theme包提供PlainTheme、ColorHelper、Shapes、Type等主题原语,保证各端观感一致。

七、总结

plain-ui 的定位决定了它在 PlainApp 技术栈中的角色:界面层的高复用底座 + 两个重型能力模块。base 包的组件把骨架、系统栏适配、快速滚动、下拉刷新、拖拽选择等共性能力标准化;scanner 与 codeeditor 则通过"宿主回调 + 独立引擎"的架构,把扫码与大文件编辑的复杂状态机留在库内,业务方只需实现少量契约即可获得完整能力。无论是复用其 UI 原语,还是学习 Compose Multiplatform 组件库的工程化组织方式,plain-ui 源码 都是值得精读的参考实现。

  • 移动开发
  • 后端
  • 即时通讯
  • 音视频

【免费下载链接】plain-app

🔥 PlainApp is an open-source app that lets you securely manage your phone from a web browser. Access files, media, contacts, SMS, calls, and more through a simple, easy-to-use interface on your desktop.

项目地址:https://gitcode.com/gh_mirrors/pl/plain-app
点击查看免费下载
上一篇:harelba/q大数据集成:与Hadoop、Spark的消息传递方案
下一篇:AlphaFold预测结果解读指南:pLDDT、PAE与五模型判定方法一次讲清

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询