1# 如何开发自适应布局 2 3## 场景说明 4开发者经常需要将一个应用适配到不同的设备上运行。为了保证用户的浏览体验,就需要根据不同设备的屏幕尺寸设计相应的UI布局变化。常见的如阅读软件,在小屏上显示一页内容,在大屏设备上就可以显示两页内容,这样才能给用户更好的阅读体验。 5针对上述场景,OpenHarmony为开发者提供了较为灵活的自适应布局能力,本文将为大家做一个简单的介绍。 6 7## 两个重要的自适应布局组件 8使用OpenHarmony进行自适应布局的开发离不开以下两个组件:GridRow、GridCol。 9- GridRow用来将屏幕等分为特定列数,并设置区分屏幕大小的临界点(breakpoints),比如可以将屏幕列数根据屏幕大小划分为:小屏设备4列,中屏8列,大屏12列(假设屏幕宽度<=520vp为小屏,520vp<屏幕宽度<=840vp为中屏,屏幕宽度>840vp为大屏)。那么就可以将520vp和840vp设置为临界点,当屏幕尺寸由[0,520vp]区间跨越到[520vp,840vp]区间时,屏幕划分的列数也会由4列变为8列。临界点的详细介绍可参考[GridRow指南](../application-dev/reference/apis-arkui/arkui-ts/ts-container-gridrow.md#breakpoints)。 10 11- GridCol是GridRow的子组件,可以根据屏幕大小设置其所覆盖的列数,那么GridCol中的子组件就可以相应的随屏幕的大小变化而变化。比如界面中有一个搜索框,我们希望其长度在小屏设备上占2列,中屏设备占4列,大屏设备占8列,那么就可以将搜索框放在一个GridCol组件中,并设置GridCol组件在小、中、大设备上所占的列数分别为2、4、8。 12 13 可参考如下GridRow和GridCol的示意图理解两者之间的关系: 14 15  16 17 18## 开发示例 19接下来用一个示例来展示GridRow和GridCol是怎么协同实现自适应布局的。 20 21### 页面元素 22页面中包含一个文本框,一个搜索框,一个滑动条。 23 24### 实现效果 25- 小屏页面时,文本框、搜索框和滚动条纵向排列,搜索框和滚动条跟屏幕等宽,滚动条一次显示一个页面。 26- 当拖动页面至中屏大小时,文本框和搜索框在一行显示,滚动条一次显示两个页面。 27 28效果图如下: 29 30 31 32### 开发步骤 33开发步骤中仅展示相关步骤代码,全量代码请参考完整代码章节的内容。 341. 使用GridRow对屏幕进行划分,本例按照不同屏幕大小进行如下划分:最小屏4列、小屏4列、中屏8列。设置最小屏到小屏,小屏到中屏两个断点(最小屏到小屏的断点本例效果没有演示) 35 ```ts 36 GridRow({ 37 // 划分屏幕:最小屏4列、小屏4列、中屏8列 38 columns:{xs: 4, sm: 4, md: 8}, 39 gutter:{x:$r('app.float.gutter_home')}, 40 // 设置最小屏到小屏,小屏到中屏两个断点 41 breakpoints: { value: ["320vp", "520vp"] } 42 }){ 43 //... 44 } 45 .height("100%") 46 .width("100%") 47 .padding({top:10,bottom:10,left:10,right:10}) 48 ``` 49 502. 补充UI元素,将文本框、搜索框、滑动条放在GridCol中,GridCol为GridRow的子组件。 51 ```ts 52 GridRow({ 53 // 划分屏幕:最小屏4列、小屏4列、中屏8列 54 columns:{xs: 4, sm: 4, md: 8}, 55 gutter:{x:$r('app.float.gutter_home')}, 56 // 设置最小屏到小屏,小屏到中屏两个断点 57 breakpoints: { value: ["320vp", "520vp"] } 58 }){ 59 60 // 文本框:首页 61 GridCol(){ 62 Row() { 63 Text("首页") 64 //... 65 } 66 } 67 .height(56) 68 .margin({ bottom: 8}) 69 70 // 搜索框 71 GridCol(){ 72 Search({placeholder:'搜索...'}) 73 //... 74 } 75 .height(48) 76 77 // 滚动条 78 GridCol(){ 79 Swiper(){ 80 //... 81 } 82 .height(144) 83 .itemSpace(12) 84 .autoPlay(true) 85 .displayCount(this.currentbp == 'md' ? 2 : 1) 86 } 87 .height(144) 88 } 89 .height('100%') 90 .width('100%') 91 .padding({top:10,bottom:10,left:10,right:10}) 92 ``` 93 943. 分别设置文本框、搜索框、滚动条所在GridCol在不同屏幕尺寸下所占的列数。 95 - 由于小屏时文本框、搜索框和滚动条纵向排列,且搜索框和滚动条跟屏幕等宽,所以可以让三个元素占满屏幕划分的列数,即4列。 96 - 中屏大小时,文本框和搜索框在一行显示,此时屏幕总共划分为8列,所以可以让文本框占2列,搜索框占6列(只要两个元素占用的列数之和小于8都可以在一行显示)。 97 - 中屏大小时,滚动条一次显示两个页面。滚动条占满8列,然后通过displayCount属性控制显示的页数。 98 99 代码如下: 100 101 ```ts 102 GridRow({ 103 // 划分屏幕:最小屏4列、小屏4列、中屏8列 104 columns:{xs: 4, sm: 4, md: 8}, 105 gutter:{x:$r('app.float.gutter_home')}, 106 // 设置最小屏到小屏,小屏到中屏两个断点 107 breakpoints: { value: ["320vp", "520vp"] } 108 }){ 109 110 // 文本框:首页 111 // 小屏sm:4列,中屏md:2列 112 GridCol({span:{ xs: 2, sm: 4, md: 2}}){ 113 Row() { 114 Text("首页") 115 //... 116 } 117 } 118 .height(56) 119 .margin({ bottom: 8}) 120 121 // 搜索框 122 // 小屏sm:4列,中屏md:6列 123 GridCol({span:{xs: 2, sm: 4, md: 6}}){ 124 Search({placeholder:'搜索...'}) 125 //... 126 } 127 .height(48) 128 129 // 滚动条 130 // 小屏sm:4列,中屏md:8列 131 GridCol({span:{ xs: 4, sm: 4, md:8 }}){ 132 Swiper(){ 133 //... 134 } 135 .height(144) 136 .itemSpace(12) 137 .autoPlay(true) 138 // 小屏时显示一个页面,中屏时显示两个页面 139 .displayCount(this.currentbp == 'md' ? 2 : 1) 140 } 141 .height(144) 142 } 143 .height('100%') 144 .width('100%') 145 .padding({top:10,bottom:10,left:10,right:10}) 146 // 获取断点 147 .onBreakpointChange((breakpoint)=>{ 148 this.currentbp = breakpoint 149 }) 150 ``` 151 152### 完整代码 153本例完整代码如下: 154示例中用到的资源请替换为开发者本地资源。 155```ts 156//AdaptiveUI.ets 157@Entry 158@Component 159export struct HomePage{ 160 @State currentbp: string = '' 161 162 build(){ 163 GridRow({ 164 // 划分屏幕:最小屏4列、小屏4列、中屏8列 165 columns:{xs: 4, sm: 4, md: 8}, 166 gutter:{x:24}, 167 // 设置最小屏到小屏,小屏到中屏两个断点 168 breakpoints: { value: ["320vp", "520vp"] } 169 }){ 170 171 // 文本框:首页 172 // 小屏sm:4列,中屏md:2列 173 GridCol({span:{ xs: 2, sm: 4, md: 2}}){ 174 Row() { 175 Text("首页") 176 .fontSize(24) 177 .fontWeight(500) 178 .width('100%') 179 .margin({ 180 top: 12, 181 bottom: 12, 182 left: 12 183 }) 184 } 185 } 186 .height(56) 187 .margin({ bottom: 8}) 188 189 // 搜索框 190 // 小屏sm:4列,中屏md:6列 191 GridCol({span:{xs: 2, sm: 4, md: 6}}){ 192 Search({placeholder:'搜索...'}) 193 .width('100%') 194 .height(40) 195 .margin({ bottom: 8}) 196 .placeholderFont({ size: 16 }) 197 } 198 .height(48) 199 200 // 滚动条 201 // 小屏sm:4列,中屏md:8列 202 GridCol({span:{ xs: 4, sm: 4, md:8 }}){ 203 Swiper(){ 204 ForEach(SwiperList, (item: CardItem) => { 205 Stack({ alignContent: Alignment.TopStart }) { 206 Image(item.img) 207 .width('100%') 208 .height('100%') 209 .borderRadius(16) 210 .objectFit(ImageFit.Cover) 211 212 Column() { 213 Text(item.title) 214 .fontSize(16) 215 .fontColor(Color.White) 216 .margin({ bottom: 4 }) 217 Text(item.info) 218 .fontSize(12) 219 .fontColor(Color.White) 220 .opacity(0.6) 221 } 222 .alignItems(HorizontalAlign.Start) 223 .margin({ top: 20, left: 14 }) 224 } 225 }, item => JSON.stringify(item)) 226 } 227 .height(144) 228 .itemSpace(12) 229 .autoPlay(true) 230 // 小屏时显示一个页面,中屏时显示两个页面 231 .displayCount(this.currentbp == 'md' ? 2 : 1) 232 } 233 .height(144) 234 235 } 236 .height('100%') 237 .width('100%') 238 .padding({top:10,bottom:10,left:10,right:10}) 239 // 获取断点 240 .onBreakpointChange((breakpoint)=>{ 241 this.currentbp = breakpoint 242 }) 243 } 244} 245``` 246 247## 参考 248- [GridRow组件的使用](../application-dev/reference/apis-arkui/arkui-ts/ts-container-gridrow.md) 249- [GridCol组件的使用](../application-dev/reference/apis-arkui/arkui-ts/ts-container-gridcol.md)