鸿蒙开发5.0【基于tabs实现页面布局】

场景描述

在页面布局过程中,Tabs可以将产品包含的所有内容进行清晰分类,一目了然地呈现应用的内容范围,方便概览与跳转

场景一:tab嵌套list的吸顶效果

场景二:tabbar样式自定义:

1、tabs切换、监听

2、样式自定义

3、tabbar尾端文字渐变

场景三:tabContent切换动画

方案描述

场景一:tab嵌套list的吸顶效果

方案一:

实现思路:

1、最外层为tabs组件,首页tabContent主要用的stack组件嵌套了scroll组件+导航输入框组件,其中scroll组件嵌套了tabs组件,tabs里面嵌套list组件。

2、外层的滚动组件scroll主要通过onScroll,onScrollEdge以及onScrollFrameBegin回调判断页面是否在顶部,中间还是底部。

3、里层list组件也是通过onReachStart,onReachEnd,onScrollFrameBegin回调来判断list列表是否在顶部,中间还是底部,使用scrollBy滑动指定距离。如Scroll嵌套List滚动时,List组件的edgeEffect属性需设置为EdgeEffect.None。

核心代码

// scroll部分主要逻辑

enum ScrollPosition{

  start,

  center,

  end

}

@Entry

@Component

struct NestedScroll {

  @State listPosition: number = ScrollPosition.start; // 0代表滚动到List顶部,1代表中间值,2代表滚动到List底部。

  @State scrollPosition: number = ScrollPosition.start; // 0代表滚动到页面顶部,1代表中间值,2代表滚动到页面底部。

  ...

  build() {

    Column() {

      Tabs({ barPosition: BarPosition.End, index: this.currentIndex, controller: this.TabsController }) {

        TabContent() {

          Stack({ alignContent: Alignment.Top }) {

            Scroll(this.scrollerForScroll) {

              Column() {

                Column(){

                }

                .width("100%")

                .height("40%")

                .backgroundColor(Color.Pink)



                // tabbar

                Row({ space: 7 }) {

                  Scroll() {

                    ...

                  }

                }

                //tabs

                Tabs({ barPosition: BarPosition.Start, controller: this.subsController }) {

                  TabContent() {

                    List({ space: 10, scroller: this.scrollerForList }) {

                      ...

                    }

                    .onReachStart(() => {

                      this.listPosition = ScrollPosition.start

                    })

                    .onReachEnd(() => {

                      this.listPosition = ScrollPosition.end

                    })

                    .onScrollFrameBegin((offset: number, state: ScrollState) => {

                      console.info('chenoffset::'+offset)

                      // 滑动到列表中间时

                      if (!((this.listPosition == ScrollPosition.start && offset < 0)

                        || (this.listPosition == ScrollPosition.end && offset > 0))) {

                        this.listPosition = ScrollPosition.center

                      }

                      // 如果页面已滚动到底部 且 列表不在顶部或列表有正向偏移量

                      if (this.scrollPosition == ScrollPosition.end

                        && (this.listPosition != ScrollPosition.start || offset > 0)) {

                        console.info('chenoffsetscrollBy::'+offset)

                        return { offsetRemain: offset };

                      } else {

                        // scrollBy滑动指定距离

                        console.info('chenoffsetscrollBy滑动指定距离::'+offset)

                        this.scrollerForScroll.scrollBy(0, offset)

                        return { offsetRemain: 0 };

                      }

                    })

                  }.tabBar('关注')

                  ...

                }

              }

              .width("100%")

              .height("92%")

              .backgroundColor('#F1F3F5')

            }

          }

          .scrollBar(BarState.Off)

          .width("100%")

          .height("100%")

          // 滚动事件回调, 返回竖直方向偏移量,单位vp

          .onScroll((xOffset: number, yOffset: number) => {

            this.currentYOffset = this.scrollerForScroll.currentOffset().yOffset;

            console.info('this.currentYOffset'+this.currentYOffset)

            // 非(页面在顶部或页面在底部),则页面在中间

            if (!((this.scrollPosition == ScrollPosition.start && yOffset < 0)

              || (this.scrollPosition == ScrollPosition.end && yOffset > 0))) {

              this.scrollPosition = ScrollPosition.center

            }

          })

          // 当组件滚动到边缘时触发

          .onScrollEdge((side: Edge) => {

            if (side == Edge.Top) {

              // 页面在顶部

              this.scrollPosition = ScrollPosition.start

            } else if (side == Edge.Bottom) {

              // 页面在底部

              this.scrollPosition = ScrollPosition.end

            }

          })

          .onScrollFrameBegin(offset => {

            if (this.scrollPosition == ScrollPosition.end) {

              return { offsetRemain: 0 };

            } else {

              return { offsetRemain: offset };

            }

          })

          // 顶部导航输入框

          Row() {

            TextInput({ text: '', placeholder: 'input your word...', controller: this.controller }).fontSize(24)

          }

          .justifyContent(FlexAlign.Center)

          .backgroundColor('#00ffffff')

          .width('100%')

          .height('8%')

        }

      }.tabBar(this.tabBuilder(0, '首页'))

      ...

    }

    ...

  }.width('100%')

}

}复制

方案二:

通过原生属性nestedScroll,结合calc计算高度实现上述效果

核心代码

Tabs({ barPosition: BarPosition.Start, controller: this.subsController }) {

  TabContent() {

    List({ space: 10, scroller: this.scrollerForList }) {

      ...

    }

    .nestedScroll({

      scrollForward: NestedScrollMode.PARENT_FIRST,

      scrollBackward: NestedScrollMode.SELF_FIRST

    })复制

场景二:tabbar样式自定义

方案

由于tabs本身是有组件进行封装的,如果需要自定义样式,可以使用swiper自定义实现,Swiper在能力演进上会比Tabs能力强,比如使用swiper自定义的tabs组件可以实现数据懒加载功能

通过swiper实现tabs以下功能点:

1.下划线跟手动画:通过swiper的onGestureSwipe在页面跟手滑动过程中的回调,返回index以及extraInfo动画相关信息来判断当前index、页签距离左边margin,以及当前页签的宽度信息等,再利用动画开始以及动画结束回调结合animateTo实现下划线的动效。

2.tabbar 选中文字颜色变化:判断是否为currentIndex设置为不一样的文字颜色。

3.tabbar 选中页签位置居中:用scroll+row自定义页签栏,通过scroll实现页签停留位置居中效果。

4.使用图像效果blendMode,将当前控件的内容与下方画布已有内容进行混合,给自定义tabbar的组件row设置.blendMode,给row的父组件设置linearGradient以及blendMode来实现文字尾端渐变效果。

关于blendMode枚举说明,s表示源像素,d表示目标像素,sa表示原像素透明度,da表示目标像素透明度,r表示混合后像素,ra表示混合后像素透明度。

BlendMode.SRC_IN:r = s * da,只显示源像素中与目标像素重叠的部分。

BlendMode.SRC_OVER:r = s * (1 - da),只显示源像素中与目标像素不重叠的部分。

BlendApplyType.OFFSCREEN:将此组件和子组件内容绘制到离屏画布上,然后整体进行混合

核心代码

第一步:通过scroll组件+row组件实现自定义可滑动的tabbar

Row(){

  Column() {

    Scroll(this.scroller) {

      Row() {

        ForEach(this.arr, (item: string, index: number) => {

          Column() {

            Text(item)

              .fontSize(16)

              .borderRadius(5)

                //字体颜色粗细变化

              .fontColor(this.indicatorIndex === index ? Color.Red : Color.Black)

              .fontWeight(this.indicatorIndex === index ? FontWeight.Bold : FontWeight.Normal)

              .margin({ left: this.initialTabMargin, right: this.initialTabMargin })

              .id(index.toString())

              .onAreaChange((oldValue: Area, newValue: Area) => {

                if (this.indicatorIndex === index && (this.indicatorMarginLeft === 0 || this.indicatorWidth === 0)) {

                  if (newValue.globalPosition.x != undefined) {

                    let positionX = Number.parseFloat(newValue.globalPosition.x.toString());

                    this.indicatorMarginLeft = Number.isNaN(positionX) ? 0 : positionX;

                  }

                  let width = Number.parseFloat(newValue.width.toString());

                  this.indicatorWidth = Number.isNaN(width) ? 0 : width;

                }

              })

              .onClick(() => {

                this.indicatorIndex = index;

                this.underlineScrollAuto(this.animationDuration, index);

                this.scrollIntoView(index);

                // swiper进行联动

                this.swiperIndex = index;

              })

          }

          .width(this.textLength[index] * 28)

        }, (item: string) => item)

      }

      .height(32)

    }

    .width('100%')

    .scrollable(ScrollDirection.Horizontal)

    .scrollBar(BarState.Off)

    .edgeEffect(EdgeEffect.Spring)

    .onScroll((xOffset: number, yOffset: number) => {

      console.info(xOffset + ' ' + yOffset)

      this.indicatorMarginLeft -= xOffset;

    })

    .onScrollStop(() => {

      console.info('Scroll Stop')

      this.underlineScrollAuto(0, this.indicatorIndex);

    })

    //下划线

    Column()

      .width(this.indicatorWidth)

      .height(2)

      .borderRadius(2)

      .backgroundColor(Color.Red)

      .alignSelf(ItemAlign.Start)

      .margin({ left: this.indicatorMarginLeft, top: 5 })

  }

  .width('92%')

  .margin({ top: 15, bottom: 10})

  Text('更多')

    .width(36)

    .height(50)

    .backgroundColor(Color.Pink)

    .fontSize(16)

    .borderRadius(5)

}复制

第二步:通过swiper组件来写tabContent对应的区域,主要用swiper的属性index(this.swiperIndex)来联动上面的自定义tabbar,swiper里面可以使用LazyForEach来实现数据懒加载功能

Swiper(this.swiperController) {

  LazyForEach(this.data, (item: number) => {

    Column() {

      Text(item.toString())

      ...

    }

    .onAreaChange((oldValue: Area, newValue: Area) => {

      let width = Number.parseFloat(newValue.width.toString());

      this.swiperWidth = Number.isNaN(width) ? 0 : width;

    })

  }, (item: string) => item)

}

.onChange((index: number)=>{

  this.swiperIndex = index;

})

.cachedCount(2)

.index(this.swiperIndex)

.indicator(false)

.curve(this.animationCurve)

.loop(false)复制

第三步:

1、通过swiper的onGestureSwipe,实现跟手过程中是左滑还是右滑,计算当前以及下一个目标页面的索引值,当前距离左边的距离,以及当前tabbar的宽度

2、通过用componentUtils.getRectangleById,获取指定id的组件大小、位置、平移缩放旋转及仿射矩阵属性信息,得到当前距离左边的距离以及对应tabbar的宽度,用onAnimationStart在切换动画开始触发的时候,下划线跟踪页面一起滑动,同时宽度渐变

3、当滑动结束时通过onAnimationEnd以及自定义tabbar的scrollTo等回调实现tabbar在滚动结束之后再中间位置

.onAnimationStart((index: number, targetIndex: number, event: SwiperAnimationEvent) => {

  // 切换动画开始时触发该回调。下划线跟着页面一起滑动,同时宽度渐变。

  this.indicatorIndex = targetIndex;

  this.underlineScrollAuto(this.animationDuration, targetIndex);

})

  .onAnimationEnd((index: number, event: SwiperAnimationEvent) => {

    // 切换动画结束时触发该回调。下划线动画停止。

    let currentIndicatorInfo = this.getCurrentIndicatorInfo(index, event);

    this.startAnimateTo(0, currentIndicatorInfo.left, currentIndicatorInfo.width);

    this.scrollIntoView(index);

  })

  .onGestureSwipe((index: number, event: SwiperAnimationEvent) => {

    // 在页面跟手滑动过程中,逐帧触发该回调。

    let currentIndicatorInfo = this.getCurrentIndicatorInfo(index, event);

    this.indicatorIndex = currentIndicatorInfo.index;//当前页签index

    this.indicatorMarginLeft = currentIndicatorInfo.left;//当前页签距离左边margin

    this.indicatorWidth = currentIndicatorInfo.width;//当前页签宽度

  })

// 获取屏幕宽度,单位vp

private getDisplayWidth(): number {

  return this.displayInfo != null ? px2vp(this.displayInfo.width) : 0;

}

// 获取组件大小、位置、平移缩放旋转及仿射矩阵属性信息。

private getTextInfo(index: number): Record<string, number> {

  let modePosition :componentUtils.ComponentInfo = componentUtils.getRectangleById(index.toString());

  try {

  return { 'left': px2vp(modePosition.windowOffset.x), 'width': px2vp(modePosition.size.width) }

} catch (error) {

  return { 'left': 0, 'width': 0 }

}

}

// 当前下划线动画

private getCurrentIndicatorInfo(index: number, event: SwiperAnimationEvent): Record<string, number> {

  let nextIndex = index;

  // 滑动范围限制,Swiper不可循环,Scroll保持不可循环

  if (index > 0 && event.currentOffset > 0) {

  nextIndex--; // 左滑

} else if (index < this.data.totalCount() - 1 && event.currentOffset < 0) {

  nextIndex++; // 右滑

}

this.nextIndicatorIndex = nextIndex;

// 获取当前tabbar的属性信息

let indexInfo = this.getTextInfo(index);

// 获取目标tabbar的属性信息

let nextIndexInfo = this.getTextInfo(nextIndex);

// 滑动页面超过一半时页面切换

this.swipeRatio = Math.abs(event.currentOffset / this.swiperWidth);

let currentIndex = this.swipeRatio > 0.5 ? nextIndex : index; // 页面滑动超过一半,tabBar切换到下一页。

let currentLeft = indexInfo.left + (nextIndexInfo.left - indexInfo.left) * this.swipeRatio;

let currentWidth = indexInfo.width + (nextIndexInfo.width - indexInfo.width) * this.swipeRatio;

this.indicatorIndex = currentIndex;

return { 'index': currentIndex, 'left': currentLeft, 'width': currentWidth };

}



private scrollIntoView(currentIndex: number): void {

  const indexInfo = this.getTextInfo(currentIndex);

  let tabPositionLeft = indexInfo.left;

  let tabWidth = indexInfo.width;

  // 获取屏幕宽度,单位vp

  const screenWidth = this.getDisplayWidth();

  const currentOffsetX: number = this.scroller.currentOffset().xOffset;//当前滚动的偏移量

  this.scroller.scrollTo({

                         // 将tabbar可滑动时候定位在正中间

                           xOffset: currentOffsetX + tabPositionLeft - screenWidth / 2 + tabWidth / 2,

                           yOffset: 0,

                           animation: {

                             duration: this.animationDuration,

                             curve: this.animationCurve, // 动画曲线

                           }

                         });

  this.underlineScrollAuto(this.animationDuration, currentIndex);

}

private startAnimateTo(duration: number, marginLeft: number, width: number): void {

  animateTo({

  duration: duration, // 动画时长

  curve: this.animationCurve, // 动画曲线

  onFinish: () => {

    console.info('play end')

  }

}, () => {

  this.indicatorMarginLeft = marginLeft;

  this.indicatorWidth = width;

})

}

// 下划线动画

private underlineScrollAuto(duration: number, index: number): void {

  let indexInfo = this.getTextInfo(index);

  this.startAnimateTo(duration, indexInfo.left, indexInfo.width);

}复制

第四步:使用图像效果blendMode以及颜色渐变linearGradient实现文字尾端有渐变的效果

Scroll(this.scroller) {

  Row() {

    ForEach(this.arr, (item: string, index: number) => {

      ...

    }, (item: string) => item)

  }

  .blendMode(BlendMode.SRC_IN, BlendApplyType.OFFSCREEN)

  .backgroundColor(Color.Transparent)

  .height(32)

}

// 设置tabbar文字尾端显隐

.linearGradient({

  angle: 90,

  colors: [['rgba(0, 0, 0, 0)', 0], ['rgba(0, 0, 0, 1)', 0], ['rgba(0, 0, 0, 1)', 0.9], ['rgba(0, 0, 0, 0)', 1]]

})

.blendMode(BlendMode.SRC_OVER, BlendApplyType.OFFSCREEN)复制

场景三:tabContent切换动画

方案

通过customContentTransition实现了自定义Tabs页面的切换动画,index0-1,2-3是缩放,其他页面切换时显隐from:动画开始时,当前页面的index值。to:动画开始时,目标页面的index值。使用customContentTransition注意事项:1、当使用自定义切换动画时,Tabs组件自带的默认切换动画会被禁用,同时,页面也无法跟手滑动。2、当设置为undefined时,表示不使用自定义切换动画,仍然使用组件自带的默认切换动画。3、当前自定义切换动画不支持打断。4、目前自定义切换动画只支持两种场景触发:点击页签和调用TabsController.changeIndex()接口。

核心代码

// 自定义tabContent切换效果

// customContentTransition 控制是否为undefined

@State useCustomAnimation: boolean = true

// tabContent对应内容区域缩放值

@State tabContent0Scale: number = 1.0

@State tabContent1Scale: number = 1.0

@State tabContent2Scale: number = 1.0

@State tabContent3Scale: number = 1.0

// tabContent对应内容区域显隐值

@State tabContent0Opacity: number = 1.0

@State tabContent1Opacity: number = 1.0

@State tabContent2Opacity: number = 1.0

@State tabContent3Opacity: number = 1.0

private firstTimeout: number = 1000

private secondTimeout: number = 1000

private first2secondDuration: number = 2000

private second2thirdDuration: number = 2000

private first2thirdDuration: number = 2000

// - from:动画开始时,当前页面的index值。

// - to:动画开始时,目标页面的index值。

private baseCustomAnimation: (from: number, to: number) => TabContentAnimatedTransition = (from: number, to: number) => {

  if ((from === 0 && to === 1) || (from === 1 && to === 0)|| (from === 2 && to === 3)||(from ===3 && to === 2)) {

    // 缩放动画

    let firstCustomTransition = {

      timeout: this.firstTimeout,

      transition: (proxy: TabContentTransitionProxy) => {

        if (proxy.from === 0 && proxy.to === 1) {

          this.tabContent0Scale = 1.0

          this.tabContent1Scale = 0.5

        } else {

          this.tabContent0Scale = 0.5

          this.tabContent1Scale = 1.0

        }

        if (proxy.from === 2 && proxy.to === 3) {

          this.tabContent2Scale = 1.0

          this.tabContent3Scale = 0.5

          this.tabContent3Opacity = 1.0

        } else {

          this.tabContent2Scale = 0.5

          this.tabContent3Scale = 1.0

          this.tabContent2Opacity = 1.0 //透明度

        }

        animateTo({

          duration: this.first2secondDuration,

          onFinish: () => {

            proxy.finishTransition()

          }

        }, () => {

          if (proxy.from === 0 && proxy.to === 1) {

            this.tabContent0Scale = 0.5

            this.tabContent1Scale = 1.0

          } else {

            this.tabContent0Scale = 1.0

            this.tabContent1Scale = 0.5

          }

          if (proxy.from === 2 && proxy.to === 3) {

            this.tabContent2Scale = 0.5

            this.tabContent3Scale = 1.0

            this.tabContent2Opacity = 1.0 //透明度

          } else {

            this.tabContent2Scale = 1.0

            this.tabContent3Scale = 0.5

          }

        })

      }

    } as TabContentAnimatedTransition;

    return firstCustomTransition;

  } else {

    // 透明度动画

    let secondCustomTransition = {

      timeout: this.secondTimeout,

      transition: (proxy: TabContentTransitionProxy) => {

        if ((proxy.from === 1 && proxy.to === 2) || (proxy.from === 2 && proxy.to === 1)) {

          if (proxy.from === 1 && proxy.to === 2) {

            this.tabContent1Opacity = 1.0

            this.tabContent2Opacity = 0.5

          } else {

            this.tabContent1Opacity = 0.5

            this.tabContent2Opacity = 1.0

            this.tabContent1Scale = 1.0

          }

          animateTo({

            duration: this.second2thirdDuration,

            onFinish: () => {

              proxy.finishTransition()

            }

          }, () => {

            if (proxy.from === 1 && proxy.to === 2) {

              this.tabContent1Opacity = 0.5

              this.tabContent2Opacity = 1.0

              this.tabContent2Scale = 1.0

            } else {

              this.tabContent1Opacity = 1.0

              this.tabContent2Opacity = 0.5

            }

          })

        } else if ((proxy.from === 0 && proxy.to === 2) || (proxy.from === 2 && proxy.to === 0) || (proxy.from === 0 && proxy.to === 3) || (proxy.from === 3 && proxy.to === 0) ) {

          if (proxy.from === 0 && proxy.to === 2) {

            this.tabContent0Opacity = 1.0

            this.tabContent2Opacity = 0.5

          } else {

            this.tabContent0Opacity = 0.5

            this.tabContent2Opacity = 1.0

          }

          if (proxy.from === 0 && proxy.to === 3) {

            this.tabContent0Opacity = 1.0

            this.tabContent3Opacity = 0.5

          } else {

            this.tabContent0Opacity = 0.5

            this.tabContent3Opacity = 1.0

          }

          animateTo({

            duration: this.first2thirdDuration,

            onFinish: () => {

              proxy.finishTransition()

            }

          }, () => {

            if (proxy.from === 0 && proxy.to === 2) {

              this.tabContent0Opacity = 0.5

              this.tabContent2Opacity = 1.0

            } else {

              this.tabContent0Opacity = 1.0

              this.tabContent2Opacity = 0.5

            }

            if (proxy.from === 0 && proxy.to === 3) {

              this.tabContent0Opacity = 0.5

              this.tabContent3Opacity = 1.0

            } else {

              this.tabContent0Opacity = 1.0

              this.tabContent3Opacity = 0.5

            }

          })

        }

      }

    } as TabContentAnimatedTransition;

    return secondCustomTransition;

  }

}

以上就是本篇文章所带来的鸿蒙开发中一小部分技术讲解;想要学习完整的鸿蒙全栈技术。可以在结尾找我可全部拿到!
下面是鸿蒙的完整学习路线,展示如下:
1

除此之外,根据这个学习鸿蒙全栈学习路线,也附带一整套完整的学习【文档+视频】,内容包含如下

内容包含了:(ArkTS、ArkUI、Stage模型、多端部署、分布式应用开发、音频、视频、WebGL、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、鸿蒙南向开发、鸿蒙项目实战)等技术知识点。帮助大家在学习鸿蒙路上快速成长!

鸿蒙【北向应用开发+南向系统层开发】文档

鸿蒙【基础+实战项目】视频

鸿蒙面经

2

为了避免大家在学习过程中产生更多的时间成本,对比我把以上内容全部放在了↓↓↓想要的可以自拿喔!谢谢大家观看!
3

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值