鴻蒙一次開發(fā),多端部署(八)典型布局場(chǎng)景

雖然不同應(yīng)用的頁面千變?nèi)f化,但對(duì)其進(jìn)行拆分和分析,頁面中的很多布局場(chǎng)景是相似的。本小節(jié)將介紹如何借助自適應(yīng)布局、響應(yīng)式布局以及常見的容器類組件,實(shí)現(xiàn)應(yīng)用中的典型布局場(chǎng)景。

說明: 在本文 媒體查詢 小節(jié)中已經(jīng)介紹了如何通過媒體查詢監(jiān)聽斷點(diǎn)變化,后續(xù)的示例中不再重復(fù)介紹此部分代碼。

頁簽欄

布局效果

實(shí)現(xiàn)方案

不同斷點(diǎn)下,頁簽在頁面中的位置及尺寸都有差異,可以結(jié)合響應(yīng)式布局能力,設(shè)置不同斷點(diǎn)下 Tab組件 的barPosition、vertical、barWidth和barHeight屬性實(shí)現(xiàn)目標(biāo)效果。

另外,頁簽欄中的文字和圖片的相對(duì)位置不同,同樣可以通過設(shè)置不同斷點(diǎn)下 tabBar 對(duì)應(yīng)的CustomBuilder中的布局方向,實(shí)現(xiàn)目標(biāo)效果。

參考代碼

import { BreakpointSystem, BreakPointType } from 'common/breakpointsystem'

interface TabBar  {
  name: string
  icon: Resource
  selectIcon: Resource
}

@Entry
@Component
struct Home {
  @State currentIndex: number = 0
  @State tabs: Array<TabBar> = [{
                                  name: '首頁',
                                  icon: $r('app.media.ic_music_home'),
                                  selectIcon: $r('app.media.ic_music_home_selected')
                                }, {
                                  name: '排行榜',
                                  icon: $r('app.media.ic_music_ranking'),
                                  selectIcon: $r('app.media.ic_music_ranking_selected')
                                }, {
                                  name: '我的',
                                  icon: $r('app.media.ic_music_me_nor'),
                                  selectIcon: $r('app.media.ic_music_me_selected')
                                }]

  @Builder TabBarBuilder(index: number, tabBar: TabBar) {
    Flex({
      direction: new BreakPointType({
        sm: FlexDirection.Column,
        md: FlexDirection.Row,
        lg: FlexDirection.Column
      }).getValue(this.currentBreakpoint),
      justifyContent: FlexAlign.Center,
      alignItems: ItemAlign.Center
    }) {
      Image(this.currentIndex === index ? tabBar.selectIcon : tabBar.icon)
        .size({ width: 36, height: 36 })
      Text(tabBar.name)
        .fontColor(this.currentIndex === index ? '#FF1948' : '#999')
        .margin(new BreakPointType<(Length|Padding)>({
          sm: { top: 4 },
          md: { left: 8 },
          lg: { top: 4 } }).getValue(this.currentBreakpoint)!)
        .fontSize(16)
    }
    .width('100%')
    .height('100%')
  }

  @StorageLink('currentBreakpoint') currentBreakpoint: string = 'md'
  private breakpointSystem: BreakpointSystem = new BreakpointSystem()

  aboutToAppear() {
    this.breakpointSystem.register()
  }

  aboutToDisappear() {
    this.breakpointSystem.unregister()
  }

  build() {
    Tabs({
      barPosition: new BreakPointType({
        sm: BarPosition.End,
        md: BarPosition.End,
        lg: BarPosition.Start
      }).getValue(this.currentBreakpoint)
    }) {
      ForEach(this.tabs, (item:TabBar, index) => {
        TabContent() {
          Stack() {
            Text(item.name).fontSize(30)
          }.width('100%').height('100%')
        }.tabBar(this.TabBarBuilder(index!, item))
      })
    }
    .vertical(new BreakPointType({ sm: false, md: false, lg: true }).getValue(this.currentBreakpoint)!)
    .barWidth(new BreakPointType({ sm: '100%', md: '100%', lg: '96vp' }).getValue(this.currentBreakpoint)!)
    .barHeight(new BreakPointType({ sm: '72vp', md: '56vp', lg: '60%' }).getValue(this.currentBreakpoint)!)
    .animationDuration(0)
    .onChange((index: number) => {
      this.currentIndex = index
    })
  }
}

運(yùn)營(yíng)橫幅(Banner)

布局效果

實(shí)現(xiàn)方案

運(yùn)營(yíng)橫幅通常使用 Swiper組件 實(shí)現(xiàn)。不同斷點(diǎn)下,運(yùn)營(yíng)橫幅中展示的圖片數(shù)量不同。只需要結(jié)合響應(yīng)式布局,配置不同斷點(diǎn)下Swiper組件的displayCount屬性,即可實(shí)現(xiàn)目標(biāo)效果。

參考代碼

import { BreakpointSystem, BreakPointType } from 'common/breakpointsystem'

@Entry
@Component
export default struct Banner {
  private data: Array<Resource> = [
    $r('app.media.banner1'),
    $r('app.media.banner2'),
    $r('app.media.banner3'),
    $r('app.media.banner4'),
    $r('app.media.banner5'),
    $r('app.media.banner6'),
  ]
  private breakpointSystem: BreakpointSystem = new BreakpointSystem()
  @StorageProp('currentBreakpoint') currentBreakpoint: string = 'md'

  aboutToAppear() {
    this.breakpointSystem.register()
  }

  aboutToDisappear() {
    this.breakpointSystem.unregister()
  }

  build() {
    Swiper() {
      ForEach(this.data, (item:Resource) => {
        Image(item)
          .size({ width: '100%', height: 200 })
          .borderRadius(12)
          .padding(8)
      })
    }
    .indicator(new BreakPointType({ sm: true, md: false, lg: false }).getValue(this.currentBreakpoint)!)
    .displayCount(new BreakPointType({ sm: 1, md: 2, lg: 3 }).getValue(this.currentBreakpoint)!)
  }
}

網(wǎng)格

布局效果

實(shí)現(xiàn)方案

不同斷點(diǎn)下,頁面中圖片的排布不同,此場(chǎng)景可以通過響應(yīng)式布局能力結(jié)合 Grid組件 實(shí)現(xiàn),通過調(diào)整不同斷點(diǎn)下的Grid組件的columnsTemplate屬性即可實(shí)現(xiàn)目標(biāo)效果。

另外,由于本例中各列的寬度相同,也可以通過響應(yīng)式布局能力結(jié)合 List組件 實(shí)現(xiàn),通過調(diào)整不同斷點(diǎn)下的List組件的lanes屬性也可實(shí)現(xiàn)目標(biāo)效果。

參考代碼

通過Grid組件實(shí)現(xiàn)

import { BreakpointSystem, BreakPointType } from 'common/breakpointsystem'

interface GridItemInfo {
  name: string
  image: Resource
}

@Entry
@Component
struct MultiLaneList {
  private data: GridItemInfo[] = [
    { name: '歌單集合1', image: $r('app.media.1') },
    { name: '歌單集合2', image: $r('app.media.2') },
    { name: '歌單集合3', image: $r('app.media.3') },
    { name: '歌單集合4', image: $r('app.media.4') },
    { name: '歌單集合5', image: $r('app.media.5') },
    { name: '歌單集合6', image: $r('app.media.6') },
    { name: '歌單集合7', image: $r('app.media.7') },
    { name: '歌單集合8', image: $r('app.media.8') },
    { name: '歌單集合9', image: $r('app.media.9') },
    { name: '歌單集合10', image: $r('app.media.10') },
    { name: '歌單集合11', image: $r('app.media.11') },
    { name: '歌單集合12', image: $r('app.media.12') }
  ]
  private breakpointSystem: BreakpointSystem = new BreakpointSystem()
  @StorageProp('currentBreakpoint') currentBreakpoint: string = 'md'

  aboutToAppear() {
    this.breakpointSystem.register()
  }

  aboutToDisappear() {
    this.breakpointSystem.unregister()
  }

  build() {
    Grid() {
      ForEach(this.data, (item: GridItemInfo) => {
        GridItem() {
          Column() {
            Image(item.image)
              .aspectRatio(1.8)
            Text(item.name)
              .margin({ top: 8 })
              .fontSize(20)
          }.padding(4)
        }
      })
    }
    .columnsTemplate(new BreakPointType({
      sm: '1fr 1fr',
      md: '1fr 1fr 1fr 1fr',
      lg: '1fr 1fr 1fr 1fr 1fr 1fr'
    }).getValue(this.currentBreakpoint)!)
  }
}

通過List組件實(shí)現(xiàn)

import { BreakpointSystem, BreakPointType } from 'common/breakpointsystem'

interface ListItemInfo {
  name: string
  image: Resource
}

@Entry
@Component
struct MultiLaneList {
  private data: ListItemInfo[] = [
    { name: '歌單集合1', image: $r('app.media.1') },
    { name: '歌單集合2', image: $r('app.media.2') },
    { name: '歌單集合3', image: $r('app.media.3') },
    { name: '歌單集合4', image: $r('app.media.4') },
    { name: '歌單集合5', image: $r('app.media.5') },
    { name: '歌單集合6', image: $r('app.media.6') },
    { name: '歌單集合7', image: $r('app.media.7') },
    { name: '歌單集合8', image: $r('app.media.8') },
    { name: '歌單集合9', image: $r('app.media.9') },
    { name: '歌單集合10', image: $r('app.media.10') },
    { name: '歌單集合11', image: $r('app.media.11') },
    { name: '歌單集合12', image: $r('app.media.12') }
  ]
  private breakpointSystem: BreakpointSystem = new BreakpointSystem()
  @StorageProp('currentBreakpoint') currentBreakpoint: string = 'md'

  aboutToAppear() {
    this.breakpointSystem.register()
  }

  aboutToDisappear() {
    this.breakpointSystem.unregister()
  }

  build() {
    List() {
      ForEach(this.data, (item: ListItemInfo) => {
        ListItem() {
          Column() {
            Image(item.image)
            Text(item.name)
              .margin({ top: 8 })
              .fontSize(20)
          }.padding(4)
        }
      })
    }
    .lanes(new BreakPointType({ sm: 2, md: 4, lg: 6 }).getValue(this.currentBreakpoint)!)
    .width('100%')
  }
}

側(cè)邊欄

布局效果

實(shí)現(xiàn)方案

側(cè)邊欄通常通過 SideBarContainer組件 實(shí)現(xiàn),結(jié)合響應(yīng)式布局能力,在不同斷點(diǎn)下為SiderBarConContainer組件的sideBarWidth、showControlButton等屬性配置不同的值,即可實(shí)現(xiàn)目標(biāo)效果。

參考代碼

import { BreakpointSystem, BreakPointType } from 'common/breakpointsystem'

interface imagesInfo{
  label:string,
  imageSrc:Resource
}
const images:imagesInfo[]=[
  {
    label:'moon',
    imageSrc:$r('app.media.my_image_moon')
  },
  {
    label:'sun',
    imageSrc:$r('app.media.my_image')
  }
]

@Entry
@Component
struct SideBarSample {
  @StorageLink('currentBreakpoint') private currentBreakpoint: string = "md";
  private breakpointSystem: BreakpointSystem = new BreakpointSystem()
  @State selectIndex: number = 0;
  @State showSideBar:boolean=false;

  aboutToAppear() {
    this.breakpointSystem.register() 
  }

  aboutToDisappear() {
    this.breakpointSystem.unregister()
  }

  @Builder itemBuilder(index: number) {
    Text(images[index].label)
      .fontSize(24)
      .fontWeight(FontWeight.Bold)
      .borderRadius(5)
      .margin(20)
      .backgroundColor('#ffffff')
      .textAlign(TextAlign.Center)
      .width(180)
      .height(36)
      .onClick(() => {
        this.selectIndex = index;
        if(this.currentBreakpoint === 'sm'){
          this.showSideBar=false
        }
      })
  }

  build() {
    SideBarContainer(this.currentBreakpoint === 'sm' ? SideBarContainerType.Overlay : SideBarContainerType.Embed) {
      Column() {
        this.itemBuilder(0)
        this.itemBuilder(1)
      }.backgroundColor('#F1F3F5')
      .justifyContent(FlexAlign.Center)

      Column() {
        Image(images[this.selectIndex].imageSrc)
          .objectFit(ImageFit.Contain)
          .height(300)
          .width(300)
      }
      .justifyContent(FlexAlign.Center)
      .width('100%')
      .height('100%')
    }
    .height('100%')
    .sideBarWidth(this.currentBreakpoint === 'sm' ? '100%' : '33.33%')
    .minSideBarWidth(this.currentBreakpoint === 'sm' ? '100%' : '33.33%')
    .maxSideBarWidth(this.currentBreakpoint === 'sm' ? '100%' : '33.33%')
    .showControlButton(this.currentBreakpoint === 'sm')
    .autoHide(false)
    .showSideBar(this.currentBreakpoint !== 'sm'||this.showSideBar)
    .onChange((isBarShow: boolean) => {
      if(this.currentBreakpoint === 'sm'){
          this.showSideBar=isBarShow
        }         
    })
  }
}

單/雙欄

布局效果

實(shí)現(xiàn)方案

單/雙欄場(chǎng)景可以使用 Navigation組件 實(shí)現(xiàn),Navigation組件可以根據(jù)窗口寬度自動(dòng)切換單/雙欄顯示,減少開發(fā)工作量。

參考代碼

@Component
struct Details {
  private imageSrc: Resource=$r('app.media.my_image_moon')
  build() {
    Column() {
      Image(this.imageSrc)
        .objectFit(ImageFit.Contain)
        .height(300)
        .width(300)
    }
    .justifyContent(FlexAlign.Center)
    .width('100%')
    .height('100%')
  }
}

@Component
struct Item {
  private imageSrc?: Resource
  private label?: string

  build() {
    NavRouter() {
      Text(this.label)
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .borderRadius(5)
        .backgroundColor('#FFFFFF')
        .textAlign(TextAlign.Center)
        .width(180)
        .height(36)
      NavDestination() {
        Details({imageSrc: this.imageSrc})
      }.title(this.label)
      .backgroundColor('#FFFFFF')
    }
  }
}

@Entry
@Component
struct NavigationSample {
  build() {
    Navigation() {
      Column({space: 30}) {
        Item({label: 'moon', imageSrc: $r('app.media.my_image_moon')})
        Item({label: 'sun', imageSrc: $r('app.media.my_image')})
      }
      .justifyContent(FlexAlign.Center)
      .height('100%')
      .width('100%')
    }
    .mode(NavigationMode.Auto)
    .backgroundColor('#F1F3F5')
    .height('100%')
    .width('100%')
    .navBarWidth(360)
    .hideToolBar(true)
    .title('Sample')
  }
}

三分欄

布局效果

場(chǎng)景說明

為充分利用設(shè)備的屏幕尺寸優(yōu)勢(shì),應(yīng)用在大屏設(shè)備上常常有二分欄或三分欄的設(shè)計(jì),即“A+C”,“B+C”或“A+B+C”的組合,其中A是側(cè)邊導(dǎo)航區(qū),B是列表導(dǎo)航區(qū),C是內(nèi)容區(qū)。在用戶動(dòng)態(tài)改變窗口寬度時(shí),當(dāng)窗口寬度大于或等于840vp時(shí)頁面呈現(xiàn)A+B+C三列,放大縮小優(yōu)先變化C列;當(dāng)窗口寬度小于840vp大于等于600vp時(shí)呈現(xiàn)B+C列,放大縮小時(shí)優(yōu)先變化C列;當(dāng)窗口寬度小于600vp大于等于360vp時(shí),僅呈現(xiàn)C列。

實(shí)現(xiàn)方案

三分欄場(chǎng)景可以組合使用 SideBarContainer 組件與 Navigation組件 實(shí)現(xiàn),SideBarContainer組件可以通過側(cè)邊欄控制按鈕控制顯示/隱藏,Navigation組件可以根據(jù)窗口寬度自動(dòng)切換該組件內(nèi)單/雙欄顯示,結(jié)合響應(yīng)式布局能力,在不同斷點(diǎn)下為SiderBarConContainer組件的minContentWidth屬性配置不同的值,即可實(shí)現(xiàn)目標(biāo)效果。設(shè)置minContentWidth屬性的值可以通過 斷點(diǎn) 監(jiān)聽窗口尺寸變化的同時(shí)設(shè)置不同的值并儲(chǔ)存成一個(gè)全局對(duì)象。

參考代碼

// MainAbility.ts
import window from '@ohos.window'
import display from '@ohos.display'
import Ability from '@ohos.app.ability.Ability'

export default class MainAbility extends Ability {
  private windowObj?: window.Window
  private curBp?: string
  private myWidth?: number
  // ...
  // 根據(jù)當(dāng)前窗口尺寸更新斷點(diǎn)
  private updateBreakpoint(windowWidth:number) :void{
    // 將長(zhǎng)度的單位由px換算為vp
    let windowWidthVp = windowWidth / (display.getDefaultDisplaySync().densityDPI / 160)
    let newBp: string = ''
    let newWd: number
    if (windowWidthVp < 320) {
      newBp = 'xs'
      newWd = 360
    } else if (windowWidthVp < 600) {
      newBp = 'sm'
      newWd = 360
    } else if (windowWidthVp < 840) {
      newBp = 'md'
      newWd = 600
    } else {
      newBp = 'lg'
      newWd = 600
    }
    if (this.curBp !== newBp) {
      this.curBp = newBp
      this.myWidth = newWd
      // 使用狀態(tài)變量記錄當(dāng)前斷點(diǎn)值
      AppStorage.setOrCreate('currentBreakpoint', this.curBp)
      // 使用狀態(tài)變量記錄當(dāng)前minContentWidth值
      AppStorage.setOrCreate('myWidth', this.myWidth)
    }
  }

  onWindowStageCreate(windowStage: window.WindowStage) :void{
    windowStage.getMainWindow().then((windowObj) => {
      this.windowObj = windowObj
      // 獲取應(yīng)用啟動(dòng)時(shí)的窗口尺寸
      this.updateBreakpoint(windowObj.getWindowProperties().windowRect.width)
      // 注冊(cè)回調(diào)函數(shù),監(jiān)聽窗口尺寸變化
      windowObj.on('windowSizeChange', (windowSize)=>{
        this.updateBreakpoint(windowSize.width)
      })
    });
   // ...
  }
    
  // 窗口銷毀時(shí),取消窗口尺寸變化監(jiān)聽
  onWindowStageDestroy() :void {
    if (this.windowObj) {
      this.windowObj.off('windowSizeChange')
    }
  }
  //...
}


// tripleColumn.ets
@Component
struct Details {
  private imageSrc: Resource=$r('app.media.icon')
  build() {
    Column() {
      Image(this.imageSrc)
        .objectFit(ImageFit.Contain)
        .height(300)
        .width(300)
    }
    .justifyContent(FlexAlign.Center)
    .width('100%')
    .height('100%')
  }
}

@Component
struct Item {
  private imageSrc?: Resource
  private label?: string

  build() {
    NavRouter() {
      Text(this.label)
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .backgroundColor('#66000000')
        .textAlign(TextAlign.Center)
        .width('100%')
        .height('30%')
      NavDestination() {
        Details({imageSrc: this.imageSrc})
      }.title(this.label)
      .hideTitleBar(false)
      .backgroundColor('#FFFFFF')
    }
    .margin(10)
  }
}

@Entry
@Component
struct TripleColumnSample {
  @State arr: number[] = [1, 2, 3]
  @StorageProp('myWidth') myWidth: number = 360

  @Builder NavigationTitle() {
    Column() {
      Text('Sample')
        .fontColor('#000000')
        .fontSize(24)
        .width('100%')
        .height('100%')
        .align(Alignment.BottomStart)
        .margin({left:'5%'})
    }.alignItems(HorizontalAlign.Start)
  }

  build() {
    SideBarContainer() {
      Column() {
        List() {
          ForEach(this.arr, (item:number, index) => {
            ListItem() {
              Text('A'+item)
                .width('100%').height("20%").fontSize(24)
                .fontWeight(FontWeight.Bold)
                .textAlign(TextAlign.Center).backgroundColor('#66000000')
            }
          })
        }.divider({ strokeWidth: 5, color: '#F1F3F5' })
      }.width('100%')
      .height('100%')
      .justifyContent(FlexAlign.SpaceEvenly)
      .backgroundColor('#F1F3F5')

      Column() {
        Navigation() {
          List(){
            ListItem() {
              Column() {
                Item({ label: 'B1', imageSrc: $r('app.media.icon') })
                Item({ label: 'B2', imageSrc: $r('app.media.icon') })
              }
            }.width('100%')
          }
        }
        .mode(NavigationMode.Auto)
        .minContentWidth(360)
        .navBarWidth(240)
        .backgroundColor('#FFFFFF')
        .height('100%')
        .width('100%')
        .hideToolBar(true)
        .title(this.NavigationTitle)
      }.width('100%').height('100%')
    }.sideBarWidth(240)
    .minContentWidth(this.myWidth)
  }
}

自定義彈窗

布局效果

實(shí)現(xiàn)方案

自定義彈窗通常通過 CustomDialogController 實(shí)現(xiàn),有兩種方式實(shí)現(xiàn)本場(chǎng)景的目標(biāo)效果:

  • 通過gridCount屬性配置自定義彈窗的寬度。

    系統(tǒng)默認(rèn)對(duì)不同斷點(diǎn)下的窗口進(jìn)行了柵格化:sm斷點(diǎn)下為4柵格,md斷點(diǎn)下為8柵格,lg斷點(diǎn)下為12柵格。通過gridCount屬性可以配置彈窗占據(jù)柵格中的多少列,將該值配置為4即可實(shí)現(xiàn)目標(biāo)效果。

  • 將customStyle設(shè)置為true,即彈窗的樣式完全由開發(fā)者自定義。

    開發(fā)者自定義彈窗樣式時(shí),開發(fā)者可以根據(jù)需要配置彈窗的寬高和背景色(非彈窗區(qū)域保持默認(rèn)的半透明色)。自定義彈窗樣式配合 柵格組件 同樣可以實(shí)現(xiàn)目標(biāo)效果。

參考代碼:

@Entry
@Component
struct CustomDialogSample {
  // 通過gridCount配置彈窗的寬度
  dialogControllerA: CustomDialogController = new CustomDialogController({
    builder: CustomDialogA ({
      cancel: this.onCancel,
      confirm: this.onConfirm
    }),
    cancel: this.onCancel,
    autoCancel: true,
    gridCount: 4,
    customStyle: false
  })
  // 自定義彈窗樣式
  dialogControllerB: CustomDialogController = new CustomDialogController({
    builder: CustomDialogB ({
      cancel: this.onCancel,
      confirm: this.onConfirm
    }),
    cancel: this.onCancel,
    autoCancel: true,
    customStyle: true
  })

  onCancel() {
    console.info('callback when dialog is canceled')
  }

  onConfirm() {
    console.info('callback when dialog is confirmed')
  }

  build() {
    Column() {
      Button('CustomDialogA').margin(12)
        .onClick(() => {
          this.dialogControllerA.open()
        })
      Button('CustomDialogB').margin(12)
        .onClick(() => {
          this.dialogControllerB.open()
        })
    }.width('100%').height('100%').justifyContent(FlexAlign.Center)
  }
}

@CustomDialog
struct CustomDialogA {
  controller?: CustomDialogController
  cancel?: () => void
  confirm?: () => void

  build() {
    Column() {
      Text('是否刪除此聯(lián)系人?')
        .fontSize(16)
        .fontColor('#E6000000')
        .margin({bottom: 8, top: 24, left: 24, right: 24})
      Row() {
        Text('取消')
          .fontColor('#007DFF')
          .fontSize(16)
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
          .onClick(()=>{
            if(this.controller){
                 this.controller.close()
             }
            this.cancel!()
          })
        Line().width(1).height(24).backgroundColor('#33000000').margin({left: 4, right: 4})
        Text('刪除')
          .fontColor('#FA2A2D')
          .fontSize(16)
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
          .onClick(()=>{
             if(this.controller){
                 this.controller.close()
             }
            this.confirm!()
          })
      }.height(40)
      .margin({left: 24, right: 24, bottom: 16})
    }.borderRadius(24)
  }
}

@CustomDialog
struct CustomDialogB {
  controller?: CustomDialogController
  cancel?: () => void
  confirm?: () => void

  build() {
    GridRow({columns: {sm: 4, md: 8, lg: 12}}) {
      GridCol({span: 4, offset: {sm: 0, md: 2, lg: 4}}) {
        Column() {
          Text('是否刪除此聯(lián)系人?')
            .fontSize(16)
            .fontColor('#E6000000')
            .margin({bottom: 8, top: 24, left: 24, right: 24})
          Row() {
            Text('取消')
              .fontColor('#007DFF')
              .fontSize(16)
              .layoutWeight(1)
              .textAlign(TextAlign.Center)
              .onClick(()=>{
                if(this.controller){
                 this.controller.close()
                }
                this.cancel!()
              })
            Line().width(1).height(24).backgroundColor('#33000000').margin({left: 4, right: 4})
            Text('刪除')
              .fontColor('#FA2A2D')
              .fontSize(16)
              .layoutWeight(1)
              .textAlign(TextAlign.Center)
              .onClick(()=>{
                 if(this.controller){
                 this.controller.close()
                }
                this.confirm!()
              })
          }.height(40)
          .margin({left: 24, right: 24, bottom: 16})
        }.borderRadius(24).backgroundColor('#FFFFFF')
      }
    }.margin({left: 24, right: 24})
  }
}

大圖瀏覽

布局效果

實(shí)現(xiàn)方案

圖片通常使用 Image組件 展示,Image組件的objectFit屬性默認(rèn)為ImageFit.Cover,即保持寬高比進(jìn)行縮小或者放大以使得圖片兩邊都大于或等于顯示邊界。在大圖瀏覽場(chǎng)景下,因屏幕與圖片的寬高比可能有差異,常常會(huì)發(fā)生圖片被截?cái)嗟膯栴}。此時(shí)只需將Image組件的objectFit屬性設(shè)置為ImageFit.Contain,即保持寬高比進(jìn)行縮小或者放大并使得圖片完全顯示在顯示邊界內(nèi),即可解決該問題。

參考代碼:

@Entry
@Component
struct BigImage {
  build() {
    Row() {
      Image($r("app.media.image"))
        .objectFit(ImageFit.Contain)
    }
  }
}

操作入口

布局效果

實(shí)現(xiàn)方案

Scroll(內(nèi)容超出寬度時(shí)可滾動(dòng)) + Row(橫向均分:justifyContent(FlexAlign.SpaceAround)、 最小寬度約束:constraintSize({ minWidth: '100%' })

參考代碼

interface OperationItem {
  name: string
  icon: Resource
}

@Entry
@Component
export default struct OperationEntries {
  @State listData: Array<OperationItem> = [
    { name: '私人FM', icon: $r('app.media.self_fm') },
    { name: '歌手', icon: $r('app.media.singer') },
    { name: '歌單', icon: $r('app.media.song_list') },
    { name: '排行榜', icon: $r('app.media.rank') },
    { name: '熱門', icon: $r('app.media.hot') },
    { name: '運(yùn)動(dòng)音樂', icon: $r('app.media.sport') },
    { name: '音樂FM', icon: $r('app.media.audio_fm') },
    { name: '福利', icon: $r('app.media.bonus') }]

  build() {
    Scroll() {
      Row() {
        ForEach(this.listData, (item:OperationItem) => {
          Column() {
            Image(item.icon)
              .width(48)
              .aspectRatio(1)
            Text(item.name)
              .margin({ top: 8 })
              .fontSize(16)
          }
          .justifyContent(FlexAlign.Center)
          .height(104)
          .padding({ left: 12, right: 12 })
        })
      }
      .constraintSize({ minWidth: '100%' }).justifyContent(FlexAlign.SpaceAround)
    }
    .width('100%')
    .scrollable(ScrollDirection.Horizontal)
  }
}

頂部

布局效果

實(shí)現(xiàn)方案

最外層使用柵格行組件GridRow布局

文本標(biāo)題使用柵格列組件GridCol

搜索框使用柵格列組件GridCol

參考代碼

@Entry
@Component
export default struct Header {
  @State needWrap: boolean = true

  build() {
    GridRow() {
      GridCol({ span: { sm: 12, md: 6, lg: 7 } }) {
        Row() {
          Text('推薦').fontSize(24)
          Blank()
          Image($r('app.media.ic_public_more'))
            .width(32)
            .height(32)
            .objectFit(ImageFit.Contain)
            .visibility(this.needWrap ? Visibility.Visible : Visibility.None)
        }
        .width('100%').height(40)
        .alignItems(VerticalAlign.Center)
      }

      GridCol({ span: { sm: 12, md: 6, lg: 5 } }) {
        Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center }) {
          Search({ placeholder: '猜您喜歡: 萬水千山' })
            .placeholderFont({ size: 16 })
            .margin({ top: 4, bottom: 4 })
          Image($r('app.media.audio_fm'))
            .width(32)
            .height(32)
            .objectFit(ImageFit.Contain)
            .flexShrink(0)
            .margin({ left: 12 })
          Image($r('app.media.ic_public_more'))
            .width(32)
            .height(32)
            .objectFit(ImageFit.Contain)
            .flexShrink(0)
            .margin({ left: 12 })
            .visibility(this.needWrap ? Visibility.None : Visibility.Visible)
        }
      }
    }.onBreakpointChange((breakpoint: string) => {
      if (breakpoint === 'sm') {
        this.needWrap = true
      } else {
        this.needWrap = false
      }
    })
    .padding({ left: 12, right: 12 })
  }
}

縮進(jìn)布局

布局效果

實(shí)現(xiàn)方案

借助 柵格組件 ,控制待顯示內(nèi)容在不同的斷點(diǎn)下占據(jù)不同的列數(shù),即可實(shí)現(xiàn)不同設(shè)備上的縮進(jìn)效果。另外還可以調(diào)整不同斷點(diǎn)下柵格組件與兩側(cè)的間距,獲得更好的顯示效果。

參考代碼

@Entry
@Component
struct IndentationSample {
  @State private gridMargin: number = 24
  build() {
    Row() {
      GridRow({columns: {sm: 4, md: 8, lg: 12}, gutter: 24}) {
        GridCol({span: {sm: 4, md: 6, lg: 8}, offset: {md: 1, lg: 2}}) {
          Column() {
            ForEach([0, 1, 2, 4], () => {
              Column() {
                ItemContent()
              }
            })
          }.width('100%')
        }
      }
      .margin({left: this.gridMargin, right: this.gridMargin})
      .onBreakpointChange((breakpoint: string) => {
        if (breakpoint === 'lg') {
          this.gridMargin = 48
        } else if (breakpoint === 'md') {
          this.gridMargin = 32
        } else {
          this.gridMargin = 24
        }
      })
    }
    .height('100%')
    .alignItems((VerticalAlign.Center))
    .backgroundColor('#F1F3f5')
  }
}

@Component
struct ItemContent {
  build() {
    Column() {
      Row() {
        Row() {
        }
        .width(28)
        .height(28)
        .borderRadius(14)
        .margin({ right: 15 })
        .backgroundColor('#E4E6E8')

        Row() {
        }
        .width('30%').height(20).borderRadius(4)
        .backgroundColor('#E4E6E8')
      }.width('100%').height(28)

      Row() {
      }
      .width('100%')
      .height(68)
      .borderRadius(16)
      .margin({ top: 12 })
      .backgroundColor('#E4E6E8')
    }
    .height(128)
    .borderRadius(24)
    .backgroundColor('#FFFFFF')
    .padding({ top: 12, bottom: 12, left: 18, right: 18 })
    .margin({ bottom: 12 })
  }
}

挪移布局

布局效果

實(shí)現(xiàn)方案

不同斷點(diǎn)下,柵格子元素占據(jù)的列數(shù)會(huì)隨著開發(fā)者的配置發(fā)生改變。當(dāng)一行中的列數(shù)超過柵格組件在該斷點(diǎn)的總列數(shù)時(shí),可以自動(dòng)換行,即實(shí)現(xiàn)”上下布局”與”左右布局”之間切換的效果。

參考代碼

@Entry
@Component
struct DiversionSample {
  @State private currentBreakpoint: string = 'md'
  @State private imageHeight: number = 0
  build() {
    Row() {
      GridRow() {
        GridCol({span: {sm: 12, md: 6, lg: 6}}) {
          Image($r('app.media.illustrator'))
          .aspectRatio(1)
          .onAreaChange((oldValue: Area, newValue: Area) => {
            this.imageHeight = Number(newValue.height)
          })
          .margin({left: 12, right: 12})
        }

        GridCol({span: {sm: 12, md: 6, lg: 6}}) {
          Column(){
            Text($r('app.string.user_improvement'))
              .textAlign(TextAlign.Center)
              .fontSize(20)
              .fontWeight(FontWeight.Medium)
            Text($r('app.string.user_improvement_tips'))
              .textAlign(TextAlign.Center)
              .fontSize(14)
              .fontWeight(FontWeight.Medium)
          }
          .margin({left: 12, right: 12})
          .justifyContent(FlexAlign.Center)
          .height(this.currentBreakpoint === 'sm' ? 100 : this.imageHeight)
        }
      }.onBreakpointChange((breakpoint: string) => {
        this.currentBreakpoint = breakpoint;
      })
    }
    .height('100%')
    .alignItems((VerticalAlign.Center))
    .backgroundColor('#F1F3F5')
  }
}

重復(fù)布局

布局效果

實(shí)現(xiàn)方案

不同斷點(diǎn)下,配置柵格子組件占據(jù)不同的列數(shù),即可實(shí)現(xiàn)“小屏單列顯示、大屏雙列顯示”的效果。另外,還可以通過柵格組件的onBreakpointChange事件,調(diào)整頁面中顯示的元素?cái)?shù)量。

參考代碼

@Entry
@Component
struct RepeatSample {
  @State private currentBreakpoint: string = 'md'
  @State private listItems: number[] = [1, 2, 3, 4, 5, 6, 7, 8]
  @State private gridMargin: number = 24

  build() {
    Row() {
      // 當(dāng)目標(biāo)區(qū)域不足以顯示所有元素時(shí),可以通過上下滑動(dòng)查看不同的元素
      Scroll() {
        GridRow({gutter: 24}) {
          ForEach(this.listItems, () => {
           // 通過配置元素在不同斷點(diǎn)下占的列數(shù),實(shí)現(xiàn)不同的布局效果
            GridCol({span: {sm: 12, md: 6, lg: 6}}) {
              Column() {
                RepeatItemContent()
              }
            }
          })
        }
        .margin({left: this.gridMargin, right: this.gridMargin})
        .onBreakpointChange((breakpoint: string) => {
          this.currentBreakpoint = breakpoint;
          if (breakpoint === 'lg') {
            this.gridMargin = 48
          } else if (breakpoint === 'md') {
            this.gridMargin = 32
          } else {
            this.gridMargin = 24
          }
        })
      }.height(348)
    }
    .height('100%')
    .backgroundColor('#F1F3F5')
  }
}

@Component
struct RepeatItemContent {
  build() {
    Flex() {
      Row() {
      }
      .width(43)
      .height(43)
      .borderRadius(12)
      .backgroundColor('#E4E6E8')
      .flexGrow(0)

      Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Start, justifyContent: FlexAlign.SpaceAround }) {
        Row() {
        }
        .height(10)
        .width('80%')
        .backgroundColor('#E4E6E8')

        Row() {
        }
        .height(10)
        .width('50%')
        .backgroundColor('#E4E6E8')
      }
      .flexGrow(1)
      .margin({ left: 13 })
    }
    .padding({ top: 13, bottom: 13, left: 13, right: 37 })
    .height(69)
    .backgroundColor('#FFFFFF')
    .borderRadius(24)
  }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容