鸿蒙一次开发,多端部署(八)典型布局场景

news2024/11/15 21:27:54

虽然不同应用的页面千变万化,但对其进行拆分和分析,页面中的很多布局场景是相似的。本小节将介绍如何借助自适应布局、响应式布局以及常见的容器类组件,实现应用中的典型布局场景。

说明: 在本文 媒体查询 小节中已经介绍了如何通过媒体查询监听断点变化,后续的示例中不再重复介绍此部分代码。

页签栏

布局效果

实现方案

不同断点下,页签在页面中的位置及尺寸都有差异,可以结合响应式布局能力,设置不同断点下 Tab组件 的barPosition、vertical、barWidth和barHeight属性实现目标效果。

另外,页签栏中的文字和图片的相对位置不同,同样可以通过设置不同断点下 tabBar 对应的CustomBuilder中的布局方向,实现目标效果。

参考代码

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
    })
  }
}

运营横幅(Banner)

布局效果

实现方案

运营横幅通常使用 Swiper组件 实现。不同断点下,运营横幅中展示的图片数量不同。只需要结合响应式布局,配置不同断点下Swiper组件的displayCount属性,即可实现目标效果。

参考代码

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)!)
  }
}

网格

布局效果

实现方案

不同断点下,页面中图片的排布不同,此场景可以通过响应式布局能力结合 Grid组件 实现,通过调整不同断点下的Grid组件的columnsTemplate属性即可实现目标效果。

另外,由于本例中各列的宽度相同,也可以通过响应式布局能力结合 List组件 实现,通过调整不同断点下的List组件的lanes属性也可实现目标效果。

参考代码

通过Grid组件实现

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组件实现

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%')
  }
}

侧边栏

布局效果

实现方案

侧边栏通常通过 SideBarContainer组件 实现,结合响应式布局能力,在不同断点下为SiderBarConContainer组件的sideBarWidth、showControlButton等属性配置不同的值,即可实现目标效果。

参考代码

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
        }         
    })
  }
}

单/双栏

布局效果

实现方案

单/双栏场景可以使用 Navigation组件 实现,Navigation组件可以根据窗口宽度自动切换单/双栏显示,减少开发工作量。

参考代码

@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')
  }
}

三分栏

布局效果

场景说明

为充分利用设备的屏幕尺寸优势,应用在大屏设备上常常有二分栏或三分栏的设计,即“A+C”,“B+C”或“A+B+C”的组合,其中A是侧边导航区,B是列表导航区,C是内容区。在用户动态改变窗口宽度时,当窗口宽度大于或等于840vp时页面呈现A+B+C三列,放大缩小优先变化C列;当窗口宽度小于840vp大于等于600vp时呈现B+C列,放大缩小时优先变化C列;当窗口宽度小于600vp大于等于360vp时,仅呈现C列。

实现方案

三分栏场景可以组合使用 SideBarContainer 组件与 Navigation组件 实现,SideBarContainer组件可以通过侧边栏控制按钮控制显示/隐藏,Navigation组件可以根据窗口宽度自动切换该组件内单/双栏显示,结合响应式布局能力,在不同断点下为SiderBarConContainer组件的minContentWidth属性配置不同的值,即可实现目标效果。设置minContentWidth属性的值可以通过 断点 监听窗口尺寸变化的同时设置不同的值并储存成一个全局对象。

参考代码

// 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
  // ...
  // 根据当前窗口尺寸更新断点
  private updateBreakpoint(windowWidth:number) :void{
    // 将长度的单位由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
      // 使用状态变量记录当前断点值
      AppStorage.setOrCreate('currentBreakpoint', this.curBp)
      // 使用状态变量记录当前minContentWidth值
      AppStorage.setOrCreate('myWidth', this.myWidth)
    }
  }

  onWindowStageCreate(windowStage: window.WindowStage) :void{
    windowStage.getMainWindow().then((windowObj) => {
      this.windowObj = windowObj
      // 获取应用启动时的窗口尺寸
      this.updateBreakpoint(windowObj.getWindowProperties().windowRect.width)
      // 注册回调函数,监听窗口尺寸变化
      windowObj.on('windowSizeChange', (windowSize)=>{
        this.updateBreakpoint(windowSize.width)
      })
    });
   // ...
  }
    
  // 窗口销毁时,取消窗口尺寸变化监听
  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)
  }
}

自定义弹窗

布局效果

实现方案

自定义弹窗通常通过 CustomDialogController 实现,有两种方式实现本场景的目标效果:

  • 通过gridCount属性配置自定义弹窗的宽度。

    系统默认对不同断点下的窗口进行了栅格化:sm断点下为4栅格,md断点下为8栅格,lg断点下为12栅格。通过gridCount属性可以配置弹窗占据栅格中的多少列,将该值配置为4即可实现目标效果。

  • 将customStyle设置为true,即弹窗的样式完全由开发者自定义。

    开发者自定义弹窗样式时,开发者可以根据需要配置弹窗的宽高和背景色(非弹窗区域保持默认的半透明色)。自定义弹窗样式配合 栅格组件 同样可以实现目标效果。

参考代码:

@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('是否删除此联系人?')
        .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('是否删除此联系人?')
            .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})
  }
}

大图浏览

布局效果

实现方案

图片通常使用 Image组件 展示,Image组件的objectFit属性默认为ImageFit.Cover,即保持宽高比进行缩小或者放大以使得图片两边都大于或等于显示边界。在大图浏览场景下,因屏幕与图片的宽高比可能有差异,常常会发生图片被截断的问题。此时只需将Image组件的objectFit属性设置为ImageFit.Contain,即保持宽高比进行缩小或者放大并使得图片完全显示在显示边界内,即可解决该问题。

参考代码:

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

操作入口

布局效果

实现方案

Scroll(内容超出宽度时可滚动) + 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: '运动音乐', 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)
  }
}

顶部

布局效果

实现方案

最外层使用栅格行组件GridRow布局

文本标题使用栅格列组件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 })
  }
}

缩进布局

布局效果

实现方案

借助 栅格组件 ,控制待显示内容在不同的断点下占据不同的列数,即可实现不同设备上的缩进效果。另外还可以调整不同断点下栅格组件与两侧的间距,获得更好的显示效果。

参考代码

@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 })
  }
}

挪移布局

布局效果

实现方案

不同断点下,栅格子元素占据的列数会随着开发者的配置发生改变。当一行中的列数超过栅格组件在该断点的总列数时,可以自动换行,即实现”上下布局”与”左右布局”之间切换的效果。

参考代码

@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')
  }
}

重复布局

布局效果

实现方案

不同断点下,配置栅格子组件占据不同的列数,即可实现“小屏单列显示、大屏双列显示”的效果。另外,还可以通过栅格组件的onBreakpointChange事件,调整页面中显示的元素数量。

参考代码

@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() {
      // 当目标区域不足以显示所有元素时,可以通过上下滑动查看不同的元素
      Scroll() {
        GridRow({gutter: 24}) {
          ForEach(this.listItems, () => {
           // 通过配置元素在不同断点下占的列数,实现不同的布局效果
            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)
  }
}

为了能让大家更好的学习鸿蒙(HarmonyOS NEXT)开发技术,这边特意整理了《鸿蒙开发学习手册》(共计890页),希望对大家有所帮助:https://qr21.cn/FV7h05

《鸿蒙开发学习手册》:

如何快速入门:https://qr21.cn/FV7h05

  1. 基本概念
  2. 构建第一个ArkTS应用
  3. ……

开发基础知识:https://qr21.cn/FV7h05

  1. 应用基础知识
  2. 配置文件
  3. 应用数据管理
  4. 应用安全管理
  5. 应用隐私保护
  6. 三方应用调用管控机制
  7. 资源分类与访问
  8. 学习ArkTS语言
  9. ……

基于ArkTS 开发:https://qr21.cn/FV7h05

  1. Ability开发
  2. UI开发
  3. 公共事件与通知
  4. 窗口管理
  5. 媒体
  6. 安全
  7. 网络与链接
  8. 电话服务
  9. 数据管理
  10. 后台任务(Background Task)管理
  11. 设备管理
  12. 设备使用信息统计
  13. DFX
  14. 国际化开发
  15. 折叠屏系列
  16. ……

鸿蒙开发面试真题(含参考答案):https://qr18.cn/F781PH

鸿蒙开发面试大盘集篇(共计319页):https://qr18.cn/F781PH

1.项目开发必备面试题
2.性能优化方向
3.架构方向
4.鸿蒙开发系统底层方向
5.鸿蒙音视频开发方向
6.鸿蒙车载开发方向
7.鸿蒙南向开发方向

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1534744.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

一、rv1126开发之视频输入和视频编码

RV1126 H264/HEVC编码流程 一、RV1126编码的流程图&#xff1a; 二、每个代码模块详细讲解 2.1. VI模块的创建 VI模块的初始化&#xff1a;关键在于VI_CHN_ATTR_S结构体&#xff0c;这个结构体是VI设置的结构体。这个结构体的成员变量包括&#xff1a;pcVideoNode&#xff0…

【Java Web基础】一些网页设计基础(五)

文章目录 1. 图片在盒子内部垂直居中2. 底部设计&#xff0c;图片或子元素居中于父盒子&#xff08;水平、垂直&#xff09;&#xff0c;随着屏幕大小放大或缩小3. 使用thymeleaf的th:each会导致样式失效错乱问题 1. 图片在盒子内部垂直居中 用弹性盒子实现垂直居中及用css实现…

html5cssjs代码 035 课程表

html5&css&js代码 035 课程表 一、代码二、解释基本结构示例代码常用属性样式和装饰响应式表格辅助技术 一个具有亮蓝色背景的网页&#xff0c;其中包含一个样式化的表格用于展示一周课程安排。表格设计了交替行颜色、鼠标悬停效果以及亮色表头&#xff0c;并对单元格设…

使用Oxygen编辑器的项目来做团队协作

▲ 搜索“大龙谈智能内容”关注公众号▲ 扫码见我视频号上的视频 今天&#xff0c;分享一种在Oxygen中使用项目文件进行团队协作的高效方法。这种方法不仅能帮助我们轻松共享文件和文件夹&#xff0c;还能确保团队成员使用统一的项目级别选项和发布配置&#xff0c;从而提高工…

模板高级使用(非类型模板参数,特化,分离编译)

文章目录 模板没有实例化取内嵌类型报错问题非类型模板参数模板的特化函数模板的特化类模板的特化1.全特化2.偏特化 模板的分离编译 模板没有实例化取内嵌类型报错问题 首先在这里分享一个模板的常见报错问题。就是模板的在没有实例化的情况下去取模板类里面的内嵌类型这时候的…

【面试题】HashMap为什么可以插入null而Hashtable就不可以(源码分析)

首先hashmap可以插入null值&#xff0c;但是hashtable和hashcurrentHashmap是不支持的&#xff1b;这是因为在 hashmap对插入key为null进行了特殊处理&#xff0c;当插入的值为null的时候会将哈希值设置为0 但是hashtable会直接抛出异常&#xff1a; 并且hashmap是线程不…

2024全国水科技大会【发言单位】天健水务集团(杭州)有限公司

天健水务&#xff0c;始创于2003年&#xff0c;下属浙江天行健水务有限公司、杭州天勤水处理技术有限公司、杭州天行健新能源有限公司&#xff0c;是一家致力于现代化水处理设备与系统研发、生产及工程应用的国家高新技术企业。以天健智造、天健工程、天健运维的“一站式全流程…

uniapp ios证书失效

前面是按照网上查找的方法 作者大大的地址 1、一个ios账户&#xff08;688付费版&#xff09; 2、登录 Apple Developer 3、创建Identifiers ps&#xff1a;创建时需继承苹果的sdk&#xff0c;只需要一个就行 点击continue再点击Register即可 4、创建.cer证书 &…

软件测试简历,你真的会写简历吗?一周疯狂面试6家...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 简历元素 一份合格…

SqlServer数据库复习总结资料

基于课堂上学到的以及书上的看到的&#xff0c;总结出的数据库复习资料 一、数据库概述 基本概念 1.数据 数据&#xff08;Data&#xff09;是事物的符号表示&#xff0c;可以是声音、图像、文字、数字&#xff0c;也可以是计算机代码。 2.数据库 数据库&#xff08;DataBase…

WebSocket 使用示例,后台为nodejs

效果图 页面代码 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-width, initial-scale1.0" /><title>WebSocket Client</title&g…

java NIO群聊系统

demo要求&#xff1a; 1&#xff09;编写一个NIO群聊系统&#xff0c;实现服务器端和客户端之间的数据简单通讯&#xff08;非阻塞&#xff09; 2&#xff09;实现多人群聊 3&#xff09;服务器端&#xff1a;可以监测用户上线&#xff0c;离线&#xff0c;并实现消息转发功…

利用autodl服务器跑模型

1. 租用服务器 本地改模型 服务器 将改进好的、数据集处理好的模型压缩为zip文件上传到阿里云盘打开服务器AUTODL服务器&#xff0c;在主页中选择容器实例 在此位置进行开关机操作&#xff0c;若停止服务器&#xff0c;必须关机&#xff0c;不然会一直扣钱 2. 运行模型 选择…

【Unity】捕捉PC桌面的插件

【背景】 之前介绍了如何用一款名为uWindowCapture的Unity免费插件在Unity的Canvas上展示PC桌面。经过一段时间的使用,本篇继续分享此插件的一些功能和限制。 在此感谢作者Hecomi。 【特征和限制】 一般局域网络环境只能最多达到15帧的帧率,所以别幻想用来窜流游戏或者看电…

【个人记录】CentOS7安装MySQL 5.7和libmysqlclient.so.20

记录 之前使用MariaDB 发现使用的libmysqlclient.so是18版本的&#xff0c;一些程序需要20版本的库&#xff0c;查了一下需要安装5.7以上版本的才有libmysqlclient.so.20&#xff0c;这里简单记录一下怎么安装。 安装MySQL 5.7 Yum源 yum install -y https://repo.mysql.com…

【C++】狗屁不通文章生成器2.0

【C】狗屁不通文章生成器2.0 1 前言2 改进2.1 字词的前后关系2.2 文章生成系统 3 实现(部分)3.1 class wordpair3.1.1 转化为 json3.1.2 添加后缀词3.1.3 选择后缀词 3.2 class createArticle3.2.1文本分割3.2.2生成文章 4演示4.1 wordpair(3x2), 启动词(春天)4.2 wordpair(2x1…

电脑维修的相关资料,有需要的自取

电脑维修的相关资料&#xff0c;有需要的自取。 链接&#xff1a;https://pan.baidu.com/s/1X81sBNAOmomFvug6mK56Bw 提取码&#xff1a;52pj 爆笑幽默段子&#xff1a;电脑出故障了&#xff0c;准备拿去修&#xff0c;结果被女朋 友拦住了。女朋友&#xff1a;“你们男人一定…

ginblog博客系统/golang+vue

ginblog博客系统 前台&#xff1a; 后台&#xff1a; Gitee的项目地址&#xff0c;点击进入下载 注意&#xff1a; 数据库文件导入在model里面&#xff0c;直接导入即可。 admin和front前后台系统记住修改https里的地址为自己的IP地址&#xff1a; front同上。

Doris实战——工商信息查询平台的湖仓一体建设

目录 前言 一、架构1.0&#xff1a;传统Lambda架构 二、OLAP引擎调研 三、架构2.0&#xff1a;数据服务层All in Apache Doris 四、架构 3.0&#xff1a;基于Doris Multi-Catalog的湖仓一体架构 五、实践经验 5.1 引入Merge-on-Write&#xff0c;百亿级单表查询提速近三…

学习vue3第九节(新加指令 v-pre/v-once/v-memo/v-cloak )

1、v-pre 作用&#xff1a;防止编译器解析某个特定的元素及其内容&#xff0c;即v-pre 会跳过当前元素以及其子元素的vue语法解析&#xff0c;并将其保持原样输出&#xff1b; 用于&#xff1a;vue 中一些没有指令和插值表达式的节点的元素&#xff0c;使用 v-pre 可以提高 Vu…