# UIResponsive **Repository Path**: MoistenT/ui-responsive ## Basic Information - **Project Name**: UIResponsive - **Description**: 鸿蒙多设备页面布局适配 - **Primary Language**: JavaScript - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2025-05-16 - **Last Updated**: 2025-05-29 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 页面布局多设备适配 ## 响应式逻辑示意图 ```mermaid graph TD A[设备尺寸变化] --> B{断点判断} B -->|sm| C[手机竖向布局] B -->|md| D[折叠屏展开态竖向布局] B -->|lg| E[平板横向布局] ``` ## 首页信息流布局适配 ### 阶段一:基础框架搭建 #### 基础设置 - 设备断点值的初次获取和更新:在 windowStage.loadContent()页面加载后使用 UIContext 提供的 getWindowWidthBreakpoint 和 getWindowHeightBreakpoint API获取设备横向和纵向断点值,添加 onWindowSizeChange 监听回调,当窗口尺寸变化时更新横向和纵向断点值,获取的端点值使用 AppStorage 保存。 ```arkts // EntryAbility.ets onWindowStageCreate(windowStage: window.WindowStage): void { // ... windowStage.loadContent('pages/Index', (err) => { // ... windowStage.getMainWindow().then((data: window.Window) => { this.uiContext = data.getUIContext(); // 初次获取断点值 let widthBp: WidthBreakpoint = this.uiContext.getWindowWidthBreakpoint(); let heightBp: HeightBreakpoint = this.uiContext.getWindowHeightBreakpoint(); AppStorage.setOrCreate('currentWidthBreakpoint', widthBp); AppStorage.setOrCreate('currentHeightBreakpoint', heightBp); // 添加窗口尺寸变化监听 data.on('windowSizeChange', this.onWindowSizeChange); }); }); } // 窗口尺寸变化更新断点值 private onWindowSizeChange: (windowSize: window.Size) => void = (windowSize: window.Size) => { let widthBp: WidthBreakpoint = this.uiContext!.getWindowWidthBreakpoint(); AppStorage.setOrCreate('currentWidthBreakpoint', widthBp); let heightBp: HeightBreakpoint = this.uiContext!.getWindowHeightBreakpoint(); AppStorage.setOrCreate('currentHeightBreakpoint', heightBp); }; ``` - 设计断点工具类:根据应用的一多需求,设计工具类BreakpointType,此处首页信息流场景仅需适配手机、折叠屏和平板设备,所以BreakpointType工具类覆盖三个横向断点sm/md/lg。 ```arkts // utils/BreakpointType.ets export class BreakpointType { // 定义三个断点类型:sm(手机)、md(折叠屏)、lg(平板) sm: T; md: T; lg: T; constructor(sm: T, md: T, lg: T) { this.sm = sm; this.md = md; this.lg = lg; } // 根据当前断点值获取对应配置 getValue(currentWidthBreakpoint: WidthBreakpoint): T { switch (currentWidthBreakpoint) { case WidthBreakpoint.WIDTH_SM: return this.sm; case WidthBreakpoint.WIDTH_MD: return this.md; case WidthBreakpoint.WIDTH_LG: return this.lg; default: return this.sm; } } } ``` - 路由管理:通过 Navigation 组件统一管理多页面路由,支持后续扩展更多页面。 #### 页面线框布局搭建 - Tabs导航框架: 使用Tabs组件实现应用Tabs导航,预留首页、动态、会员购、我的四个标签页位置,导航栏默认位于底部(BarPosition.End) ```arkts // pages/Index.ets Tabs({ barPosition: BarPosition.End }) { TabContent() { HomeView() }.tabBar('首页') // ...其他标签页 } ``` - 首页整体布局:首页使用Stack组件实现顶部固定区域与滚动区域的叠加布局。顶部固定区域使用Flex布局,包含二级菜单导航和搜索功能区;滚动区域使用Scroll嵌套Column布局,包含轮播图、内容分类和视频图文列表。 ```arkts // home/Index.ets Stack({ alignContent: Alignment.TopStart }) { // 滚动区域 Scroll() { Column({ space: 10 }) { BannerBar() ContentTabs() VideoList() } .width('100%') } .width('100%') .height('100%') // 顶部固定区域 Flex() { MenuBar() SearchBar() } .width('100%') .padding({ left: 12, right: 12 }) } ``` - 各功能区组件线框搭建:模块化创建各功能区组件,每个组件仅实现外层容器,并使用背景色和边框样式进行区域占位,通过文字标识各个区域用途。 ```arkts // home/view/MenuBar.ets @Component export struct MenuBar { build() { Row() { Text('二级导航菜单区域') .fontSize(16) } .width('100%') .height(40) .backgroundColor('#F1F3F5') .border({ width: 1, color: '#CCCCCC' }) } } // home/view/SearchBar.ets @Component export struct SearchBar { build() { Row() { Text('搜索功能区') .fontSize(16) } .width('100%') .height(40) .backgroundColor('#F1F3F5') } } // 其他功能区组件类似实现... ``` #### 线框效果图 首页信息流线框图 ### 阶段二:模块化组件实现 #### Tab导航实现 - 断点值获取和监听: - 断点值通过AppStorage全局管理,页面和组件中使用@StorageLink初始化获取设备横向断点值以及监听横向断点变化。 - Tabs组件的barPosition属性根据断点调整Tab导航栏的位置: - sm/md:barPosition设置为BarPosition.End,Tab导航栏位于底部,适合竖屏操作习惯。 - lg:barPosition设置为BarPosition.Start,Tab导航栏位于左侧,适合横屏大屏操作。 - Tabs组件的vertical属性根据断点切换: - sm/md:vertical为false,Tab导航栏横向排列。 - lg:vertical为true,Tab导航栏纵向排列。 - TabBar自定义: - 通过tabBuilder自定义TabBar内容,支持图标和文字高亮,点击切换。 - TabBar样式和布局可根据断点调整(如图标大小、字体、间距等)。 #### 菜单导航实现 - 断点值获取和监听: - 使用@StorageLink监听设备横向断点值变化,根据断点值调整菜单显示内容。 - List组件的menuItems属性根据断点调整显示数量: - sm:使用slice(0, 5)截取前5个菜单项显示,其余通过更多按钮展开。 - md/lg:显示完整menuItems数组内容,支持水平滚动。 #### 搜索功能区实现 - 断点值获取和监听: - 使用@StorageLink监听设备横向断点值变化,根据断点值调整布局结构。 - GridRow组件的columns属性根据断点调整列数: - sm:设置columns为6,搜索区域占6列,实现上下布局。 - md:设置columns为12,搜索区域占6列,实现左右布局。 - lg:设置columns为12,搜索区域占4列,实现左右布局。 - TextInput组件的width属性根据断点调整: - sm:使用BreakpointType设置宽度为280vp。 - md/lg:使用BreakpointType设置宽度为240vp。 #### Banner轮播图实现 - 断点值获取和监听: - 使用@StorageLink监听设备横向断点值变化,根据断点值调整轮播图样式。 - Swiper组件的height属性根据断点调整: - sm/md:使用BreakpointType设置高度为280vp。 - lg:使用BreakpointType设置高度为320vp。 - Indicator组件的right属性根据断点调整: - sm:使用BreakpointType设置right为undefined保持居中。 - md/lg:使用BreakpointType设置right为30vp。 #### 视频内容区实现 - 断点值获取和监听: - 使用@StorageLink监听设备横向断点值变化,根据断点值调整网格布局。 - Grid组件的columnsTemplate属性根据断点调整: - sm:使用BreakpointType设置为'1fr 1fr'实现双列布局。 - md:使用BreakpointType设置为'1fr 1fr 1fr'实现三列布局。 - lg:使用BreakpointType设置为'1fr 1fr 1fr 1fr'实现四列布局。 - padding属性根据断点调整: - sm:使用BreakpointType设置左右边距为12vp。 - md:使用BreakpointType设置左右边距为20vp。 - lg:使用BreakpointType设置左右边距为24vp。 ## 搜索页适配实现 ### 阶段一:页面线框布局搭建 #### 线框效果图 首页信息流线框图 ### 阶段二:模块化组件实现 #### 搜索历史区实现 - 标签自动换行响应式适配:采用Flex布局,设置FlexWrap.Wrap,当历史项较多时自动换行,保证每行内容不会溢出,适应不同屏幕宽度。 - 标签右侧margin根据断点自适应: - sm:margin较小,适合紧凑排列。 - md:margin适中,标签间距更舒适。 - lg:margin较大,标签分布更稀疏。 #### 搜索发现区实现 - List条目数量根据断点自适应: - sm/md:设置只显示前6项,避免内容过多导致拥挤。 - lg:显示全部条目,充分利用大屏空间。 - List列数属性 lanes 根据断点自适应: - sm/md:2列,内容分布均匀。 - lg:3列,内容显示更多,分布适中。 #### 热门趋势区实现 - 热门视频内容区List高度和lanes根据断点自适应: - sm:列表高度312vp,lanes为1,适合窄屏内容分布。 - md:列表高度312vp,lanes为2,展示更多内容。 - lg:高度更大为372vp,lanes为3,适合大屏多内容并列展示。 ## IM分栏适配实现 聊天页的分栏布局主体为聊天列表页和聊天详情页两个页面,二者关系是在聊天列表页通过点击可跳转至聊天详情页。在折叠屏展开态等屏幕尺寸较大的设备上,通常会采用两个页面左右分栏的方式进行显示。 ### UX分析 根据设计稿分析,聊天页在不同设备上的分栏布局主要是以页面为单位的布局变化,页面内布局基本没有变化,因此设计图中的共性部分主要可分为以下两部分: - 带有导航栏的聊天列表页 - 聊天详情页 进一步识别线性变化: - 横向断点lg:左侧显示聊天列表页,右侧显示聊天详情页,聊天详情页占比更大。 ### 代码实现 #### 分栏布局实现 应用使用Navigation组件实现路由导航。Navigation组件能够根据窗口大小自动适配,当窗口较大时,Navigation组件会自动切换为分栏展示效果。但是当应用在不同断点设备上需要使用不同的显示模式时,可以通过设置Navigation组件的mode属性切换单页面和分栏。 当横向断点小于或等于sm时,mode值设为Stack,使用单页面显示模式;横向断点大于或等于md时,mode值设为Split,聊天列表页和聊天详情页左右分栏显示。需要注意的是使用mode控制应用分栏时,实际上控制的是导航页与子页面分栏,所以聊天列表页需要直接包含在Navigation子组件中,作为导航页的内容。 ```arkts Navigation(this.pageInfos) { Tabs(){ // ... TabContent() { ChatView() } // ... } } .mode(this.currentSelectTabIndex !== 0 && ![WidthBreakpoint.WIDTH_SM, WidthBreakpoint.WIDTH_XS].includes(this.currentWidthBreakpoint) ? NavigationMode.Split : NavigationMode.Stack) ``` 在设备横向断点大于md时应用都是分栏状态,还需要根据断点进一步设置Navigation组件的navBarWidth属性调整导航页和子页面,即聊天列表页和聊天详情页的分布比例。当横向断点小于lg时,navBarWidth设置为50%,即聊天列表页和聊天详情页1:1分布;当横向断点大于等于lg时,navBarWidth设置为44.5%,即聊天列表页和Tab导航占据44.5%,聊天详情页占据剩余的55.5%。 ```arkts Navigation(this.pageInfos) { Tabs(){ // ... TabContent() { ChatView() } // ... } } .navBarWidth([WidthBreakpoint.WIDTH_LG, WidthBreakpoint.WIDTH_XL].includes(this.currentWidthBreakpoint) ? '44.5%' : '50%') ``` #### 路由控制实现 当应用从其他不分栏的Tab页签进入聊天页时,如果此时横向断点值大于等于md,应用可以在路由栈中push一个占位页面,代替聊天详情页与聊天列表页分栏显示。 ```arkts Navigation(this.pageInfos) { Tabs(){ // ... } } .onAnimationEnd((index: number, event: TabsAnimationEvent) => { if (index === 1 && ![WidthBreakpoint.WIDTH_XS, WidthBreakpoint.WIDTH_SM].includes(this.currentWidthBreakpoint)) { this.pageInfos.pushPath({ name: 'ConversationDetailNone' }); } }) ``` 由于折叠屏展开与折叠时横向断点会发生变化,所以需要在聊天列表页中监听横向断点值的变化,根据变化后的横向断点结合路由栈的大小决定占位页面是否显示。当应用的横向断点变化后超过sm且路由栈大小为0,说明变化前设备处于单页面模式且显示聊天列表页,则需要往路由栈中添加占位页面;当应用的横向断点变化后小于或等于sm且路由栈大小为1,说明变化前设备处于分栏模式且显示聊天列表页和占位页面,则需要清空路由栈,移除占位页面。 ```arkts @StorageLink('currentWidthBreakpoint') @Watch('currentWidthBreakpointChange') currentWidthBreakpoint: WidthBreakpoint = WidthBreakpoint.WIDTH_SM; currentWidthBreakpointChange(){ if (![WidthBreakpoint.WIDTH_XS, WidthBreakpoint.WIDTH_SM].includes(this.currentWidthBreakpoint) && this.pageInfos.size() === 0) { this.pageInfos.pushPath({ name: 'ConversationDetailNone' }); } else if([WidthBreakpoint.WIDTH_XS, WidthBreakpoint.WIDTH_SM].includes(this.currentWidthBreakpoint) && this.pageInfos.size() === 1) { this.pageInfos.clear(); } } ``` 应用处于单页面显示和分栏显示时,聊天详情页的返回按钮点击后执行的操作也有所不同。这里可以在返回按钮的点击事件中,根据设置分栏的断点条件判断当前应用处于单页面模式还是分栏模式。当横向断点小于或等于sm时,应用处于单页面模式,点击返回按钮后使用Navigation绑定的NavPathStack实例pageInfos清空路由,回到聊天列表页;横向断点大于或等于md时,应用处于分栏模式,点击返回按钮时先清空路由,再推入占位页面与聊天列表页分栏显示。 ```arkts StandardIcon({ icon: $r('app.media.ic_public_back') }) .onClick(() => { if ([WidthBreakpoint.WIDTH_SM, WidthBreakpoint.WIDTH_XS].includes(this.currentWidthBreakpoint)) { this.pageInfos.clear(); } else { this.pageInfos.clear(); this.pageInfos.pushPath({ name: 'ConversationDetailNone' }); } ``` ## 图文分栏适配实现 图文分栏布局是指在不同尺寸设备上,图片和文字的布局关系随屏幕尺寸变化而变化。在横向断点≤sm设备上通常采用上下布局(图片在上,文字在下),而在横向断点≥lg设备上则采用左右分栏布局(图片在左,文字在右)。 ### UX分析 根据设计稿分析,图文分栏布局主要有以下特点: - 横向断点=lg:图片和文字左右分栏排列,左侧显示图片,右侧显示文字说明 此外,在不同的布局模式下,各个组件的样式、间距、圆角等也需要进行相应调整,以适应不同的屏幕尺寸和布局方式。 ### 代码实现 #### 整体布局实现 图文分栏布局通过GridRow和GridCol组件实现栅格化的响应式布局。根据断点变化,动态调整列宽和布局结构: 1. 栅格列数设置:通过GridRow的columns属性设置断点md下的栅格列数为10列,其他断点保持默认的12列,为布局调整提供基础。 ```arkts GridRow({ columns: { md: 10, // 仅md设置为10列,其余断点使用默认的12列 }, gutter: 20 }) ``` 2. 动态调整列宽:通过GridCol的span属性设置不同断点下的宽度占比,实现布局结构变化。 - 横向断点≤sm:图片区域和文本区域各占12列宽度(100%宽度),形成上下布局 - 横向断点=md且上下布局:两个区域各占10列宽度(100%宽度),形成上下布局 - 横向断点=md且左右布局:图片区域占6列,文本区域占4列,形成左右分栏 - 横向断点≥lg:图片区域和文本区域各占6列(50%宽度),形成左右分栏 ```arkts GridRow({ columns: { md: 10, // 仅md设置为10列,其余断点使用默认的12列 }, gutter: 20 }){ // 图片区域 GridCol({ span: { xs: 12, // 横向断点xs时占满全部宽度 sm: 12, // 横向断点sm时占满全部宽度 md: this.upDownStructure ? 10 : 6, // md断点可切换上下布局或左右布局 lg: 6, // 横向断点lg时占一半宽度 xl: 6 // 横向断点xl时占一半宽度 } }) { // ... } // 文本区域 GridCol({ span: { xs: 12, // 横向断点xs时占满全部宽度 sm: 12, // 横向断点sm时占满全部宽度 md: this.upDownStructure ? 10 : 4, // md断点可切换上下布局或左右布局 lg: 6, // 横向断点lg时占一半宽度 xl: 6 // 横向断点xl时占一半宽度 } }) { // ... } } ``` 3. 高度自适应:通过WidthBreakpointType工具类设置不同断点下的高度值。 - 上下布局模式(横向断点≤sm或md选择上下布局):高度设为'auto',内容自然撑开 - 左右布局模式(横向断点≥lg或md选择左右布局):高度设为'100%',确保两个区域等高,上下充满屏幕 ```arkts GridCol({ // ... }){ // ... } .height(new WidthBreakpointType('auto', 'auto', this.upDownStructure ? 'auto' : '100%', '100%', '100%').getValue(this.currentWidthBreakpoint)) ``` #### 布局模式切换实现 针对横向断点=md的折叠屏设备,图文分栏页面提供了上下布局与左右分栏布局的切换功能。在GraphicTextHeader组件中设置了一个布局切换按钮,通过visibility属性控制其仅在md断点设备上显示,点击时切换upDownStructure变量值,根据当前布局方式显示不同图标: - 当upDownStructure为true时,显示上下布局,按钮图标显示为列表图标 - 当upDownStructure为false时,显示左右布局,按钮图标显示为网格图标 ```arkts Button() { Image(this.upDownStructure ? $r('app.media.v2_ic_public_view_list') : $r('app.media.v2_qieHuang')) .height('100%') .width('100%') } .height(24) .width(24) .visibility(this.currentWidthBreakpoint === WidthBreakpoint.WIDTH_MD ? Visibility.Visible : Visibility.None) .buttonStyle(ButtonStyleMode.TEXTUAL) .onClick(() => { this.upDownStructure = !this.upDownStructure; }) ``` #### 轮播图适配实现 轮播图组件根据横向断点和布局模式调整以下样式属性: 1. 高度设置:根据布局模式调整轮播图高度 - 上下布局模式(横向断点≤sm或md选择上下布局):高度固定为360vp - 左右布局模式(横向断点≥lg或md选择左右布局):高度设为100%,填充整个容器 ```arkts Swiper() { // ... 轮播图内容 } .indicator(Indicator.dot() // ... 指示器样式 ) .height((![WidthBreakpoint.WIDTH_LG, WidthBreakpoint.WIDTH_XL].includes(this. currentWidthBreakpoint) && this.upDownStructure) ? 360 : '100%') // 上下布局时固定高度,左右布局时高度为100% ``` 2. 图片圆角与边距:根据布局模式调整图片样式 - 上下布局模式(横向断点≤sm或md选择上下布局):无圆角,边距为0,最大化利用屏幕空间 - 左右布局模式(横向断点≥lg或md选择左右布局):圆角设为20vp,添加左右内边距,增加视觉层次感 ```arkts Image(item) .width('100%') .height('100%') .objectFit(ImageFit.Cover) .borderRadius((!this.upDownStructure || [WidthBreakpoint.WIDTH_LG, WidthBreakpoint.WIDTH_XL].includes(this.currentWidthBreakpoint)) ? 20 : 0) ``` #### 页脚组件适配实现 页脚组件(包含点赞、评论等功能按钮)根据布局模式调整显示位置和样式: 1. 位置适配:通过两个不同位置的页脚组件实例,分别为上下布局和左右布局服务 - 上下布局模式(横向断点≤sm或md选择上下布局):页脚显示在主布局底部。 ```arkts GraphicTextFooter() .visibility(([WidthBreakpoint.WIDTH_XS, WidthBreakpoint.WIDTH_SM, WidthBreakpoint.WIDTH_MD].includes(this. currentWidthBreakpoint) && this.upDownStructure) ? Visibility.Visible : Visibility.None) ``` - 左右布局模式(横向断点≥lg或md选择左右布局):页脚显示在图片区域下方 ```arkts GraphicTextFooter() .visibility((![WidthBreakpoint.WIDTH_XS, WidthBreakpoint.WIDTH_SM].includes(this.currentWidthBreakpoint) && !this. upDownStructure) ? Visibility.Visible : Visibility.None) ``` 2. 宽度与按钮排列:根据断点调整页脚内部布局 - 横向断点=lg:宽度设为50%,按钮使用SpaceBetween排列,按钮间距较小 - 其他断点:宽度设为100%,按钮使用SpaceAround排列,按钮间距较大 ```arkts .width([WidthBreakpoint.WIDTH_LG, WidthBreakpoint.WIDTH_XL].includes(this. currentWidthBreakpoint) ? '50%' : '100%') .justifyContent([WidthBreakpoint.WIDTH_LG, WidthBreakpoint.WIDTH_XL].includes (this.currentWidthBreakpoint) ? FlexAlign.SpaceBetween : FlexAlign.SpaceAround) ``` #### 文本描述区适配实现 文本描述区根据横向断点调整内边距,提供更符合不同屏幕尺寸的阅读体验: ```arkts .padding({ right: new WidthBreakpointType(16, 16, 24, 32, 32).getValue(this.currentWidthBreakpoint), left: new WidthBreakpointType(16, 16, 24, 0, 0).getValue(this.currentWidthBreakpoint) }) ```