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  ![adaptive-ui](figures/adaptive-ui.png)
16
17
18## 开发示例
19接下来用一个示例来展示GridRow和GridCol是怎么协同实现自适应布局的。
20
21### 页面元素
22页面中包含一个文本框,一个搜索框,一个滑动条。
23
24### 实现效果
25- 小屏页面时,文本框、搜索框和滚动条纵向排列,搜索框和滚动条跟屏幕等宽,滚动条一次显示一个页面。
26- 当拖动页面至中屏大小时,文本框和搜索框在一行显示,滚动条一次显示两个页面。
27
28效果图如下:
29
30![adaptive-ui1](figures/adaptive-ui1.gif)
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)