HarmonyOS 鸿蒙Next关于自定义tab点击滑动相关问题

发布于 1周前 作者 yibo5220 最后一次编辑是 5天前 来自 鸿蒙OS

HarmonyOS 鸿蒙Next关于自定义tab点击滑动相关问题

如何解决ListItem的高度较低时,切换tab不能精准定位问题,而且最后一个tab点击无法高亮显示;TabBar超出屏幕宽度时,如何点击屏幕栏边缘tab时自动滑动tab栏。

2 回复

参考以下demo

import curves from '[@ohos](/user/ohos).curves';

import display from '[@ohos](/user/ohos).display';

import componentUtils from '[@ohos](/user/ohos).arkui.componentUtils';

class ItemClass {

  content: string = '';

  color: Color = Color.White;

}

[@Entry](/user/Entry)

[@Component](/user/Component)

struct SwiperTab {

  private displayInfo: display.Display | null = null;

  private listScroller: ListScroller = new ListScroller();

  private animationDuration: number = 300; //动画时间

  private animationCurve: ICurve = curves.interpolatingSpring(7, 1, 328, 34); // 动画曲线

  [@State](/user/State) indicatorIndex: number = 0; //当前页签Index

  private scroller: Scroller = new Scroller();

  private barArr: string[] = ['关注', '推荐', '热点','上海', '视频', '新时代','新歌', '新碟', '新片'];

  [@State](/user/State) currentIndex: number = 0;

  [@State](/user/State) endIndex: number = 0;

  private arrList: ItemClass[] = [];

  private colorArr: Color[] = [Color.White, Color.Blue, Color.Brown, Color.Green, Color.Gray];

  aboutToAppear(): void {

    this.displayInfo = display.getDefaultDisplaySync(); //获取屏幕实例

    for (let i = 0; i < 9; i++) {

      let data: ItemClass = {

        content: i.toString(),

        color: this.colorArr[i % 5]

      }

      this.arrList.push(data);

    }

  }

  // 获取屏幕宽度,单位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 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({

      // 将页签bar可滑动时候定位在正中间

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

      yOffset: 0,

      animation: {

        duration: this.animationDuration,

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

      }

    });

  }

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

    animateTo({

      duration: duration, // 动画时长

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

      onFinish: () => {

      }

    }, () => {

    })

  }

  // 下划线动画

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

    let indexInfo = this.getTextInfo(index);

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

  }

  build() {

    Column() {

      Column(){

        Scroll(this.scroller) {

          Row() {

            ForEach(this.barArr, (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: 5, right: 5 })

                  .padding({left: 8, right: 8})

                  .id(index.toString())

                  .onClick(() => {

                    this.indicatorIndex = index;

                    this.scrollIntoView(index);

                    // 跟List进行联动

                    this.listScroller.scrollToIndex(index)

                  })

                  .border(this.indicatorIndex == index ? {

                    width: { left: 0, right: 0, top: 0, bottom: 2 },

                    color: { bottom: Color.Red },

                    radius: 0,

                    style: {

                      bottom: BorderStyle.Solid,

                    }

                  } : null)

              }

            }, (item: string) => item)

          }

          .height(32)

        }

        .width('100%')

        .scrollable(ScrollDirection.Horizontal)

        .scrollBar(BarState.Off)

        .edgeEffect(EdgeEffect.None)

        .onScrollStop(() => {

          this.underlineScrollAuto(0, this.indicatorIndex);

        })

      }

      .width('90%')

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

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

        ForEach(this.arrList, (item: ItemClass, index: number) => {

          ListItem() {

            Column() {

              Text(item.content)

            }

            .width('100%')

            .height(400)

            .backgroundColor(item.color)

          }

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

            if (this.currentIndex === index) {

              let posY = Number(newValue.position.y || 0)

              let offsetY = Number(newValue.width) / 3

              // 这里的距离根据自身业务来调整

              if (posY>= 0 && posY< offsetY) {

                this.indicatorIndex = this.currentIndex

              }

            }

          })

        }, (item: ItemClass) => item.content)

      }

      .edgeEffect(EdgeEffect.None)

      .scrollBar(BarState.Off)

      .onScrollIndex((start: number, end: number, center: number) => {

        this.currentIndex = center

        this.endIndex = end

      })

      .onReachEnd(() => {

        // 处理最后一个页签

        this.indicatorIndex = this.endIndex

      })

      .onScrollStop(() => {

        this.scrollIntoView(this.indicatorIndex);

      })

      .width("100%")

      .height("calc(100% - 50vp)")

      .backgroundColor('#F1F3F5')

    }

    .width('100%')

  }

}

可以根据自身业务来调整list的item滑动到某个top的某个范围时,显示对应的tabbar,

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

     if (this.currentIndex === index) {

         // 这里的距离根据自身业务来调整

         let posY = Number(newValue.position.y || 0)

         let offsetY = Number(newValue.width) / 3

         if (posY>= 0 && posY< offsetY) {

              this.indicatorIndex = this.currentIndex

         }

      }

 })

修改后的demo:

import curves from '[@ohos](/user/ohos).curves';

import display from '[@ohos](/user/ohos).display';

import componentUtils from '[@ohos](/user/ohos).arkui.componentUtils';

class ItemClass {

  content: string = '';

  color: Color = Color.White;

}

[@Entry](/user/Entry)

[@Component](/user/Component)

struct SwiperTab {

  private displayInfo: display.Display | null = null;

  private listScroller: ListScroller = new ListScroller();

  private animationDuration: number = 300; //动画时间

  private animationCurve: ICurve = curves.interpolatingSpring(7, 1, 328, 34); // 动画曲线

  [@State](/user/State) indicatorIndex: number = 0; //当前页签Index

  private scroller: Scroller = new Scroller();

  private barArr: string[] = ['关注', '推荐', '热点', '上海', '视频', '新时代', '新歌', '新碟', '新片'];

  [@State](/user/State) currentIndex: number = 0;

  [@State](/user/State) endIndex: number = 0;

  private arrList: ItemClass[] = [];

  private colorArr: Color[] = [Color.White, Color.Blue, Color.Brown, Color.Green, Color.Gray];

  [@State](/user/State) isClickBar: boolean = false

  aboutToAppear(): void {

    //获取屏幕实例

    this.displayInfo = display.getDefaultDisplaySync();

    for (let i = 0; i < 9; i++) {

      let data: ItemClass = {

        content: i.toString(),

        color: this.colorArr[i % 5]

      }

      this.arrList.push(data);

    }

  }

  // 获取屏幕宽度,单位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 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;

    // 将页签bar定位在正中间

    this.scroller.scrollTo({

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

      yOffset: 0,

      animation: {

        duration: this.animationDuration,

        // 动画曲线

        curve: this.animationCurve,

      }

    });

  }

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

    animateTo({

      duration: duration,

      // 动画曲线

      curve: this.animationCurve,

      onFinish: () => {

      }

    }, () => {

    })

  }

  // 下划线动画

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

    let indexInfo = this.getTextInfo(index);

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

  }

  build() {

    Column() {

      Column() {

        Scroll(this.scroller) {

          Row() {

            ForEach(this.barArr, (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: 5, right: 5 })

                  .padding({ left: 8, right: 8 })

                  .id(index.toString())

                  .onClick(() => {

                    this.indicatorIndex = index;

                    this.isClickBar = true

                    this.scrollIntoView(index);

                    // 跟List进行联动

                    this.listScroller.scrollToIndex(index)

                  })

                  .border(this.indicatorIndex == index ? {

                    width: {

                      left: 0,

                      right: 0,

                      top: 0,

                      bottom: 2

                    },

                    color: { bottom: Color.Red },

                    radius: 0,

                    style: {

                      bottom: BorderStyle.Solid,

                    }

                  } : null)

              }

            }, (item: string) => item)

          }

          .height(32)

        }

        .width('100%')

        .scrollable(ScrollDirection.Horizontal)

        .scrollBar(BarState.Off)

        .edgeEffect(EdgeEffect.None)

        .onScrollStop(() => {

          this.underlineScrollAuto(0, this.indicatorIndex);

        })

      }

      .width('90%')

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

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

        ForEach(this.arrList, (item: ItemClass, index: number) => {

          ListItem() {

            Column() {

              Text(item.content)

            }

            .width('100%')

            .height(200)

            .backgroundColor(item.color)

          }

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

            if (this.currentIndex === index) {

              // 这里的距离根据自身业务来调整

              let posY = Number(newValue.position.y || 0)

              let offsetY = Number(newValue.width) / 3

              if (posY >= 0 && posY < offsetY) {

                this.indicatorIndex = this.currentIndex

              }

            }

          })

        }, (item: ItemClass) => item.content)

      }

      .edgeEffect(EdgeEffect.None)

      .scrollBar(BarState.Off)

      .onScrollIndex((start: number, end: number, center: number) => {

        this.currentIndex = start

        this.endIndex = end

      })

      .onReachEnd(() => {

        // 处理最后一个页签

        if (!this.isClickBar) {

          this.indicatorIndex = this.endIndex

        }

      })

      .onScrollStop(() => {

        this.scrollIntoView(this.indicatorIndex);

      })

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

        this.isClickBar = false

        return { offsetRemain:offset }

      })

      .width("100%")

      .height("calc(100% - 50vp)")

      .backgroundColor('#F1F3F5')

    }

    .width('100%')

  }

}

第二个问题是由于预览器不支持该动画效果,可以使用真机或者模拟器进行测试

更多关于HarmonyOS 鸿蒙Next关于自定义tab点击滑动相关问题的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


针对HarmonyOS 鸿蒙Next关于自定义tab点击滑动相关问题,以下提供直接解决方案:

  1. 自定义Tab组件实现

    • 利用HarmonyOS提供的CustomComponentCustomLayout来创建自定义Tab组件。
    • 通过ListContainerDirectionalLayout来组织Tab项的显示。
  2. Tab点击事件处理

    • 为每个Tab项设置点击事件监听器,使用Component.setClickedListener方法。
    • 在点击事件回调中,通过ComponentgetId或自定义属性来识别被点击的Tab项。
  3. 实现滑动效果

    • 利用ScrollerScrollComponent来实现滑动效果。
    • 根据Tab项的宽度和容器宽度,计算滑动距离和位置。
    • 在Tab点击事件中,调用滑动组件的滑动方法,如scrollTosmoothScrollTo
  4. 同步Tab状态与滑动内容

    • 维护一个当前选中Tab的索引。
    • 在滑动结束时,根据滑动位置更新选中Tab的索引,并更新UI以反映当前选中状态。
    • 在Tab点击事件中,直接跳转到对应内容位置,并更新选中Tab索引和UI。

如果问题依旧没法解决请联系官网客服,官网地址是:https://www.itying.com/category-93-b0.html

回到顶部