# MessageDialog - 消息弹窗

## 说明

带标题、正文和底部按钮的消息弹窗，可选 checkbox。基于 `BaseDialog`，按钮布局、遮罩、关闭行为与其他 HyperOS 弹窗一致。

## 属性

| 属性 | 说明 | 类型 | 默认值 | 必填 |
|------|------|------|--------|------|
| **visible** | 是否显示 | `boolean` | - | 是 |
| **title** | 标题 | `string` | - | 否 |
| **contentText** | 正文。为空不展示 | `string` | - | 否 |
| **label** | checkbox 文案。不传或 `'none'` 不展示 checkbox | `string` | - | 否 |
| **checkboxChecked** | checkbox 选中状态（受控） | `boolean` | `false` | 否 |
| **onCheckboxChange** | checkbox 变化回调 | `(checked: boolean) => void` | - | 否 |
| **buttons** | 底部按钮，1~3 个 | `AbstractDialogButtonType[]` | 取消按钮 | 否 |
| **canDismiss** / **cancelable** | 点遮罩 / 返回键是否可关闭 | `boolean` | - | 否 |
| **onDismiss** | 未点按钮关闭（遮罩、返回键）时回调 | `() => void` | - | 否 |
| **colorDepth** | 背景色深度 | `'base' \| 'low'` | - | 否 |
| **titleIcon** / **titleIconStyle** | 标题上方图标及其容器样式 | `ReactNode` / `StyleProp<ViewStyle>` | - | 否 |
| **hasShade** | 是否显示遮罩 | `boolean` | - | 否 |
| **accessible** | 无障碍：是否启用 | `boolean` | `true` | 否 |
| **checkboxAccessibilityLabel** / **checkboxAccessibilityHint** | checkbox 无障碍标签 / 提示 | `string` | - | 否 |

## 使用示例

```tsx
import { MessageDialog } from 'miot/ui/hyperOSUI';

<MessageDialog
  visible={visible}
  title="提示"
  contentText="检测到设备有最新固件版本2.8.0_1264.1264，是否升级？"
  buttons={[
    { title: '取消', callback: () => setVisible(false) },
    { title: '升级', colorType: 'blue', callback: onUpgrade },
  ]}
  onDismiss={() => setVisible(false)}
/>
```

## 正文对齐规则

| 场景 | 对齐方式 |
|------|----------|
| 无 checkbox，正文单行 | 居中 |
| 无 checkbox，正文多行 | 自然对齐（`textAlign: 'auto'`，LTR 语言左对齐，RTL 语言右对齐） |
| 有 checkbox | 正文与 checkbox 始终自然对齐 |

### 实现方式

- 正文外层 `ScrollView` 设置 `alignSelf: 'stretch'`，正文 `Text` 双端都撑满弹窗内容宽度。
- 通过 `Text` 的 `onTextLayout` 拿到原生排版后的实际行数 `nativeEvent.lines.length`，单行设 `textAlign: 'center'`，多行设 `'auto'`。
- 首帧默认按单行（居中）渲染，拿到行数后再切换。居中与自然对齐使用同一宽度，切换不会改变折行位置，不会来回抖动。

### 为什么这么改（MIIO-134805）

旧实现正文 `Text` 不设 `textAlign`，只靠外层 `alignItems: 'center'` 把 `Text` 收缩后整块居中。整块的宽度取决于 RN 对多行 `Text` 的测量结果，而双端测量方式不同：

| 平台 | 多行 `Text` 的测量宽度 | 旧实现表现 |
|------|------------------------|-----------|
| iOS | 最长一行的实际宽度 | 整块被居中，视觉上居中 |
| Android | 可用宽度（`ReactTextShadowNode` 多行分支用 `StaticLayout(..., width)` 并返回 `layout.getWidth()`） | 撑满整行，文字从左侧开始，视觉上左对齐 |

**纯中文多行文案测不出这个问题**：中文可以在任意字符处折行，第一行总能排满，最长行宽度就等于可用宽度，双端表现一致。只有文案中含**不可断开的长串**（版本号、英文单词、URL、型号等）导致提前折行、最长一行明显短于可用宽度时，iOS 才会整块居中、Android 左对齐。例如：

```
检测到设备有最新固件版本2.8.0_1264.1264，是否升级？
```

`2.8.0_1264.1264` 无法拆开，只能在"版本"后折行，第一行偏短，双端对齐不一致。

### 为什么用 onTextLayout 而不是 onLayout 或纯样式

- **纯样式做不到**：RN 的 `textAlign` 和原生对齐方式都没有"单行才居中"这种取值，不知道行数就无法区分。
- **onLayout + 高度阈值**（`Description` 组件的做法）会受字体缩放、行高、内边距影响，阈值可能失准。
- **onTextLayout** 直接返回原生排版的行数，RN 0.61 双端均支持。`@types/react-native` 0.61 未声明该属性，类型见 `interface.ts` 中的 `MessageTextLayoutEvent`。

### 验证用例

修改正文布局时，至少在 iOS、Android 双端验证以下文案：

1. 单行短文案：如 `确定删除吗？`，应居中。
2. 纯中文多行：应自然对齐。
3. 含长版本号或英文的多行：如上例，双端都应自然对齐，不能一端居中一端靠左。
4. 带 checkbox：正文与 checkbox 均自然对齐。

单元测试见 `__tests__/MessageDialog.test.tsx`。

## 注意事项

- 插件中 `MessageDialog` 属于 SDK 公共 bundle 模块，插件打包时不会打入，运行时使用米家 App 内置 SDK 中的版本。本次修复需随 SDK 版本发布后才在线上插件中生效。

## 修改记录

| 日期 | 版本 | 修改内容 | 修改原因 | 关联 |
|------|------|----------|----------|------|
| 2026-10-19 | 待发布 | 正文单行居中、多行自然对齐；正文容器撑满宽度，按 `onTextLayout` 行数显式设置 `textAlign` | 门锁固件升级弹窗文案含版本号长串时，iOS 正文整块居中、Android 靠左，双端不一致；根因是多行 Text 测量宽度双端不同，详见上文「为什么这么改」 | MIIO-134805 |
| 2026-05-26 | v1.18.9 | checkbox 文案关闭系统字体缩放（`allowFontScaling={false}`） | — | 50526bcc |
| 2026-05-25 | v1.18.8 | 正文关闭系统字体缩放（`allowFontScaling={false}`） | — | bd7536c6 |
| 2026-05-20 | v1.18.8 | 无正文且无 checkbox 时不渲染内容区 | — | 89004446 |
| 2026-05-13 | v1.18.7 | checkbox 顶部 16 间距只在有正文时生效 | — | 37be8b18 |
| 2026-05-12 | v1.18.7 | 移除 `align` 属性及正文默认居中 | — （该改动引入了 MIIO-134805 的双端差异） | ce96e445 |
| 2026-05-06 | v1.18.7 | 尝试将正文滚动交给子元素实现，当天撤销，恢复正文 ScrollView | — | 766cec55 |
| 2026-04-15 | v1.18.5-alpha.0 | 内部 Checkbox 改用 `onChange` 事件名，对外 `onCheckboxChange` 不变 | — | c2532717 |
| 2026-04-06 | v1.18.5-alpha.0 | 组件更名为 MessageDialog | — | fc8dee02 |
