基于tabs实现页面布局

基于tabs实现页面布局

HarmonyOS
2024-06-11 23:15:43
浏览
收藏 0
回答 1
待解决
回答 1
按赞同
/
按时间
jmzgh

在页面布局过程中,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; 
  } 
}
分享
微博
QQ
微信
回复
2024-06-12 23:28:43
相关问题
ArkTS布局组件实现瀑布流式布局
778浏览 • 1回复 待解决
Navigation实现Tabs切换效果
1071浏览 • 1回复 待解决
基于原生实现高级显示效果
519浏览 • 1回复 待解决
基于measure实现的文本测量
601浏览 • 1回复 待解决
怎么基于Java实现视频播放?
2861浏览 • 1回复 待解决
HarmonyOS 如何实现流式布局
304浏览 • 1回复 待解决
HarmonyOS如何实现布局
267浏览 • 1回复 待解决
tabs组件和页面组合联动的方式
540浏览 • 1回复 待解决
基于Code Linter实现代码检查
355浏览 • 1回复 待解决
基于原生能力实现图文混排
365浏览 • 1回复 待解决
自适应缩放布局如何实现
350浏览 • 1回复 待解决
基于UI Observer实现UI组件埋点
389浏览 • 1回复 待解决
商品详情页面的常规布局方式
296浏览 • 1回复 待解决
tabs结合scroll实现吸顶效果
1169浏览 • 1回复 待解决
栅格布局怎么实现滚动效果?
345浏览 • 0回复 待解决
HarmonyOS 如何实现附件中的布局
126浏览 • 1回复 待解决
Stack实现叠层布局的方式
338浏览 • 1回复 待解决
使用List嵌套实现表格布局
866浏览 • 1回复 待解决
使用List组件实现多列布局
410浏览 • 1回复 待解决
如何实现文本内容的竖向布局
428浏览 • 1回复 待解决
HarmonyOS 点击tabs如何跳转到二级页面
230浏览 • 1回复 待解决