SOUI Android 适配指南¶
概述¶
SOUI 通过 soui-android-lib 模块实现了完整的 Android 平台适配,使开发者能够使用同一套 SOUI C++ 代码在 Android 设备上运行应用。本指南详细介绍了 SOUI Android 适配的架构设计、编译配置和使用方法。
核心设计理念¶
SOUI 的 Android 适配遵循以下原则:
- 窗口系统仿真:在 Android 上模拟 Win32 窗口模型(HWND、消息队列、SetTimer 等),使 SOUI 核心无需修改即可运行
- View 树映射:每个 SOUI HWND 对应一个 Android
View,窗口层级通过自定义ViewGroup(SouiAbsLayout)管理 - 离屏渲染:SOUI 在离屏 Bitmap 上完成 Skia 渲染,再由 Android Canvas 绘制到屏幕
- 事件桥接:Java 层触摸/键盘事件通过 JNI 转换为 Win32 消息,C++ 层处理后再回调 Java 重绘
架构设计¶
分层架构¶
┌─────────────────────────────────────────────────┐
│ SOUI C++ 应用层 │
│ (SHostWnd, SApplication, 控件, 皮肤, 布局) │
├─────────────────────────────────────────────────┤
│ swinx 抽象层 │
│ (窗口管理、消息系统、资源管理、渲染接口) │
├─────────────────────────────────────────────────┤
│ AndroidPlatformAPI 实现层 (C++) │
│ (HWND 创建/销毁、定时器、焦点、剪贴板、IME) │
├─────────────────────────────────────────────────┤
│ JNI 桥接层 │
│ (Soui4Android.cpp 中的 JNI 函数实现) │
├─────────────────────────────────────────────────┤
│ SouiPlatformBridge (Java) │
│ (单例桥接、窗口工厂、定时器调度、消息循环) │
├─────────────────────────────────────────────────┤
│ Android View 系统 │
│ SouiScreen → SouiWindow → SouiSurface │
│ (绝对布局、事件分发、离屏渲染、焦点管理) │
└─────────────────────────────────────────────────┘
关键类说明¶
Java 层核心类¶
| 类名 | 职责 |
|---|---|
SouiPlatformBridge |
全局桥接单例。管理 HWND→View 映射、定时器(SetTimer/KillTimer)、焦点(SetFocus)、输入捕获(SetCapture)、剪贴板、消息调度 |
SouiScreen |
屏幕根容器,对应 GetDesktopWindow。承载所有顶层窗口,通过 screenStartup/screenShutdown 驱动 SOUI 生命周期 |
SouiWindow |
窗口容器,对应一个 HWND。内含一个主 Surface(index=0)+ 子窗口组 |
SouiSurface |
主渲染 Surface。在 onDraw 中渲染 C++ 生成的离屏 Bitmap |
SouiBaseSurface |
Surface 基类。实现 INativeWindow 接口,负责事件分发(触摸/键盘/鼠标/悬停/滚轮)和 native 生命周期 |
SouiAbsLayout |
绝对布局 ViewGroup。为所有 SOUI 窗口提供 (x, y, width, height) 绝对定位引擎 |
NativeWindowDelegate |
窗口状态操作委托。集中实现 Show/Move/Enable/Destroy 等窗口操作 |
INativeWindow |
Java↔C++ 窗口接口。定义 nativeGetHwnd、nativeShow、nativeMove、nativeSendMessage 等方法 |
C++ 层核心类¶
| 类名 | 职责 |
|---|---|
AndroidPlatformAPI |
Android 平台 API 单例。提供窗口创建/销毁、定时器、消息、剪贴板等全量实现;由 AndroidPlatformAPIReg.cpp 填充到 swinx 的 C 结构体 PlatformAPI(swinx/include/platform_api.h)并调用 PlatformAPI_Init 完成注册 |
SouiSurfaceProxy |
C++ 端 Surface 代理。持有 INativeWindow GlobalRef,将 C++ 事件(onMotionEvent、onKeyEvent、render)派发到 Java 层 |
Soui4AndroidEntry |
应用入口基类。业务应用继承此类实现 InitApp、ScreenStartup、ScreenShutdown、UninitApp |
AutoStringSlot |
字符串槽 RAII 封装。用于 C++↔Java 之间的字符串参数传递(如 WM_SETTEXT/WM_GETTEXT) |
窗口创建流程¶
sequenceDiagram
participant Java as SouiPlatformBridge
participant JNI as Soui4Android.cpp
participant Cpp as AndroidPlatformAPI
participant Swinx as swinx::CreateWindow
participant Soui as SOUI 控件树
Java->>Java: createWindow(parent, className, ...)
Java->>Java: finishCreateWindow(parent, ...)
Java->>Java: new SouiWindow(context)
Java->>Java: window.newSurface(className, title, ...)
Java->>Java: createViewByClassName(className, title)
Java->>Java: 创建 SouiSurface/NativeEditView
Java->>Java: surface.addToParent(SouiAbsLayout)
Java->>JNI: nativeCreate(INativeWindow)
JNI->>Cpp: new SouiSurfaceProxy(env, window)
Cpp->>Cpp: nativeViewInsert(nativeId, proxy)
JNI-->>Java: 返回 nativeId (=HWND)
Java->>Java: mViewMap.put(hwnd, surface)
Java-->>Java: 返回 HWND
消息处理流程¶
sequenceDiagram
participant Android as Android Input
participant Surface as SouiBaseSurface
participant JNI as nativeOnMotionEvent
participant Cpp as AndroidPlatformAPI
participant Swinx as swinx 消息队列
participant Soui as SOUI 窗口
Android->>Surface: onTouchEvent(MotionEvent)
Surface->>Surface: dispatchTouchEventToNative(event)
Surface->>Surface: tryDispatchCapturedMotion(bridge)
Surface->>JNI: nativeOnMotionEvent(nativeId, action, x, y, ...)
JNI->>Cpp: nativeViewLookup(nativeId)
Cpp->>Cpp: SouiSurfaceProxy::onMotionEvent(...)
Cpp->>Swinx: 投递 WM_LBUTTONDOWN/WM_MOUSEMOVE 等
Swinx->>Soui: 分发消息到目标窗口
Soui-->>Surface: invalidate(rect) 请求重绘
Surface->>JNI: onDraw(canvas) → nativeRender(nativeId, bitmap)
JNI->>Cpp: SouiSurfaceProxy::render(env, bitmap)
Cpp->>Soui: 渲染到 Bitmap
Surface->>Android: canvas.drawBitmap(bitmap)
环境要求¶
开发环境¶
| 工具 | 版本要求 | 说明 |
|---|---|---|
| Android Studio | Hedgehog (2023.1) 或更新 | 推荐使用最新稳定版 |
| Android Gradle Plugin | 8.2+ | 适配 AGP 8.x |
| Gradle | 8.2+ | 与 AGP 版本匹配 |
| NDK | r25c+ | 需要 C++11 支持 |
| CMake | 3.18.1+ | Android NDK 自带 |
| compileSdk | 34 | Android 14 |
| minSdk | 28 | Android 9 |
| targetSdk | 34 | Android 14 |
| JDK | 17 | Android Studio 自带 |
支持的 ABI¶
ndk {
abiFilters 'x86', 'arm64-v8a', 'x86_64', 'armeabi-v7a'
}
编译与构建¶
构建模型:AAR 只编 Java,native 由业务 app 编译¶
重要变更
soui-android-lib 不再自行编译 C++。它退化为「Java 壳 + C++ 源码提供方」,native 代码统一由业务 app 的 CMake 编译。
变更原因:旧模型中 AAR 自己用 externalNativeBuild 编出 libsoui4android.so,业务 app 再去链接 AAR 产出的 .so。但 Gradle 不保证 AAR 的 native 先于 app 编完,且 AAR 的 .so 输出路径(intermediates/library_jni)app 侧取不到,只能先手动编 AAR、再手动复制 library_jni 才能链接——构建顺序与产物路径都不可控。
新模型的职责划分:
| 模块 | 职责 |
|---|---|
soui-android-lib |
纯 Java/资源库(AAR)。提供 SouiPlatformBridge 等 Java 层,并保留 src/main/cpp/ 下的 C++ 源码与头文件供业务引用。不再产出 .so,不再收集头文件 assets |
soui4_android.cmake |
可复用的 native 构建片段(位于 soui-android-lib/src/main/cpp/)。负责 add_subdirectory 编译 SOUI 核心,并 add_library(soui4android SHARED) 编译 JNI 桥 |
| 业务 app | include(soui4_android.cmake),再 add_library(自己的 so) 并链接 soui4android |
该模型天然支持多业务复用:多个 Android app 各自 include() 同一份片段即可,无需复制任何 add_subdirectory 代码。
库名不可更改
JNI 采用静态注册(Java_com_soui_android_* 全部在 soui-android-lib/src/main/cpp/src/Soui4Android.cpp),且 SouiPlatformBridge.java 中写死 System.loadLibrary("soui4android")。导出库名必须保持 soui4android,改名会导致 Java 侧加载时找不到符号。
项目结构¶
soui4/
├── soui-android-lib/ # Android 库模块(仅 Java/资源)
│ ├── build.gradle # 库模块构建配置(无 externalNativeBuild)
│ ├── src/main/
│ │ ├── cpp/ # C++ 桥接源码(不参与本模块编译,供业务引用)
│ │ │ ├── soui4_android.cmake # ★ 可复用 native 构建片段
│ │ │ ├── include/ # 头文件(AndroidPlatformAPI.h、soui4android.h 等)
│ │ │ └── src/ # JNI 实现(Soui4Android.cpp 等)
│ │ ├── java/com/soui/android/ # Java 层
│ │ └── res/values/attrs.xml # 自定义属性
├── games/cnchess/client/android/ # 示例应用:中国象棋
│ └── app/
│ ├── build.gradle
│ └── src/main/
│ ├── cpp/ # 业务 C++ 代码(android_entry.cc)
│ ├── java/ # Activity/Application
│ └── assets/ # SOUI 资源文件
├── demos/android-demo/ # 示例应用:基础 demo
│ └── app/src/main/cpp/ # 业务 C++ 代码
└── swinx/src/platform/android/ # swinx Android 实现
业务 app 的 CMakeLists.txt¶
cmake_minimum_required(VERSION 3.18.1)
project("my-android-app")
# 以下变量从 Gradle 的 externalNativeBuild.cmake.arguments 传入
# SOUI_ROOT_DIR - soui4 仓库根目录
# SOUI_ANDROID_LIB_CPP_DIR - soui-android-lib/src/main/cpp 目录
if(NOT EXISTS ${SOUI_ROOT_DIR})
message(FATAL_ERROR "SOUI_ROOT_DIR: ${SOUI_ROOT_DIR} is not existed")
endif()
if(NOT EXISTS ${SOUI_ANDROID_LIB_CPP_DIR})
message(FATAL_ERROR "SOUI_ANDROID_LIB_CPP_DIR: ${SOUI_ANDROID_LIB_CPP_DIR} is not existed")
endif()
# ★ 引入可复用片段:编译 SOUI 核心 + libsoui4android.so
include(${SOUI_ANDROID_LIB_CPP_DIR}/soui4_android.cmake)
add_library(my-android-app SHARED ${MY_SRC})
target_include_directories(my-android-app PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${SOUI_ANDROID_LIB_CPP_DIR}/include
${SOUI_ROOT_DIR}/SOUI/include
${SOUI_ROOT_DIR}/swinx/include
${SOUI_ROOT_DIR}/utilities/include
${SOUI_ROOT_DIR}/components
${PROJECT_BINARY_DIR}/config
)
target_link_libraries(my-android-app PUBLIC
soui4android soui4 utilities4 swinx
android log jnigraphics
)
业务 app 的 build.gradle¶
android {
defaultConfig {
externalNativeBuild {
cmake {
cppFlags "-std=c++17 -fexceptions -frtti"
arguments "-DANDROID_STL=c++_shared",
"-DSOUI_ROOT_DIR=${rootDir.absolutePath.replace('\\\\', '/')}/../..",
"-DSOUI_ANDROID_LIB_CPP_DIR=${project(':soui-android-lib').projectDir.absolutePath.replace('\\\\', '/')}/src/main/cpp"
}
}
ndk {
abiFilters 'x86', 'arm64-v8a', 'x86_64', 'armeabi-v7a'
}
}
}
dependencies {
implementation project(':soui-android-lib') // 仅提供 Java/资源
}
同时在 settings.gradle 中引入 AAR 模块:
include ':soui-android-lib'
project(':soui-android-lib').projectDir = new File('../../soui-android-lib')
可选:关闭不需要的重量级模块¶
共享片段 soui4_android.cmake 默认开启 SOUI_BUILD_WS(WebSocket / OpenSSL / libcurl)与 SOUI_BUILD_RICHEDIT。纯 UI 应用若不需要联网能力,可关闭以缩短配置与编译时间:
arguments "-DSOUI_BUILD_WS=OFF"
片段中这两个开关以 if(NOT DEFINED ...) set(... ON) 形式给出,业务侧可用 -D 覆盖,默认值不影响已依赖它们的业务(如 cnchess)。
构建¶
# 直接构建业务 app,native 一次性编出
cd games/cnchess/client/android # 或 demos/android-demo
./gradlew assembleDebug
不再需要先编 AAR、再手动复制 library_jni。产物为 libsoui4android.so 与业务自己的 .so。
首次 CMake 配置很慢,请勿中断
共享片段会编译完整的 SOUI 核心与整个 third-part(Skia、OpenSSL、curl、richedit、Scintilla 等)。在 Android NDK 下,首次 CMake 配置通常需要 7~11 分钟(Skia 的 gn/ninja 引导与数百次 TRY_COMPILE 是主要耗时)。
若配置被中断(超时、内存不足或手动取消),Gradle 会报 CXX1405 并出现 Configuring incomplete, errors occurred!。这不是代码错误,按如下方式处理:
- 删除陈旧缓存后重新构建:
rm -rf app/.cxx
rm -rf app/build/intermediates/cxx
- 重新构建并耐心等待首次配置跑完(不要取消)。首次通过后,后续增量构建很快。
若总是在最早的 Looking for sys/types.h 处失败,多半是本机 NDK clang 的 TRY_COMPILE 被杀软拦截或权限不足,而非构建脚本问题。
使用指南¶
1. Application 初始化¶
在 Application.onCreate() 中初始化 SOUI Android 平台桥:
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// 1. 可选:将主题/资源从 assets 复制到 filesDir
copyAssetDir("myapp/theme", getFilesDir() + "/myapp/theme");
// 2. 初始化 SOUI Android 平台桥
SouiPlatformBridge.getInstance().init(
this, // Context
getAssets(), // AssetManager
getFilesDir().getAbsolutePath() // 文件系统根目录
);
}
@Override
public void onTerminate() {
SouiPlatformBridge.getInstance().destroy();
super.onTerminate();
}
}
2. 创建 Activity 并承载 SOUI 界面¶
public class MyActivity extends AppCompatActivity {
// 唯一的屏幕 ID,应用内全局唯一
public static final long SCREEN_ID = 0x0000_C001L;
// SOUI 布局资源 ID(对应 uires.idx 中定义)
public static final String LAYOUT_MAIN = "layout:XML_MAINWND";
private SouiScreen mScreen;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 创建 SouiScreen,传入 screenId 和初始布局
mScreen = new SouiScreen(this, SCREEN_ID, LAYOUT_MAIN);
// 用 FrameLayout 包裹 SouiScreen
FrameLayout decor = new FrameLayout(this);
decor.addView(mScreen, new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
// 当 SOUI 主窗口销毁后自动关闭 Activity
mScreen.setOnEmptyListener(screen -> finish());
setContentView(decor);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// 横竖屏切换时同步屏幕尺寸
if (mScreen != null) {
mScreen.post(() -> {
View root = (View) mScreen.getParent();
mScreen.syncScreenSize(root.getWidth(), root.getHeight());
});
}
}
}
3. C++ 侧应用入口¶
继承 Soui4AndroidEntry 实现应用生命周期:
#include <soui4android.h>
class MyAndroidApp : public Soui4AndroidEntry {
SApplication* m_souiApp = nullptr;
std::map<long, SAutoRefPtr<SHostWnd>> m_screenHostMap;
public:
MyAndroidApp() {
InitSoui4AndroidEntry(this); // 注册到框架
}
// 应用初始化(在 SouiPlatformBridge.init 时调用)
BOOL InitApp(AAssetManager* assetMgr, LPCSTR pszAssetDir) override {
m_souiApp = new SApplication((HINSTANCE)nullptr);
SAppCfg cfg;
cfg.SetRender(Render_Skia)
.SetImgDecoder(ImgDecoder_Stb)
.SetAppDir(S_CA2T(pszAssetDir, CP_UTF8))
.SetSysResAndroidAsset(assetMgr, _T("soui_sys_res"))
.SetAppResAndroidAsset(assetMgr, _T("uires"));
if (!cfg.DoConfig(m_souiApp)) {
delete m_souiApp;
return FALSE;
}
// 注册自定义窗口类、皮肤类等
m_souiApp->RegisterWindowClass<MyCustomControl>();
return TRUE;
}
// 屏幕启动(创建 SOUI 主窗口)
HWND ScreenStartup(long screenId, LPCSTR pszLayout) override {
// 创建主窗口(与 Windows 平台 CreateWindow 语义一致)
CMainDlg* pDlg = new CMainDlg();
if (!pDlg->Create(NULL, 0, 0, 50, 50)) {
delete pDlg;
return 0;
}
pDlg->SendMessage(WM_INITDIALOG);
pDlg->ShowWindow(SW_SHOW);
HWND hwnd = pDlg->m_hWnd;
m_screenHostMap[screenId] = pDlg;
pDlg->Release();
return hwnd;
}
// 屏幕销毁
void ScreenShutdown(long screenId) override {
auto it = m_screenHostMap.find(screenId);
if (it != m_screenHostMap.end()) {
it->second->DestroyWindow();
m_screenHostMap.erase(it);
}
}
// 应用反初始化
void UninitApp() override {
if (m_souiApp) {
m_souiApp->Release();
m_souiApp = nullptr;
}
}
};
// 全局唯一实例
static MyAndroidApp theApp;
4. Activity 配置¶
<!-- AndroidManifest.xml -->
<manifest>
<application
android:name=".MyApplication"
android:theme="@style/Theme.MyApp">
<activity
android:name=".MyActivity"
android:exported="true"
android:windowSoftInputMode="adjustNothing"
android:configChanges="orientation|screenSize"
android:theme="@style/Theme.MyApp.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
注意:
android:windowSoftInputMode="adjustNothing"防止软键盘弹出时窗口尺寸变化。SOUI 自行处理键盘高度变化(通过nativeSetKeyboardHeight回调)。
5. SOUI 资源组织¶
app/src/main/assets/
├── soui_sys_res/ # 系统资源(SOUI 内置皮肤、样式)
│ ├── theme_sys_res/
│ ├── uires.idx
│ └── uidesign/
├── uires/ # 应用资源
│ ├── values/
│ │ ├── color.xml
│ │ ├── skin.xml
│ │ └── string.xml
│ ├── xml/ # SOUI 布局 XML
│ │ └── dlg_main.xml
│ ├── svg/ # SVG 资源
│ └── uires.idx
└── myapp/theme/ # 文件系统资源(可选)
└── ... # 通过 filesDir 访问
资源加载优先级:
1. soui_sys_res:SOUI 系统资源,通过 SetSysResAndroidAsset 加载
2. uires:应用资源,通过 SetAppResAndroidAsset 加载
3. 文件系统资源:如 def_theme,由 Java 侧从 assets 复制到 filesDir 后通过 SetAppDir 加载
SResProviderAndroidAsset:基于 AssetManager 的资源提供器¶
Android 没有 Windows 那样的传统文件系统路径,SOUI 的 uires 资源包(含 uires.idx、xml、image、values 等)被打进 APK 的 assets 目录。SOUI 为此提供了一个专属于 Android 的 IResProvider 实现:SResProviderAndroidAsset(SOUI/include/res.mgr/SResProviderAndroidAsset.h,实现在 SOUI/src/res.mgr/SResProviderAndroidAsset.cpp)。
为什么需要它¶
- 资源就在 APK 的
assets里,但assets没有"文件系统中的绝对路径",只能用 NDK 的AAssetManager打开。 - 它直接从 APK assets 读取资源,无需先把
uires拷贝到filesDir—— 既省磁盘空间,又省启动时的拷贝时间(对比"文件系统资源"那种先把def_theme从 assets 复制出来的做法)。 - 与桌面的
SResProviderFiles(读磁盘目录)、SResProviderPE(读 PE 资源段)、SResProviderZip/SResProvider7Z(读压缩包)是同一级抽象的不同实现,只是后端换成了AAssetManager。
关键实现要点(源码核对)¶
Init(WPARAM, LPARAM):wParam=(AAssetManager*)Android 原生 AssetManager(由 Java 侧AAssetManager_fromJava(env, javaAssetManager)取得)。lParam=assets下的前缀目录名,如_T("uires")或_T("soui_sys_res")。uires.idx约定:前缀目录下必须有标准的uires.idx。索引里的path="uidef\\init.xml"会被NormalizeAssetPath标准化为uidef/init.xml(把\转/、去掉开头多余/),再拼上前缀得到 assets 相对路径uires/uidef/init.xml。- 路径缓存:
m_mapFiles(SMap<SResID, SStringT>)缓存type+name → 规范化后的 assets 相对路径,GetAssetPath(type, name)据此查表;当strType == nullptr时则把pszResName直接当相对路径与前缀拼接(用于直接按文件读)。 - 所有权:
m_assetMgr由 JVM 持有,本类不拥有、也不释放;OpenAsset返回AAsset*,调用方负责AAsset_close。 - 其余
IResProvider接口(HasResource/LoadBitmap/LoadImage/LoadImgX/GetRawBuffer/EnumResource/EnumFile等)均与SResProviderFiles对齐,业务代码无需区分资源来自哪里。
用法¶
最简方式是用 SAppCfg 的专属助手(内部会 CreateResProvider(RES_ANDROID_ASSET) 并 Init),见 SOUI/include/SAppCfg.h:
// games/cnchess/client/android_entry.cc
cfg.SetRender(Render_Skia)
...
.SetSysResAndroidAsset(assetMgr, _T("soui_sys_res")) // 系统资源
.SetAppResAndroidAsset(assetMgr, _T("uires")); // 应用资源
也可以手动创建并挂到资源树(与桌面 SResProviderFiles::Init 接口对齐,但参数类型不同):
#include <android/asset_manager_jni.h> // AAssetManager_fromJava
AAssetManager* mgr = AAssetManager_fromJava(env, javaAssetManager);
SResProviderAndroidAsset* p = new SResProviderAndroidAsset();
p->Init((WPARAM)mgr, (LPARAM)_T("uires"));
GETRESPROVIDER->AddResProvider(p, _T("uidef:xml_init")); // 或 LoadSystemNamedResource
与桌面 ResProvider 对照
| 提供器 | 后端 | 典型平台 |
|---|---|---|
SResProviderFiles |
磁盘目录 | Windows / Linux / macOS |
SResProviderPE |
PE 资源段 | Windows |
SResProviderZip / SResProvider7Z |
压缩包 | 跨平台 |
SResProviderAndroidAsset |
APK assets(AAssetManager) |
仅 Android |
注意
- 前缀目录必须落在
assets下且含uires.idx,否则Init拿不到资源表。 AAssetManager生命周期归 JVM,不要在 SOUI 销毁后再用本提供器;本类不会替你释放它。- 需要"随版本更新、可写"的资源(如主题热更新文件)仍走
SetAppDir+filesDir,不要塞进assets。
关键特性说明¶
窗口系统仿真¶
SOUI 在 Android 上完整实现了 Win32 窗口语义:
| Win32 API | Android 实现 |
|---|---|
CreateWindowEx |
SouiPlatformBridge.createWindow() → 创建 SouiWindow + SouiSurface |
DestroyWindow |
SouiPlatformBridge.destroyView() → 从 SouiAbsLayout 中移除 |
ShowWindow |
SouiPlatformBridge.showView() → View.setVisibility() |
MoveWindow |
SouiPlatformBridge.moveWindow() → updateChildFrame() |
SetWindowPos |
同 MoveWindow |
SetTimer/KillTimer |
SouiPlatformBridge.setTimer()/killTimer() → Handler 定时调度 |
SetCapture |
SouiPlatformBridge.setCapture() → 捕获后续触摸事件到目标 HWND |
SetFocus |
SouiPlatformBridge.setFocus() → 同步 Android View 焦点 + IME |
GetDC/ReleaseDC |
通过 IRenderTarget 直接渲染到离屏 Bitmap |
InvalidateRect |
SouiPlatformBridge.requestInvalidate() → View.invalidate() |
Clipboard |
SouiPlatformBridge.clipboard*() → ClipboardManager |
PlaySound |
SouiPlatformBridge.playSound() → MediaPlayer |
输入法(IME)处理¶
SOUI 在 Android 上处理了完整的 IME 流程:
- 键盘弹起:
SouiScreen监听WindowInsetsAnimation,将键盘高度通过nativeSetKeyboardHeight回调到 C++ 层 - 文本输入:
SouiBaseSurface.onCreateInputConnection()返回自定义InputConnection,commitText将文本发送到当前焦点 HWND - 焦点同步:
SouiPlatformBridge.setFocus()同步 Android View 焦点和 IME 显示状态 - 原生 EditText:
NativeEditView类(继承EditText)用于需要原生输入法体验的场景(如文本输入框)
离屏渲染流程¶
// Java 层 SouiSurface.onDraw()
protected void onDraw(Canvas canvas) {
nativeRender(nativeId, offscreenBitmap); // C++ 渲染到 bitmap
canvas.drawBitmap(offscreenBitmap, 0, 0, paint); // 绘制到屏幕
}
C++ 侧 SouiSurfaceProxy::render():
1. 获取 Bitmap 的像素地址(通过 JNIGraphics)
2. 使用 Skia Canvas 在该内存上绘制
3. 完成后 Bitmap 直接反映更新
消息循环¶
SOUI 在 Android 上的消息驱动机制:
- 定时器消息:
SouiPlatformBridge.setTimer()使用Handler.postDelayed()调度,触发时回调nativeOnTimerExpired→ C++ 层投递WM_TIMER - 自定义消息:C++ 层调用
PostMessage→ Java 层scheduleMessageProcessing()通过Handler.post()延迟执行 →nativeProcessPendingMessages()处理消息队列 - 空闲处理:
SouiScreen.screenStartup()注册IdleHandler,在消息队列空闲时调用nativeProcessIdle()执行OnIdle处理器
屏幕管理¶
支持多屏幕(多 SouiScreen):
- 每个
SouiScreen有唯一的screenId - C++ 层通过
m_activeScreenStack管理活动 screen 栈 SHostWnd::Create(NULL)时根据活动栈顶路由到正确的 screenSouiScreen.onAttachedToWindow()自动调用screenStartupSouiScreen.onDetachedFromWindow()自动调用screenShutdown
原生控件支持¶
对于需要使用 Android 原生控件(如 EditText、WebView)的场景,SOUI 提供了 View 工厂注册机制:
注册自定义 View 工厂¶
SouiPlatformBridge.getInstance().registerViewFactory("richedit", title -> {
// 创建 NativeEditView(继承 EditText)
return new NativeEditView(context);
});
C++ 侧创建时指定 className¶
// 在 SOUI XML 或代码中创建窗口时
// className="richedit" 会查找已注册的工厂
// 未注册时 fallback 到 SouiSurface(SOUI 自绘)
SOUI 内置注册的原生控件:
- "edit" → NativeEditView(原生 EditText,支持完整 IME 体验)
调试与问题排查¶
日志标签¶
| 标签 | 来源 | 说明 |
|---|---|---|
soui4android |
JNI 层 | 窗口创建/销毁、消息处理 |
SouiPlatformBridge |
Java 桥接层 | 定时器、焦点、捕获 |
SouiBaseSurface |
Surface 基类 | 事件分发、生命周期 |
SouiScreen |
屏幕容器 | screenStartup/Shutdown |
SConnection |
swinx 消息连接 | 消息队列状态 |
常见问题¶
Q1: 编译时找不到 SOUI 头文件¶
当前构建模型不再使用 collectHeaders 任务复制头文件,业务 app 直接引用 SOUI 源码树。请依次检查:
- Gradle 传入的
SOUI_ROOT_DIR与SOUI_ANDROID_LIB_CPP_DIR是否存在(CMake 配置阶段会打印这两个变量,并以FATAL_ERROR校验)。 settings.gradle是否已include ':soui-android-lib'且projectDir指向正确路径。- 业务 CMakeLists 的
target_include_directories是否包含${SOUI_ANDROID_LIB_CPP_DIR}/include与${SOUI_ROOT_DIR}/SOUI/include。
若仍报 CXX1405 ... Configuring incomplete,请先删除 app/.cxx 与 app/build/intermediates/cxx 后重新构建(详见上文「编译与构建」章节的注意事项)。
Q2: 运行时崩溃 "nativeId == 0"¶
说明 SouiBaseSurface.init() 在 onAttachedToWindow 之前就尝试绘制。确保 Surface 已正确添加到 View 树。
Q3: 触摸事件无响应¶
检查 SouiScreen 是否已通过 screenStartup 启动。确认 nativeId 非 0。查看 SConnection 日志确认消息是否投递。
Q4: 软键盘不弹出¶
确认窗口已获得焦点(SouiPlatformBridge.getFocus() != 0)。检查 android:windowSoftInputMode 设置。
Q5: 中文输入异常¶
确保 NativeEditView 被正确注册。对于 SOUI 自绘编辑控件,检查 SouiInputConnection.commitText 是否正确调用 sendImeString。
Q6: 屏幕尺寸为 0¶
SouiScreen 需要完成 layout 后才有正确尺寸。在 onAttachedToWindow 中使用 post() 延迟获取尺寸。
性能优化¶
1. Bitmap 复用¶
SouiSurface 实现了 Bitmap 复用机制:
private void createOffscreenBitmap(int width, int height) {
// 尺寸不变时复用,避免内存抖动
if (offscreenBitmap != null
&& offscreenBitmap.getWidth() == width
&& offscreenBitmap.getHeight() == height
&& !offscreenBitmap.isRecycled()) {
return;
}
// 释放旧 bitmap
if (offscreenBitmap != null && !offscreenBitmap.isRecycled()) {
offscreenBitmap.recycle();
}
offscreenBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
}
2. 渲染节流¶
- SOUI 内部的
InvalidateRect已做去重优化 - 避免在
onDraw中执行耗时操作 - 使用
requestInvalidate(left, top, right, bottom)进行局部重绘
3. 内存管理¶
- Bitmap 在
onDetachedFromWindow中及时recycle() SouiSurfaceProxy使用shared_ptr管理生命周期- 窗口销毁时自动清理关联的定时器和捕获状态
4. 多 ABI 策略¶
发布时可以只保留目标 ABI 以减小包体积:
buildTypes {
release {
ndk {
abiFilters 'arm64-v8a', 'armeabi-v7a' // 仅保留主流架构
}
}
}
与 Windows 平台的差异¶
| 方面 | Windows | Android |
|---|---|---|
| SOUI 渲染工厂 | Render_Skia / Render_Gdi / Render_D2d |
Render_Skia |
| swinx 2D 后端 | 不适用(Windows 直接用系统 GDI,不编译 swinx) | Cairo(swinx/src/gdi/cairo) |
| 上屏方式 | 系统合成 | 离屏缓冲 → AndroidBitmap_lockPixels → Canvas.drawBitmap |
| 窗口创建 | CreateWindowEx |
SouiPlatformBridge.createWindow |
| 消息循环 | 原生 GetMessage/DispatchMessage |
Handler + nativeProcessPendingMessages |
| DPI | Per-Monitor DPI | density-independent pixels (dp) |
| IME | TSF/IMM | InputMethodManager + InputConnection |
| 资源 | 文件系统 / 资源段 | AssetManager + 文件系统 |
| 定时器 | SetTimer 原生实现 |
Handler.postDelayed |
| 剪贴板 | OpenClipboard Win32 API |
ClipboardManager |
| 音频 | PlaySound Win32 API |
MediaPlayer |
| 文件路径 | 绝对路径 | getFilesDir() + AssetManager |
总结¶
SOUI 的 Android 适配通过精心设计的分层架构,实现了:
- 完整的 Win32 窗口系统仿真:HWND、消息、定时器、焦点、捕获等核心语义完整还原
- 优秀的渲染性能:离屏 Skia 渲染 + Bitmap 复用 + 局部重绘
- 流畅的输入体验:触摸/键盘/鼠标/IME 全链路桥接
- 灵活的原生控件集成:View 工厂机制支持嵌入任意 Android View
- 统一的 C++ 代码:业务层代码无需修改即可在 Windows 和 Android 之间移植
通过参考 games/cnchess/client/android(中国象棋,含联网对战)与 demos/android-demo(基础示例)两个项目,您可以快速上手 SOUI Android 开发,将现有的 SOUI 应用移植到移动平台。
若还需接入 iOS 或鸿蒙,请继续阅读 iOS 适配指南 与 鸿蒙(OHOS)适配指南。