7 移动端多窗口

原文链接: https://tauri.app/learn/mobile-multiwindow/

Tauri 在 Android 和 iOS 上支持多窗口,让你的应用可以在平板上并排显示内容,或在 iPad 上以独立场景(scene)显示。

在 Android 上,多窗口是通过 Activity Embedding 实现的,它让系统可以在大屏上并排显示两个 activity。

在 iOS 上,多窗口使用 UIScene API,它允许 iPad 用户在独立窗口中打开应用的多个实例。

在手机上,系统通常不会把两个窗口并排布局。在 Android 上,创建另一个窗口仍会启动一个独立的 activity,但在手机尺寸的屏幕上它通常会被压入 activity 返回栈——因此按 Back 会返回上一个 activity,而不是关闭分屏。在 iOS 上(尤其是 iPhone),打开或创建另一个窗口往往会用新场景的内容替换当前 UI,而不是让两者同时可见;真正并发的窗口仍然是 iPad(以及 Stage Manager)的体验。

共同的前置设置

两个平台都需要一项能力(capability)权限才能从前端创建新窗口。

能力

把 core:webview:allow-create-webview-window 权限加入你的能力文件,这样前端才能创建新窗口。

如果你要创建多个窗口,请使用通配符,或在 windows 数组中列出每个窗口标签:

1
2
3
4
5
6
7
{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "Capability for the main window",
  "windows": ["main"],
  "permissions": ["core:default", "core:webview:allow-create-webview-window"]
}

Android

Android 多窗口使用 Activity Embedding,在大屏(平板、折叠屏)上把 activity 分屏并排显示。你需要为每种窗口类型创建一个 Android Activity、配置分屏规则,并注册一个 initializer。

1. 添加依赖

把所需的 AndroidX 库加入你的 build.gradle.kts:

1
2
3
4
5
dependencies {
    // ... 已有依赖
    implementation("androidx.window:window:1.5.0")
    implementation("androidx.startup:startup-runtime:1.2.0")
}

2. 创建新的 Activity

为每个额外的窗口类型创建一个 Kotlin 类。每个 activity 都必须继承 TauriActivity:

1
2
3
4
5
6
7
8
9

import android.os.Bundle
import android.os.PersistableBundle

class DetailActivity: TauriActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
  }
}

3. 更新 AndroidManifest.xml

注册新的 activity,并通过添加 tools 命名空间和 embedding 属性来启用 activity embedding:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools">

  <application ...>

    <property
      android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED"
      android:value="true" />

    <!-- 已有的 MainActivity -->
    <activity
      android:name=".MainActivity"
      android:exported="true"
      ...>
      ...
    </activity>

    <!-- 用于详情窗口的新 activity -->
    <activity android:name=".DetailActivity" android:exported="true" />

    <!-- 注册分屏 initializer -->
    <provider android:name="androidx.startup.InitializationProvider"
      android:authorities="${applicationId}.androidx-startup"
      android:exported="false"
      tools:node="merge">
      <meta-data android:name="${applicationId}.SplitInitializer"
        android:value="androidx.startup" />
    </provider>

  </application>
</manifest>

4. 创建分屏 Initializer

该 initializer 在应用启动时加载分屏配对规则:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18

import android.content.Context
import androidx.startup.Initializer
import androidx.window.core.ExperimentalWindowApi
import androidx.window.embedding.RuleController

@OptIn(ExperimentalWindowApi::class)
class SplitInitializer : Initializer<RuleController> {
  override fun create(context: Context): RuleController {
    return RuleController.getInstance(context).apply {
      setRules(RuleController.parseRules(context, R.xml.main_split_config))
    }
  }

  override fun dependencies(): List<Class<out Initializer<*>>> {
    return emptyList()
  }
}

5. 定义分屏规则

创建一个 XML 资源,告诉系统如何配对 activity 并分屏:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
  xmlns:window="http://schemas.android.com/apk/res-auto">

  <SplitPairRule
    window:splitRatio="0.33"
    window:splitLayoutDirection="locale"
    window:splitMinWidthDp="840"
    window:splitMaxAspectRatioInPortrait="alwaysAllow"
    window:finishPrimaryWithSecondary="never"
    window:finishSecondaryWithPrimary="never"
    window:clearTop="false">
    <SplitPairFilter
      window:primaryActivityName=".MainActivity"
      window:secondaryActivityName=".DetailActivity"/>
  </SplitPairRule>

</resources>

关键属性:

  • splitRatio —— 屏幕如何分割(0.33 表示主 activity 占三分之一)
  • splitMinWidthDp —— 激活分屏的最小屏幕宽度(840dp 面向平板)
  • splitMaxAspectRatioInPortrait —— 设为 alwaysAllow 可在竖屏模式下启用分屏
  • primaryActivityName / secondaryActivityName —— 哪一对 activity 触发分屏

iOS

在 iOS 上,多窗口使用 UIScene API。iPad 用户可以通过长按应用图标并选择 “New window” 来打开新窗口,你的应用也可以以编程方式创建它们。

1. 启用 Scene 支持

在你的 src-tauri 目录中创建 Info.ios.plist 文件来声明 scene 支持:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>UIApplicationSceneManifest</key>
  <dict>
    <key>UIApplicationSupportsMultipleScenes</key>
    <true/>
    <key>UISceneConfigurations</key>
    <dict/>
  </dict>
</dict>
</plist>

2. 处理 Scene 请求

当用户在 iPad 上请求新窗口时(例如长按应用图标),Tauri 会发出 RunEvent::SceneRequested 事件。处理它以创建新窗口:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
pub fn run() {
    #[cfg(target_os = "ios")]
    let mut counter = 0;

    tauri::Builder::default()
        .setup(|app| {
            tauri::WebviewWindowBuilder::new(
                app, "main", tauri::WebviewUrl::default()
            ).build()?;
            Ok(())
        })
        .build(tauri::generate_context!())
        .expect("error while running tauri application")
        .run(move |app, event| {
            #[cfg(target_os = "ios")]
            if let tauri::RunEvent::SceneRequested { .. } = event {
                counter += 1;
                tauri::WebviewWindowBuilder::new(
                    app,
                    format!("main-{counter}"),
                    tauri::WebviewUrl::default(),
                )
                .build()
                .unwrap();
            }
            #[cfg(not(target_os = "ios"))]
            let _ = (app, event);
        });
}

创建窗口

你既可以从 Rust 创建额外窗口,也可以从前端 JavaScript API 创建。WebviewWindowBuilder(Rust)和 WebviewWindow(JavaScript)都接受平台特定的选项:

Android 选项:

  • activityName —— 为该窗口创建的 Android Activity 类名。
  • createdByActivityName —— 正在创建该窗口的 Activity 名称。它决定新 activity 属于哪个 activity 栈,这对分屏规则正确工作很重要。未设置时,它会自动从 manager 继承(例如从 Window 或 Webview 句柄构建时)。

iOS 选项:

  • requestedBySceneIdentifier —— 设置正在请求创建这个新 scene 的 UIScene 标识符,从而在两个 scene 之间建立关系。默认情况下系统使用前台 scene。未设置时,它会自动从 manager 继承。

语言

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import { WebviewWindow } from '@tauri-apps/api/webviewWindow';

function openDetail(id) {
  const webview = new WebviewWindow(`detail-${id}`, {
    url: `detail/${id}`,
    activityName: 'DetailActivity',
  });
  webview.once('tauri://created', () => {
    console.log('window created');
  });
  webview.once('tauri://error', (e) => {
    console.error(e);
  });
}
1
2
3
4
5
6
7
8
9

let main_window = app.get_webview_window("main").unwrap();
// 使用 main_window 实例,这样关系会被自动确定
let builder = tauri::WebviewWindowBuilder::new(main_window, "detail", tauri::WebviewUrl::App("detail/1".into()));

#[cfg(target_os = "android")]
let builder = builder.activity_name("DetailActivity");

let window = builder.build()?;

窗口实例 API

窗口创建之后,你可以获取它平台特定的标识符:

语言

1
const sceneId = await window.sceneIdentifier();
1
2
3
4
let activity = window.activity_name()?;

#[cfg(target_os = "ios")]
let scene_id = window.scene_identifier()?;
这些 getter 在创建相关窗口时用于引用某个窗口的身份很有用。例如,你可以读取某个窗口的 activityName 并把它作为新窗口的 createdByActivityName 传入,或者读取 sceneIdentifier 并作为 requestedBySceneIdentifier 传入。

最后修改 September 25, 2026: 更新 (4c0ee2db0)