Commit 16ff06ae authored by dsq's avatar dsq

300M

parent 02bc9d98

Too many changes to show.

To preserve performance only 1000 of 1000+ files are displayed.

# KMP Compose Sample
**版本信息**
| 组件 | 版本 |
| --- | --- |
| Kotlin | 2.2.21-ohos-01 |
| Compose Multiplatform | 1.9.2-eazytec-001 |
| Gradle | 8.14.3 |
| Android Gradle Plugin | 8.11.2 |
| Android SDK | Compile 36 / Min 24 / Target 36 |
一个使用 Kotlin Multiplatform + Compose Multiplatform 的示例项目,目标平台:Android、iOS。项目演示了按库分组的组件导航与属性效果展示,包括以下分组:
- `org.jetbrains.compose.ui:ui`
- `org.jetbrains.compose.foundation:foundation`
- `org.jetbrains.compose.material:material`
- `org.jetbrains.compose.material3:material3`
**首页为分组 → 组件列表,点击组件进入属性演示页,展示该组件的主要属性效果与交互。**
**功能特点**
- 分组导航:首页按四个分组展示组件/插件名称列表。
- 详情演示:进入组件页面,可通过控件调整属性并实时预览效果。
- 跨平台:共享 UI 代码,一套 Compose 在 Android/iOS 同步运行。
- 可扩展:每个分组支持新增更多组件演示页,Material 分组单独目录组织。
**目录结构**
- `composeApp/src/commonMain/kotlin/com/example/kmptpc_compose_sample/`
- `App.kt`:应用入口,负责分组列表与页面导航。
- `ComponentDemos.kt`:首页列表与各组件演示页的路由与部分演示实现。
- `navigation/Navigation.kt`:分组模型、组件清单、页面模型(`Page`)。
- `demos/material/`:Material 分组的演示页目录(建议每个组件一个文件)。
- `composeApp/src/androidMain/kotlin/com/example/kmptpc_compose_sample/MainActivity.kt`:Android 入口。
- `composeApp/src/iosMain/kotlin/com/example/kmptpc_compose_sample/MainViewController.kt`:iOS 入口控制器。
- `iosApp/iosApp/ContentView.swift`:在 SwiftUI 中桥接 `MainViewController()`
**组件演示导航**
- 分组标题使用库坐标字符串(如 `org.jetbrains.compose.ui:ui`)。
- 组件列表点击后跳转到演示页面,页面包含:
- 常用属性控制(启用状态、大小、颜色、形状、阴影等)
- 示例交互(点击计数、滚动列表等)
- Material 分组包含但不限于以下演示页(目录:`demos/material/`):
- `AlertDialogSample.kt`
- `AppBarSamples.kt`
- `BackdropScaffoldSamples.kt`
- `BadgeSamples.kt`
- `BottomNavigationSamples.kt`
- `BottomSheetScaffoldSamples.kt`
- `ButtonSamples.kt`
- `CardSamples.kt`
- `ChipSamples.kt`
- `ContentAlphaSamples.kt`
- `DrawerSamples.kt`
- `ElevationSamples.kt`
- `ExposedDropdownMenuSamples.kt`
- `FloatingActionButtonSamples.kt`
- `IconButtonSamples.kt`
- `ListSamples.kt`
- `MenuSamples.kt`
- `ModalBottomSheetSamples.kt`
- `NavigationRailSample.kt`
- `ProgressIndicatorSamples.kt`
- `PullRefreshSamples.kt`
- `ScaffoldSamples.kt`
- `SelectionControlsSamples.kt`
- `SliderSample.kt`
- `SurfaceSamples.kt`
- `SwipeableSamples.kt`
- `SwipeToDismissSamples.kt`
- `TabSamples.kt`
- `TextFieldSamples.kt`
- `TextSamples.kt`
- `ThemeSamples.kt`
可按需要逐步补充/细化每个演示页的“全部属性效果”。
**依赖说明**
- Compose Runtime/Foundation/UI/Material3:已在 `composeApp/build.gradle.kts``commonMain` 依赖中。
- Material(旧版 Material 组件):需在 `commonMain` 添加 `implementation(compose.material)`
- 其他:AndroidX Lifecycle 等按需引用。
**运行与构建**
- Android(推荐在 Android Studio 运行,或使用命令行):
```bash
./gradlew :composeApp:assembleDebug
```
- Windows:
```shell
.\gradlew.bat :composeApp:assembleDebug
```
- iOS 应用构建与运行
使用 IDE 工具栏中的运行配置来构建并运行 iOS 应用开发版本,或者在 Xcode 中打开 [/iosApp](./iosApp) 目录并运行。
- HarmonyOS (鸿蒙) 应用构建与运行
1. 编译并发布 Shared Library 到鸿蒙工程目录:
```bash
./gradlew :composeApp:publishDebugBinariesToHarmonyApp
```
2. 使用 DevEco Studio 打开 `harmonyApp` 目录。
3. 在 DevEco Studio 中同步项目并运行到鸿蒙设备或模拟器。
---
Learn more about [Kotlin Multiplatform](https://www.jetbrains.com/help/kotlin-multiplatform-dev/get-started.html)
## Material3 命名规范与常见错误
- 命名规范
- 所有 Material3 示例函数统一命名为 `XXXSamplesM3()`,避免与 Material(M2)下同名函数导致的重载解析歧义。
- 如需保留同名,可在调用处使用导入别名:
- `import com.example...demos.material.AlertDialogSamples as AlertDialogSamplesM2`
- `import com.example...demos.material3.AlertDialogSamples as AlertDialogSamplesM3`
- 必要导入
- `import androidx.compose.ui.Modifier`
- `import androidx.compose.ui.unit.dp`(需要尺寸单位时)
- `import androidx.compose.ui.unit.sp`(需要字号单位时)
- `import androidx.compose.foundation.layout.*`(需要 `Row/Column/Box/padding/size/Arrangement` 等)
- 若使用对齐:`import androidx.compose.ui.Alignment`
- 实验性 API 选择加入
- Material3 中部分组件(`ModalBottomSheet``DatePicker``TimePicker``SearchBar` 等)需要:
- 文件顶部添加:`@file:OptIn(androidx.compose.material3.ExperimentalMaterial3Api::class)`
- 手势与滑动
- Material(M2)可使用 `SwipeToDismiss`;Material3 版本可使用 `SwipeToDismissBox`(视版本)。
- 基础手势层替代:`AnchoredDraggableState` + `anchoredDraggable`(foundation),避免使用已移除的 `swipeable` 系列。
## 目录结构(示例)
- `demos/ui/*`: UI 分组示例(如 `BoxDemo.kt`
- `demos/foundation/*`: Foundation 分组示例(如 `BasicTextDemo.kt`, `LazyColumnDemo.kt`
- `demos/material/*`: Material(M2)示例
- `demos/material3/*`: Material3 示例(统一 `XXXSamplesM3()` 命名)
## 导航与展示
- 首页按分组展示来自 `navigation/Navigation.kt``componentDemos` 列表。
- `DemoScreen(demo: ComponentDemo)` 根据 `demo.id` 路由到对应示例。
- Material3 示例全部接入,标题与路由统一。
## 常见错误
- `Unresolved reference 'Modifier'/'dp'/'sp'`:缺少必要导入,参考上文。
- `SmallTopAppBar` 未解析:使用 `TopAppBar` 等 M3 顶栏组件。
- 实验 API(`DatePicker`, `TimePicker`, `ModalBottomSheet`, `SearchBar` 等):文件顶部添加
- `@file:OptIn(androidx.compose.material3.ExperimentalMaterial3Api::class)`
## HarmonyOS 常见问题与解决方案
### 1. 运行时闪退:`ArkUIViewController_setId` 或 `Not mapped` 错误
**现象**
App 启动或跳转到 Compose 页面时立即闪退,日志中包含 `ArkUIViewController_setId``Not mapped``libComposeApp.so` 相关错误。
**原因**
1. **符号解析错误**:Native 层的 `napi_init.cpp` 中未能正确加载 Compose 的初始化函数 `androidx_compose_ui_arkui_init`
2. **Controller 初始化时机**:在 Native 模块未加载完成前就尝试创建 `ArkUIViewController`,或者未在 `aboutToAppear` 生命周期中正确初始化。
3. **Library Name 缺失**:在 ArkTS 的 `Compose` 组件中未指定正确的 `libraryName`
**解决方案**
1. **修正 `napi_init.cpp`**
确保动态查找符号时包含回退机制(兼容不同版本的符号名称),并使用 `dlsym(RTLD_DEFAULT, ...)`
```cpp
// entry/src/main/cpp/napi_init.cpp
typedef void (*init_fn)(napi_env, napi_value);
static napi_value Init(napi_env env, napi_value exports) {
// 尝试加载新版符号
init_fn fn = reinterpret_cast<init_fn>(dlsym(RTLD_DEFAULT, "androidx_compose_ui_arkui_utils_init"));
if (fn == nullptr) {
// 回退到旧版符号
fn = reinterpret_cast<init_fn>(dlsym(RTLD_DEFAULT, "androidx_compose_ui_arkui_init"));
}
if (fn != nullptr) {
fn(env, exports);
}
// ...
}
```
2. **正确初始化 Controller (`Index.ets`)**
`aboutToAppear` 中检查 `nativeApi` 并初始化 Controller,添加异常捕获。
```typescript
// entry/src/main/ets/pages/Index.ets
aboutToAppear() {
if (nativeApi) {
try {
this.controller = nativeApi.MainArkUIViewController();
} catch (e) {
hilog.error(DOMAIN, 'Compose', 'Controller creation failed: %{public}s', JSON.stringify(e));
}
}
}
```
3. **指定 Library Name**
在使用 `Compose` 组件时,明确传入 `libraryName`(通常为 `'entry'` 或你的动态库名称)。
```typescript
Compose({
controller: this.controller,
libraryName: 'entry', // 必须指定
onBackPressed: () => false
})
```
### 2. 构建错误:`input file does not exist` (:cinteropResourceOhosArm64)
**原因**`resource.def` 文件位置与 `build.gradle.kts` 中配置的路径不一致。
**解决**:确保文件位于 `src/ohosArm64Main/cinterop/resource.def`,或调整 `defFile(file(...))` 的路径配置。
### 3. 依赖冲突:Variant mismatch / 403 Forbidden
**原因**:部分 AndroidX 库(如 `lifecycle`, `savedstate`)尚未完全适配 OHOS 目标,导致 Gradle 解析时拉取错误的构件或无权限。
**解决**:在 `build.gradle.kts``ohos` 配置中排除冲突的组:
```kotlin
configurations.all {
if (name.contains("ohos", ignoreCase = true)) {
exclude(group = "org.jetbrains.androidx.lifecycle")
exclude(group = "org.jetbrains.androidx.savedstate")
}
}
```
## Gradle 构建常见问题与避坑指南
### 1. `Unresolved reference` 错误
**现象**:`build.gradle.kts` 报错 `Unresolved reference: libs.xxx`。
**原因**:`libs.versions.toml` 中未定义对应的 library 或 plugin 别名,或者别名拼写不一致(Gradle 会将 TOML 中的 `-` 转换为 `.`)。
**解决**:
- 检查 `libs.versions.toml` 中的 `[libraries]` 和 `[plugins]` 块。
- 确认别名拼写。例如 `compose-multiplatform-export` 在 Kotlin DSL 中应通过 `libs.compose.multiplatform.export` 引用。
### 2. Skiko 版本不兼容
**现象**:OHOS 运行时崩溃或渲染异常,提示 Skiko 版本问题。
**原因**:Compose Multiplatform 默认依赖的 Skiko 版本可能未适配特定的 OHOS 版本。
**解决**:
在 `build.gradle.kts` 中强制指定 Skiko 版本:
```kotlin
resolutionStrategy {
eachDependency {
if (requested.group == "org.jetbrains.skiko" && requested.name == "skiko") {
if (configName.contains("ohos", ignoreCase = true)) {
useVersion("0.9.22.2-ohos-001") // 使用适配的版本
}
}
}
}
```
### 3. 依赖冲突:`androidx.collection`
**现象**:构建失败,提示依赖版本冲突。
**原因**:不同库依赖了不同版本的 `androidx.collection`。
**解决**:
强制指定版本并排除冲突模块:
```kotlin
configurations.all {
resolutionStrategy {
eachDependency {
if (requested.group == "androidx.collection" && requested.name == "collection") {
useVersion(libs.versions.androidx.collection.get())
}
}
}
exclude(group = "androidx.collection", module = "collection-jvm")
}
```
# MaxHap - 跨平台 Compose 应用框架
# KMP Compose Sample
English | [简体中文](./README-zh_CN.md)
**Version Information**
| Component | Version |
| --- | --- |
| Kotlin | 2.2.21-ohos-01 |
| Compose Multiplatform | 1.9.2-eazytec-001 |
| Gradle | 8.14.3 |
| Android Gradle Plugin | 8.11.2 |
| Android SDK | Compile 36 / Min 24 / Target 36 |
A sample project using Kotlin Multiplatform + Compose Multiplatform, targeting Android and iOS. This project demonstrates component navigation and property showcase grouped by library, including:
- `org.jetbrains.compose.ui:ui`
- `org.jetbrains.compose.foundation:foundation`
- `org.jetbrains.compose.material:material`
- `org.jetbrains.compose.material3:material3`
**The home page displays Groups -> Component List. Clicking a component navigates to its property demo page, showcasing its main properties and interactions.**
**Features**
- Grouped Navigation: The home page lists component/plugin names by four groups.
- Detailed Demos: Enter a component page to adjust properties via controls and preview effects in real-time.
- Cross-Platform: Shared UI code, running synchronously on Android and iOS with a single Compose codebase.
- Extensible: Support adding more component demo pages to each group; Material group is organized in a separate directory.
**Directory Structure**
- `composeApp/src/commonMain/kotlin/com/example/kmptpc_compose_sample/`
- `App.kt`: App entry point, handling group lists and page navigation.
- `ComponentDemos.kt`: Home page list, routing for component demo pages, and some demo implementations.
- `navigation/Navigation.kt`: Group models, component manifest, page models (`Page`).
- `demos/material/`: Demo pages for the Material group (recommended: one file per component).
- `composeApp/src/androidMain/kotlin/com/example/kmptpc_compose_sample/MainActivity.kt`: Android entry point.
- `composeApp/src/iosMain/kotlin/com/example/kmptpc_compose_sample/MainViewController.kt`: iOS entry controller.
- `iosApp/iosApp/ContentView.swift`: Bridges `MainViewController()` in SwiftUI.
**Component Demo Navigation**
- Group titles use library coordinate strings (e.g., `org.jetbrains.compose.ui:ui`).
- Clicking a component list item navigates to the demo page, which includes:
- Common property controls (enabled state, size, color, shape, shadow, etc.)
- Interactive examples (click counters, scrolling lists, etc.)
- Material group includes but is not limited to the following demo pages (directory: `demos/material/`):
- `AlertDialogSample.kt`
- `AppBarSamples.kt`
- `BackdropScaffoldSamples.kt`
- `BadgeSamples.kt`
- `BottomNavigationSamples.kt`
- `BottomSheetScaffoldSamples.kt`
- `ButtonSamples.kt`
- `CardSamples.kt`
- `ChipSamples.kt`
- `ContentAlphaSamples.kt`
- `DrawerSamples.kt`
- `ElevationSamples.kt`
- `ExposedDropdownMenuSamples.kt`
- `FloatingActionButtonSamples.kt`
- `IconButtonSamples.kt`
- `ListSamples.kt`
- `MenuSamples.kt`
- `ModalBottomSheetSamples.kt`
- `NavigationRailSample.kt`
- `ProgressIndicatorSamples.kt`
- `PullRefreshSamples.kt`
- `ScaffoldSamples.kt`
- `SelectionControlsSamples.kt`
- `SliderSample.kt`
- `SurfaceSamples.kt`
- `SwipeableSamples.kt`
- `SwipeToDismissSamples.kt`
- `TabSamples.kt`
- `TextFieldSamples.kt`
- `TextSamples.kt`
- `ThemeSamples.kt`
You can supplement/refine the "full property effects" for each demo page as needed.
**Dependencies**
- Compose Runtime/Foundation/UI/Material3: Included in `commonMain` dependencies in `composeApp/build.gradle.kts`.
- Material (Legacy Material Components): Add `implementation(compose.material)` to `commonMain`.
- Others: AndroidX Lifecycle, etc., included as needed.
**Build & Run**
- Android (Recommended: Run in Android Studio or via command line):
```bash
./gradlew :composeApp:assembleDebug
```
- Windows:
```shell
.\gradlew.bat :composeApp:assembleDebug
```
- iOS Build & Run
Use the run configuration in the IDE toolbar to build and run the iOS development version, or open the [/iosApp](./iosApp) directory in Xcode and run.
- HarmonyOS Build & Run
1. Compile and publish Shared Library to the HarmonyOS project directory:
```bash
./gradlew :composeApp:publishDebugBinariesToHarmonyApp
```
2. Open the `harmonyApp` directory with DevEco Studio.
3. Sync the project in DevEco Studio and run on a HarmonyOS device or emulator.
<div align="center">
[![Kotlin](https://img.shields.io/badge/Kotlin-1.9+-blue.svg?logo=kotlin)](https://kotlinlang.org)
[![Compose Multiplatform](https://img.shields.io/badge/Compose-Multiplatform-red.svg)](https://www.jetbrains.com/lp/compose-multiplatform/)
[![Android](https://img.shields.io/badge/Android-支持-green.svg?logo=android)](https://developer.android.com)
[![HarmonyOS](https://img.shields.io/badge/HarmonyOS-支持-orange.svg)](https://developer.harmonyos.com)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
**一个基于 Kotlin Multiplatform 和 Jetpack Compose 的高性能跨平台应用开发框架**
</div>
MaxHap 是一个创新的跨平台移动应用开发解决方案,专注于提供极致的开发体验和卓越的应用性能。通过统一的代码库,开发者可以同时为 Android 和 HarmonyOS 平台构建功能丰富、界面精美的现代化应用。
## 🏗️ 项目架构
```
MaxHap/
├── composeApp/ # 🎨 核心 Compose UI 层(跨平台共享)
├── harmonyApp/ # 🔷 HarmonyOS 原生集成层
├── iosApp/ # 🍎 iOS 原生壳应用
├── nestedlib1-5/ # 📚 模块化嵌套页面库(1000层深度导航测试)
├── scripts/ # ⚙️ 自动化构建和测试脚本
├── build.gradle.kts # 🛠️ 根项目构建配置
└── settings.gradle.kts # 📋 项目模块管理配置
```
## 🚀 核心功能与工具集
### 🧪 深度嵌套页面测试框架
MaxHap 内置了一套完整的深度嵌套页面测试系统,用于验证跨平台导航和性能表现:
#### 📁 generate_hap_test_files.sh - 基础嵌套页面生成器
生成5个独立的模块化库,构建1000层深度嵌套导航结构。
**核心特性:**
- 🏗️ **模块化架构**:5个独立的Kotlin Multiplatform库(nestedlib1-5)
- 📊 **海量文件**:每个库200个.kt文件(总计1000个页面组件)
- 🔗 **智能跨库调用**:每库第200层自动跳转至下个库第201层
- 🎨 **视觉差异化**:每层页面拥有独特配色方案
-**自动配置**:一键生成完整的Gradle构建配置
---
**技术亮点:**
Learn more about [Kotlin Multiplatform](https://www.jetbrains.com/help/kotlin-multiplatform-dev/get-started.html)
## Material3 Naming Conventions & Common Errors
- Naming Conventions
- All Material3 sample functions are uniformly named `XXXSamplesM3()` to avoid overload resolution ambiguity with Material (M2) functions of the same name.
- If you need to keep the same name, use import aliases at the call site:
- `import com.example...demos.material.AlertDialogSamples as AlertDialogSamplesM2`
- `import com.example...demos.material3.AlertDialogSamples as AlertDialogSamplesM3`
- Necessary Imports
- `import androidx.compose.ui.Modifier`
- `import androidx.compose.ui.unit.dp` (when size units are needed)
- `import androidx.compose.ui.unit.sp` (when font size units are needed)
- `import androidx.compose.foundation.layout.*` (when `Row/Column/Box/padding/size/Arrangement` etc. are needed)
- If using alignment: `import androidx.compose.ui.Alignment`
- Opt-in for Experimental APIs
- Some components in Material3 (`ModalBottomSheet`, `DatePicker`, `TimePicker`, `SearchBar`, etc.) require:
- Adding at the top of the file: `@file:OptIn(androidx.compose.material3.ExperimentalMaterial3Api::class)`
- Gestures & Scrolling
- Material (M2) can use `SwipeToDismiss`; Material3 version can use `SwipeToDismissBox` (depending on version).
- Basic gesture layer replacement: `AnchoredDraggableState` + `anchoredDraggable` (foundation), avoiding the removed `swipeable` series.
## Directory Structure (Example)
- `demos/ui/*`: UI group samples (e.g., `BoxDemo.kt`)
- `demos/foundation/*`: Foundation group samples (e.g., `BasicTextDemo.kt`, `LazyColumnDemo.kt`)
- `demos/material/*`: Material (M2) samples
- `demos/material3/*`: Material3 samples (uniformly named `XXXSamplesM3()`)
## Navigation & Display
- The home page displays the `componentDemos` list from `navigation/Navigation.kt` by group.
- `DemoScreen(demo: ComponentDemo)` routes to the corresponding sample based on `demo.id`.
- All Material3 samples are integrated with unified titles and routing.
## Common Errors
- `Unresolved reference 'Modifier'/'dp'/'sp'`: Missing necessary imports, refer to above.
- `SmallTopAppBar` unresolved: Use `TopAppBar` or other M3 top bar components.
- Experimental APIs (`DatePicker`, `TimePicker`, `ModalBottomSheet`, `SearchBar`, etc.): Add at the top of the file
- `@file:OptIn(androidx.compose.material3.ExperimentalMaterial3Api::class)`
## HarmonyOS Common Issues & Solutions
### 1. Runtime Crash: `ArkUIViewController_setId` or `Not mapped` Error
**Symptom**:
App crashes immediately upon launch or navigation to a Compose page, with logs containing `ArkUIViewController_setId`, `Not mapped`, or `libComposeApp.so` related errors.
**Cause**:
1. **Symbol Resolution Error**: The `napi_init.cpp` in the Native layer failed to correctly load Compose's initialization function `androidx_compose_ui_arkui_init`.
2. **Controller Initialization Timing**: Attempting to create `ArkUIViewController` before the Native module is fully loaded, or not initializing it correctly in the `aboutToAppear` lifecycle.
3. **Missing Library Name**: Correct `libraryName` was not specified in the ArkTS `Compose` component.
**Solution**:
1. **Fix `napi_init.cpp`**:
Ensure the dynamic symbol lookup includes a fallback mechanism (compatible with different versions of symbol names) and uses `dlsym(RTLD_DEFAULT, ...)`.
```cpp
// entry/src/main/cpp/napi_init.cpp
typedef void (*init_fn)(napi_env, napi_value);
static napi_value Init(napi_env env, napi_value exports) {
// Try loading new version symbol
init_fn fn = reinterpret_cast<init_fn>(dlsym(RTLD_DEFAULT, "androidx_compose_ui_arkui_utils_init"));
if (fn == nullptr) {
// Fallback to old version symbol
fn = reinterpret_cast<init_fn>(dlsym(RTLD_DEFAULT, "androidx_compose_ui_arkui_init"));
}
if (fn != nullptr) {
fn(env, exports);
}
// ...
}
```
2. **Correct Controller Initialization (`Index.ets`)**:
Check `nativeApi` and initialize the Controller in `aboutToAppear`, adding exception handling.
```typescript
// entry/src/main/ets/pages/Index.ets
aboutToAppear() {
if (nativeApi) {
try {
this.controller = nativeApi.MainArkUIViewController();
} catch (e) {
hilog.error(DOMAIN, 'Compose', 'Controller creation failed: %{public}s', JSON.stringify(e));
}
}
}
```
3. **Specify Library Name**:
Explicitly pass `libraryName` (usually `'entry'` or your dynamic library name) when using the `Compose` component.
```typescript
Compose({
controller: this.controller,
libraryName: 'entry', // Must be specified
onBackPressed: () => false
})
```
### 2. Build Error: `input file does not exist` (:cinteropResourceOhosArm64)
**Cause**: The `resource.def` file location does not match the path configured in `build.gradle.kts`.
**Solution**: Ensure the file is located at `src/ohosArm64Main/cinterop/resource.def`, or adjust the `defFile(file(...))` path configuration.
### 3. Dependency Conflict: Variant mismatch / 403 Forbidden
**Cause**: Some AndroidX libraries (like `lifecycle`, `savedstate`) are not fully adapted to the OHOS target, causing Gradle to pull incorrect artifacts or face permission issues during resolution.
**Solution**: Exclude conflicting groups in the `ohos` configuration in `build.gradle.kts`:
```kotlin
// 示例:第200层跨库调用
if (showNextPage200) {
com.dong.maxhap.nestedlib2.NestedPage201()
return
configurations.all {
if (name.contains("ohos", ignoreCase = true)) {
exclude(group = "org.jetbrains.androidx.lifecycle")
exclude(group = "org.jetbrains.androidx.savedstate")
}
}
```
**快速开始:**
```bash
# 赋予执行权限(首次使用)
chmod +x generate_hap_test_files.sh
# 一键生成全部测试文件
./generate_hap_test_files.sh
```
**输出目录:**
```
📁 nestedlib1/src/commonMain/kotlin/com/dong/maxhap/nestedlib1/
📁 nestedlib2/src/commonMain/kotlin/com/dong/maxhap/nestedlib2/
...
📁 nestedlib5/src/commonMain/kotlin/com/dong/maxhap/nestedlib5/
```
### 🧹 测试环境管理工具
#### 🔄 cleanup_hap_test_files.sh - 交互式清理工具
安全可靠地清理所有生成的测试文件,防止误删。
**安全保障机制:**
- 📊 **文件统计**:显示待删除文件数量和大小
-**双重确认**:执行前需要用户明确确认
- 🧼 **智能清理**:可选删除空目录,保持项目整洁
- 🛡️ **防误操作**:避免意外删除重要文件
**使用示例:**
```bash
# 添加执行权限
chmod +x cleanup_hap_test_files.sh
# 启动交互式清理流程
./cleanup_hap_test_files.sh
# 输出示例:
# 🔍 发现 1000 个测试文件 (约 50MB)
# ❓ 确认删除这些文件吗?(y/N): y
# ✅ 清理完成!
```
#### ⚡ quick_cleanup.sh - 快速清理工具
为CI/CD和自动化测试场景设计的一键清理方案。
**适用场景:**
- 🤖 **持续集成**:自动化测试流水线
- 🚀 **快速迭代**:频繁重构和重试
- 📦 **批量操作**:脚本化部署流程
**注意事项:** ⚠️ 无确认机制,请谨慎使用
**使用方式:**
```bash
# 快速清理(无确认)
chmod +x quick_cleanup.sh
./quick_cleanup.sh
# CI/CD 集成示例
./quick_cleanup.sh && ./generate_hap_test_files.sh && ./gradlew test
```
## 🛠️ 开发环境与技术栈
### 📱 支持平台
| 平台 | 版本要求 | 构建工具 |
|------|----------|----------|
| Android | API 24+ | Gradle + AGP |
| HarmonyOS | 6.0+ | Hvigor |
| iOS | 12.0+ | Xcode + CocoaPods |
### 🔧 核心技术栈
**语言与框架:**
- Kotlin Multiplatform 1.9+
- Jetpack Compose Multiplatform
- Android Jetpack
- HarmonyOS ArkUI
## Gradle Build Common Issues & Pitfalls Guide
**构建与工具链:**
- Gradle 8.0+
- Hvigor 6.0+ (HarmonyOS)
- Android Gradle Plugin 8.0+
- Kotlin Compiler 1.9+
## 🏗️ 构建与部署
### 🤖 Android 平台
```bash
# Debug 构建与安装
./gradlew assembleDebug
./gradlew installDebug
# Release 构建
./gradlew assembleRelease
# 运行单元测试
./gradlew test
```
### 1. `Unresolved reference` Error
**Symptom**: `build.gradle.kts` reports `Unresolved reference: libs.xxx`.
**Cause**: The corresponding library or plugin alias is not defined in `libs.versions.toml`, or the alias spelling is inconsistent (Gradle converts `-` in TOML to `.`).
**Solution**:
- Check the `[libraries]` and `[plugins]` blocks in `libs.versions.toml`.
- Confirm alias spelling. For example, `compose-multiplatform-export` should be referenced as `libs.compose.multiplatform.export` in Kotlin DSL.
### 🔷 HarmonyOS 平台
```bash
# 进入HarmonyOS项目目录
cd harmonyApp
# 构建HAP包
hvigorw assembleHap
# 构建Debug版本
hvigorw assembleHap -p module=entry@default -p buildMode=debug
# 查看构建报告
hvigorw --info
```
### 🍎 iOS 平台
```bash
# 在Xcode中打开项目
open iosApp/iosApp.xcworkspace
# 或使用命令行构建
xcodebuild -workspace iosApp/iosApp.xcworkspace \
-scheme iosApp \
-configuration Debug \
-destination 'platform=iOS Simulator,name=iPhone 15' \
build
```
## 🌟 项目特色优势
### 💡 核心价值
- **🎯 一次编写,多端运行**:真正意义上的代码复用
- **⚡ 高性能渲染**:原生级别的用户体验
- **🎨 统一设计语言**:一致的视觉和交互体验
- **🔧 模块化架构**:高内聚低耦合的设计原则
### 🛡️ 质量保障
- **🧪 深度测试覆盖**:1000层嵌套页面的压力测试
- **📊 性能监控**:实时跟踪内存和CPU使用情况
- **🔒 类型安全**:Kotlin强类型系统保驾护航
- **🔄 热重载支持**:开发效率大幅提升
## ⚠️ 重要提醒与最佳实践
### 📈 性能考量
- **调用栈深度**:1000层嵌套可能触发栈溢出,请根据设备能力调整
- **内存占用**:大量Composable组件需关注内存泄漏问题
- **启动时间**:复杂嵌套结构会影响冷启动性能
### 🔧 使用建议
- **开发阶段**:建议减少嵌套层数进行快速迭代
- **生产环境**:务必进行全面的性能测试和优化
- **数据备份**:重要修改前请及时提交代码或备份
- **渐进式采用**:可以从简单页面开始逐步迁移到复杂结构
### 🛡️ 安全须知
- 清理脚本仅删除自动生成的测试文件
- 不会影响手动编写的业务代码
- 建议定期检查.gitignore确保测试文件不会被提交
## 📋 常见问题与解决方案
### 🔧 HarmonyOS 配置文件缺失
**问题描述:** 构建时提示 `main_pages.json` 或相关配置文件缺失
**解决方案:**
`/harmonyApp/entry/src/main/resources/base/profile/` 目录下创建以下配置文件:
**📄 main_pages.json**
```json
{
"src": [
"pages/Index"
]
### 2. Skiko Version Incompatibility
**Symptom**: OHOS runtime crash or rendering exception, indicating Skiko version issues.
**Cause**: The default Skiko version depended on by Compose Multiplatform may not be adapted to the specific OHOS version.
**Solution**:
Force a specific Skiko version in `build.gradle.kts`:
```kotlin
resolutionStrategy {
eachDependency {
if (requested.group == "org.jetbrains.skiko" && requested.name == "skiko") {
if (configName.contains("ohos", ignoreCase = true)) {
useVersion("0.9.22.2-ohos-001") // Use adapted version
}
}
}
}
```
**📄 backup_config.json**
```json
{
"allowToBackupRestore": true
### 3. Dependency Conflict: `androidx.collection`
**Symptom**: Build failure indicating dependency version conflict.
**Cause**: Different libraries depend on different versions of `androidx.collection`.
**Solution**:
Force a specific version and exclude conflicting modules:
```kotlin
configurations.all {
resolutionStrategy {
eachDependency {
if (requested.group == "androidx.collection" && requested.name == "collection") {
useVersion(libs.versions.androidx.collection.get())
}
}
}
exclude(group = "androidx.collection", module = "collection-jvm")
}
```
### 🚀 其他常见问题
<details>
<summary><strong>构建失败:依赖下载超时</strong></summary>
```bash
# 切换到国内镜像源
# 在 ~/.gradle/gradle.properties 中添加:
systemProp.http.proxyHost=your-proxy-host
systemProp.http.proxyPort=your-proxy-port
```
</details>
<details>
<summary><strong>HarmonyOS模拟器无法启动</strong></summary>
```bash
# 检查Hvigor版本兼容性
hvigorw --version
# 清理缓存重新构建
cd harmonyApp && rm -rf .hvigor/cache && hvigorw clean
```
</details>
## 🤝 贡献指南
我们欢迎各种形式的贡献!
### 📝 如何参与
1. Fork 本项目
2. 创建功能分支 (`git checkout -b feature/AmazingFeature`)
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
4. 推送到分支 (`git push origin feature/AmazingFeature`)
5. 开启 Pull Request
### 🎯 贡献方向
- ✨ 新功能开发
- 🐛 Bug修复
- 📚 文档完善
- 🧪 测试用例补充
- 💡 性能优化建议
## 📄 许可证
本项目采用 Apache License 2.0 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。
## 📞 联系方式
- **项目维护者**:dong.sq@example.com
- **GitHub Issues**[提交问题](https://github.com/yourusername/MaxHap/issues)
- **讨论交流**:QQ群 123456789
---
<div align="center">
**如果你觉得这个项目有帮助,请给个⭐Star支持一下!**
[![GitHub stars](https://img.shields.io/github/stars/yourusername/MaxHap?style=social)](https://github.com/yourusername/MaxHap)
</div>
#!/bin/bash
# 为所有NestedPage文件添加@Stable元素的脚本
BASE_DIR="/Users/dongsq/TestDemo/IR013/TestProject/MaxHap"
LIB_NAMES=("nestedlib1" "nestedlib2" "nestedlib3" "nestedlib4" "nestedlib5")
echo "开始为NestedPage文件添加@Stable元素..."
# 为每个库处理文件
for lib_name in "${LIB_NAMES[@]}"; do
LIB_DIR="$BASE_DIR/$lib_name/src/commonMain/kotlin/com/dong/maxhap/$lib_name"
if [ ! -d "$LIB_DIR" ]; then
echo "目录不存在: $LIB_DIR"
continue
fi
echo "处理库: $lib_name"
# 获取该库中的所有NestedPage文件
PAGE_FILES=$(find "$LIB_DIR" -name "NestedPage*.kt" | sort -V)
for file_path in $PAGE_FILES; do
filename=$(basename "$file_path")
page_number=$(echo "$filename" | sed 's/NestedPage\(.*\)\.kt/\1/')
# 读取原文件内容
ORIGINAL_CONTENT=$(cat "$file_path")
# 生成8个@Stable数据类
STABLE_CLASSES=""
for i in {1..8}; do
STABLE_CLASSES+="@Stable
data class StableData${page_number}_${i}(
val id: Int = $((page_number * 1000 + i)),
val name: String = \"Stable_${page_number}_${i}\",
val value: Double = ${page_number}.${i},
val isActive: Boolean = true,
val timestamp: Long = System.currentTimeMillis(),
val tags: List<String> = listOf(\"tag1\", \"tag2\", \"tag3\"),
val metadata: Map<String, String> = mapOf(\"key1\" to \"value1\", \"key2\" to \"value2\"),
val nestedData: NestedStableData${page_number}_${i}? = null
)
@Stable
data class NestedStableData${page_number}_${i}(
val subId: Int = $((page_number * 100 + i)),
val description: String = \"Nested stable data for page ${page_number}, item ${i}\",
val count: Int = ${i}00,
val isEnabled: Boolean = ${i}%2==0
)
"
done
# 在imports后面插入@Stable类定义
echo "$ORIGINAL_CONTENT" | awk -v stable_classes="$STABLE_CLASSES" '
/^import / {
print $0
found_import = 1
next
}
found_import && !/^import / && !printed_stable {
print ""
print stable_classes
printed_stable = 1
}
{ print }
' > "${file_path}.tmp"
# 替换原文件
mv "${file_path}.tmp" "$file_path"
# 显示进度
if [ $((page_number % 50)) -eq 0 ]; then
echo " 已处理 ${lib_name} 的第 ${page_number} 个页面"
fi
done
done
echo "完成!所有NestedPage文件已添加@Stable元素。"
echo "每个页面现在包含8个@Stable数据类。"
\ No newline at end of file
#!/usr/bin/env python3
import os
import re
def add_stable_elements_to_file(file_path, page_number):
"""为指定的NestedPage文件添加@Stable元素"""
# 读取原文件内容
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# 在imports部分后插入Stable imports
import_section = '''import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.runtime.Stable'''
# 生成8个@Stable数据类
stable_classes = []
for i in range(1, 9):
stable_class = f'''@Stable
data class StableData{page_number}_{i}(
val id: Int = {page_number}{i:03d},
val name: String = "Stable_{page_number}_{i}",
val value: Double = {page_number}.{i},
val isActive: Boolean = true,
val timestamp: Long = System.currentTimeMillis(),
val tags: List<String> = listOf("tag1", "tag2", "tag3"),
val metadata: Map<String, String> = mapOf("key1" to "value1", "key2" to "value2"),
val nestedData: NestedStableData{page_number}_{i}? = null
)
@Stable
data class NestedStableData{page_number}_{i}(
val subId: Int = {page_number}{i:02d},
val description: String = "Nested stable data for page {page_number}, item {i}",
val count: Int = {i}00,
val isEnabled: Boolean = {'true' if i % 2 == 0 else 'false'}
)
'''
stable_classes.append(stable_class)
# 组合所有Stable类
stable_section = '\n'.join(stable_classes)
# 查找@Composable函数的位置
composable_match = re.search(r'(@Composable\s+internal\s+fun\s+NestedPage\d+\(\)\s*{)', content)
if composable_match:
# 在@Composable函数前插入Stable类定义
insert_pos = composable_match.start()
new_content = content[:insert_pos] + stable_section + '\n' + content[insert_pos:]
else:
# 如果没找到@Composable,就在文件末尾添加
new_content = content.rstrip() + '\n\n' + stable_section
# 写回文件
with open(file_path, 'w', encoding='utf-8') as f:
f.write(new_content)
def process_all_nested_pages():
"""处理所有NestedPage文件"""
base_dir = "/Users/dongsq/TestDemo/IR013/TestProject/MaxHap"
lib_names = ["nestedlib1", "nestedlib2", "nestedlib3", "nestedlib4", "nestedlib5"]
total_processed = 0
for lib_name in lib_names:
lib_path = os.path.join(base_dir, lib_name, "src", "commonMain", "kotlin", "com", "dong", "maxhap", lib_name)
if not os.path.exists(lib_path):
print(f"目录不存在: {lib_path}")
continue
print(f"处理库: {lib_name}")
# 获取所有NestedPage文件
page_files = []
for file_name in os.listdir(lib_path):
if file_name.startswith("NestedPage") and file_name.endswith(".kt"):
page_files.append(file_name)
page_files.sort(key=lambda x: int(re.search(r'NestedPage(\d+)\.kt', x).group(1)))
for file_name in page_files:
file_path = os.path.join(lib_path, file_name)
page_number_match = re.search(r'NestedPage(\d+)\.kt', file_name)
if page_number_match:
page_number = int(page_number_match.group(1))
add_stable_elements_to_file(file_path, page_number)
total_processed += 1
if total_processed % 100 == 0:
print(f" 已处理 {total_processed} 个文件")
print(f"完成!总共处理了 {total_processed} 个NestedPage文件。")
print("每个页面现在包含8个@Stable数据类。")
if __name__ == "__main__":
process_all_nested_pages()
\ No newline at end of file
#!/bin/bash
# 为所有NestedPage文件添加@Stable元素的简化脚本
BASE_DIR="/Users/dongsq/TestDemo/IR013/TestProject/MaxHap"
LIB_NAMES=("nestedlib1" "nestedlib2" "nestedlib3" "nestedlib4" "nestedlib5")
echo "开始为NestedPage文件添加@Stable元素..."
# 重新生成所有文件(备份原始文件)
for lib_name in "${LIB_NAMES[@]}"; do
LIB_DIR="$BASE_DIR/$lib_name/src/commonMain/kotlin/com/dong/maxhap/$lib_name"
if [ ! -d "$LIB_DIR" ]; then
echo "目录不存在: $LIB_DIR"
continue
fi
echo "处理库: $lib_name"
# 获取该库中的所有NestedPage文件数量
TOTAL_FILES=$(find "$LIB_DIR" -name "NestedPage*.kt" | wc -l)
echo "找到 $TOTAL_FILES 个文件"
# 重新生成每个文件
for ((i=1; i<=200; i++)); do
FILE_PATH="$LIB_DIR/NestedPage$(( (${lib_name: -1} - 1) * 200 + i )).kt"
GLOBAL_INDEX=$(( (${lib_name: -1} - 1) * 200 + i ))
if [ ! -f "$FILE_PATH" ]; then
continue
fi
# 读取原文件内容(除了imports部分)
ORIGINAL_CONTENT=$(sed -n '/^import /,/^@Composable/p' "$FILE_PATH" | head -n -1)
# 生成8个@Stable数据类
STABLE_CLASSES=""
for j in {1..8}; do
STABLE_CLASSES+="@Stable
data class StableData${GLOBAL_INDEX}_${j}(
val id: Int = $((GLOBAL_INDEX * 1000 + j)),
val name: String = \"Stable_${GLOBAL_INDEX}_${j}\",
val value: Double = ${GLOBAL_INDEX}.${j},
val isActive: Boolean = true,
val timestamp: Long = System.currentTimeMillis(),
val tags: List<String> = listOf(\"tag1\", \"tag2\", \"tag3\"),
val metadata: Map<String, String> = mapOf(\"key1\" to \"value1\", \"key2\" to \"value2\"),
val nestedData: NestedStableData${GLOBAL_INDEX}_${j}? = null
)
@Stable
data class NestedStableData${GLOBAL_INDEX}_${j}(
val subId: Int = $((GLOBAL_INDEX * 100 + j)),
val description: String = \"Nested stable data for page ${GLOBAL_INDEX}, item ${j}\",
val count: Int = ${j}00,
val isEnabled: Boolean = $((j % 2 == 0))
)
"
done
# 重新构建文件
{
echo "package com.dong.maxhap.$lib_name"
echo ""
echo "import androidx.compose.foundation.layout.*"
echo "import androidx.compose.material.*"
echo "import androidx.compose.runtime.*"
echo "import androidx.compose.ui.Alignment"
echo "import androidx.compose.ui.Modifier"
echo "import androidx.compose.ui.graphics.Color"
echo "import androidx.compose.ui.text.font.FontWeight"
echo "import androidx.compose.ui.unit.dp"
echo "import androidx.compose.ui.unit.sp"
echo "import androidx.compose.runtime.Stable"
echo ""
echo "$STABLE_CLASSES"
sed -n '/^@Composable/,$p' "$FILE_PATH"
} > "${FILE_PATH}.new"
mv "${FILE_PATH}.new" "$FILE_PATH"
# 显示进度
if [ $((i % 50)) -eq 0 ]; then
echo " 已处理 ${lib_name} 的第 $i 个页面"
fi
done
done
echo "完成!所有NestedPage文件已添加@Stable元素。"
echo "每个页面现在包含8个@Stable数据类。"
\ No newline at end of file
#!/usr/bin/env python3
"""
清理和重新组织NestedPage文件,确保@Stable数据类被正确使用
"""
import os
import re
from pathlib import Path
def cleanup_file(file_path):
"""清理单个文件"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# 移除多余的空行
content = re.sub(r'\n{3,}', '\n\n', content)
# 修复状态变量声明的位置
lines = content.split('\n')
new_lines = []
i = 0
while i < len(lines):
line = lines[i]
# 如果找到Composable函数开始
if '@Composable' in line and 'fun NestedPage' in line:
new_lines.append(line)
i += 1
# 跳过直到找到var showNextPage声明
while i < len(lines) and 'var showNextPage' not in lines[i]:
i += 1
if i < len(lines):
# 添加状态变量声明
new_lines.append(' var showNextPage by remember { mutableStateOf(false) }')
new_lines.append(' val stableData1 by remember { mutableStateOf(StableDataXXX_1()) }')
new_lines.append(' val stableData2 by remember { mutableStateOf(StableDataXXX_2()) }')
new_lines.append(' val stableData3 by remember { mutableStateOf(StableDataXXX_3()) }')
new_lines.append(' val stableData4 by remember { mutableStateOf(StableDataXXX_4()) }')
new_lines.append('')
i += 1 # 跳过原来的var showNextPage行
else:
new_lines.append(line)
i += 1
content = '\n'.join(new_lines)
# 替换XXX为实际的页面编号
page_num_match = re.search(r'NestedPage(\d+)', file_path.name)
if page_num_match:
page_num = page_num_match.group(1)
content = content.replace('StableDataXXX', f'StableData{page_num}')
# 写回文件
with open(file_path, 'w', encoding='utf-8') as f:
f.write(content)
return True
except Exception as e:
print(f"处理文件时出错 {file_path}: {e}")
return False
def main():
base_path = Path("/Users/dongsq/TestDemo/IR013/TestProject/MaxHap")
nested_libs = ["nestedlib1", "nestedlib2", "nestedlib3", "nestedlib4", "nestedlib5"]
total_files = 0
cleaned_files = 0
for lib in nested_libs:
lib_path = base_path / lib / "src/commonMain/kotlin/com/dong/maxhap" / lib
if not lib_path.exists():
continue
# 处理该库中的所有NestedPage文件
for file_path in lib_path.glob("NestedPage*.kt"):
if file_path.is_file():
total_files += 1
if cleanup_file(file_path):
cleaned_files += 1
print(f"\n清理完成!")
print(f"总文件数: {total_files}")
print(f"成功清理: {cleaned_files}")
if __name__ == "__main__":
main()
\ No newline at end of file
#!/bin/bash
# 设置目标目录
TARGET_DIR="composeApp/src/commonMain/kotlin/com/dong/maxhap/demos/test"
# 检查目录是否存在
if [ ! -d "$TARGET_DIR" ]; then
echo "目录 $TARGET_DIR 不存在"
exit 1
fi
# 统计要删除的文件数量
FILE_COUNT=$(find "$TARGET_DIR" -name "NestedPage*.kt" -type f | wc -l)
if [ "$FILE_COUNT" -eq 0 ]; then
echo "没有找到需要删除的NestedPage*.kt文件"
exit 0
fi
echo "找到 $FILE_COUNT 个文件需要删除"
# 确认删除
echo "警告:这将删除 $TARGET_DIR 目录下的所有 NestedPage*.kt 文件"
read -p "是否继续?(y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "取消删除操作"
exit 0
fi
# 删除文件
echo "正在删除文件..."
find "$TARGET_DIR" -name "NestedPage*.kt" -type f -delete
# 检查删除结果
REMAINING_COUNT=$(find "$TARGET_DIR" -name "NestedPage*.kt" -type f | wc -l)
if [ "$REMAINING_COUNT" -eq 0 ]; then
echo "删除成功!所有NestedPage*.kt文件已删除"
# 询问是否删除空目录
if [ -z "$(ls -A "$TARGET_DIR")" ]; then
read -p "HapTestPage目录现在为空,是否删除该目录?(y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
rmdir "$TARGET_DIR"
echo "目录 $TARGET_DIR 已删除"
else
echo "保留空目录 $TARGET_DIR"
fi
fi
else
echo "删除完成,但仍剩余 $REMAINING_COUNT 个文件"
fi
\ No newline at end of file
/**
能导入的包
1. 来源插件
在 当前的 build.gradle.kts 定义可以搭配的插件
plugins {
alias(libs.plugins.androidApplication) // 提供 android.* 相关类
alias(libs.plugins.kotlinMultiplatform) // 提供 org.jetbrains.kotlin.gradle.* 相关类
alias(libs.plugins.composeMultiplatform) // 提供 compose 相关类
}
插件的版本在 libs.versions.toml 中定义:
2.Gradle 核心类
Gradle 本身提供的核心类(如 Copy、JavaVersion 等)自动可用,无需额外配置
*/
// 导入大写工具(capitalizeUS),用于字符串首字母大写
import android.databinding.tool.ext.capitalizeUS
// 导入ExperimentalKotlinGradlePluginApi注解,用于开启Kotlin插件实验特性
import org.gradle.api.file.DuplicatesStrategy
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
// 导入JvmTarget,用于指定JVM的目标版本
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
// 应用Kotlin多平台插件
alias(libs.plugins.kotlinMultiplatform)
// 应用Android应用插件
alias(libs.plugins.androidApplication)
// 应用Compose多平台插件
alias(libs.plugins.composeMultiplatform)
// 应用Compose编译器插件
alias(libs.plugins.composeCompiler)
// 开启 kotlinx.serialization(用于 navigation 类型安全路由)
alias(libs.plugins.kotlinSerialization)
}
/**
Kotlin Multiplatform 配置
插件 libs.plugins.kotlinMultiplatform
负责 Kotlin 代码编译
Android: Kotlin/JVM → .class 文件 │
iOS: Kotlin/Native → LLVM IR │
OHOS: Kotlin/Native → LLVM IR
kotlin {
├── 目标平台 (androidTarget, jvm, iosX64, etc.)
│ ├── compilerOptions
│ └── binaries
├── cocoapods (iOS 特定)
├── sourceSets
│ ├── commonMain/commonTest
│ ├── platformMain/platformTest
│ └── customSourceSets
└── 其他配置
}
*/
kotlin {
// 配置Android目标
androidTarget {
@OptIn(ExperimentalKotlinGradlePluginApi::class) // 允许使用实验API
@OptIn(ExperimentalKotlinGradlePluginApi::class)
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11) // 设置JVM target为11
jvmTarget.set(JvmTarget.JVM_11)
}
}
// 配置iOS三种架构目标
// 编译配置: Kotlin 源码 → LLVM IR -> 机器码
listOf(
iosX64(), // iOS 模拟器(x64架构)
iosArm64(), // iOS 真机(arm64架构)
iosSimulatorArm64() // iOS 模拟器(arm64架构)
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach { iosTarget ->
iosTarget.binaries.framework {
baseName = "ComposeApp" // 设置framework名称
// 静态库 (isStatic = true) - 代码直接嵌入到最终应用中
// 动态库 (isStatic = false) - 运行时加载的库
isStatic = true // 设置framework为静态库
baseName = "ComposeApp"
isStatic = true
}
iosTarget.compilations.getByName("main") {
compilerOptions.configure {
freeCompilerArgs.add("-Xbinary=sanitizer=address")
freeCompilerArgs.add("-Xbinary=splitBCfile=1")
}
}
}
// 配置OHOS(华为鸿蒙)arm64目标
ohosArm64 {
binaries.sharedLib {
baseName = "kn" // 共享库名称为kn
export(libs.compose.multiplatform.export) // 导出compose多平台库的接口
// 确保链接系统 zlib,使 libkn.so 的 NEEDED 包含 libz.so
linkerOpts("-lz")
}
val main by compilations.getting // 获取主编译内容
val main by compilations.getting
main.compilerOptions.configure {
freeCompilerArgs.add("-Xbinary=sanitizer=address")
freeCompilerArgs.add("-Xbinary=splitBCfile=1")
// freeCompilerArgs.add("-Xbinary=sanitizer=address")
}
val resource by main.cinterops.creating {
//配置C interop(cinterop)资源
defFile(file("src/ohosArm64Main/cinterop/resource.def")) // cinterop定义文件
includeDirs(file("src/ohosArm64Main/cinterop/include")) // cinterop包含目录
defFile(file("src/ohosArm64Main/cinterop/resource.def"))
includeDirs(file("src/ohosArm64Main/cinterop/include"))
}
}
ohosX64 {
binaries.sharedLib {
baseName = "kn"
export(libs.compose.multiplatform.export)
linkerOpts("-lz")
}
val main by compilations.getting
main.compilerOptions.configure {
// freeCompilerArgs.add("-Xbinary=sanitizer=address")
}
val resource by main.cinterops.creating {
defFile(file("src/ohosX64Main/cinterop/resource.def"))
includeDirs(file("src/ohosX64Main/cinterop/include"))
}
}
// 配置各平台的依赖关系
sourceSets {
androidMain.dependencies {
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.lifecycle.viewmodelCompose)
implementation(libs.androidx.lifecycle.runtimeCompose)
implementation(libs.androidx.collection)
implementation(libs.compose.ui.backhandler) //需要单独依赖
implementation(project(":lib-01"))
implementation(project(":lib-02"))
implementation(project(":lib-03"))
implementation(project(":lib-04"))
implementation(project(":lib-05"))
implementation(project(":lib-06"))
implementation(project(":lib-07"))
implementation(project(":lib-08"))
implementation(project(":lib-09"))
implementation(project(":lib-10"))
implementation(project(":lib-11"))
implementation(project(":lib-12"))
implementation(project(":lib-13"))
implementation(project(":lib-14"))
implementation(project(":lib-15"))
implementation(project(":lib-16"))
implementation(project(":lib-17"))
implementation(project(":lib-18"))
implementation(project(":lib-19"))
implementation(project(":lib-20"))
}
commonMain.dependencies {
implementation(compose.runtime) // 添加compose runtime依赖
implementation(compose.foundation) // 添加compose基础依赖
implementation(compose.material) // 添加compose material依赖
implementation(compose.ui) // 添加compose ui依赖
implementation(compose.components.resources) // 添加compose组件资源依赖
implementation(compose.components.uiToolingPreview) // 添加compose ui工具预览依赖
implementation(libs.kotlinx.coroutines.core) // 官方协程核心库
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material)
implementation(compose.material3)
implementation(compose.ui)
implementation(compose.components.resources)
implementation(compose.components.uiToolingPreview)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.atomicFu)
implementation(project(":nestedlib1"))
implementation(project(":nestedlib2"))
implementation(project(":nestedlib3"))
implementation(project(":nestedlib4"))
implementation(project(":nestedlib5"))// Kotlin AtomicFu原子库
implementation(libs.compose.navigation)
// lib-01..lib-20 仅 Android/OHOS,不放在 commonMain,避免 appleMain 解析失败
}
val ohosArm64Main by getting {
dependencies {
api(libs.compose.multiplatform.export)
implementation("org.jetbrains.skiko:skiko-ohosarm64")
implementation(project(":lib-01"))
implementation(project(":lib-02"))
implementation(project(":lib-03"))
implementation(project(":lib-04"))
implementation(project(":lib-05"))
implementation(project(":lib-06"))
implementation(project(":lib-07"))
implementation(project(":lib-08"))
implementation(project(":lib-09"))
implementation(project(":lib-10"))
implementation(project(":lib-11"))
implementation(project(":lib-12"))
implementation(project(":lib-13"))
implementation(project(":lib-14"))
implementation(project(":lib-15"))
implementation(project(":lib-16"))
implementation(project(":lib-17"))
implementation(project(":lib-18"))
implementation(project(":lib-19"))
implementation(project(":lib-20"))
}
}
val ohosX64Main by getting {
dependencies {
api(libs.compose.multiplatform.export) // 导出compose多平台接口给依赖消费者
api(libs.compose.multiplatform.export)
implementation("org.jetbrains.skiko:skiko-ohosx64")
implementation(project(":lib-01"))
implementation(project(":lib-02"))
implementation(project(":lib-03"))
implementation(project(":lib-04"))
implementation(project(":lib-05"))
implementation(project(":lib-06"))
implementation(project(":lib-07"))
implementation(project(":lib-08"))
implementation(project(":lib-09"))
implementation(project(":lib-10"))
implementation(project(":lib-11"))
implementation(project(":lib-12"))
implementation(project(":lib-13"))
implementation(project(":lib-14"))
implementation(project(":lib-15"))
implementation(project(":lib-16"))
implementation(project(":lib-17"))
implementation(project(":lib-18"))
implementation(project(":lib-19"))
implementation(project(":lib-20"))
}
}
}
}
/**
应用 AGP : Android Gradle Plugin
Android 应用构建 : .class → DEX(Dalvik 字节码) → APK │
由 androidApplication 插件提供
将 .class 文件转换为 DEX(Dalvik 字节码) 文件
Android 构建工具 → 打包成 APK
作用:
配置 Android 应用 的构建行为
控制 APK 生成、打包、签名等
*/
android {
namespace = "com.dong.n4.n4test010" // 设置包名
compileSdk = libs.versions.android.compileSdk.get().toInt() // 指定编译SDK版本
namespace = "com.example.kmptpc_compose_sample" // 设置包名
compileSdk = libs.versions.android.compileSdk.get().toInt()
defaultConfig {
applicationId = "com.dong.n4.n4test010" // 应用ID
minSdk = libs.versions.android.minSdk.get().toInt() // 最低SDK版本
targetSdk = libs.versions.android.targetSdk.get().toInt()// 目标SDK版本
versionCode = 1 // 应用版本号
versionName = "1.0" // 应用版本名
applicationId = "com.example.kmptpc_compose_sample" // 应用ID
minSdk = libs.versions.android.minSdk.get().toInt()
targetSdk = libs.versions.android.targetSdk.get().toInt()
versionCode = 1
versionName = "1.0"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"// 排除打包时的冗余license资源文件
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
buildTypes {
getByName("release") {
isMinifyEnabled = false // 发布包不混淆代码
isMinifyEnabled = false
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11 // 源码兼容Java 11
targetCompatibility = JavaVersion.VERSION_11 // 输出兼容Java 11
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
dependencies {
debugImplementation(libs.compose.ui.tooling) // debug模式下依赖compose调试工具
debugImplementation(libs.compose.ui.tooling)
}
compose{
ohos{
skia("0.9.22.2-ez-001")
// ohrender(" 0.9.22.2-ohrende")
skia("0.9.22.2-OH.0.1.2-06") // 启用自渲染
// ohrender(" 0.9.22.2-ohrende") //启用统一渲染
}
}
// 为 ohosArm64 目标配置依赖处理
val versionCatalog = extensions.getByType<VersionCatalogsExtension>().named("libs")
val cmpVersion = versionCatalog.findVersion("composeMultiplatform")
.orElseThrow { IllegalStateException("Missing version composeMultiplatform in version catalog") }
.requiredVersion
configurations.all {
resolutionStrategy {
// TODO: 强制刷新 compose ui ohosarm64 依赖
force("org.jetbrains.compose.export:export-ohosarm64:${cmpVersion}")
force("org.jetbrains.compose.ui:ui-ohosarm64:${cmpVersion}")
force("org.jetbrains.compose.ui:ui-arkui-ohosarm64:${cmpVersion}")
force("org.jetbrains.compose.foundation:foundation-ohosarm64:${cmpVersion}")
force("org.jetbrains.compose.export:export-ohosx64:${cmpVersion}")
force("org.jetbrains.compose.ui:ui-ohosx64:${cmpVersion}")
force("org.jetbrains.compose.ui:ui-arkui-ohosx64:${cmpVersion}")
force("org.jetbrains.compose.foundation:foundation-ohosx64:${cmpVersion}")
}
}
// Harmony App 输出目录(支持命令行 --harmonyAppPath)
val harmonyAppDir: File = run {
val cliPath = project.findProperty("harmonyAppPath") as String?
if (cliPath.isNullOrBlank()) {
// 默认:项目根目录 /harmonyApp
rootProject.file("harmonyApp")
} else {
// 命令行传入的路径
file(cliPath)
}
}
// 字符串首字母大写工具函数
fun String.capitalizeUS(): String = this.replaceFirstChar {
if (it.isLowerCase()) it.titlecase() else it.toString()
// 生成约 180MB 填充资源,使 HAP 从约 120MB 增至约 300MB
tasks.register("generateHapPadding") {
group = "harmony"
val paddingFile = project.layout.buildDirectory.file("padding/padding.dat").get().asFile
outputs.file(paddingFile)
doLast {
paddingFile.parentFile.mkdirs()
val buf = ByteArray(1024 * 1024)
paddingFile.outputStream().use { out ->
for (i in 0 until 180) { out.write(buf) }
}
}
}
// 为不同类型(debug、release)OHOS构建注册Copy任务并发布到Harmony App目录
arrayOf("debug", "release").forEach { type ->
tasks.register<Copy>("publish${type.capitalizeUS()}BinariesToHarmonyApp") {
group = "harmony" // 归类到harmony任务组
dependsOn("link${type.capitalizeUS()}SharedOhosArm64") // 依赖于OHOS shared lib的链接任务
into(harmonyAppDir) // 输出目标目录
from("build/bin/ohosArm64/${type}Shared/libkn_api.h") { // 复制头文件
into("entry/src/main/cpp/include/") // 指定目录
dependsOn("link${type.capitalizeUS()}SharedOhosArm64", "link${type.capitalizeUS()}SharedOhosX64", "generateHapPadding")
duplicatesStrategy = DuplicatesStrategy.INCLUDE
into(rootProject.file("harmonyApp"))
from("build/bin/ohosArm64/${type}Shared/libkn_api.h") {
into("entry/src/main/cpp/include/")
}
from(project.file("build/bin/ohosArm64/${type}Shared/libkn.so")) {
into("entry/libs/arm64-v8a/")
}
from("build/bin/ohosX64/${type}Shared/libkn_api.h") {
into("entry/src/main/cpp/include/")
}
from(project.file("build/bin/ohosX64/${type}Shared/libkn.so")) {
into("entry/libs/x86_64/")
}
val composeResourcePackage = "${rootProject.name}.${project.name.lowercase()}.generated.resources"
from("src/commonMain/composeResources") {
into("entry/src/main/resources/rawfile/composeResources/$composeResourcePackage/")
}
from(project.file("build/bin/ohosArm64/${type}Shared/libkn.so")) { // 复制共享库文件
into("/entry/libs/arm64-v8a/") // 指定目标目录
from(layout.buildDirectory.dir("padding")) {
into("entry/src/main/resources/rawfile/padding/")
}
}
}
package com.dong.maxhap
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.Composable
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
setContent {
App()
}
}
}
package com.dong.maxhap
import android.os.Build
class AndroidPlatform : Platform {
override val name: String = "Android ${Build.VERSION.SDK_INT}"
}
actual fun getPlatform(): Platform = AndroidPlatform()
internal actual fun getPlatformDemoList(): List<PlatformDemoItem> = emptyList()
@androidx.compose.runtime.Composable
internal actual fun PlatformDemo(id: String) {
}
package com.example.kmptpc_compose_sample
import androidx.compose.runtime.Composable
import com.example.lib01.Lib01Screen as Lib01ScreenImpl
@Composable
internal actual fun Lib01Screen(onBack: () -> Unit) {
Lib01ScreenImpl(onBack = onBack)
}
package com.example.kmptpc_compose_sample
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.Composable
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
setContent {
App()
}
}
}
package com.example.kmptpc_compose_sample.demos.foundation
import android.content.ClipData
import android.view.View
import androidx.compose.ui.draganddrop.DragAndDropEvent
import androidx.compose.ui.draganddrop.DragAndDropTransferData
import androidx.compose.ui.draganddrop.toAndroidDragEvent
actual fun createDragAndDropTransferData(item: String): DragAndDropTransferData? {
return DragAndDropTransferData(
clipData = ClipData.newPlainText("text/plain", item),
flags = View.DRAG_FLAG_GLOBAL
)
}
actual fun extractItemFromEvent(event: DragAndDropEvent): String? {
return try {
val dragEvent = event.toAndroidDragEvent()
val clipData = dragEvent.clipData
if (clipData != null && clipData.itemCount > 0) {
clipData.getItemAt(0).text?.toString()
} else {
null
}
} catch (e: Exception) {
null
}
}
package com.example.kmptpc_compose_sample.demos.foundation
import androidx.compose.foundation.layout.*
import androidx.compose.material.Card
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
internal actual fun OrientationAndSafeAreaDemoContent() {
Card(
modifier = Modifier.fillMaxWidth(),
backgroundColor = MaterialTheme.colors.surface,
elevation = 2.dp
) {
Column(Modifier.padding(16.dp)) {
Text(
text = "此 Demo 仅在 OHOS 平台可用",
style = MaterialTheme.typography.body1
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "在 OHOS 设备上可查看:当前界面方向(Portrait/LandscapeLeft 等)、系统安全区(left/top/right/bottom,单位 dp)。",
style = MaterialTheme.typography.caption,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.7f)
)
}
}
}
package com.example.kmptpc_compose_sample.demos.foundation
import androidx.compose.foundation.layout.*
import androidx.compose.material.Card
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
internal actual fun SkikoCapabilitiesDemoContent() {
Card(
modifier = Modifier.fillMaxWidth(),
backgroundColor = MaterialTheme.colors.surface,
elevation = 2.dp
) {
Column(Modifier.padding(16.dp)) {
Text(
text = "此 Demo 仅在 OHOS 平台可用",
style = MaterialTheme.typography.body1
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "在 OHOS 设备上运行可测试:打开链接、剪贴板 setText/getText/hasText、当前系统主题。",
style = MaterialTheme.typography.caption,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.7f)
)
}
}
}
<resources>
<string name="app_name">MaxHap</string>
<string name="app_name">kmptpc_compose_sample</string>
</resources>
\ No newline at end of file
This diff is collapsed.
package com.dong.maxhap
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.safeContentPadding
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@Composable
internal fun App() {
MaterialTheme {
// 使用 Page 类型的状态,修复类型推断报错
var page by remember { mutableStateOf<com.dong.maxhap.navigation.Page>(com.dong.maxhap.navigation.Page.Home) }
// 首页状态提升:滚动、搜索、分组展开状态
val listState = androidx.compose.foundation.lazy.rememberLazyListState()
var searchQuery by remember { mutableStateOf("") }
val expandedGroups = remember {
mutableStateMapOf<com.dong.maxhap.navigation.ComposeGroup, Boolean>().apply {
com.dong.maxhap.navigation.ComposeGroup.entries.forEach { put(it, true) }
}
}
Column(
modifier = Modifier
.safeContentPadding()
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
when (val p = page) {
is com.dong.maxhap.navigation.Page.Home -> {
HomeScreen(
onSelect = { demo ->
page = com.dong.maxhap.navigation.Page.Detail(demo)
},
listState = listState,
searchQuery = searchQuery,
onSearchChange = { searchQuery = it },
expandedGroups = expandedGroups
)
}
is com.dong.maxhap.navigation.Page.Detail -> {
DemoScreen(demo = p.demo, onBack = { page = com.dong.maxhap.navigation.Page.Home })
}
}
}
}
}
package com.dong.maxhap
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.dong.maxhap.navigation.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import com.dong.maxhap.demos.material.*
import com.dong.maxhap.demos.ui.UiBoxDemo
import com.dong.maxhap.demos.benchmark.*
import com.dong.maxhap.demos.foundation.FoundationBasicTextDemo
import com.dong.maxhap.demos.foundation.FoundationLazyColumnDemo
import com.dong.maxhap.demos.test.NestedPagesMain
@Composable
internal fun HomeScreen(
onSelect: (com.dong.maxhap.navigation.ComponentDemo) -> Unit,
listState: androidx.compose.foundation.lazy.LazyListState,
searchQuery: String,
onSearchChange: (String) -> Unit,
expandedGroups: MutableMap<com.dong.maxhap.navigation.ComposeGroup, Boolean>
) {
val grouped = com.dong.maxhap.navigation.demosByGroup()
val filtered = grouped.mapValues { (_, demos) ->
if (searchQuery.isBlank()) demos
else demos.filter { d ->
d.title.contains(searchQuery, ignoreCase = true) ||
d.id.contains(searchQuery, ignoreCase = true)
}
}.filterValues { it.isNotEmpty() || searchQuery.isBlank() }
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 8.dp)
) {
// 顶部固定搜索框(避免 iOS LazyColumn 首项兼容性问题)
Card(backgroundColor = MaterialTheme.colors.surface) {
OutlinedTextField(
value = searchQuery,
onValueChange = onSearchChange,
label = { Text("搜索") },
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
singleLine = true
)
}
Spacer(Modifier.height(12.dp))
androidx.compose.foundation.lazy.LazyColumn(
modifier = Modifier.fillMaxSize(),
state = listState,
contentPadding = PaddingValues(vertical = 12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
com.dong.maxhap.navigation.ComposeGroup.entries.forEach { group ->
val demos = filtered[group] ?: emptyList()
if (demos.isEmpty() && searchQuery.isNotBlank()) {
// 搜索无结果时该组隐藏
} else {
val expanded = expandedGroups[group] != false
item {
Card(
backgroundColor = Color.LightGray.copy(alpha = 0.3f), // surfaceVariant replacement
modifier = Modifier
.fillMaxWidth()
.clickable { expandedGroups[group] = !expanded }
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(
com.dong.maxhap.navigation.groupTitles[group] ?: group.name,
style = MaterialTheme.typography.h6
)
val countText =
if (searchQuery.isNotBlank()) "匹配:" + demos.size else "共 " + demos.size + " 项"
Text(countText, style = MaterialTheme.typography.caption)
}
Text(
text = if (expanded) "收起 ▾" else "展开 ▸",
color = MaterialTheme.colors.primary
)
}
}
}
if (expanded) {
items(demos) { demo ->
Card(backgroundColor = MaterialTheme.colors.surface) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onSelect(demo) }
.padding(16.dp), // Increased padding for better touch target
verticalAlignment = Alignment.CenterVertically
) {
Text(demo.title, style = MaterialTheme.typography.body1)
}
}
Divider()
}
}
}
}
}
}
}
@Composable
internal fun DemoScreen(demo: ComponentDemo, onBack: () -> Unit) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(text = demo.title, style = MaterialTheme.typography.h5)
OutlinedButton(onClick = onBack) { Text("返回") }
}
Divider()
when (demo.id) {
// ui
"ui_box" -> UiBoxDemo()
"ui_hap" -> NestedPagesMain()
// foundation
"foundation_basic_text" -> FoundationBasicTextDemo()
"foundation_lazy_column" -> FoundationLazyColumnDemo()
// benchmark
"benchmark_1500_text" -> Compose1500Text()
"benchmark_1500_view" -> ComposeView1500Page()
// material
"material_alert_dialog" -> AlertDialogSamples()
"material_app_bar" -> AppBarSamples()
"material_backdrop_scaffold" -> BackdropScaffoldSamples()
"material_badge" -> BadgeSamples()
"material_bottom_navigation" -> BottomNavigationSamples()
"material_bottom_sheet_scaffold" -> BottomSheetScaffoldSamples()
"material_button_samples" -> ButtonSamples()
"material_card_samples" -> CardSamples()
"material_content_alpha" -> ContentAlphaSamples()
"material_drawer_samples" -> DrawerSamples()
"material_elevation_samples" -> ElevationSamples()
"material_exposed_dropdown_menu" -> ExposedDropdownMenuSamples()
"material_fab_samples" -> FloatingActionButtonSamples()
"material_icon_button_samples" -> IconButtonSamples()
"material_list_samples" -> ListSamples()
"material_menu_samples" -> MenuSamples()
"material_modal_bottom_sheet" -> ModalBottomSheetSamples()
"material_navigation_rail" -> NavigationRailSample()
"material_progress_indicator" -> ProgressIndicatorSamples()
"material_pull_refresh" -> PullRefreshSamples()
"material_selection_controls" -> SelectionControlsSamples()
"material_slider_sample" -> SliderSample()
"material_surface_samples" -> SurfaceSamples()
"material_swipeable_samples" -> SwipeableSamples()
"material_swipe_to_dismiss" -> SwipeToDismissSamples()
"material_tab_samples" -> TabSamples()
"material_text_field_samples" -> TextFieldSamples()
"material_text_samples" -> TextSamples()
"material_theme_samples" -> ThemeSamples()
else -> {
if (demo.group == com.dong.maxhap.navigation.ComposeGroup.Platform) {
com.dong.maxhap.PlatformDemo(demo.id)
}
}
}
}
}
package com.dong.maxhap
class Greeting {
private val platform = getPlatform()
fun greet(): String {
return "Hello, ${platform.name}!"
}
}
\ No newline at end of file
package com.dong.maxhap
interface Platform {
val name: String
}
expect fun getPlatform(): Platform
internal data class PlatformDemoItem(val id: String, val title: String)
internal expect fun getPlatformDemoList(): List<PlatformDemoItem>
@androidx.compose.runtime.Composable
internal expect fun PlatformDemo(id: String)
package com.dong.maxhap.demos
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.dong.maxhap.nestedlib1.NestedPage1
@Composable
internal fun NestedLibsDemo() {
var showEntryPoint by remember { mutableStateOf(false) }
if (showEntryPoint) {
// 从第一个库的第一个页面开始
NestedPage1()
return
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "嵌套库演示",
fontSize = 28.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFF2196F3),
modifier = Modifier.padding(bottom = 30.dp)
)
Text(
text = "5个库,每个包含200个嵌套页面",
fontSize = 18.sp,
modifier = Modifier.padding(bottom = 10.dp)
)
Text(
text = "总共1000层嵌套页面",
fontSize = 16.sp,
color = Color.Gray,
modifier = Modifier.padding(bottom = 30.dp)
)
Button(
onClick = { showEntryPoint = true },
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
colors = ButtonDefaults.buttonColors(
backgroundColor = Color(0xFF4CAF50)
)
) {
Text(
text = "开始嵌套之旅",
fontSize = 18.sp,
fontWeight = FontWeight.Medium,
color = Color.White
)
}
Spacer(modifier = Modifier.height(20.dp))
Text(
text = "页面分布:",
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 10.dp)
)
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.Start
) {
Text("• nestedlib1: 页面 1-200", fontSize = 14.sp, color = Color(0xFF100000))
Text("• nestedlib2: 页面 201-400", fontSize = 14.sp, color = Color(0xFF320000))
Text("• nestedlib3: 页面 401-600", fontSize = 14.sp, color = Color(0xFF640000))
Text("• nestedlib4: 页面 601-800", fontSize = 14.sp, color = Color(0xFF960000))
Text("• nestedlib5: 页面 801-1000", fontSize = 14.sp, color = Color(0xFFC80000))
}
}
}
\ No newline at end of file
package com.dong.maxhap.demos.benchmark
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
internal fun Compose1500Text() {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
repeat(1500) { index ->
Text(
text = "Compose1500Text Item #$index",
fontSize = 16.sp,
modifier = Modifier
.width(300.dp)
.height(50.dp)
.border(1.dp, Color.Gray)
.padding(10.dp)
)
}
}
}
package com.dong.maxhap.demos.benchmark
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.ExperimentalResourceApi
@OptIn(ExperimentalResourceApi::class)
@Composable
internal fun ComposeView1500Page() {
val loaded = remember { mutableStateOf(false) }
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
repeat(1500) { index ->
Box(
modifier = Modifier
.size(300.dp, 100.dp)
.border(
width = 2.dp,
color = Color.Red,
)
.then(
if (index == 1499) Modifier.onGloballyPositioned {
loaded.value = true
//trace_tag_end()
} else Modifier
)
) {
Text(
text = "Item #$index",
)
}
}
}
if (loaded.value) {
println("页面加载完成 ✅")
}
}
\ No newline at end of file
package com.dong.maxhap.demos.foundation
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.BasicText
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
internal fun FoundationBasicTextDemo() {
var text by remember { mutableStateOf("Hello Compose") }
var size by remember { mutableStateOf(18f) }
var colorToggle by remember { mutableStateOf(false) }
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(value = text, onValueChange = { text = it }, label = { Text("内容") })
Text("字体大小: ${size.toInt()}sp")
Slider(value = size, onValueChange = { size = it }, valueRange = 12f..48f)
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = colorToggle, onCheckedChange = { colorToggle = it })
Text("使用高亮颜色")
}
Box(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colors.surface)
.padding(12.dp)
) {
BasicText(
text = text,
style = androidx.compose.ui.text.TextStyle(
fontSize = size.sp,
color = if (colorToggle) MaterialTheme.colors.primary else Color.Unspecified
)
)
}
}
}
package com.dong.maxhap.demos.foundation
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Slider
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
internal fun FoundationLazyColumnDemo() {
var count by remember { mutableStateOf(10) }
var spaced by remember { mutableStateOf(4) }
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("条目数量: $count")
Slider(value = count.toFloat(), onValueChange = { count = it.toInt() }, valueRange = 1f..50f)
Text("条目间距: ${spaced}dp")
Slider(value = spaced.toFloat(), onValueChange = { spaced = it.toInt() }, valueRange = 0f..24f)
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.height(220.dp)
.background(MaterialTheme.colors.surface),
contentPadding = PaddingValues(8.dp),
verticalArrangement = Arrangement.spacedBy(spaced.dp)
) {
items((1..count).toList()) { idx ->
Box(
modifier = Modifier
.fillMaxWidth()
.height(40.dp)
.background(MaterialTheme.colors.secondary),
contentAlignment = Alignment.Center
) {
Text("Item #$idx")
}
}
}
}
}
package com.dong.maxhap.demos.game
import kotlin.random.Random
data class Game2048State(
val grid: List<List<Int>>,
val score: Int,
val gameOver: Boolean,
val won: Boolean
)
class Game2048Logic {
companion object {
const val GRID_SIZE = 4
const val WIN_VALUE = 2048
}
fun doInitGrid(): List<List<Int>> {
val grid = List(GRID_SIZE) { List(GRID_SIZE) { 0 } }
return addRandomTile(addRandomTile(grid))
}
fun moveLeft(grid: List<List<Int>>): Pair<List<List<Int>>, Int> {
var score = 0
val newGrid = grid.map { row ->
val filteredRow = row.filter { it != 0 }
val mergedRow = mutableListOf<Int>()
var i = 0
while (i < filteredRow.size) {
if (i < filteredRow.size - 1 && filteredRow[i] == filteredRow[i + 1]) {
val mergedValue = filteredRow[i] * 2
mergedRow.add(mergedValue)
score += mergedValue
i += 2
} else {
mergedRow.add(filteredRow[i])
i++
}
}
// 填充到GRID_SIZE长度
while (mergedRow.size < GRID_SIZE) {
mergedRow.add(0)
}
mergedRow
}
return Pair(newGrid, score)
}
fun moveRight(grid: List<List<Int>>): Pair<List<List<Int>>, Int> {
val reversedGrid = grid.map { it.reversed() }
val (movedGrid, score) = moveLeft(reversedGrid)
val newGrid = movedGrid.map { it.reversed() }
return Pair(newGrid, score)
}
fun moveUp(grid: List<List<Int>>): Pair<List<List<Int>>, Int> {
val transposedGrid = transpose(grid)
val (movedGrid, score) = moveLeft(transposedGrid)
val newGrid = transpose(movedGrid)
return Pair(newGrid, score)
}
fun moveDown(grid: List<List<Int>>): Pair<List<List<Int>>, Int> {
val transposedGrid = transpose(grid)
val (movedGrid, score) = moveRight(transposedGrid)
val newGrid = transpose(movedGrid)
return Pair(newGrid, score)
}
private fun transpose(grid: List<List<Int>>): List<List<Int>> {
return List(GRID_SIZE) { col ->
List(GRID_SIZE) { row ->
grid[row][col]
}
}
}
fun addRandomTile(grid: List<List<Int>>): List<List<Int>> {
val emptyCells = mutableListOf<Pair<Int, Int>>()
for (i in grid.indices) {
for (j in grid[i].indices) {
if (grid[i][j] == 0) {
emptyCells.add(Pair(i, j))
}
}
}
if (emptyCells.isEmpty()) return grid
val (row, col) = emptyCells.random()
val newValue = if (Random.nextFloat() < 0.9f) 2 else 4
return grid.mapIndexed { i, gridRow ->
if (i == row) {
gridRow.mapIndexed { j, cell ->
if (j == col) newValue else cell
}
} else {
gridRow
}
}
}
fun hasEmptyCell(grid: List<List<Int>>): Boolean {
return grid.any { row -> row.any { it == 0 } }
}
fun canMerge(grid: List<List<Int>>): Boolean {
// 检查水平方向
for (i in 0 until GRID_SIZE) {
for (j in 0 until GRID_SIZE - 1) {
if (grid[i][j] != 0 && grid[i][j] == grid[i][j + 1]) {
return true
}
}
}
// 检查垂直方向
for (j in 0 until GRID_SIZE) {
for (i in 0 until GRID_SIZE - 1) {
if (grid[i][j] != 0 && grid[i][j] == grid[i + 1][j]) {
return true
}
}
}
return false
}
fun checkWin(grid: List<List<Int>>): Boolean {
return grid.any { row -> row.any { it >= WIN_VALUE } }
}
fun checkGameOver(grid: List<List<Int>>): Boolean {
return !hasEmptyCell(grid) && !canMerge(grid)
}
}
\ No newline at end of file
package com.dong.maxhap.demos.game
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
internal fun Game2048Screen(onBack: () -> Unit) {
val logic = remember { Game2048Logic() }
var gameState by remember {
mutableStateOf(
Game2048State(
grid = logic.doInitGrid(),
score = 0,
gameOver = false,
won = false
)
)
}
fun handleMove(moveFunction: (List<List<Int>>) -> Pair<List<List<Int>>, Int>) {
if (gameState.gameOver) return
val (newGrid, scoreGain) = moveFunction(gameState.grid)
if (newGrid != gameState.grid) {
val updatedGrid = logic.addRandomTile(newGrid)
val newScore = gameState.score + scoreGain
val won = logic.checkWin(updatedGrid)
val gameOver = logic.checkGameOver(updatedGrid)
gameState = Game2048State(
grid = updatedGrid,
score = newScore,
gameOver = gameOver,
won = won
)
}
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// 标题和分数栏
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "🎮 2048",
fontSize = 24.sp,
fontWeight = FontWeight.Bold
)
Column(
horizontalAlignment = Alignment.End
) {
Text(
text = "得分: ${gameState.score}",
fontSize = 18.sp,
fontWeight = FontWeight.Medium
)
if (gameState.won) {
Text(
text = "🎉 恭喜获胜!",
color = Color.Green,
fontSize = 14.sp
)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
// 游戏网格
Box(
modifier = Modifier
.size(320.dp)
.background(Color(0xFFBBADA0), RoundedCornerShape(8.dp))
.padding(8.dp)
) {
GameGrid(grid = gameState.grid)
}
Spacer(modifier = Modifier.height(16.dp))
// 控制按钮
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
DirectionButton("↑") { handleMove { logic.moveUp(it) } }
DirectionButton("↓") { handleMove { logic.moveDown(it) } }
DirectionButton("←") { handleMove { logic.moveLeft(it) } }
DirectionButton("→") { handleMove { logic.moveRight(it) } }
}
Spacer(modifier = Modifier.height(16.dp))
// 操作按钮
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Button(
onClick = {
gameState = Game2048State(
grid = logic.doInitGrid(),
score = 0,
gameOver = false,
won = false
)
},
colors = ButtonDefaults.buttonColors(backgroundColor = Color(0xFF8F7A66)),
modifier = Modifier.weight(1f)
) {
Text("重新开始", color = Color.White)
}
Spacer(modifier = Modifier.width(16.dp))
Button(
onClick = onBack,
colors = ButtonDefaults.buttonColors(backgroundColor = Color.Gray),
modifier = Modifier.weight(1f)
) {
Text("返回", color = Color.White)
}
}
// 游戏结束提示
if (gameState.gameOver) {
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "游戏结束! 😢",
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = Color.Red,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}
Spacer(modifier = Modifier.height(16.dp))
// 操作说明
Text(
text = "操作说明:\n• 点击方向键移动方块\n• 相同数字的方块相遇时会合并\n• 达到2048即获胜\n• 无法移动时游戏结束",
fontSize = 12.sp,
color = Color.Gray,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}
}
@Composable
private fun GameGrid(grid: List<List<Int>>) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
grid.forEach { row ->
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
row.forEach { value ->
GameTile(value = value)
}
}
}
}
}
@Composable
private fun GameTile(value: Int) {
val (backgroundColor, textColor) = getTileColors(value)
Box(
modifier = Modifier
.size(70.dp)
.clip(RoundedCornerShape(4.dp))
.background(backgroundColor)
.border(1.dp, Color(0xFFCDC1B4), RoundedCornerShape(4.dp)),
contentAlignment = Alignment.Center
) {
if (value != 0) {
Text(
text = value.toString(),
fontSize = when {
value < 10 -> 28.sp
value < 100 -> 24.sp
value < 1000 -> 20.sp
else -> 16.sp
},
fontWeight = FontWeight.Bold,
color = textColor
)
}
}
}
private fun getTileColors(value: Int): Pair<Color, Color> {
return when (value) {
0 -> Color(0xFFCDC1B4) to Color.Transparent
2 -> Color(0xFFEEE4DA) to Color(0xFF776E65)
4 -> Color(0xFFEDE0C8) to Color(0xFF776E65)
8 -> Color(0xFFF2B179) to Color.White
16 -> Color(0xFFF59563) to Color.White
32 -> Color(0xFFF67C5F) to Color.White
64 -> Color(0xFFF65E3B) to Color.White
128 -> Color(0xFFEDCF72) to Color.White
256 -> Color(0xFFEDCC61) to Color.White
512 -> Color(0xFFEDC850) to Color.White
1024 -> Color(0xFFEDC53F) to Color.White
2048 -> Color(0xFFEDC22E) to Color.White
else -> Color(0xFF3C3A32) to Color.White
}
}
@Composable
private fun DirectionButton(text: String, onClick: () -> Unit) {
Button(
onClick = onClick,
modifier = Modifier.size(60.dp),
colors = ButtonDefaults.buttonColors(backgroundColor = Color(0xFF8F7A66)),
shape = RoundedCornerShape(8.dp)
) {
Text(
text = text,
fontSize = 20.sp,
color = Color.White,
fontWeight = FontWeight.Bold
)
}
}
\ No newline at end of file
package com.dong.maxhap.demos.game
import androidx.compose.ui.geometry.Offset
import kotlin.random.Random
data class SnakeGameState(
val snake: List<Offset>,
val food: Offset,
val direction: Offset,
val score: Int,
val gameOver: Boolean,
val gamePaused: Boolean
)
class SnakeGameLogic {
companion object {
const val GRID_WIDTH = 20
const val GRID_HEIGHT = 20
const val CELL_SIZE = 20f
val INITIAL_DIRECTION = Offset(1f, 0f)
}
fun initializeGame(): SnakeGameState {
val initialSnake = listOf(
Offset(10f, 10f),
Offset(9f, 10f),
Offset(8f, 10f)
)
return SnakeGameState(
snake = initialSnake,
food = generateFood(initialSnake),
direction = INITIAL_DIRECTION,
score = 0,
gameOver = false,
gamePaused = false
)
}
fun moveSnake(state: SnakeGameState): SnakeGameState {
if (state.gameOver || state.gamePaused) return state
val head = state.snake.first()
val newHead = Offset(
(head.x + state.direction.x).coerceIn(0f, (GRID_WIDTH - 1).toFloat()),
(head.y + state.direction.y).coerceIn(0f, (GRID_HEIGHT - 1).toFloat())
)
// 检查是否撞到自己
if (newHead in state.snake) {
return state.copy(gameOver = true)
}
val newSnake = mutableListOf(newHead)
newSnake.addAll(state.snake.dropLast(1))
// 检查是否吃到食物
if (newHead == state.food) {
// 蛇身增长
newSnake.addAll(state.snake.takeLast(1))
return state.copy(
snake = newSnake,
food = generateFood(newSnake),
score = state.score + 10
)
}
return state.copy(snake = newSnake)
}
fun changeDirection(state: SnakeGameState, newDirection: Offset): SnakeGameState {
// 防止反向移动
if (newDirection.x == -state.direction.x && newDirection.y == -state.direction.y) {
return state
}
return state.copy(direction = newDirection)
}
fun togglePause(state: SnakeGameState): SnakeGameState {
return state.copy(gamePaused = !state.gamePaused)
}
fun restartGame(): SnakeGameState {
return initializeGame()
}
private fun generateFood(snake: List<Offset>): Offset {
var food: Offset
do {
food = Offset(
Random.nextInt(GRID_WIDTH).toFloat(),
Random.nextInt(GRID_HEIGHT).toFloat()
)
} while (food in snake)
return food
}
fun isGameOver(state: SnakeGameState): Boolean {
val head = state.snake.first()
// 检查边界碰撞
if (head.x < 0 || head.x >= GRID_WIDTH || head.y < 0 || head.y >= GRID_HEIGHT) {
return true
}
// 检查自我碰撞
return head in state.snake.drop(1)
}
}
\ No newline at end of file
package com.dong.maxhap.demos.game
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@Composable
internal fun SnakeGameScreen(onBack: () -> Unit) {
val logic = remember { SnakeGameLogic() }
var gameState by remember { mutableStateOf(logic.initializeGame()) }
val coroutineScope = rememberCoroutineScope()
// 游戏循环
LaunchedEffect(gameState.gameOver, gameState.gamePaused) {
if (!gameState.gameOver && !gameState.gamePaused) {
while (true) {
delay(200) // 控制游戏速度
gameState = logic.moveSnake(gameState)
if (logic.isGameOver(gameState)) {
gameState = gameState.copy(gameOver = true)
break
}
}
}
}
// 处理按键输入
fun handleDirectionChange(direction: Offset) {
if (!gameState.gameOver) {
gameState = logic.changeDirection(gameState, direction)
}
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
.background(Color.Black),
horizontalAlignment = Alignment.CenterHorizontally
) {
// 标题和分数
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "🐍 贪吃蛇",
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
color = Color.White
)
Text(
text = "得分: ${gameState.score}",
fontSize = 18.sp,
fontWeight = FontWeight.Medium,
color = Color.Yellow
)
}
Spacer(modifier = Height(16.dp))
// 游戏区域
Box(
modifier = Modifier
.size(
(SnakeGameLogic.GRID_WIDTH * SnakeGameLogic.CELL_SIZE.toInt()).dp,
(SnakeGameLogic.GRID_HEIGHT * SnakeGameLogic.CELL_SIZE.toInt()).dp
)
.background(Color.DarkGray)
.pointerInput(Unit) {
detectTapGestures { offset ->
// 简单的触摸控制 - 根据点击位置改变方向
val centerX = size.width / 2
val centerY = size.height / 2
val direction = when {
offset.x < centerX && abs(offset.x - centerX) > abs(offset.y - centerY) ->
Offset(-1f, 0f) // 左
offset.x > centerX && abs(offset.x - centerX) > abs(offset.y - centerY) ->
Offset(1f, 0f) // 右
offset.y < centerY ->
Offset(0f, -1f) // 上
else ->
Offset(0f, 1f) // 下
}
handleDirectionChange(direction)
}
}
) {
Canvas(modifier = Modifier.fillMaxSize()) {
// 绘制食物
drawCircle(
color = Color.Red,
radius = SnakeGameLogic.CELL_SIZE / 2,
center = Offset(
gameState.food.x * SnakeGameLogic.CELL_SIZE + SnakeGameLogic.CELL_SIZE / 2f,
gameState.food.y * SnakeGameLogic.CELL_SIZE + SnakeGameLogic.CELL_SIZE / 2f
)
)
// 绘制蛇身
gameState.snake.forEachIndexed { index, segment ->
val color = if (index == 0) Color.Green else Color(0xFF90EE90)
drawRect(
color = color,
topLeft = Offset(
segment.x * SnakeGameLogic.CELL_SIZE,
segment.y * SnakeGameLogic.CELL_SIZE
),
size = androidx.compose.ui.geometry.Size(
SnakeGameLogic.CELL_SIZE,
SnakeGameLogic.CELL_SIZE
)
)
}
}
}
Spacer(modifier = Height(16.dp))
// 控制按钮
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
DirectionButton("↑") { handleDirectionChange(Offset(0f, -1f)) }
DirectionButton("↓") { handleDirectionChange(Offset(0f, 1f)) }
DirectionButton("←") { handleDirectionChange(Offset(-1f, 0f)) }
DirectionButton("→") { handleDirectionChange(Offset(1f, 0f)) }
}
Spacer(modifier = Height(16.dp))
// 操作按钮
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Button(
onClick = {
gameState = if (gameState.gamePaused) {
logic.togglePause(gameState)
} else {
logic.restartGame()
}
},
colors = ButtonDefaults.buttonColors(
backgroundColor = if (gameState.gamePaused) Color.Green else Color(0xFF8F7A66)
),
modifier = Modifier.weight(1f)
) {
Text(
text = if (gameState.gamePaused) "继续" else "重新开始",
color = Color.White
)
}
Spacer(modifier = Width(16.dp))
Button(
onClick = onBack,
colors = ButtonDefaults.buttonColors(backgroundColor = Color.Gray),
modifier = Modifier.weight(1f)
) {
Text("返回", color = Color.White)
}
}
// 游戏状态提示
if (gameState.gameOver) {
Spacer(modifier = Height(16.dp))
Text(
text = "游戏结束! 😢",
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = Color.Red,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
} else if (gameState.gamePaused) {
Spacer(modifier = Height(16.dp))
Text(
text = "游戏暂停 ⏸️",
fontSize = 18.sp,
fontWeight = FontWeight.Medium,
color = Color.Yellow,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}
Spacer(modifier = Height(16.dp))
// 操作说明
Text(
text = "操作说明:\n• 点击屏幕或使用方向键控制蛇的移动\n• 吃到红色食物可以增长身体并获得分数\n• 避免撞到自己或边界\n• 按暂停键可以暂停/继续游戏",
fontSize = 12.sp,
color = Color.Gray,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}
}
@Composable
private fun DirectionButton(text: String, onClick: () -> Unit) {
Button(
onClick = onClick,
modifier = Modifier.size(60.dp),
colors = ButtonDefaults.buttonColors(backgroundColor = Color(0xFF4A4A4A)),
shape = androidx.compose.foundation.shape.RoundedCornerShape(8.dp)
) {
Text(
text = text,
fontSize = 20.sp,
color = Color.White,
fontWeight = FontWeight.Bold
)
}
}
// 注意:这里可能需要导入或定义Height和Width,如果不存在的话
// 如果编译报错,可以替换为Modifier.height()和Modifier.width()
private fun Height(dp: androidx.compose.ui.unit.Dp) = Modifier.height(dp)
private fun Width(dp: androidx.compose.ui.unit.Dp) = Modifier.width(dp)
private fun abs(value: Float): Float = if (value < 0) -value else value
\ No newline at end of file
package com.dong.maxhap.demos.game
import androidx.compose.ui.geometry.Offset
// 象棋棋子类型枚举
enum class ChessPieceType {
JIANG, SHI, XIANG, MA, CHE, PAO, BING
}
// 棋子颜色枚举
enum class ChessPieceColor {
RED, BLACK
}
// 象棋棋子数据类
data class ChessPiece(
val type: ChessPieceType,
val color: ChessPieceColor,
val position: Offset // (x, y) 坐标,范围 0-8 (列) 和 0-9 (行)
)
// 象棋游戏状态
data class XiangqiGameState(
val board: List<List<ChessPiece?>>, // 9x10 棋盘
val currentPlayer: ChessPieceColor,
val selectedPiece: Offset?, // 当前选中的棋子位置
val gameOver: Boolean,
val winner: ChessPieceColor?
)
// 象棋游戏逻辑类
class XiangqiLogic {
companion object {
const val BOARD_WIDTH = 9
const val BOARD_HEIGHT = 10
}
// 初始化棋盘
internal fun initializeBoard(): List<List<ChessPiece?>> {
val board = List(BOARD_HEIGHT) { List<ChessPiece?>(BOARD_WIDTH) { null } }
val mutableBoard = board.toMutableList().map { it.toMutableList() }
// 初始化红方棋子
mutableBoard[0][0] = ChessPiece(ChessPieceType.CHE, ChessPieceColor.RED, Offset(0f, 0f))
mutableBoard[0][1] = ChessPiece(ChessPieceType.MA, ChessPieceColor.RED, Offset(1f, 0f))
mutableBoard[0][2] = ChessPiece(ChessPieceType.XIANG, ChessPieceColor.RED, Offset(2f, 0f))
mutableBoard[0][3] = ChessPiece(ChessPieceType.SHI, ChessPieceColor.RED, Offset(3f, 0f))
mutableBoard[0][4] = ChessPiece(ChessPieceType.JIANG, ChessPieceColor.RED, Offset(4f, 0f))
mutableBoard[0][5] = ChessPiece(ChessPieceType.SHI, ChessPieceColor.RED, Offset(5f, 0f))
mutableBoard[0][6] = ChessPiece(ChessPieceType.XIANG, ChessPieceColor.RED, Offset(6f, 0f))
mutableBoard[0][7] = ChessPiece(ChessPieceType.MA, ChessPieceColor.RED, Offset(7f, 0f))
mutableBoard[0][8] = ChessPiece(ChessPieceType.CHE, ChessPieceColor.RED, Offset(8f, 0f))
mutableBoard[2][1] = ChessPiece(ChessPieceType.PAO, ChessPieceColor.RED, Offset(1f, 2f))
mutableBoard[2][7] = ChessPiece(ChessPieceType.PAO, ChessPieceColor.RED, Offset(7f, 2f))
mutableBoard[3][0] = ChessPiece(ChessPieceType.BING, ChessPieceColor.RED, Offset(0f, 3f))
mutableBoard[3][2] = ChessPiece(ChessPieceType.BING, ChessPieceColor.RED, Offset(2f, 3f))
mutableBoard[3][4] = ChessPiece(ChessPieceType.BING, ChessPieceColor.RED, Offset(4f, 3f))
mutableBoard[3][6] = ChessPiece(ChessPieceType.BING, ChessPieceColor.RED, Offset(6f, 3f))
mutableBoard[3][8] = ChessPiece(ChessPieceType.BING, ChessPieceColor.RED, Offset(8f, 3f))
// 初始化黑方棋子
mutableBoard[9][0] = ChessPiece(ChessPieceType.CHE, ChessPieceColor.BLACK, Offset(0f, 9f))
mutableBoard[9][1] = ChessPiece(ChessPieceType.MA, ChessPieceColor.BLACK, Offset(1f, 9f))
mutableBoard[9][2] = ChessPiece(ChessPieceType.XIANG, ChessPieceColor.BLACK, Offset(2f, 9f))
mutableBoard[9][3] = ChessPiece(ChessPieceType.SHI, ChessPieceColor.BLACK, Offset(3f, 9f))
mutableBoard[9][4] = ChessPiece(ChessPieceType.JIANG, ChessPieceColor.BLACK, Offset(4f, 9f))
mutableBoard[9][5] = ChessPiece(ChessPieceType.SHI, ChessPieceColor.BLACK, Offset(5f, 9f))
mutableBoard[9][6] = ChessPiece(ChessPieceType.XIANG, ChessPieceColor.BLACK, Offset(6f, 9f))
mutableBoard[9][7] = ChessPiece(ChessPieceType.MA, ChessPieceColor.BLACK, Offset(7f, 9f))
mutableBoard[9][8] = ChessPiece(ChessPieceType.CHE, ChessPieceColor.BLACK, Offset(8f, 9f))
mutableBoard[7][1] = ChessPiece(ChessPieceType.PAO, ChessPieceColor.BLACK, Offset(1f, 7f))
mutableBoard[7][7] = ChessPiece(ChessPieceType.PAO, ChessPieceColor.BLACK, Offset(7f, 7f))
mutableBoard[6][0] = ChessPiece(ChessPieceType.BING, ChessPieceColor.BLACK, Offset(0f, 6f))
mutableBoard[6][2] = ChessPiece(ChessPieceType.BING, ChessPieceColor.BLACK, Offset(2f, 6f))
mutableBoard[6][4] = ChessPiece(ChessPieceType.BING, ChessPieceColor.BLACK, Offset(4f, 6f))
mutableBoard[6][6] = ChessPiece(ChessPieceType.BING, ChessPieceColor.BLACK, Offset(6f, 6f))
mutableBoard[6][8] = ChessPiece(ChessPieceType.BING, ChessPieceColor.BLACK, Offset(8f, 6f))
return mutableBoard.map { it.toList() }
}
// 初始化游戏状态
internal fun initializeGame(): XiangqiGameState {
return XiangqiGameState(
board = initializeBoard(),
currentPlayer = ChessPieceColor.RED,
selectedPiece = null,
gameOver = false,
winner = null
)
}
// 检查移动是否合法
internal fun isValidMove(
board: List<List<ChessPiece?>>,
from: Offset,
to: Offset,
playerColor: ChessPieceColor
): Boolean {
val piece = board[from.y.toInt()][from.x.toInt()] ?: return false
if (piece.color != playerColor) return false
if (to.x < 0 || to.x >= BOARD_WIDTH || to.y < 0 || to.y >= BOARD_HEIGHT) return false
val targetPiece = board[to.y.toInt()][to.x.toInt()]
if (targetPiece?.color == playerColor) return false // 不能吃自己的棋子
return isValidPieceMove(board, piece, from, to)
}
// 检查特定棋子的移动规则
private fun isValidPieceMove(
board: List<List<ChessPiece?>>,
piece: ChessPiece,
from: Offset,
to: Offset
): Boolean {
val dx = (to.x - from.x).toInt()
val dy = (to.y - from.y).toInt()
val absDx = kotlin.math.abs(dx)
val absDy = kotlin.math.abs(dy)
return when (piece.type) {
ChessPieceType.JIANG -> {
// 帅/将只能在九宫格内移动一格
absDx <= 1 && absDy <= 1 && isInPalace(to, piece.color)
}
ChessPieceType.SHI -> {
// 士只能斜着走一格,且在九宫格内
absDx == 1 && absDy == 1 && isInPalace(to, piece.color)
}
ChessPieceType.XIANG -> {
// 象走田字,不能过河,且不能蹩脚
absDx == 2 && absDy == 2 &&
!isBlockedByRiver(to, piece.color) &&
!isElephantLegBlocked(board, from, to)
}
ChessPieceType.MA -> {
// 马走日字,不能蹩马腿
(absDx == 2 && absDy == 1) || (absDx == 1 && absDy == 2) &&
!isKnightLegBlocked(board, from, to)
}
ChessPieceType.CHE -> {
// 车直线移动,路径不能有阻挡
(dx == 0 || dy == 0) && !isPathBlocked(board, from, to)
}
ChessPieceType.PAO -> {
// 炮直线移动,吃子时需要隔一个棋子
(dx == 0 || dy == 0) && isValidCannonMove(board, from, to)
}
ChessPieceType.BING -> {
// 兵/卒只能向前走一格,过河后可以横向移动
isValidSoldierMove(piece, from, to)
}
}
}
// 检查是否在九宫格内
private fun isInPalace(position: Offset, color: ChessPieceColor): Boolean {
val x = position.x.toInt()
val y = position.y.toInt()
return when (color) {
ChessPieceColor.RED -> x in 3..5 && y in 0..2
ChessPieceColor.BLACK -> x in 3..5 && y in 7..9
}
}
// 检查是否被河阻挡(象的限制)
private fun isBlockedByRiver(position: Offset, color: ChessPieceColor): Boolean {
val y = position.y.toInt()
return when (color) {
ChessPieceColor.RED -> y > 4
ChessPieceColor.BLACK -> y < 5
}
}
// 检查象腿是否被蹩住
private fun isElephantLegBlocked(board: List<List<ChessPiece?>>, from: Offset, to: Offset): Boolean {
val legX = (from.x + to.x) / 2
val legY = (from.y + to.y) / 2
return board[legY.toInt()][legX.toInt()] != null
}
// 检查马腿是否被蹩住
private fun isKnightLegBlocked(board: List<List<ChessPiece?>>, from: Offset, to: Offset): Boolean {
val dx = (to.x - from.x).toInt()
val dy = (to.y - from.y).toInt()
val legX = from.x + if (kotlin.math.abs(dx) == 2) dx/2 else 0
val legY = from.y + if (kotlin.math.abs(dy) == 2) dy/2 else 0
return board[legY.toInt()][legX.toInt()] != null
}
// 检查车的路径是否被阻挡
private fun isPathBlocked(board: List<List<ChessPiece?>>, from: Offset, to: Offset): Boolean {
val dx = (to.x - from.x).toInt()
val dy = (to.y - from.y).toInt()
val stepX = if (dx > 0) 1 else if (dx < 0) -1 else 0
val stepY = if (dy > 0) 1 else if (dy < 0) -1 else 0
var x = from.x.toInt() + stepX
var y = from.y.toInt() + stepY
while (x != to.x.toInt() || y != to.y.toInt()) {
if (board[y][x] != null) return true
x += stepX
y += stepY
}
return false
}
// 检查炮的移动是否合法
private fun isValidCannonMove(board: List<List<ChessPiece?>>, from: Offset, to: Offset): Boolean {
val pathBlocked = isPathBlocked(board, from, to)
val targetPiece = board[to.y.toInt()][to.x.toInt()]
// 如果目标位置有棋子,必须路上恰好有一个棋子才能吃掉
// 如果目标位置无棋子,路上不能有棋子才能移动
return if (targetPiece != null) {
pathBlocked && countPiecesBetween(board, from, to) == 1
} else {
!pathBlocked
}
}
// 计算两点之间有多少个棋子
private fun countPiecesBetween(board: List<List<ChessPiece?>>, from: Offset, to: Offset): Int {
var count = 0
val dx = (to.x - from.x).toInt()
val dy = (to.y - from.y).toInt()
val stepX = if (dx > 0) 1 else if (dx < 0) -1 else 0
val stepY = if (dy > 0) 1 else if (dy < 0) -1 else 0
var x = from.x.toInt() + stepX
var y = from.y.toInt() + stepY
while (x != to.x.toInt() || y != to.y.toInt()) {
if (board[y][x] != null) count++
x += stepX
y += stepY
}
return count
}
// 检查兵/卒的移动是否合法
private fun isValidSoldierMove(piece: ChessPiece, from: Offset, to: Offset): Boolean {
val dx = (to.x - from.x).toInt()
val dy = (to.y - from.y).toInt()
return when (piece.color) {
ChessPieceColor.RED -> {
if (from.y.toInt() < 5) {
// 未过河:只能向前走
dx == 0 && dy == 1
} else {
// 已过河:可以向前或横向走
(dx == 0 && dy == 1) || (dy == 0 && kotlin.math.abs(dx) == 1)
}
}
ChessPieceColor.BLACK -> {
if (from.y.toInt() > 4) {
// 未过河:只能向前走
dx == 0 && dy == -1
} else {
// 已过河:可以向前或横向走
(dx == 0 && dy == -1) || (dy == 0 && kotlin.math.abs(dx) == 1)
}
}
}
}
// 执行移动
internal fun makeMove(state: XiangqiGameState, from: Offset, to: Offset): XiangqiGameState {
if (!isValidMove(state.board, from, to, state.currentPlayer)) {
return state
}
val newBoard = state.board.toMutableList().map { it.toMutableList() }
val piece = newBoard[from.y.toInt()][from.x.toInt()]!!
val newPosition = piece.copy(position = to)
newBoard[from.y.toInt()][from.x.toInt()] = null
newBoard[to.y.toInt()][to.x.toInt()] = newPosition
val winner = checkWinner(newBoard)
val gameOver = winner != null
return state.copy(
board = newBoard.map { it.toList() },
currentPlayer = if (state.currentPlayer == ChessPieceColor.RED) ChessPieceColor.BLACK else ChessPieceColor.RED,
selectedPiece = null,
gameOver = gameOver,
winner = winner
)
}
// 检查是否有赢家
private fun checkWinner(board: List<List<ChessPiece?>>): ChessPieceColor? {
var redJiangExists = false
var blackJiangExists = false
for (row in board) {
for (piece in row) {
if (piece?.type == ChessPieceType.JIANG) {
when (piece.color) {
ChessPieceColor.RED -> redJiangExists = true
ChessPieceColor.BLACK -> blackJiangExists = true
}
}
}
}
return when {
!redJiangExists -> ChessPieceColor.BLACK
!blackJiangExists -> ChessPieceColor.RED
else -> null
}
}
// 选择棋子
internal fun selectPiece(state: XiangqiGameState, position: Offset): XiangqiGameState {
val piece = state.board[position.y.toInt()][position.x.toInt()]
// 如果点击的是己方棋子,选中它
if (piece?.color == state.currentPlayer) {
return state.copy(selectedPiece = position)
}
// 如果已有选中的棋子,尝试移动
if (state.selectedPiece != null) {
return makeMove(state, state.selectedPiece, position)
}
return state
}
// 取消选择
internal fun deselectPiece(state: XiangqiGameState): XiangqiGameState {
return state.copy(selectedPiece = null)
}
// 重新开始游戏
internal fun restartGame(): XiangqiGameState {
return initializeGame()
}
}
\ No newline at end of file
package com.dong.maxhap.demos.game
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
internal fun XiangqiScreen(onBack: () -> Unit) {
val logic = remember { XiangqiLogic() }
var gameState by remember { mutableStateOf(logic.initializeGame()) }
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
.background(Color(0xFFDEB887)),
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "🐘🐎 象棋",
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFF8B4513)
)
Text(
text = when {
gameState.gameOver -> "游戏结束"
gameState.currentPlayer == ChessPieceColor.RED -> "红方行棋"
else -> "黑方行棋"
},
fontSize = 18.sp,
fontWeight = FontWeight.Medium,
color = when {
gameState.gameOver -> Color.Red
gameState.currentPlayer == ChessPieceColor.RED -> Color.Red
else -> Color.Black
}
)
}
Spacer(modifier = Modifier.height(16.dp))
Box(
modifier = Modifier
.size(360.dp)
.background(Color(0xFFDEB887))
.clickable {
gameState = logic.deselectPiece(gameState)
}
) {
ChessBoard(
gameState = gameState,
onSquareClick = { position ->
gameState = logic.selectPiece(gameState, position)
}
)
}
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Button(
onClick = {
gameState = logic.restartGame()
},
colors = ButtonDefaults.buttonColors(backgroundColor = Color(0xFF8B4513)),
modifier = Modifier.weight(1f)
) {
Text("重新开始", color = Color.White)
}
Spacer(modifier = Modifier.width(16.dp))
Button(
onClick = onBack,
colors = ButtonDefaults.buttonColors(backgroundColor = Color.Gray),
modifier = Modifier.weight(1f)
) {
Text("返回", color = Color.White)
}
}
if (gameState.gameOver) {
Spacer(modifier = Modifier.height(16.dp))
Text(
text = when (gameState.winner) {
ChessPieceColor.RED -> "红方获胜! 🎉"
ChessPieceColor.BLACK -> "黑方获胜! 🎉"
null -> "游戏结束"
},
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = Color.Red,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "操作说明:\n• 点击己方棋子选中,再点击目标位置移动\n• 红方先行,轮流走棋\n• 吃掉对方将/帅即获胜\n• 遵循中国象棋规则",
fontSize = 12.sp,
color = Color.Gray,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}
}
@Composable
private fun ChessBoard(
gameState: XiangqiGameState,
onSquareClick: (Offset) -> Unit
) {
val squareSize = 360.dp / XiangqiLogic.BOARD_WIDTH
Canvas(modifier = Modifier.fillMaxSize()) {
val canvasWidth = size.width
val canvasHeight = size.height
val cellWidth = canvasWidth / XiangqiLogic.BOARD_WIDTH
val cellHeight = canvasHeight / XiangqiLogic.BOARD_HEIGHT
drawChessBoardLines(cellWidth, cellHeight)
drawPalaceLines(cellWidth, cellHeight)
gameState.board.forEachIndexed { row, pieces ->
pieces.forEachIndexed { col, piece ->
if (piece != null) {
drawChessPiece(
piece = piece,
x = col * cellWidth + cellWidth / 2,
y = row * cellHeight + cellHeight / 2,
radius = cellWidth * 0.4f
)
}
}
}
gameState.selectedPiece?.let { pos ->
drawSelectedIndicator(
x = pos.x * cellWidth + cellWidth / 2,
y = pos.y * cellHeight + cellHeight / 2,
radius = cellWidth * 0.45f
)
}
}
Box(modifier = Modifier.fillMaxSize()) {
for (row in 0 until XiangqiLogic.BOARD_HEIGHT) {
for (col in 0 until XiangqiLogic.BOARD_WIDTH) {
Box(
modifier = Modifier
.size(squareSize)
.offset(
x = (col * squareSize.value).dp,
y = (row * squareSize.value).dp
)
.clickable {
onSquareClick(Offset(col.toFloat(), row.toFloat()))
}
)
}
}
}
}
private fun androidx.compose.ui.graphics.drawscope.DrawScope.drawChessBoardLines(
cellWidth: Float,
cellHeight: Float
) {
for (i in 0..XiangqiLogic.BOARD_WIDTH) {
val x = i * cellWidth
drawLine(
color = Color.Black,
start = Offset(x, 0f),
end = Offset(x, size.height),
strokeWidth = 2f
)
}
for (i in 0..XiangqiLogic.BOARD_HEIGHT) {
val y = i * cellHeight
drawLine(
color = Color.Black,
start = Offset(0f, y),
end = Offset(size.width, y),
strokeWidth = 2f
)
}
}
private fun androidx.compose.ui.graphics.drawscope.DrawScope.drawPalaceLines(
cellWidth: Float,
cellHeight: Float
) {
drawLine(
color = Color.Black,
start = Offset(3 * cellWidth, 0 * cellHeight),
end = Offset(5 * cellWidth, 2 * cellHeight),
strokeWidth = 2f
)
drawLine(
color = Color.Black,
start = Offset(5 * cellWidth, 0 * cellHeight),
end = Offset(3 * cellWidth, 2 * cellHeight),
strokeWidth = 2f
)
drawLine(
color = Color.Black,
start = Offset(3 * cellWidth, 7 * cellHeight),
end = Offset(5 * cellWidth, 9 * cellHeight),
strokeWidth = 2f
)
drawLine(
color = Color.Black,
start = Offset(5 * cellWidth, 7 * cellHeight),
end = Offset(3 * cellWidth, 9 * cellHeight),
strokeWidth = 2f
)
}
private fun androidx.compose.ui.graphics.drawscope.DrawScope.drawChessPiece(
piece: ChessPiece,
x: Float,
y: Float,
radius: Float
) {
drawCircle(
color = when (piece.color) {
ChessPieceColor.RED -> Color.Red
ChessPieceColor.BLACK -> Color.Black
},
radius = radius,
center = Offset(x, y)
)
drawCircle(
color = Color.White,
radius = radius - 2f,
center = Offset(x, y),
style = androidx.compose.ui.graphics.drawscope.Stroke(width = 2f)
)
}
private fun androidx.compose.ui.graphics.drawscope.DrawScope.drawSelectedIndicator(
x: Float,
y: Float,
radius: Float
) {
drawCircle(
color = Color.Blue,
radius = radius,
center = Offset(x, y),
style = androidx.compose.ui.graphics.drawscope.Stroke(width = 3f)
)
}
\ No newline at end of file
package com.dong.maxhap.demos.material
import androidx.compose.material.AlertDialog
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.*
@Composable
internal fun AlertDialogSamples() {
var open by remember { mutableStateOf(true) }
var dismissOnClickOutside by remember { mutableStateOf(true) }
var dismissOnBackPress by remember { mutableStateOf(true) }
if (open) {
AlertDialog(
onDismissRequest = {
if (dismissOnBackPress) open = false
},
title = { Text("AlertDialog") },
text = { Text("展示标题、内容、确认/取消按钮,以及关闭行为。") },
confirmButton = {
TextButton(onClick = { open = false }) { Text("确认") }
},
dismissButton = {
TextButton(onClick = { if (dismissOnClickOutside) open = false }) { Text("取消") }
}
)
}
}
package com.dong.maxhap.demos.material
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
internal fun AppBarSamples() {
var title by remember { mutableStateOf("TopAppBar 标题") }
var showActions by remember { mutableStateOf(true) }
var elevation by remember { mutableStateOf(4f) }
Column {
TopAppBar(
title = { Text(title) },
actions = {
if (showActions) {
TextButton(onClick = { title = "点击了 Action" }) { Text("Action") }
}
},
elevation = elevation.dp
)
Spacer(Modifier.height(8.dp))
BottomAppBar {
Text("BottomAppBar")
}
Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(value = title, onValueChange = { title = it }, label = { Text("标题") })
Text("阴影: ${elevation.toInt()}dp")
Slider(value = elevation, onValueChange = { elevation = it }, valueRange = 0f..16f)
Row {
Checkbox(checked = showActions, onCheckedChange = { showActions = it })
Text("显示 Action")
}
}
}
}
package com.dong.maxhap.demos.material
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class)
@Composable
internal fun BackdropScaffoldSamples() {
val scaffoldState = rememberBackdropScaffoldState(BackdropValue.Concealed)
val scope = rememberCoroutineScope()
var clicks by remember { mutableStateOf(0) }
BackdropScaffold(
scaffoldState = scaffoldState,
appBar = { TopAppBar(title = { Text("BackdropScaffold") }) },
backLayerContent = {
Column(Modifier.fillMaxWidth().padding(12.dp)) {
Text("后层内容")
Button(onClick = { clicks++ }) { Text("点击: $clicks") }
}
},
frontLayerContent = {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Button(
onClick = {
scope.launch {
if (scaffoldState.isConcealed) scaffoldState.reveal() else scaffoldState.conceal()
}
}
) { Text(if (scaffoldState.isConcealed) "展开前层" else "收起前层") }
}
}
)
}
package com.dong.maxhap.demos.material
import androidx.compose.ui.Modifier
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.*
import androidx.compose.ui.unit.dp
@Composable
internal fun BadgeSamples() {
var count by remember { mutableStateOf(3) }
BadgedBox(badge = { Text("$count") }) {
Text("带徽章的内容")
}
Row(Modifier.padding(12.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(onClick = { count++ }) { Text("+1") }
Button(onClick = { count = 0 }) { Text("清零") }
}
}
package com.dong.maxhap.demos.material
import androidx.compose.material.*
import androidx.compose.runtime.*
@Composable
internal fun BottomNavigationSamples() {
var selected by remember { mutableStateOf(0) }
BottomNavigation {
listOf("主页", "消息", "设置").forEachIndexed { index, label ->
BottomNavigationItem(
selected = selected == index,
onClick = { selected = index },
icon = { },
label = { Text(label) }
)
}
}
}
package com.dong.maxhap.demos.material
import androidx.compose.ui.Modifier
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.*
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class)
@Composable
internal fun BottomSheetScaffoldSamples() {
val sheetState = rememberBottomSheetScaffoldState()
val scope = rememberCoroutineScope()
BottomSheetScaffold(
scaffoldState = sheetState,
sheetContent = {
Column(Modifier.padding(12.dp)) {
Text("BottomSheet 内容")
Text("可在此添加交互与属性")
}
},
sheetPeekHeight = 64.dp
) { padding ->
Column(Modifier.padding(padding).padding(12.dp)) {
Button(onClick = {
scope.launch {
val state = sheetState.bottomSheetState
if (state.isCollapsed) state.expand() else state.collapse()
}
}) { Text("展开/收起 BottomSheet") }
}
}
}
package com.dong.maxhap.demos.material
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color
@Composable
internal fun ButtonSamples() {
var enabled by remember { mutableStateOf(true) }
var clicks by remember { mutableStateOf(0) }
Button(
enabled = enabled,
onClick = { clicks++ },
colors = ButtonDefaults.buttonColors(backgroundColor = Color(0xFF4CAF50), contentColor = Color.White)
) {
Text("Material Button 点击: $clicks")
}
}
package com.dong.maxhap.demos.material
import androidx.compose.material.Card
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
@Composable
internal fun CardSamples() {
var elevation by remember { mutableStateOf(4f) }
Card(elevation = elevation.dp) {
Column(Modifier.padding(12.dp)) {
Text("Material Card 标题")
Text("阴影 elevation: ${elevation.toInt()}dp")
}
}
}
package com.dong.maxhap.demos.material
import androidx.compose.ui.Modifier
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.*
import androidx.compose.ui.unit.dp
@Composable
internal fun ContentAlphaSamples() {
var disabled by remember { mutableStateOf(false) }
Column(Modifier.padding(12.dp)) {
Row {
Checkbox(checked = disabled, onCheckedChange = { disabled = it })
Text("使用 ContentAlpha.disabled")
}
CompositionLocalProvider(LocalContentAlpha provides if (disabled) ContentAlpha.disabled else ContentAlpha.high) {
Text("受 ContentAlpha 控制的文本")
}
Text("不受影响的文本")
}
}
package com.dong.maxhap.demos.material
import androidx.compose.ui.Modifier
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.*
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
@Composable
internal fun DrawerSamples() {
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope()
ModalDrawer(
drawerState = drawerState,
drawerContent = {
Column(Modifier.padding(12.dp)) {
Text("Drawer 内容")
Button(onClick = { scope.launch { drawerState.close() } }) { Text("关闭") }
}
}
) {
Column(Modifier.padding(12.dp)) {
Button(onClick = { scope.launch { drawerState.open() } }) { Text("打开 Drawer") }
}
}
}
package com.dong.maxhap.demos.material
import androidx.compose.ui.Modifier
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.Slider
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.*
import androidx.compose.ui.unit.dp
@Composable
internal fun ElevationSamples() {
var elevation by remember { mutableStateOf(4f) }
Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("阴影: ${elevation.toInt()}dp")
Slider(value = elevation, onValueChange = { elevation = it }, valueRange = 0f..24f)
Surface(elevation = elevation.dp) {
Box(Modifier.size(120.dp).padding(12.dp)) { Text("Surface with elevation") }
}
}
}
package com.dong.maxhap.demos.material
import androidx.compose.ui.Modifier
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.*
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterialApi::class)
@Composable
internal fun ExposedDropdownMenuSamples() {
val options = listOf("选项 A", "选项 B", "选项 C")
var expanded by remember { mutableStateOf(false) }
var selected by remember { mutableStateOf(options.first()) }
ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { expanded = !expanded }) {
OutlinedTextField(
value = selected, onValueChange = {}, readOnly = true, label = { Text("选择项") }
)
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
options.forEach { option ->
DropdownMenuItem(onClick = { selected = option; expanded = false }) {
Text(option)
}
}
}
}
Spacer(Modifier.height(8.dp))
Text("当前选择: $selected")
}
package com.dong.maxhap.demos.material
import androidx.compose.material.FloatingActionButton
import androidx.compose.material.Text
import androidx.compose.runtime.*
@Composable
internal fun FloatingActionButtonSamples() {
var clicks by remember { mutableStateOf(0) }
FloatingActionButton(onClick = { clicks++ }) { Text("$clicks") }
}
package com.dong.maxhap.demos.material
import androidx.compose.material.IconButton
import androidx.compose.material.Text
import androidx.compose.runtime.*
@Composable
internal fun IconButtonSamples() {
var clicks by remember { mutableStateOf(0) }
IconButton(onClick = { clicks++ }) {
Text("★")
}
Text("点击次数: $clicks")
}
@file:OptIn(androidx.compose.material.ExperimentalMaterialApi::class)
package com.dong.maxhap.demos.material
import androidx.compose.material.ListItem
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.foundation.lazy.*
import androidx.compose.foundation.layout.*
import androidx.compose.ui.unit.dp
@Composable
internal fun ListSamples() {
val items = (1..20).map { "Item $it" }
LazyColumn(contentPadding = PaddingValues(8.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
items(items) { label ->
ListItem(text = { Text(label) }, secondaryText = { Text("副标题") })
}
}
}
package com.dong.maxhap.demos.material
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.*
@Composable
internal fun MenuSamples() {
var expanded by remember { mutableStateOf(false) }
Column {
Button(onClick = { expanded = true }) { Text("打开菜单") }
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
DropdownMenuItem(onClick = { expanded = false }) { Text("菜单项 1") }
DropdownMenuItem(onClick = { expanded = false }) { Text("菜单项 2") }
}
}
}
package com.dong.maxhap.demos.material
import androidx.compose.ui.Modifier
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.*
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class)
@Composable
internal fun ModalBottomSheetSamples() {
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
val scope = rememberCoroutineScope()
ModalBottomSheetLayout(
sheetState = sheetState,
sheetContent = { Column(Modifier.padding(12.dp)) { Text("ModalBottomSheet 内容") } }
) {
Column(Modifier.padding(12.dp)) {
Button(onClick = { scope.launch { sheetState.show() } }) { Text("显示") }
Spacer(Modifier.height(8.dp))
Button(onClick = { scope.launch { sheetState.hide() } }) { Text("隐藏") }
}
}
}
package com.dong.maxhap.demos.material
import androidx.compose.material.NavigationRail
import androidx.compose.material.NavigationRailItem
import androidx.compose.material.Text
import androidx.compose.runtime.*
@Composable
internal fun NavigationRailSample() {
var selected by remember { mutableStateOf(0) }
NavigationRail {
listOf("主页", "消息", "设置").forEachIndexed { index, label ->
NavigationRailItem(
selected = selected == index,
onClick = { selected = index },
icon = { Text("★") },
label = { Text(label) }
)
}
}
}
package com.dong.maxhap.demos.material
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
internal fun ProgressIndicatorSamples() {
var progress by remember { mutableStateOf(0.3f) }
Column(Modifier.fillMaxWidth().padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
LinearProgressIndicator(progress = progress, modifier = Modifier.fillMaxWidth())
CircularProgressIndicator(progress = progress)
}
}
package com.dong.maxhap.demos.material
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@Composable
internal fun PullRefreshSamples() {
var refreshing by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Button(onClick = {
scope.launch {
refreshing = true
delay(1000)
refreshing = false
}
}) { Text("手动触发刷新") }
if (refreshing) LinearProgressIndicator(Modifier.fillMaxWidth())
Text(if (refreshing) "刷新中..." else "刷新完成")
}
}
package com.dong.maxhap.demos.material
import androidx.compose.ui.Modifier
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.dp
@Composable
internal fun SelectionControlsSamples() {
var checked by remember { mutableStateOf(true) }
var selected by remember { mutableStateOf(0) }
var switchOn by remember { mutableStateOf(true) }
Column(Modifier.padding(12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = checked, onCheckedChange = { checked = it })
Text("Checkbox: ${if (checked) "选中" else "未选"}")
}
Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) {
RadioButton(selected = selected == 0, onClick = { selected = 0 })
RadioButton(selected = selected == 1, onClick = { selected = 1 })
Text("Radio 选择: $selected")
}
Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) {
Switch(checked = switchOn, onCheckedChange = { switchOn = it })
Text("Switch: ${if (switchOn) "" else ""}")
}
}
}
package com.dong.maxhap.demos.material
import androidx.compose.ui.Modifier
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.layout.*
@Composable
internal fun SliderSample() {
var value by remember { mutableStateOf(0.5f) }
Column(Modifier.padding(12.dp)) {
Text("当前值: ${kotlin.math.round(value * 10.0f) / 10.0f}")
Slider(value = value, onValueChange = { value = it }, valueRange = 0f..1f, steps = 4)
}
}
package com.dong.maxhap.demos.material
import androidx.compose.ui.Modifier
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Composable
internal fun SurfaceSamples() {
var elevation by remember { mutableStateOf(6f) }
var rounded by remember { mutableStateOf(12f) }
var bg by remember { mutableStateOf(Color(0xFFE0F7FA)) }
Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Surface(shape = RoundedCornerShape(rounded.dp), elevation = elevation.dp, color = bg) {
Column(Modifier.padding(12.dp)) {
Text("Surface 属性演示")
Text("elevation=${elevation.toInt()} shape=${rounded.toInt()} color=${bg}")
}
}
Text("elevation: ${elevation.toInt()}dp"); Slider(value = elevation, onValueChange = { elevation = it }, valueRange = 0f..24f)
Text("圆角: ${rounded.toInt()}dp"); Slider(value = rounded, onValueChange = { rounded = it }, valueRange = 0f..32f)
}
}
package com.dong.maxhap.demos.material
import androidx.compose.ui.Modifier
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.*
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterialApi::class)
@Composable
internal fun SwipeToDismissSamples() {
val dismissState = rememberDismissState()
SwipeToDismiss(
state = dismissState,
background = {
Box(Modifier.fillMaxWidth().height(64.dp)) { Text("滑动删除背景") }
},
dismissContent = {
Box(Modifier.fillMaxWidth().height(64.dp)) { Text("可滑动删除内容") }
}
)
Text("当前状态: ${dismissState.currentValue}")
}
@file:OptIn(androidx.compose.material.ExperimentalMaterialApi::class)
package com.dong.maxhap.demos.material
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.DismissDirection
import androidx.compose.material.DismissValue
import androidx.compose.material.Divider
import androidx.compose.material.ListItem
import androidx.compose.material.SwipeToDismiss
import androidx.compose.material.Text
import androidx.compose.material.rememberDismissState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@Composable
internal fun SwipeableSamples() {
val mails = remember { mutableStateListOf("Mail 1", "Mail 2", "Mail 3") }
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(vertical = 8.dp)
) {
items(mails, key = { mail -> mail }) { mail ->
val dismissState = rememberDismissState(
confirmStateChange = { newValue: DismissValue ->
if (newValue == DismissValue.DismissedToEnd || newValue == DismissValue.DismissedToStart) {
mails.remove(mail)
}
true
}
)
SwipeToDismiss(
state = dismissState,
directions = setOf(
DismissDirection.StartToEnd,
DismissDirection.EndToStart
),
background = {
val bgColor = when (dismissState.targetValue) {
DismissValue.Default -> Color.Transparent
DismissValue.DismissedToEnd -> Color(0xFF2E7D32) // Archive
DismissValue.DismissedToStart -> Color(0xFFC62828) // Delete
}
Box(
modifier = Modifier
.fillMaxWidth()
.height(64.dp)
.background(bgColor),
contentAlignment = Alignment.CenterStart
) {
Text(
text = if (dismissState.targetValue == DismissValue.DismissedToEnd) "Archive" else "Delete",
color = Color.White,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(start = 16.dp)
)
}
},
dismissContent = {
ListItem(
text = { Text(mail) },
secondaryText = { Text("Swipe me left or right") }
)
}
)
Divider()
}
}
}
package com.dong.maxhap.demos.material
import androidx.compose.material.Tab
import androidx.compose.material.TabRow
import androidx.compose.material.Text
import androidx.compose.runtime.*
@Composable
internal fun TabSamples() {
var selectedTab by remember { mutableStateOf(0) }
val tabs = listOf("Tab1", "Tab2", "Tab3")
TabRow(selectedTabIndex = selectedTab) {
tabs.forEachIndexed { index, title ->
Tab(selected = selectedTab == index, onClick = { selectedTab = index }, text = { Text(title) })
}
}
}
package com.dong.maxhap.demos.material
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.runtime.*
@Composable
internal fun TextFieldSamples() {
var text by remember { mutableStateOf("") }
var enabled by remember { mutableStateOf(true) }
var isError by remember { mutableStateOf(false) }
OutlinedTextField(
value = text,
onValueChange = { text = it },
enabled = enabled,
isError = isError,
label = { Text("Label") },
placeholder = { Text("Placeholder") }
)
}
package com.dong.maxhap.demos.material
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
internal fun TextSamples() {
var size by remember { mutableStateOf(16f) }
var bold by remember { mutableStateOf(false) }
Column(Modifier.padding(12.dp)) {
Text("普通文本", fontSize = size.sp, fontWeight = if (bold) FontWeight.Bold else FontWeight.Normal)
Text("副标题", style = MaterialTheme.typography.subtitle1)
Text("标题", style = MaterialTheme.typography.h6)
Text("正文", style = MaterialTheme.typography.body1)
Text("按钮", style = MaterialTheme.typography.button)
Text("说明文字", style = MaterialTheme.typography.caption)
Spacer(Modifier.height(8.dp))
Text("字体大小: ${size.toInt()}sp"); Slider(value = size, onValueChange = { size = it }, valueRange = 12f..36f)
Row { Checkbox(checked = bold, onCheckedChange = { bold = it }); Text("加粗") }
}
}
package com.dong.maxhap.demos.material
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
@Composable
internal fun ThemeSamples() {
// 演示 MaterialTheme 的基本用法;完整主题切换后续扩展
Text("当前 MaterialTheme.typography.body1 示例文本")
}
package com.dong.maxhap.demos.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalLayoutApi::class)
@Composable
internal fun UiBoxDemo() {
val alignments = listOf(
Alignment.TopStart, Alignment.TopCenter, Alignment.TopEnd,
Alignment.CenterStart, Alignment.Center, Alignment.CenterEnd,
Alignment.BottomStart, Alignment.BottomCenter, Alignment.BottomEnd
)
var selected by remember { mutableStateOf(Alignment.Center) }
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Alignment")
FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
alignments.forEach { a ->
val label = a.toString().removePrefix("Alignment.")
if (selected == a) {
Button(onClick = { selected = a }) {
Text(label)
}
} else {
OutlinedButton(onClick = { selected = a }) {
Text(label)
}
}
}
}
Box(
modifier = Modifier
.size(220.dp)
.background(MaterialTheme.colors.surface) // surfaceVariant -> surface
) {
Box(
modifier = Modifier
.size(48.dp)
.align(selected)
.background(MaterialTheme.colors.primary) // primary -> primary
)
}
}
}
package com.dong.maxhap.navigation
enum class ComposeGroup {
Ui, Foundation, Material, Benchmark, Platform
}
data class ComponentDemo(
val id: String,
val title: String,
val group: ComposeGroup
)
val componentDemos: List<ComponentDemo> = listOf(
// ui
ComponentDemo("ui_hap", "测试大批量页面", ComposeGroup.Ui),
ComponentDemo("ui_box", "Box", ComposeGroup.Ui),
// foundation
ComponentDemo("foundation_basic_text", "BasicText", ComposeGroup.Foundation),
ComponentDemo("foundation_lazy_column", "LazyColumn", ComposeGroup.Foundation),
// material 组:新增完整清单
ComponentDemo("material_alert_dialog", "AlertDialogSample", ComposeGroup.Material),
ComponentDemo("material_app_bar", "AppBarSamples", ComposeGroup.Material),
ComponentDemo("material_backdrop_scaffold", "BackdropScaffoldSamples", ComposeGroup.Material),
ComponentDemo("material_badge", "BadgeSamples", ComposeGroup.Material),
ComponentDemo("material_bottom_navigation", "BottomNavigationSamples", ComposeGroup.Material),
ComponentDemo("material_bottom_sheet_scaffold", "BottomSheetScaffoldSamples", ComposeGroup.Material),
ComponentDemo("material_button_samples", "ButtonSamples", ComposeGroup.Material),
ComponentDemo("material_card_samples", "CardSamples", ComposeGroup.Material),
ComponentDemo("material_chip_samples", "ChipSamples", ComposeGroup.Material),
ComponentDemo("material_content_alpha", "ContentAlphaSamples", ComposeGroup.Material),
ComponentDemo("material_drawer_samples", "DrawerSamples", ComposeGroup.Material),
ComponentDemo("material_elevation_samples", "ElevationSamples", ComposeGroup.Material),
ComponentDemo("material_exposed_dropdown_menu", "ExposedDropdownMenuSamples", ComposeGroup.Material),
ComponentDemo("material_fab_samples", "FloatingActionButtonSamples", ComposeGroup.Material),
ComponentDemo("material_icon_button_samples", "IconButtonSamples", ComposeGroup.Material),
ComponentDemo("material_list_samples", "ListSamples", ComposeGroup.Material),
ComponentDemo("material_menu_samples", "MenuSamples", ComposeGroup.Material),
ComponentDemo("material_modal_bottom_sheet", "ModalBottomSheetSamples", ComposeGroup.Material),
ComponentDemo("material_navigation_rail", "NavigationRailSample", ComposeGroup.Material),
ComponentDemo("material_progress_indicator", "ProgressIndicatorSamples", ComposeGroup.Material),
ComponentDemo("material_pull_refresh", "PullRefreshSamples", ComposeGroup.Material),
ComponentDemo("material_scaffold_samples", "ScaffoldSamples", ComposeGroup.Material),
ComponentDemo("material_selection_controls", "SelectionControlsSamples", ComposeGroup.Material),
ComponentDemo("material_slider_sample", "SliderSample", ComposeGroup.Material),
ComponentDemo("material_surface_samples", "SurfaceSamples", ComposeGroup.Material),
ComponentDemo("material_swipeable_samples", "SwipeableSamples", ComposeGroup.Material),
ComponentDemo("material_swipe_to_dismiss", "SwipeToDismissSamples", ComposeGroup.Material),
ComponentDemo("material_tab_samples", "TabSamples", ComposeGroup.Material),
ComponentDemo("material_text_field_samples", "TextFieldSamples", ComposeGroup.Material),
ComponentDemo("material_text_samples", "TextSamples", ComposeGroup.Material),
ComponentDemo("material_theme_samples", "ThemeSamples", ComposeGroup.Material),
// benchmark
ComponentDemo("benchmark_1500_text", "1500 Text", ComposeGroup.Benchmark),
ComponentDemo("benchmark_1500_view", "1500 View", ComposeGroup.Benchmark),
)
val groupTitles: Map<ComposeGroup, String> = mapOf(
ComposeGroup.Ui to "compose.ui",
ComposeGroup.Foundation to "compose.foundation",
ComposeGroup.Material to "compose.material",
ComposeGroup.Platform to "Platform"
)
fun demosByGroup(): Map<ComposeGroup, List<ComponentDemo>> {
val standardDemos = ComposeGroup.entries.filter { it != ComposeGroup.Platform }
.associateWith { g -> componentDemos.filter { it.group == g } }
.toMutableMap()
val platformDemos = com.dong.maxhap.getPlatformDemoList().map {
ComponentDemo(
id = it.id,
title = it.title,
group = ComposeGroup.Platform
)
}
if (platformDemos.isNotEmpty()) {
standardDemos[ComposeGroup.Platform] = platformDemos
}
return standardDemos
}
fun demosJson(): String = buildString {
append('[')
componentDemos.forEachIndexed { index, demo ->
if (index > 0) append(',')
append("{\"id\":\"")
append(jsonEscape(demo.id))
append("\",\"title\":\"")
append(jsonEscape(demo.title))
append("\",\"group\":\"")
append(demo.group.name)
append("\"}")
}
append(']')
}
fun groupTitlesJson(): String = buildString {
append('{')
ComposeGroup.entries.forEachIndexed { index, group ->
if (index > 0) append(',')
append('"')
append(group.name)
append("\":\"")
append(jsonEscape(groupTitles[group] ?: group.name))
append('"')
}
append('}')
}
private fun jsonEscape(input: String): String = buildString(input.length) {
input.forEach { ch ->
when (ch) {
'\\' -> append("\\\\")
'"' -> append("\\\"")
'\n' -> append("\\n")
'\r' -> append("\\r")
'\t' -> append("\\t")
else -> append(ch)
}
}
}
sealed interface Page {
data object Home : Page
data class Detail(val demo: ComponentDemo) : Page
}
package com.dong.maxhap.demos.test
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
internal fun NestedPage1() {
var showNextPage by remember { mutableStateOf(false) }
if (showNextPage) {
NestedPage2()
return
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "第1层页面",
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFF10000),
modifier = Modifier.padding(bottom = 20.dp)
)
Text(
text = "这是第1行内容",
fontSize = 16.sp,
color = Color(17, 31, 47),
modifier = Modifier.padding(vertical = 4.dp)
)
Text(
text = "这是第2行内容",
fontSize = 16.sp,
color = Color(34, 62, 94),
modifier = Modifier.padding(vertical = 4.dp)
)
Text(
text = "这是第3行内容",
fontSize = 16.sp,
color = Color(51, 93, 141),
modifier = Modifier.padding(vertical = 4.dp)
)
Text(
text = "这是第4行内容",
fontSize = 16.sp,
color = Color(68, 124, 188),
modifier = Modifier.padding(vertical = 4.dp)
)
Text(
text = "这是第5行内容",
fontSize = 16.sp,
color = Color(85, 155, 235),
modifier = Modifier.padding(vertical = 4.dp)
)
Text(
text = "这是第6行内容",
fontSize = 16.sp,
color = Color(102, 186, 26),
modifier = Modifier.padding(vertical = 4.dp)
)
Text(
text = "这是第7行内容",
fontSize = 16.sp,
color = Color(119, 217, 73),
modifier = Modifier.padding(vertical = 4.dp)
)
Text(
text = "这是第8行内容",
fontSize = 16.sp,
color = Color(136, 248, 120),
modifier = Modifier.padding(vertical = 4.dp)
)
Text(
text = "这是第9行内容",
fontSize = 16.sp,
color = Color(153, 23, 167),
modifier = Modifier.padding(vertical = 4.dp)
)
Text(
text = "这是第10行内容",
fontSize = 16.sp,
color = Color(170, 54, 214),
modifier = Modifier.padding(vertical = 4.dp)
)
Text(
text = "这是第11行内容",
fontSize = 16.sp,
color = Color(187, 85, 5),
modifier = Modifier.padding(vertical = 4.dp)
)
Text(
text = "这是第12行内容",
fontSize = 16.sp,
color = Color(204, 116, 52),
modifier = Modifier.padding(vertical = 4.dp)
)
Text(
text = "这是第13行内容",
fontSize = 16.sp,
color = Color(221, 147, 99),
modifier = Modifier.padding(vertical = 4.dp)
)
Text(
text = "这是第14行内容",
fontSize = 16.sp,
color = Color(238, 178, 146),
modifier = Modifier.padding(vertical = 4.dp)
)
Text(
text = "这是第15行内容",
fontSize = 16.sp,
color = Color(255, 209, 193),
modifier = Modifier.padding(vertical = 4.dp)
)
Spacer(modifier = Modifier.height(30.dp))
Button(
onClick = { showNextPage = true },
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
colors = ButtonDefaults.buttonColors(
backgroundColor = Color(0xFF2196F3)
)
) {
Text(
text = "进入第2层",
fontSize = 18.sp,
fontWeight = FontWeight.Medium,
color = Color.White
)
}
}
}
package com.dong.maxhap.demos.test
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
internal fun NestedPage10() {
var showNextPage by remember { mutableStateOf(false) }
if (showNextPage) {
NestedPage11()
return
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "第10层页面",
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFF100000),
modifier = Modifier.padding(bottom = 20.dp)
)
Text(
text = "这是第136行内容",
fontSize = 16.sp,
color = Color(8, 120, 248),
modifier = Modifier.padding(vertical = 4.dp)
)
Text(
text = "这是第137行内容",
fontSize = 16.sp,
color = Color(25, 151, 39),
modifier = Modifier.padding(vertical = 4.dp)
)
Text(
text = "这是第138行内容",
fontSize = 16.sp,
color = Color(42, 182, 86),
modifier = Modifier.padding(vertical = 4.dp)
)
Text(
text = "这是第139行内容",
fontSize = 16.sp,
color = Color(59, 213, 133),
modifier = Modifier.padding(vertical = 4.dp)
)
Text(
text = "这是第140行内容",
fontSize = 16.sp,
color = Color(76, 244, 180),
modifier = Modifier.padding(vertical = 4.dp)
)
Text(
text = "这是第141行内容",
fontSize = 16.sp,
color = Color(93, 19, 227),
modifier = Modifier.padding(vertical = 4.dp)
)
Text(
text = "这是第142行内容",
fontSize = 16.sp,
color = Color(110, 50, 18),
modifier = Modifier.padding(vertical = 4.dp)
)
Text(
text = "这是第143行内容",
fontSize = 16.sp,
color = Color(127, 81, 65),
modifier = Modifier.padding(vertical = 4.dp)
)
Text(
text = "这是第144行内容",
fontSize = 16.sp,
color = Color(144, 112, 112),
modifier = Modifier.padding(vertical = 4.dp)
)
Text(
text = "这是第145行内容",
fontSize = 16.sp,
color = Color(161, 143, 159),
modifier = Modifier.padding(vertical = 4.dp)
)
Text(
text = "这是第146行内容",
fontSize = 16.sp,
color = Color(178, 174, 206),
modifier = Modifier.padding(vertical = 4.dp)
)
Text(
text = "这是第147行内容",
fontSize = 16.sp,
color = Color(195, 205, 253),
modifier = Modifier.padding(vertical = 4.dp)
)
Text(
text = "这是第148行内容",
fontSize = 16.sp,
color = Color(212, 236, 44),
modifier = Modifier.padding(vertical = 4.dp)
)
Text(
text = "这是第149行内容",
fontSize = 16.sp,
color = Color(229, 11, 91),
modifier = Modifier.padding(vertical = 4.dp)
)
Text(
text = "这是第150行内容",
fontSize = 16.sp,
color = Color(246, 42, 138),
modifier = Modifier.padding(vertical = 4.dp)
)
Spacer(modifier = Modifier.height(30.dp))
Button(
onClick = { showNextPage = true },
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
colors = ButtonDefaults.buttonColors(
backgroundColor = Color(0xFF2196F3)
)
) {
Text(
text = "进入第11层",
fontSize = 18.sp,
fontWeight = FontWeight.Medium,
color = Color.White
)
}
}
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This image diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment