鸿蒙 HarmonyOS 6 | Pura X Max 鸿蒙原生适配 19:设置页在 Pura X Max 上改成分组布局
前言设置页很容易被写成一条长列表。账号、通知、权限、缓存、关于应用全都从上到下排。手机外屏上这么写没有太大问题用户打开设置页以后从顶部一路往下找看到需要的设置项就点进去。设置项数量不多时单列列表甚至是最省事的写法。我把同样的设置页放到 Pura X Max 展开态里看时第一眼注意到的是设置项被拉得太长。左侧是设置标题右侧是开关、状态或者跳转入口中间被屏幕宽度拉开了一大段。用户看相机权限时视线要从左侧标题扫到右侧状态看版本号时也要在一条很长的横向区域里找到对应值。单个设置项还能读整页的分组关系却变弱了。设置页和搜索页、列表页的使用方式不一样。搜索页会频繁调整条件列表页主要用来浏览内容设置页更像一个低频但要快速定位的功能入口集合。用户不一定每天打开设置页但一旦打开通常是带着明确目的进来的比如改通知、看权限、清理缓存、查看版本、打开某个开关。这个时候分类定位比单纯把列表拉宽更有价值。这类设置页通常会包含几组内容。基础设置比如显示方式、通知提醒、自动保存权限设置比如相机、相册、麦克风、通知权限数据设置比如缓存、同步、导入导出关于应用比如版本号、隐私协议、用户协议、反馈入口Pura X Max 展开态空间足够把设置页拆成左侧分类和右侧设置项外屏和较窄窗口继续保留单列设置列表。鸿蒙里的全屏、分屏、自由窗口都会改变应用可用宽度设置页如果只把手机端长列表拉宽很快会遇到分类弱、行距远、定位慢这些问题。我这次用一个设置页示例来验证这种改法。页面包含基础设置、权限设置、关于应用三个分类。小屏下仍然按分组从上到下展示展开态下左侧显示分类卡片右侧只展示当前分类下的设置项。这样用户在大屏里可以先选分类再看具体设置不需要在一条很长的设置列表里不断向下扫。一、设置页不能只拉宽1.1 外屏单列适合连续浏览外屏下设置页用单列结构是合理的。设置项按分组向下排列用户进入页面以后从顶部看到基础设置再往下看到权限设置、关于应用。屏幕窄单列列表能保证每个设置项都有足够的横向空间标题、说明、右侧状态或开关都能放得下。最常见的写法是这样的。Column() { this.SettingSection(基础设置) this.SettingSection(权限设置) this.SettingSection(关于应用) } .width(100%)这个结构维护起来也简单。每个分组是一段列表新增设置项时继续往对应分组里加。手机外屏上用户滚动一下就能看到所有设置内容页面也不会被左右拆开。我以前做手机端设置页时也会先用这种结构。设置页不是高频操作页单列列表的可理解性很好。只要分组标题明确设置项说明写得具体小屏体验通常不会出大问题。这个时候没有必要为了大屏思维提前把页面拆复杂。1.2 展开态单列会拉长信息路径展开态里继续使用单列问题会换一种方式出现。每一行设置项被拉得很宽右侧开关或状态离左侧标题很远。用户看通知提醒时标题在左开关在右看缓存清理时说明在左操作入口在右。单行看起来没有错但整页的阅读路径被横向拉长了。设置页在大屏里更需要分类定位。用户进来以后应该先看到基础设置、权限设置、关于应用这些方向再进入某一组具体设置。比如我想检查相机权限就不该从通知提醒、自动保存、深色模式一路扫过去我想看版本和隐私协议也不需要穿过所有基础设置。我会把大屏设置页拆成三个区域关系进行分析。区域小屏处理展开态处理主要目的设置分类作为分组标题嵌在列表中固定在左侧先定位方向设置项列表所有分组连续排列只展示当前分类减少纵向寻找状态与操作跟在每一行右侧保留在右侧列表内保持原有设置行为这个表格能帮我避免一个误区。展开态不一定要展示更多设置项它更应该把设置项的组织关系摆出来。左侧分类不是装饰它承担的是入口定位右侧设置项才是具体操作区域。二、先拆分类再拆设置项2.1 分类要从标题变成入口在单列设置页里基础设置、权限设置、关于应用只是分组标题。它们帮助用户理解下面的设置项属于哪一类但不会参与交互。到了展开态这些分组标题可以升级成左侧分类导航。用户点击左侧分类右侧切换对应设置项。这个变化不只是把 UI 从上下结构改成左右结构还牵涉到一个基础的状态设计。当前选中的设置分类要放在页面层因为左侧分类和右侧设置项都要读取它。左侧需要知道哪个分类高亮右侧需要知道展示哪组设置项。示例里用selectedGroupId保存当前分类。State private selectedGroupId: number 1;这个状态不能放在左侧分类组件里。左侧分类只是触发切换的入口右侧内容也需要它。把状态放在页面层以后外屏和展开态都能读同一份当前分类状态窗口宽度变化时也不会重置。这里其实是一个很常见的编程常识当两个区域都依赖同一个状态时状态应该提升到它们共同的父级。设置页左右分栏只是一个具体场景列表详情、搜索筛选、图片预览里的左右区域也会遇到同样的问题。2.2 设置项要用同一套数据我不建议为基础设置、权限设置、关于应用分别写三套 UI。它们的业务含义不同但展示结构很接近标题、说明、右侧内容、类型和状态。这样就可以抽成统一的设置项数据再根据type决定右侧展示开关、文本、按钮还是跳转状态。设置项可以先按这样的结构理解。设置项类型页面表现示例switch右侧显示开关通知提醒、自动保存link右侧显示状态或入口相机权限、相册权限value右侧显示文本值版本号、缓存大小action右侧显示操作按钮清理缓存、导入示例不同类型的设置项可以走同一个SettingRow()再根据type决定右侧展示什么。这个写法比每个分组手写一堆 Row 更适合维护。后面新增数据同步、订阅管理、隐私协议、实验功能这些内容时也只是新增数据不需要改布局结构。这里还要留意设置项说明的长度。说明文字太长小屏里会把行高撑得过高展开态里也会让右侧列表变得松散。设置项说明只解释当前设置的影响就够了完整帮助文档不要放进设置行。三、分栏前先保住右侧3.1 分类栏不能抢设置项宽度设置页的左侧分类栏不需要太宽但右侧设置项列表必须能读。展开态分栏如果只看窗口是否超过某个阈值可能会出现左侧分类栏出现了右侧设置项却被压得很窄。标题、说明、开关、状态挤在一行里页面看起来进入了大屏结构实际操作并没有变轻松。我会先给左侧分类栏和右侧设置项列表分别留宽度。示例里左侧分类栏是 260vp右侧设置项列表至少保留 560vp中间间距是 16vp。进入双栏前先计算这些区域是否真的放得下。private readonly groupPanelWidth: number 260; private readonly detailMinWidth: number 560; private readonly twoColumnGap: number 16;这些数字不用照搬。设置项比较短时右侧 520vp 也可以如果设置项说明更多或者右侧同时出现开关、状态、按钮右侧最小宽度就要提高。大屏适配不该看到宽度变大就马上分栏分栏前要先确认主操作区域还能正常阅读和点击。3.2 可用宽度比屏幕宽度更有用示例里的判断会先扣掉左右 padding再计算左侧分类栏、右侧详情区和中间间距。private canUseSplitSettings(): boolean { const width this.getEffectiveWidth(); const availableWidth width - this.getPagePadding() * 2; const requiredWidth this.groupPanelWidth this.twoColumnGap this.detailMinWidth; return width this.expandedThreshold availableWidth requiredWidth; }这里有个常见误区很多布局判断会直接拿窗口宽度和阈值比较。这个写法短期能用但只要页面加了左右 padding、卡片间距、侧栏宽度就容易在中间尺寸出问题。设置页这种页面尤其容易被忽略因为它看起来只是普通列表实际右侧每一行都有标题、说明、开关或状态不能被压得太窄。我会把canUseSplitSettings()放在页面层。页面层负责判断采用单列还是双栏左侧分类组件只负责分类展示右侧设置项列表只负责当前分类的设置项。这样组件职责会更清楚后面新增设置分类时也不会影响断点逻辑。四、实际运行结果4.1 外屏先保留单列列表这个示例会模拟一个设置页。小屏下基础设置、权限设置、关于应用三个分组按顺序排列用户从上往下滚动。展开态下左侧显示三个设置分类右侧显示当前分类下的设置项。点击左侧分类时右侧内容切换。小屏状态下我会继续保留单列列表。原因很直接设置项虽然多但屏幕宽度有限强行做左右结构会让两边都很窄。用户在小屏里从上往下浏览不会因为多一次滚动就失去方向。只要分组标题足够清楚单列结构可以继续使用。4.2 展开态再把分类放左侧展开态截图要看左侧分类和右侧设置项之间的关系。左侧分类高亮当前分类右侧只显示当前分类下的设置项。这样用户不需要在长列表里继续寻找权限、关于应用或者缓存设置。这个示例里还保留了几个开关状态比如通知提醒、自动保存、深色模式。真实项目里这些状态可以来自本地持久化配置权限类设置则要来自系统权限状态关于应用里的版本号、协议入口、反馈入口可以来自应用配置。五、真实项目时怎么处理5.1 分类和设置项最好来自配置示例里的设置组和设置项写在页面里是为了让代码可以直接运行。真实项目里设置项通常会随着版本持续增加比如订阅、数据同步、隐私、缓存、实验功能。继续把所有设置行写死在页面里后面会越来越难维护。我会把设置页拆成配置数据分类配置负责标题、说明、图标和排序设置项配置负责标题、说明、类型、右侧文案、点击动作页面状态负责当前选中分类和开关状态具体业务逻辑交给对应服务处理这样设置页的 UI 结构会更稳定。新增一个设置项时优先改配置新增一个业务动作时再补对应处理函数。页面本身不需要因为每个设置项都去加一段重复代码。5.2 权限设置要接真实状态权限设置是设置页里比较特殊的一类。示例里用去开启、已授权这类文案模拟状态真实项目里要接系统权限查询结果。相机、相册、麦克风、通知这些权限用户可能在系统设置里改掉回到应用后页面要能刷新状态。这个地方不能只靠本地开关模拟。权限状态应该来自系统能力或应用启动后的检查结果设置页只负责展示和跳转。比如用户点击相机权限页面可以跳到授权引导或系统权限设置用户返回后再刷新当前权限状态。5.3 关于应用单独成组关于应用这类信息经常被随手放到设置页底部。手机上这么放可以接受展开态里如果继续和基础设置混在一起用户会在通知开关、自动保存、缓存清理这些设置项之间找版本号和协议入口。我会把关于应用单独做成一个分类。版本号、隐私政策、用户协议、反馈入口、开发者信息都放到同一个分类下。这样右侧区域展示时也更清楚用户不会在一条很长的设置列表里找这些低频但重要的入口。总结设置页在 Pura X Max 展开态里不一定要继续做成长单列。外屏下单列设置列表适合连续浏览展开态里把分类放到左侧、设置项放到右侧用户更容易先定位方向再处理具体设置。我后面处理设置页时会先把内容按这几类拆开基础设置放常用开关比如通知、自动保存、深色模式。权限设置放相机、相册、麦克风、通知权限并接真实权限状态。关于应用放版本号、协议、反馈和开发者信息。数据和缓存类设置可以单独成组不要混在基础设置里。展开态是否分栏要先确认右侧设置项区域还有足够宽度。真实项目里设置页会随着版本持续增长。分类和设置项最好配置化页面层只负责选中分类、布局判断和状态分发。这样外屏单列、展开态双栏都能读同一套数据后面新增设置项时也不会把页面写成越来越长的一整段 UI。附完整代码interface SettingGroup { id: number; title: string; desc: string; badge: string; } interface SettingItem { id: number; groupId: number; title: string; desc: string; type: string; value: string; key: string; } Entry Component struct Index { // 页面真实宽度由 onAreaChange 写入 State private pageWidth: number 0; // 演示宽度只用于在同一个模拟器里观察外屏和展开态 State private previewWidth: number 0; // 展开态左侧选中的设置分类 State private selectedGroupId: number 1; // 模拟几个设置项状态真实项目里可以来自持久化配置或系统权限查询 State private notifyEnabled: boolean true; State private autoSaveEnabled: boolean true; State private darkModeEnabled: boolean false; State private cacheClearedCount: number 0; private readonly expandedThreshold: number 860; private readonly groupPanelWidth: number 260; private readonly detailMinWidth: number 560; private readonly twoColumnGap: number 16; private readonly groups: SettingGroup[] [ { id: 1, title: 基础设置, desc: 通知、显示和保存偏好, badge: 常用 }, { id: 2, title: 权限设置, desc: 相机、相册和麦克风权限, badge: 权限 }, { id: 3, title: 关于应用, desc: 版本、协议和反馈入口, badge: 信息 } ]; private readonly items: SettingItem[] [ { id: 1, groupId: 1, title: 通知提醒, desc: 处理结果保存后按提醒时间发送通知, type: switch, value: , key: notify }, { id: 2, groupId: 1, title: 自动保存, desc: 识别结果确认后自动保存到本地记录, type: switch, value: , key: autoSave }, { id: 3, groupId: 1, title: 深色模式, desc: 跟随系统外观夜间查看内容时减少刺眼背景, type: switch, value: , key: darkMode }, { id: 4, groupId: 1, title: 清理缓存, desc: 清理临时缩略图和识别过程缓存, type: action, value: 清理, key: cache }, { id: 5, groupId: 2, title: 相机权限, desc: 用于拍摄通知、票据和白板照片, type: link, value: 去开启, key: camera }, { id: 6, groupId: 2, title: 相册权限, desc: 用于从相册选择已有图片进行整理, type: link, value: 已授权, key: album }, { id: 7, groupId: 2, title: 麦克风权限, desc: 用于后续语音整理和会议内容识别, type: link, value: 去开启, key: microphone }, { id: 8, groupId: 2, title: 通知权限, desc: 用于发送待办提醒和处理结果提醒, type: link, value: 已授权, key: push }, { id: 9, groupId: 3, title: 当前版本, desc: 查看当前安装的应用版本, type: value, value: 1.0.0, key: version }, { id: 10, groupId: 3, title: 隐私政策, desc: 查看数据存储、权限使用和第三方服务说明, type: link, value: 查看, key: privacy }, { id: 11, groupId: 3, title: 用户协议, desc: 查看应用使用条款和免责声明, type: link, value: 查看, key: terms }, { id: 12, groupId: 3, title: 问题反馈, desc: 提交使用过程中遇到的问题或建议, type: link, value: 反馈, key: feedback } ]; // Demo 中优先使用演示宽度真实项目里可以直接返回 pageWidth private getEffectiveWidth(): number { if (this.previewWidth 0) { return this.previewWidth; } return this.pageWidth; } private getPagePadding(): number { if (this.getEffectiveWidth() this.expandedThreshold) { return 24; } return 16; } // 分栏前先确认左侧分类栏、间距和右侧设置项区域都能放下 private canUseSplitSettings(): boolean { const width this.getEffectiveWidth(); const availableWidth width - this.getPagePadding() * 2; const requiredWidth this.groupPanelWidth this.twoColumnGap this.detailMinWidth; return width this.expandedThreshold availableWidth requiredWidth; } private isExpanded(): boolean { return this.canUseSplitSettings(); } private getContentWidth(): Length { if (this.previewWidth 0) { return this.previewWidth; } return 100%; } private getTitleSize(): number { return this.isExpanded() ? 28 : 23; } private getModeText(): string { return this.isExpanded() ? expanded · 分类 设置项 : compact · 单列设置; } private getModeDesc(): string { if (this.isExpanded()) { return 展开态下左侧显示设置分类右侧显示当前分类下的设置项。; } return 小屏下设置项按分组从上到下排列保持普通设置列表结构。; } private setPreview(width: number) { this.previewWidth width; } private getSelectedGroup(): SettingGroup { const found this.groups.find((item: SettingGroup) item.id this.selectedGroupId); return found ? found : this.groups[0]; } private getItemsByGroup(groupId: number): SettingItem[] { return this.items.filter((item: SettingItem) item.groupId groupId); } private isSwitchOn(key: string): boolean { if (key notify) { return this.notifyEnabled; } if (key autoSave) { return this.autoSaveEnabled; } if (key darkMode) { return this.darkModeEnabled; } return false; } private toggleSwitch(key: string) { if (key notify) { this.notifyEnabled !this.notifyEnabled; } else if (key autoSave) { this.autoSaveEnabled !this.autoSaveEnabled; } else if (key darkMode) { this.darkModeEnabled !this.darkModeEnabled; } } private handleAction(key: string) { if (key cache) { this.cacheClearedCount 1; } } Builder private PreviewButton(text: string, width: number) { Text(text) .fontSize(12) .fontColor(this.previewWidth width ? #FFFFFF : #2F8F83) .textAlign(TextAlign.Center) .padding({ left: 10, right: 10, top: 7, bottom: 7 }) .backgroundColor(this.previewWidth width ? #2F8F83 : #E6F4F1) .borderRadius(999) .onClick(() { this.setPreview(width); }) } Builder private HeaderPanel() { Column({ space: 10 }) { Row({ space: 10 }) { Column({ space: 4 }) { Text(设置页在 Pura X Max 上改成分组布局) .fontSize(this.getTitleSize()) .fontWeight(FontWeight.Bold) .fontColor(#111827) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text(this.getModeText()) .fontSize(14) .fontColor(#2F8F83) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) } .layoutWeight(1) Text(窗口 Math.round(this.pageWidth).toString() vp) .fontSize(12) .fontColor(#374151) .padding({ left: 10, right: 10, top: 6, bottom: 6 }) .backgroundColor(#FFFFFF) .borderRadius(999) } .width(100%) Text(演示宽度 Math.round(this.getEffectiveWidth()).toString() vp。 this.getModeDesc()) .fontSize(14) .fontColor(#6B7280) .lineHeight(21) .maxLines(2) .textOverflow({ overflow: TextOverflow.Ellipsis }) Row({ space: 8 }) { this.PreviewButton(自动, 0) this.PreviewButton(外屏, 430) this.PreviewButton(展开态, 1040) } .width(100%) } .width(100%) } Builder private GroupBadge(text: string, selected: boolean) { Text(text) .fontSize(11) .fontColor(selected ? #FFFFFF : #2F8F83) .padding({ left: 7, right: 7, top: 3, bottom: 3 }) .backgroundColor(selected ? #33FFFFFF : #E6F4F1) .borderRadius(999) } Builder private GroupCard(item: SettingGroup) { Column({ space: 8 }) { Row() { Text(item.title) .fontSize(16) .fontWeight(FontWeight.Medium) .fontColor(this.selectedGroupId item.id ? #FFFFFF : #111827) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) Blank() this.GroupBadge(item.badge, this.selectedGroupId item.id) } .width(100%) Text(item.desc) .fontSize(13) .fontColor(this.selectedGroupId item.id ? #DFF5F1 : #6B7280) .lineHeight(19) .maxLines(2) .textOverflow({ overflow: TextOverflow.Ellipsis }) } .width(100%) .padding(14) .backgroundColor(this.selectedGroupId item.id ? #2F8F83 : #FFFFFF) .borderRadius(20) .border({ width: 1, color: this.selectedGroupId item.id ? #2F8F83 : #E5E7EB }) .onClick(() { this.selectedGroupId item.id; }) } Builder private GroupPanel() { Column({ space: 14 }) { Column({ space: 4 }) { Text(设置分类) .fontSize(20) .fontWeight(FontWeight.Bold) .fontColor(#111827) Text(先选分类再看对应设置项) .fontSize(13) .fontColor(#6B7280) } .width(100%) ForEach(this.groups, (item: SettingGroup) { this.GroupCard(item) }, (item: SettingGroup) item.id.toString()) } .width(100%) .height(100%) .padding(16) .backgroundColor(#FFFFFF) .borderRadius(26) .shadow({ radius: 12, color: #10000000, offsetX: 0, offsetY: 4 }) } Builder private SwitchView(key: string) { Row() { if (this.isSwitchOn(key)) { Blank() Circle() .width(22) .height(22) .fill(#FFFFFF) .margin({ right: 3 }) } else { Circle() .width(22) .height(22) .fill(#FFFFFF) .margin({ left: 3 }) Blank() } } .width(48) .height(28) .backgroundColor(this.isSwitchOn(key) ? #2F8F83 : #CBD5E1) .borderRadius(14) .onClick(() { this.toggleSwitch(key); }) } Builder private RightContent(item: SettingItem) { if (item.type switch) { this.SwitchView(item.key) } else if (item.type action) { Text(item.value) .fontSize(13) .fontColor(#2F8F83) .padding({ left: 10, right: 10, top: 6, bottom: 6 }) .backgroundColor(#E6F4F1) .borderRadius(999) .onClick(() { this.handleAction(item.key); }) } else { Text(item.value) .fontSize(13) .fontColor(item.type value ? #6B7280 : #2F8F83) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) } } Builder private SettingRow(item: SettingItem) { Row({ space: 12 }) { Column({ space: 4 }) { Text(item.title) .fontSize(16) .fontWeight(FontWeight.Medium) .fontColor(#111827) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text(item.desc) .fontSize(13) .fontColor(#6B7280) .lineHeight(20) .maxLines(this.isExpanded() ? 2 : 3) .textOverflow({ overflow: TextOverflow.Ellipsis }) } .layoutWeight(1) this.RightContent(item) } .width(100%) .padding(15) .backgroundColor(#FFFFFF) .borderRadius(20) .border({ width: 1, color: #E5E7EB }) } Builder private SettingSection(group: SettingGroup) { Column({ space: 12 }) { Row() { Column({ space: 4 }) { Text(group.title) .fontSize(18) .fontWeight(FontWeight.Bold) .fontColor(#111827) Text(group.desc) .fontSize(13) .fontColor(#6B7280) } .layoutWeight(1) this.GroupBadge(group.badge, false) } .width(100%) .padding({ left: 4, right: 4 }) ForEach(this.getItemsByGroup(group.id), (item: SettingItem) { this.SettingRow(item) }, (item: SettingItem) item.id.toString()) } .width(100%) } Builder private DetailPanel() { Column({ space: 14 }) { Row() { Column({ space: 4 }) { Text(this.getSelectedGroup().title) .fontSize(22) .fontWeight(FontWeight.Bold) .fontColor(#111827) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text(this.getSelectedGroup().desc) .fontSize(13) .fontColor(#6B7280) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) } .layoutWeight(1) if (this.cacheClearedCount 0) { Text(清理 this.cacheClearedCount.toString() 次) .fontSize(12) .fontColor(#6B7280) } } .width(100%) .padding({ left: 4, right: 4 }) Scroll() { Column({ space: 12 }) { ForEach(this.getItemsByGroup(this.selectedGroupId), (item: SettingItem) { this.SettingRow(item) }, (item: SettingItem) item.id.toString()) } .width(100%) .padding({ bottom: 24 }) } .layoutWeight(1) .width(100%) .edgeEffect(EdgeEffect.Spring) } .width(100%) .height(100%) .padding(18) .backgroundColor(#FFFFFF) .borderRadius(26) .shadow({ radius: 12, color: #10000000, offsetX: 0, offsetY: 4 }) } Builder private CompactSettingsList() { Scroll() { Column({ space: 22 }) { ForEach(this.groups, (group: SettingGroup) { this.SettingSection(group) }, (group: SettingGroup) group.id.toString()) } .width(100%) .padding({ bottom: 24 }) } .layoutWeight(1) .width(100%) .edgeEffect(EdgeEffect.Spring) } Builder private MainContent() { if (this.isExpanded()) { Row({ space: this.twoColumnGap }) { Column() { this.GroupPanel() } .width(this.groupPanelWidth) .height(100%) .flexShrink(0) Column() { this.DetailPanel() } .layoutWeight(1) .height(100%) } .width(100%) .height(100%) } else { this.CompactSettingsList() } } build() { Column() { Column({ space: 16 }) { this.HeaderPanel() this.MainContent() } .width(this.getContentWidth()) .height(100%) .padding({ left: this.getPagePadding(), right: this.getPagePadding(), top: 18, bottom: 16 }) } .width(100%) .height(100%) .alignItems(HorizontalAlign.Center) .backgroundColor(#F6F7F9) .onAreaChange((_: Area, newValue: Area) { const width Number(newValue.width); if (!Number.isNaN(width) width 0) { this.pageWidth width; } }) } }