鸿蒙开发应用之Flex弹性布局,Scroll组件、Tabs组件的运用。

721 阅读15分钟

一. Flex弹性布局:

弹性布局(Flex)提供更加有效的方式对容器中的子元素进行排列、对齐和分配剩余空间。常用于页面头部导航栏的均匀分布、页面框架的搭建、多行数据的排列等。 容器默认存在主轴与交叉轴,子元素默认沿主轴排列,子元素在主轴方向的尺寸称为主轴尺寸,在交叉轴方向的尺寸称为交叉轴尺寸。

1. 布局方向-direction:

在弹性布局中,容器的子元素可以按照任意方向排列(默认水平排列)。通过设置参数 direction,可以决定主轴的方向,从而控制子元素的排列方向。

参数:direction

值:枚举 FlexDirection

枚举值描述
Row主轴为水平方向,默认值 ,子元素从起始端沿着水平方向开始排布
RowReverse主轴为水平方向,子元素从终点端沿着 FlexDirection. Row 相反的方向开始排布
Column主轴为垂直方向 ,子元素从起始端沿着垂直方向开始排布
ColumnReverse主轴为垂直方向 ,子元素从终点端沿着 FlexDirection. Column 相反的方向开始排布

2. 主轴对齐方式: justifyContent

参数:justifyContent

值:枚举 FlexAlign(属性与线性布局主轴对齐方式相同)

枚举值描述
Start首端对齐
Center居中对齐
End尾部对齐
Spacebetween两端对齐 ,子元素之间间距相等
SpaceAround子元素两侧间距相等 , 第一个元素到行首的距离和最后一个元素到行尾的距离是相邻元素之间距离的一半
SpaceEvenly相邻元素之间的距离、第一个元素与行首的间距、最后一个元素到行尾的间距都完全一样

3. 交叉轴对齐方式: alignItems

参数:alignItems

值:枚举 ItemAlign

枚举值描述
Start首部对齐
Center居中对齐
End尾部对齐
Stretch交叉轴方向拉伸填充,在未设置尺寸时,拉伸到容器尺寸
Baseline交叉轴方向文本基线对齐

子元素的 alignSelf 属性也可以设置子元素在父容器交叉轴的对齐格式,且会覆盖 Flex 布局容器中alignItems 配置。

4. 布局换行: wrap

弹性布局分为单行布局和多行布局。 默认情况下,Flex 容器中的子元素都排在一条线(又称“轴线”)上。 子元素尺寸总和大于 Flex 容器尺寸时,子元素尺寸会自动挤压。wrap 属性控制当子元素主轴尺寸之和大于容器主轴尺寸时,Flex 是单行布局还是多行布局。
在多行布局时,通过交叉轴方向,确认新行排列方向。
参数:wrap

值:枚举 FlexWrap.Wrap多行布局;FlexWrap.NoWrap单行布局

语法: Flex({ wrap:FlexWrap.Wrap }) { }

看完以上属性,那就可以实操了

随笔代码:

@Entry
@Component
struct  Index{
  build() {
    /*
     * Flex默认主轴方向为row,可以通过direction:FlexDirection.Column修改主轴方向为纵向
     * justifyContent:FlexAlign.方向 ,是用来对主轴方向进行对齐设置
     * alignItems:ItemAlign.方向 ,用来设置交叉轴方向
     *
     * 总结:Flex比较消耗性能,所以单纯的线性排列优先用Column和Row
     * */
    Column() {
        // Flex区域
        // 多行显示,Flex动态计算,如果内容超过一行,则从下一行重新开始排列
        // 因为这个过程是在不停计算,使用消耗性能
        Flex(
          {
          // justifyContent:FlexAlign.End,是用来对主轴方向进行对齐设置
        // direction: FlexDirection.Column  设置布局方向

            wrap:FlexWrap.Wrap} //设置Flex自动换行
        ){
          Text('英特尔')
            .backgroundColor(Color.Grey)
            .padding({left:15,right:15,top:3,bottom:3})
            .borderRadius(15)
          Text('华为')
            .backgroundColor(Color.Grey)
            .padding({left:15,right:15,top:3,bottom:3})
            .borderRadius(15)
          Text('amd')
            .backgroundColor(Color.Grey)
            .padding({left:15,right:15,top:3,bottom:3})
            .borderRadius(15)
          Text('平板')
            .backgroundColor(Color.Grey)
            .padding({left:15,right:15,top:3,bottom:3})
            .borderRadius(15)
          Text('小米')
            .backgroundColor(Color.Grey)
            .padding({left:15,right:15,top:3,bottom:3})
            .borderRadius(15)
          Text('苹果')
            .backgroundColor(Color.Grey)
            .padding({left:15,right:15,top:3,bottom:3})
            .borderRadius(15)
          Text('比亚迪')
            .backgroundColor(Color.Grey)
            .padding({left:15,right:15,top:3,bottom:3})
            .borderRadius(15)
          Text('显示器')
            .backgroundColor(Color.Grey)
            .padding({left:15,right:15,top:3,bottom:3})
            .borderRadius(15)
          Text('黑神话')
            .backgroundColor(Color.Grey)
            .padding({left:15,right:15,top:3,bottom:3})
            .borderRadius(15)
          Text('何同学')
            .backgroundColor(Color.Grey)
            .padding({left:15,right:15,top:3,bottom:3})
            .borderRadius(15)
        }
        .margin({top:20,bottom:20,left:15,right:15})
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor(Color.Pink)
  }
}

代码效果图:

image.png

挤压一块使这个效果图并不美观,我们可以让他们分开,毕竟距离产生美 在每个text组件上加上 .margin(10) 这样他们之间就会有空格。

如下图:

image.png

二、Scroll、Tabs

1. 容器组件 Scroll

可滚动的容器组件,当子组件的布局尺寸超过Scroll的尺寸时,内容可以滚动。
当页面内容由多个区域组成,并且可以滚动时,推荐使用 Scroll,比如:

  1. 小米有品:页面滚动,点击顶部区域,返回顶部
  2. 京东:页面滚动,点击右下角小火箭,返回顶部
    等等....

PixPin_2024-09-04_09-43-13.gif

1.1 核心用法

先来看看 Scroll 的最核心用法,让内容滚动

  1. Scroll 设置尺寸
  2. 设置 子组件(只支持一个子组件)
  3. 设置滚动:
    a. 竖向滚动:子组件的高度超出 Scroll
    b. 横向滚动:子组件的宽度超出 Scroll,scrollable改为横向滚动
  4. 根据需求调整属性

属性:

名称参数类型描述
scrollableScrollDirection设置滚动方向。 ScrollDirection.Vertical 纵向 ScrollDirection.Horizontal 横向
scrollBarBarState设置滚动条状态。
scrollBarColorstringnumberColor设置滚动条的颜色。
scrollBarWidthstringnumber设置滚动条的宽度
edgeEffectvalue:EdgeEffect设置边缘滑动效果。 EdgeEffect.None 无 EdgeEffect.Spring 弹簧 EdgeEffect.Fade 阴影

代码实现:

@Entry
@Component
struct Index {

  build() {
    Column(){
      //
      /*
       * scroll组件的纵向滚动基本使用
       * 总结:
       * 1.scroll默认情况下只要当子元素内容的高度超过自身高度,则垂直滚动(自动)
       * 2.我们在使用scroll组件的时候通常要加宽和高
       * */
      Text('=========垂直滚动(纵向,默认)========')
      Scroll(){
        Column(){
        }
        .width('100%')
        .height(1000)
        .linearGradient({
          colors:[['#ff0000',0],['#00ff00',1]]
        })
      }
      .width('100%')
      .height(100)
Text('=========水平(横向)滚动========')
      Scroll(){
        Column(){
        }
        .width(1000)
        .height(200)
        .linearGradient({
          angle:90,
          colors:[['#ff0000',0],['#00ff00',1]]
        })
      }
      .width('100%')
      .height(100)
      // 设置为水平滚动(横向)
      .scrollable(ScrollDirection.Horizontal)
      // 设置滚动条状态 .scrollBar(BarState.On) on常驻,off关闭
      .scrollBar(BarState.On)
      // 设置滚动条颜色
      .scrollBarColor(Color.Green)
      // 设置滚动条宽度
      .scrollBarWidth(6)
      // 设置边缘滑动效果  .edgeEffect(EdgeEffect.Fade)边缘出现阴影, Spring边缘出现弹簧空白效果
      .edgeEffect(EdgeEffect.Fade)
    }
    .width('100%')
    .height('100%')
    .backgroundColor(Color.Pink)
  }
}

实现效果图:

PixPin_2024-09-04_09-53-22.gif

1.2 Scroll 的控制器

日常开发中可能需要通过代码控制滚动,以及获取滚动的距离,比如下图
就可以通过 Scroll 的控制器来实现
比如:

  1. 页面滚动超过一定距离,显示返回顶部(火箭),反之隐藏
  2. 点击返回顶部(火箭),返回顶部

PixPin_2024-09-04_10-13-37.gif

核心步骤:

  1. 实例化 Scroller的 控制器
  2. 绑定给 Scroll
  3. 调用 控制器的方法控制滚动,通过控制器的属性获取滚动距离

这里用到了 2 个方法1 个属性:

  1. scrollEdge:滚动到边缘

  2. currentOffset:返回当前的偏移量

1.2.1 基本结构
scrollEdge方法参数

滚动到容器边缘,不区分滚动轴方向;

Edge.Top和Edge.Start表现相同;

Edge.Bottom和Edge.End表现相同。

参数:

参数名参数类型必填参数描述
valueEdge滚动到的边缘位置。 Edge.Top 顶部 Edge.Start 开头 Edge.Bottom 底部 Edge.End 结尾
currentOffset 方法返回值
类型描述
{ xOffset: number, yOffset: number }xOffset: 水平滑动偏移; yOffset: 竖直滑动偏移。 说明: 返回值单位为vp。

1.3 Scroll 事件

Scroll 组件提供了一些事件,让开发者可以在适当的时候添加逻辑。

scroll组件上的onscroll事件(Api12及其以上版本使用onWillScroll )

作用: onWillScroll事件可以监听Scroll组件的滚动行为,我们可以在里面做一些逻辑处理

语法: Scroll(){}.onWillScroll( ()=>{} )

名称功能描述
onWillScroll(event: (xOffset: number, yOffset: number) => void)滚动事件回调, 返回滚动时水平、竖直方向偏移量。 触发该事件的条件 : 1、滚动组件触发滚动时触发,支持键鼠操作等其他触发滚动的输入设置。 2、通过滚动控制器API接口调用。 3、越界回弹。
1.3.1 Scroll 事件示例代码

注意:

.onWillScroll((xoffset, yoffset) => {}
     xoffset,yoffset这个api是有bug,返回的值不准确。
    

一般通过控制器获取偏移量的值:

  • let yoffSet = this.scroll.currentOffset().yOffset
  • let xoffSet=this.scroll.currentOffset().xOffset
@Entry
@Component
struct Index {
  //   定义控制器对象
  scroll=new Scroller()
  build() {
    Column(){
      /*
       * scroll组件上的onscroll事件(Api12及其以上版本使用onWillScroll)
       * 作用:onWillScroll事件可以监听Scroll组件的滚动行为,我们可以在里面做一些逻辑处理
       * 语法:
       * Scroll(){}.onWillScroll( ()=>{} )
       * */
      // 绑定控制器
      Scroll(this.scroll){
        Column(){
        }
        .width(1000)
        .height(100)
        .linearGradient({
          angle:90,
          colors:[['#ff0000',0],['#00ff00',1]]
        })
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.On)
      .width('100%')
      .height(100)
      // .onScroll((xoffset,yoffset)=>{
      //   console.log('偏移量:',xoffset,yoffset)
      // })
      // 回调函数
      .onWillScroll((xoffset, yoffset) => {
        // xoffset,yoffset这个api是有bug,返回的值不准确
        // console.log('偏移量:',xoffset,yoffset)
        // 通过控制器获取偏移量
        let yoffSet = this.scroll.currentOffset().yOffset
        let xoffSet=this.scroll.currentOffset().xOffset
        console.log('偏移量:',yoffSet)
        console.log('偏移量:',xoffSet)
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor(Color.Pink)
  }
}

代码效果图 PixPin_2024-09-04_11-17-08.gif

Scroll综合使用示例

例子1

效果图:

下面的效果图中我们可以看到第一个scroll组件没有反应,也没有返回值,那是因为一个控制器只能对应一个组件,控制多个需要设置多个实例化。

PixPin_2024-09-04_10-42-50.gif

代码:

@Entry
@Component
struct Index {
  // 实例化scroll对象
  scroller= new Scroller()
  build() {
    Column(){
      //
      /*
       * scroll组件的纵向滚动基本使用
       * 总结:
       * 1.scroll默认情况下只要当子元素内容的高度超过自身高度,则垂直滚动(自动)
       * 2.我们在使用scroll组件的时候通常要加宽和高
       * */

      // scroll控制器 ->new scroller()   一个控制器只能对应一个组件,控制多个需要设置多个实例化
      // 作用:可以让绑定的scroll组件进行一些指定的操作
      // 1。可以让滚动的内容自定进入顶部 -> this.scroller.scrollEdge
      // top:顶部
      // start:控制横向滚动
      // bottom:底部
      // end:控制横向滚动
      Text('=========垂直滚动(纵向,默认)========')
      Scroll(this.scroller){
        Column(){
        }
        .width('100%')
        .height(1000)
        .linearGradient({
          colors:[['#ff0000',0],['#00ff00',1]]
        })
      }
      .scrollBar(BarState.On)
      .width('100%')
      .height(300)
      Button('返回顶部')
        .onClick(()=>{
          // 点击按钮让滚动条进入顶部(内容)
          this.scroller.scrollEdge(Edge.Top)
        })
      Button('获取当前滚动y上的偏移量')
        .onClick(()=>{
          let yoffset=this.scroller.currentOffset().yOffset
          let xoffset=this.scroller.currentOffset().xOffset
          AlertDialog.show({
            message:yoffset.toString()
          })
        })

      Scroll(this.scroller){
        Column(){
        }
        .width(1000)
        .height(200)
        .linearGradient({
          angle:90,
          colors:[['#ff0000',0],['#00ff00',1]]
        })
      }
      .width('100%')
      .height(100)
      .scrollable(ScrollDirection.Horizontal)
      Button('返回顶部')
        .onClick(()=>{
          // 点击按钮让滚动条进入最左边(内容)
          this.scroller.scrollEdge(Edge.Top)
        })
      Button('获取当前滚动x上的偏移量')
        .onClick(()=>{
          let yoffset=this.scroller.currentOffset().yOffset
          let xoffset=this.scroller.currentOffset().xOffset
          AlertDialog.show({
            message:xoffset.toString()
          })
        })

    }
    .width('100%')
    .height('100%')
    .backgroundColor(Color.Pink)
  }
}
注意:
  1. 一个控制器只能对应一个组件,控制多个需要设置多个实例化。
  2. scroll默认情况下只要当子元素内容的高度超过自身高度,则垂直滚动(自动)。
  3. 在使用scroll组件的时候通常要加宽和高。
  4. Scroll组件里只能放一个组件。
例子2

onWillScroll 和 控制器对象结合完成如下需求:

    1. 默认情况,按钮隐藏
    1. 当滚动偏移量超过400的时候显示按钮
    1. 点击按钮完成内容自动滚动到顶部

思路:

    1. if做条件渲染 -> if(表达式 -> true/false) -> 用状态变量来控制
    1. 获取y偏移量用if判断如果超过400则将状态变量 isShow设置true,否则重置回false
    1. 通过点击按钮使用滚动控制器的scrollEdge方法实现回到顶部功能
@Entry
@Component
struct Index {
  @State isShow: boolean = false
  scroller = new Scroller()
  build() {
    Column() {
      // 绑定控制器
      Scroll(this.scroller) {
        Column() {
        }
        // .height(200)
        // .width(1000)
        .height(1000)
        .width('100%')
        // .backgroundColor(Color.Green)
        .linearGradient({
          colors: [
            ['#ff0000', 0],
            ['#00ff00', 1]
          ]
        })
      }
      .width('100%')
      .height(300)
      .scrollBar(BarState.On)
      .onWillScroll(() => {

        let yoffSet = this.scroller.currentOffset().yOffset

        if (yoffSet > 400) {
          this.isShow = true
        } else {
          this.isShow = false
        }
      })

      if (this.isShow) {
        Button('点我让内容滚动到顶部')
          .margin({ top: 30 })
          .onClick(() => {
            this.scroller.scrollEdge(Edge.Top)
          })
      }
    }
    .height('100%')
    .width('100%')
    .backgroundColor(Color.Pink)
  }
}

例效果图:

PixPin_2024-09-04_11-30-10.gif

2. 容器组件Tabs

当页面内容较多时,可以通过Tabs组件进行分类展示,以下这些效果都可以通过Tabs组件来实现

PixPin_2024-09-04_15-56-17.gif

PixPin_2024-09-04_10-29-22.png

2.1 基本用法

先来看看最基础的用法。
Tabs组件基本使用:

  1. tabs里面只能有TabContent子组件,TabContent里面可以使用其他组件。
  2. tabBar() 里面可以传的参数:
  • string 类型数据
  • CustomBuilder 自定义构建函数
@Entry
@Component
struct TabbarDemo {
  build() {
    Tabs() { // 顶级容器
      TabContent() {
        // 内容区域:只能有一个子组件
      }
      .tabBar('首页') // 导航栏
    }
  }
}

2.2 常用属性

默认的 tabs 已经可以实现切换,接下来咱们来看看如何通过属性控制他

  1. 垂直导航
  2. 导航位置
  3. 禁用滑动切换

通过 Tabs :

  • vertical 属性即可调整导航为 水平 或 垂直
  • barPosition 即可调整导航位置为 开头 或 结尾
  • scrollable 即可调整是否允许 滑动切换
  • animationDuration 设置动画时间 毫秒

PixPin_2024-09-04_14-50-28.gif

@Entry
@Component
struct Index {
  titles:string [ ] = ['首页','关注','热门','军事','体育','八卦','数码','财经','美食','旅行']
  build() {
    Column(){
      Tabs(){
        TabContent(){
          Text('首页内容')
        }
        .tabBar('首页')
        TabContent(){
          Text('推荐内容')
        }
        .tabBar('推荐')
        TabContent(){
          Text('发现内容')
        }
        .tabBar('发现')
        TabContent(){
          Text('我的内容')
        }
        .tabBar('我的')
      }
      .height(200)
      // 控制导航条位置   End表示下面  Start:顶部
      .barPosition(BarPosition.End)
      // 控制导航栏为侧边栏  左右可以结合.barPosition(BarPosition.End)来控制
      .vertical(false)
      // false:关闭左右滑动切换
      .scrollable(false)
      // 关闭切换taBas内容的时候的动画效果
      .animationDuration(0)
    }
    .width('100%')
    .height('100%')
    .backgroundColor(Color.Pink)
  }
}

2.3 滚动导航栏

如果导航栏的内容较多,屏幕无法容纳时,可以将他设置为滚动
可以通过 Tabs 组件的 barMode 属性调整固定导航栏或滚动导航栏

PixPin_2024-09-04_14-51-31.gif

@Entry
@Component
struct Index {
  titles:string [ ] = ['首页','关注','热门','军事','体育','八卦','数码','财经','美食','旅行']
  build() {
    Column(){
      // 设置为滚动导航栏
      Tabs(){
        ForEach(this.titles,(title:string,index:number)=>{
          TabContent() {
            Text(title+'的内容')
              .fontSize(30)
          }
          .tabBar(title)
        })
      }
      // 开启滚动导航,  .Fixed 不滚动 默认值
      .barMode(BarMode.Scrollable)
    }
    .width('100%')
    .height('100%')
    .backgroundColor(Color.Pink)
  }
}

2.4 自定义tabBar

TabBar 如果放在底部的话,一般会显示图形和文字,甚至有特殊的图标,如果要实现此类效果,就需要 自定义tabBar

2.4.1 自定义tabBar-自定义外观

tabBar(string | CustomBuilder):

  • string设置TabBar上显示内容。

  • CustomBuilder: 构造器,内部可以传入组件

核心代码

Tabs() {
    TabContent() {
        // 内容略
    }
    .tabBar(this.tabBarBuilder())
  }

@Builder
tabBarBuilder() {
  // 自定义的Tabbar结构
}

实现下图自定义tabBar效果:

PixPin_2024-09-04_15-04-11.png

实现代码:

@Entry
@Component
struct Index {
  // tabBar需要特殊的样式就要设定自定义构建函数来实现
  @Builder tabbnr(imag:string,nr:string,index:number){
    if (index==2){
      //角标
      Badge({
        value:nr,
        position:{x:18,y:-4},
        style:{}
      })
      {
        Image($r(imag))
          .width(50)
      }
    }else {
      Column({space:5}){
        if (index==3){
          Badge({
            value:'7',
            position:{x:21,y:0},
            style:{}})
          {
            Image($r(imag))
              .width(30)
          }
        }else
        {
          Image($r(imag))
            .width(30)
        }
        Text(nr)
      }}
  }
  build() {
    Column() {
      Tabs(){
        TabContent(){}.tabBar(this.tabbnr('app.media.ic_tabbar_icon_0','首页',0))
        TabContent(){}.tabBar(this.tabbnr('app.media.ic_tabbar_icon_1','逛',1))
        TabContent(){}.tabBar(this.tabbnr('app.media.startIcon','上新',2))
        TabContent(){}.tabBar(this.tabbnr('app.media.ic_tabbar_icon_2','购物车',3))
        TabContent(){}.tabBar(this.tabbnr('app.media.ic_tabbar_icon_3','我的',4))
      }
      .barPosition(BarPosition.End)
      .onChange((index)=>{
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.3)')
  }
}
2.4.2 Tabs组件的事件

自定义TabBar 之后,高亮的切换效果就没有了,需要自行实现,首先咱们得知道什么时候进行了切换

名称功能描述
onChange(event: (index: number) => void)Tab页签切换后触发的事件。 - index:当前显示的index索引,索引从0开始计算。 滑动切换、点击切换 均会触发
onTabBarClick(event: (index: number) => void)10+Tab页签点击后触发的事件。 - index:被点击的index索引,索引从0开始计算。
2.4.3 自定义 tabBar-高亮切换

结合上面的事件,来实现高亮的切换效果

核心思路:

  1. 用状态变量保存,onChange,ontabBarClick中获取到的索引值
  2. 给每个 tabBar起个标记,0,1,2....
  3. 在tabBar内部比较 标记==this.index?高亮:不高亮

自定义构建函数创建出tabbar在做切换高亮功能时的步骤:

    1. 定义一个状态变量,用来存储当前用户选中的tabbar索引
    1. 在调用自定义构建函数的时候,需要传入当前tabbar的索引(固定值),同时要传入点亮的图片路径和正常的图片路径
    1. 在自定义构建函数中使用三元表达式来进行高亮功能的处理

效果图:

PixPin_2024-09-04_15-39-40.gif

代码:

@Entry
@Component
struct Index {
  @State dex:number=0
  // tabBar需要特殊的样式就要设定自定义构建函数来实现
  @Builder tabbnr(imag:string,seleimag:string,nr:string,index:number){
    if (index==2){
      Badge({
        value:nr,
        position:{x:18,y:-4},
        style:{}
      })
      {
        Image($r(imag))
          .width(50)
      }
    }else {
      Column({space:5}){
        if (index==3){
          Badge({
            value:'7',
            position:{x:21,y:0},
            style:{}})
          {
            Image(this.dex==index?$r(seleimag):$r(imag))
              .width(30)
          }
        }else
        {
          // 3. ✨✨✨在自定义构建函数中使用三元表达式来进行高亮功能的处理
          Image(this.dex==index?$r(seleimag):$r(imag))
            .width(30)
        }
        Text(nr)
          // 3. ✨✨✨在自定义构建函数中使用三元表达式来进行高亮功能的处理
          .fontColor(this.dex==index?Color.Orange:Color.Blue)
      }}
  }
  build() {
    Column() {
      Tabs(){
        // 2. ✨✨✨在调用自定义构建函数的时候,需要传入当前tabbar的索引(固定值),同时要传入点亮的图片路径和正常的图片路径
        TabContent(){}.tabBar(this.tabbnr('app.media.ic_tabbar_icon_0','app.media.ic_tabbar_icon_0_selected','首页',0))
        TabContent(){}.tabBar(this.tabbnr('app.media.ic_tabbar_icon_1','app.media.ic_tabbar_icon_1_selected','逛',1))
        TabContent(){}.tabBar(this.tabbnr('app.media.startIcon','','上新',2))
        TabContent(){}.tabBar(this.tabbnr('app.media.ic_tabbar_icon_2','app.media.ic_tabbar_icon_2_selected','购物车',3))
        TabContent(){}.tabBar(this.tabbnr('app.media.ic_tabbar_icon_3','app.media.ic_tabbar_icon_3_selected','我的',4))
      }
      .barPosition(BarPosition.End)
      // 触发时机,切完之后才触发,滑动和点击都能触发
      .onChange((index)=>{
        //   index就是被点击的tabbar对应的索引
        this.dex=index
        AlertDialog.show({
          message:index.toString()
        })
      })

      // 触发时机,点击tabbas就触发,无延迟,但是只能点击才能触发,滑动不能触发
      // .onTabBarClick((sy)=>{
      //   AlertDialog.show({
      //     message:sy.toString()
      //   })
      // })
    }
    .width('100%')
    .height('100%')
    // .backgroundColor(Color.Pink)
    .backgroundColor('rgba(0,0,0,0.3)')
  }
}

Scroll组件、Tabs组件的综合案例

结合上面学习粗略写一个案例:

PixPin_2024-09-04_16-21-51.gif

@Entry
@Component
struct Index {
  // 实例化一个scroll控制器
  scroll=new Scroller()
  @Builder  tobu(){
    Row(){
      Image($r('app.media.hot_01'))
        .width(60)
        .onClick(()=>{
          this.scroll.scrollEdge(Edge.Top)
        })

      Row({space:15}){
        Image($r('app.media.user_01'))
          .width(20)
        Image($r('app.media.more_01'))
          .width(20)
      }
    }
    .width('100%')
    .height(56)
    .justifyContent(FlexAlign.SpaceBetween)
    .padding({left:28,right:16})
  }

  @Builder stackbd(mag:string,tex:string,){
    Stack(){
      Image($r(mag))
        .width('100%')
      Text(tex)
        .fontColor(Color.White)
        .padding({left:10,bottom:30})
    }
    .alignContent(Alignment.BottomStart)
  }
//自定义构建网格布局部分函数
  @Builder griditem(gdmag:string,gdnr:string){
    GridItem(){
      Column(){
        Image($r(gdmag))
          .width(40)
          .height(40)
        Text(gdnr)
          .fontSize(14)
          .fontColor('#666666')
      }
      .height(65)
    }
  }

  // 列表自定义函数
  @Builder liebioa(){
    Row({space:5}){
      Image($r('app.media.list_02_2'))
        .width(150)
        .height(90)
        .borderRadius(10)
      Column(){
        Text('学生时代的反入股日后入会费二位开发开副将军夫人夫人')
        Text('2024-03-12')
          .width('100%')
          .fontSize(14)
          .fontColor('rgba(0,0,0,0.4)')
      }
      .justifyContent(FlexAlign.SpaceBetween)
      .layoutWeight(1)
      .height(90)
    }
    .height(90)
  }
  // 首页滚动页面
  @Builder sygund() {

    Column() {
      // HOT页面
      this.tobu()
      Scroll(this.scroll) {
        // 上部分
        Column() {
          Swiper() {
            this.stackbd('app.media.banner_01_1', '谷歌杀入大模型')
            this.stackbd('app.media.banner_01_2', '微软继续linux')
            this.stackbd('app.media.banner_01_3', '人的宝座')
            this.stackbd('app.media.banner_01_4', '生成AL扼杀')
          }
          .loop(true)
          .autoPlay(true)
          .interval(2000)
          .indicator(
            DotIndicator.dot()
              .itemWidth(30)
              .itemHeight(5)
              .selectedItemWidth(30)
              .selectedColor(Color.White)
              .left(10)
          )
          Grid() {
            this.griditem('app.media.ic_01_1', '内容精选')
            this.griditem('app.media.ic_01_2', '学堂')
            this.griditem('app.media.ic_01_3', '鸿蒙开发者社区')
            this.griditem('app.media.ic_01_4', '博客')
            this.griditem('app.media.ic_01_5', '企业培训')
            this.griditem('app.media.ic_01_6', 'next训练营')
            this.griditem('app.media.ic_01_7', '精培')
          }
          .height(100)
          .padding({ left: 12 })
          .rowsTemplate('1fr')
          .columnsGap(24)
          .scrollBar(BarState.Off)

          // 首页热点推荐区域
          Column({ space: 10 }) {
            Text('热点推荐')
              .width('100%')
              .fontSize(24)
            this.liebioa()
            this.liebioa()
            this.liebioa()
            this.liebioa()
            this.liebioa()
            this.liebioa()
            this.liebioa()
            this.liebioa()
          }
          .padding({ left: 12, right: 12 })
          // .position({ x: 0, y: 360 })
          .width('100%')
        }
      }.height(600)

    }
  }
  // 视频 第二个页面
  @Builder spym(){
    Column(){
      Image($r('app.media.banner_01_4'))
    }
  }

  //  消息 第三页面
  @Builder xxyp(){
    Column(){
      Text('待完成---消息')
    }
  }

  //
  @State newxuhao:number=0
  //自定义tabbas
  @Builder newtab(texnr:string,xuhao:number) {
    if (xuhao == 2) {
      if (this.newxuhao == 1) {
        Badge({
          value: '9',
          position: { x: 30, y: -6 },
          style: {}
        }) {
          Text(texnr)
            .fontWeight(600)
            .fontColor('rgba(255,255,255,0.5)')
        }
      } else {
        Badge({
          value: '9',
          position: { x: 30, y: -6 },
          style: {}
        }) {
          Text(texnr)
            .fontWeight(600)
            .fontColor(this.newxuhao == xuhao ? Color.Black : 'rgba(0,0,0,0.3)')
        }
      }
    } else {
      if (this.newxuhao == 1) {
        Text(texnr)
          .fontWeight(600)
          .fontColor(this.newxuhao == xuhao ? Color.White : 'rgba(255,255,255,0.5)')
      } else {
        Text(texnr)
          .fontWeight(600)
          .fontColor(this.newxuhao == xuhao ? Color.Black : 'rgba(0,0,0,0.3)')
      }
    }
  }
  build() {
    Stack() {
      // 导航区域底图
      if (this.newxuhao==1){
        Image($r('app.media.bg_02_2'))
          .height(55)
      }else {
        Image($r('app.media.bg_02_1'))
          .height(55)
      }
      // 导航区域
      Tabs() {
        TabContent() {
          this.sygund()
        }.tabBar(this.newtab('首页',0))

        TabContent() {
          this.spym()
        }.tabBar(this.newtab('视频',1))

        TabContent() {
          this.xxyp()
        }.tabBar(this.newtab('消息',2))

        TabContent() {

        }.tabBar(this.newtab('我的',4))
      }
      .barPosition(BarPosition.End)
      .onChange((xuhao)=>{
        this.newxuhao=xuhao
      })
    }
    .alignContent(Alignment.Bottom)

  }
}

这个例子有点杂,里面包含了Grid网格布局,Swiper轮播图,Scroll组件、Tabs组件的混合运用。
如果不熟悉,以下文章可以了解:
鸿蒙开发应用之网格布局 Grid/GridItemGrid: Grid的使用场景 Grid的语法 Grid及其子组件的属 - 掘金 (juejin.cn)

鸿蒙开发应用之轮播图组件SwiperSwiper: 1. Swiper作用及其描述 2. Swiper基本用法 3.Sw - 掘金 (juejin.cn)

总结

  1. Scroll
    a. 代码控制滚动:Scroller
    b. 获取滚动距离 this.scroller.currentOffset().yOffset
    c. 事件: onScroll
  2. Tabs
    a. 自定义tabBar-->Builder
    b. 高亮效果
    ⅰ. 获取当前的索引-->保存@State中
    ⅱ. 保存的索引和 各自的索引比较
    c. 如果tabBar有特殊的外观->再写一个 Builder

放在最后: 本文正在参加华为鸿蒙有奖征文征文活动。