如何使用GriRow和GriCol開發(fā)自適應(yīng)布局
場景說明
開發(fā)者經(jīng)常需要將一個應(yīng)用適配到不同的設(shè)備上運(yùn)行,比如手機(jī)、平板、折疊屏等等。為了保證用戶的瀏覽體驗,就需要根據(jù)不同設(shè)備的屏幕尺寸設(shè)計相應(yīng)的UI布局變化。常見的如閱讀軟件,在手機(jī)上顯示一頁內(nèi)容,在折疊屏上就可以顯示兩頁內(nèi)容,這樣才能給用戶更好的閱讀體驗。
針對上述場景,OpenHarmony為開發(fā)者提供了較為靈活的自適應(yīng)布局能力,本文即為大家做一個簡單的介紹。
兩個重要的自適應(yīng)布局組件
使用OpenHarmony進(jìn)行自適應(yīng)布局的開發(fā)離不開以下兩個組件:GridRow、GridCol。
- GridRow用來將屏幕等分為特定列數(shù),并設(shè)置區(qū)分屏幕大小的臨界點(diǎn)(breakpoints),比如可以將屏幕列數(shù)根據(jù)屏幕大小劃分為:小屏設(shè)備4列,中屏8列,大屏12列(假設(shè)屏幕寬度<=520vp為小屏,520vp<屏幕寬度<=840vp為中屏,屏幕寬度>840vp為大屏)。那么就可以將520vp和840vp設(shè)置為臨界點(diǎn),當(dāng)屏幕尺寸由[0,520vp]區(qū)間跨越到[520vp,840vp]區(qū)間時,屏幕劃分的列數(shù)也會由4列變?yōu)?列。臨界點(diǎn)的詳細(xì)介紹可參考GridRow指南。
- GridCol是Gridrow的子組件,可以根據(jù)屏幕大小設(shè)置其所覆蓋的列數(shù),那么GridCol中的子組件就可以相應(yīng)的隨屏幕的大小變化而變化。比如界面中有一個搜索框,我們希望其長度在小屏設(shè)備上占2列,中屏設(shè)備占4列,大屏設(shè)備占8列,那么就可以將搜索框放在一個GridCol組件中,并設(shè)置GridCol組件在小、中、大設(shè)備上所占的列數(shù)分別為2、4、8。
可參考如下GridRow和GridCol的示意圖理解兩者之間的關(guān)系:
一個小示例
接下來用一個小示例來展示GridRow和GridCol是怎么協(xié)同實(shí)現(xiàn)自適應(yīng)布局的。
頁面元素
頁面中包含一個文本框,一個搜索框,一個滑動條。
實(shí)現(xiàn)效果
- 小屏頁面時,文本框、搜索框和滾動條縱向排列,搜索框和滾動條跟屏幕等寬,滾動條一次顯示一個頁面。
- 當(dāng)拖動頁面至中屏大小時,文本框和搜索框在一行顯示,滾動條一次顯示兩個頁面。
效果圖如下:
開發(fā)步驟
接下來我們來看如何完成上述效果的開發(fā)。開發(fā)步驟中僅展示相關(guān)步驟代碼,全量代碼請參考完整代碼章節(jié)的內(nèi)容。
首先,使用GridRow對屏幕進(jìn)行劃分,本例按照不同屏幕大小進(jìn)行如下劃分:最小屏4列、小屏4列、中屏8列。設(shè)置最小屏到小屏,小屏到中屏兩個斷點(diǎn)(最小屏到小屏的斷點(diǎn)本例效果沒有演示)。
GridRow({
// 劃分屏幕:最小屏4列、小屏4列、中屏8列
columns:{xs: 4, sm: 4, md: 8},
gutter:{x:$r('app.float.gutter_home')},
// 設(shè)置最小屏到小屏,小屏到中屏兩個斷點(diǎn)
breakpoints: { value: ["320vp", "520vp"] }
}){
//...
}
.height("100%")
.width("100%")
.padding({top:10,bottom:10,left:10,right:10})
補(bǔ)充UI元素,將文本框、搜索框、滑動條放在GridCol中,GridCol為GridRow的子組件。
GridRow({
// 劃分屏幕:最小屏4列、小屏4列、中屏8列
columns:{xs: 4, sm: 4, md: 8},
gutter:{x:$r('app.float.gutter_home')},
// 設(shè)置最小屏到小屏,小屏到中屏兩個斷點(diǎn)
breakpoints: { value: ["320vp", "520vp"] }
}){
// 文本框:首頁
GridCol(){
Row() {
Text("首頁")
//...
}
}
.height(56)
.margin({ bottom: 8})
// 搜索框
GridCol(){
Search({placeholder:'搜索...'})
//...
}
.height(48)
// 滾動條
GridCol(){
Swiper(){
//...
}
.height(144)
.itemSpace(12)
.autoPlay(true)
.displayCount(this.currentbp == 'md' ? 2 : 1)
}
.height(144)
}
.height('100%')
.width('100%')
.padding({top:10,bottom:10,left:10,right:10})
分別設(shè)置文本框、搜索框、滾動條所在GridCol在不同屏幕尺寸下所占的列數(shù)。
- 由于小屏?xí)r文本框、搜索框和滾動條縱向排列,且搜索框和滾動條跟屏幕等寬,所以可以讓三個元素占滿屏幕劃分的列數(shù),即4列。
- 中屏大小時,文本框和搜索框在一行顯示,此時屏幕總共劃分為8列,所以可以讓文本框占2列,搜索框占6列(只要兩個元素占用的列數(shù)之和小于8都可以在一行顯示)。
- 中屏大小時,滾動條一次顯示兩個頁面。滾動條占滿8列,然后通過displayCount屬性控制顯示的頁數(shù)。
代碼如下:
GridRow({
// 劃分屏幕:最小屏4列、小屏4列、中屏8列
columns:{xs: 4, sm: 4, md: 8},
gutter:{x:$r('app.float.gutter_home')},
// 設(shè)置最小屏到小屏,小屏到中屏兩個斷點(diǎn)
breakpoints: { value: ["320vp", "520vp"] }
}){
// 文本框:首頁
// 小屏sm:4列,中屏md:2列
GridCol({span:{ xs: 2, sm: 4, md: 2}}){
Row() {
Text("首頁")
//...
}
}
.height(56)
.margin({ bottom: 8})
// 搜索框
// 小屏sm:4列,中屏md:6列
GridCol({span:{xs: 2, sm: 4, md: 6}}){
Search({placeholder:'搜索...'})
//...
}
.height(48)
// 滾動條
// 小屏sm:4列,中屏md:8列
GridCol({span:{ xs: 4, sm: 4, md:8 }}){
Swiper(){
//...
}
.height(144)
.itemSpace(12)
.autoPlay(true)
// 小屏?xí)r顯示一個頁面,中屏?xí)r顯示兩個頁面
.displayCount(this.currentbp == 'md' ? 2 : 1)
}
.height(144)
}
.height('100%')
.width('100%')
.padding({top:10,bottom:10,left:10,right:10})
// 獲取斷點(diǎn)
.onBreakpointChange((breakpoint)=>{
this.currentbp = breakpoint
})
完整代碼
本例完整代碼如下:
示例中用到的資源請?zhí)鎿Q為開發(fā)者本地資源。
//AdaptiveUI.ets
@Entry
@Component
export struct HomePage{
@State currentbp: string = ''
build(){
GridRow({
// 劃分屏幕:最小屏4列、小屏4列、中屏8列
columns:{xs: 4, sm: 4, md: 8},
gutter:{x:24},
// 設(shè)置最小屏到小屏,小屏到中屏兩個斷點(diǎn)
breakpoints: { value: ["320vp", "520vp"] }
}){
// 文本框:首頁
// 小屏sm:4列,中屏md:2列
GridCol({span:{ xs: 2, sm: 4, md: 2}}){
Row() {
Text("首頁")
.fontSize(24)
.fontWeight(500)
.width('100%')
.margin({
top: 12,
bottom: 12,
left: 12
})
}
}
.height(56)
.margin({ bottom: 8})
// 搜索框
// 小屏sm:4列,中屏md:6列
GridCol({span:{xs: 2, sm: 4, md: 6}}){
Search({placeholder:'搜索...'})
.width('100%')
.height(40)
.margin({ bottom: 8})
.placeholderFont({ size: 16 })
}
.height(48)
// 滾動條
// 小屏sm:4列,中屏md:8列
GridCol({span:{ xs: 4, sm: 4, md:8 }}){
Swiper(){
ForEach(SwiperList, (item: CardItem) => {
Stack({ alignContent: Alignment.TopStart }) {
Image(item.img)
.width('100%')
.height('100%')
.borderRadius(16)
.objectFit(ImageFit.Cover)
Column() {
Text(item.title)
.fontSize(16)
.fontColor(Color.White)
.margin({ bottom: 4 })
Text(item.info)
.fontSize(12)
.fontColor(Color.White)
.opacity(0.6)
}
.alignItems(HorizontalAlign.Start)
.margin({ top: 20, left: 14 })
}
}, item => JSON.stringify(item))
}
.height(144)
.itemSpace(12)
.autoPlay(true)
// 小屏?xí)r顯示一個頁面,中屏?xí)r顯示兩個頁面
.displayCount(this.currentbp == 'md' ? 2 : 1)
}
.height(144)
}
.height('100%')
.width('100%')
.padding({top:10,bottom:10,left:10,right:10})
// 獲取斷點(diǎn)
.onBreakpointChange((breakpoint)=>{
this.currentbp = breakpoint
})
}
}