- 移动开发
- 后端
- 即时通讯
- 音视频
【免费下载链接】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.
导读
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, )各回调的职责:
| 参数 | 类型 | 职责 |
|---|---|---|
cameraPermissionGranted | Boolean | 相机权限是否已授予,决定是否渲染相机预览 |
multipleCodesHint | String | 同时识别到多个码时展示的提示文案 |
imagePickerDescription | String | 相册取图按钮的无障碍描述 |
closeRequest | Int | 宿主发来的"关闭/重置"请求信号(增量计数触发) |
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中体现为"逐帧确认"流程:
- 每帧的识别结果先交给
ScanCodeTracker去重确认(见 ScanCodeTracker.kt):同一码需在CONFIRM_FRAMES = 2帧内连续出现才被确认,允许MISSED_FRAMES_TOLERANCE = 1帧的丢失,从而保证单帧误识别永远不会直接冒给用户;标签顺序按首次出现排序,保持稳定。 - 确认结果若同时出现 ≥2 个码,则冻结画面并渲染可点击的码标签(
ScanCodeTags),配合半透明遮罩与底部提示;用户点选某个码后进入对应处理。 - 若确认出恰好 1 个码,先经
ScanAutoOpenPolicy(ScanAutoOpenPolicy.kt)裁决是否立即自动弹出结果:当画面中还有其他"未确认"的码时,最多等待PENDING_FRAME_CAP = 3帧,避免"多码场景中先确认的那个码抢先弹出结果弹层"。 - 结果弹出后,通过
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 的推荐方式是:
- 声明依赖并配置 Maven 仓库(见第二节);
- 页面骨架:用
PScaffold+PTopAppBar(传onNavigateBack或自定义navigationIcon)搭出统一框架; - 列表页:
LazyColumnScrollbar/LazyVerticalGridScrollbar提供快速滚动,PullToRefresh提供下拉刷新,DragSelectState+ListDragSelect/GridDragSelect提供长按拖拽多选; - 重型能力按需取用:
QrCodeScanner(com.ismartcoding.plain.ui.scanner)负责扫码 UI 与回调协作,宿主自持 TopBar 与结果弹层;CodeEditor(com.ismartcoding.plain.ui.components.codeeditor)负责大文件查看/编辑; - 主题统一:
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.
相关推荐
Turbolinks组件库开发:共享UI组件
Turbolinks组件库开发:共享UI组件 组件架构概述 Turbolinks通过模块化设计实现页面导航加速,核心组件分布在 src/ https://lin
前端plain-common:从 PlainApp 中抽取的可复用 Kotlin Multiplatform 工具库与发布指南
plain common:从 PlainApp 中抽取的可复用 Kotlin Multiplatform 工具库与发布指南 导读 :本文以 plain comm
移动开发后端即时通讯音视频GitHub_Trending/core97/coreReact组件库:共享UI组件开发
GitHub_Trending/core97/coreReact组件库:共享UI组件开发 项目概述 GitHub_Trending/core97/core项目基
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考