(鸿蒙)实现页面切换时底部指示器效果

1.概述

  最近在做一个项目结合小红书首页顶部Tab切换底部指示器的效果。

2.最终效果展示

indicator.gif

3.效果解析

  1. 抛出疑问
    • 指示器的位置如何控制?
    • 如何控制Swiper滑动后选中的Tab也滑动到屏幕中心位置呢?
    • Swiper的滑动如何控制指示器的宽度变化(先增在减呢)?
  2. 实现方案选择
    • List + Divider + Swiper来实现(也可以尝试组件Tabs试一试)
    • 整体结构垂直布局Column嵌套List(横向)+ Divider(指示器分割线)+ Swiper(滑块视图容器)
  3. 问题分析
    • 指示器的位置如何控制?
      • 指示器的位置需要结合标题文本的宽度计算来计算指示器显示位置。
      • 标题位置如何获取?
        • 组件区域变化事件 onAreaChange 通过该事件记录每一个Tab的文本区域信息(左侧位置,宽度)
        • 指示器位置 / 标题位置获取代码实现
          @Builder
          itemNormal(index: number) {
            ListItem() {
              Stack({ alignContent: Alignment.Bottom }) {
                Row() {
                  Text(this.tabList[index])
                    .id(index.toString())
                    .onAreaChange((oldValue: Area, newValue: Area) => {
                        console.error('ych' , `当前text: ${this.tabList[index]} --- newValue的属性值: globalPosition值:${newValue.globalPosition.x as number}vp ---- width值:${newValue.width}vp`)
                        this.textInfos[index] = [newValue.globalPosition.x as number, newValue.width as number]
                        if (this.selectedIndex === index) {
                            this.indicatorWidth = 20
                            //指示器位置 = 当前角标对应的Tab的左边距 + Tab的宽度 / 2 - 指示器默认宽度 / 2
                            this.indicatorLeftMargin = this.textInfos[index][0] + this.textInfos[index][1] / 2 - 10
                            console.error('ych' , `++++++++++ 当前的indicatorWidth的值为:${this.indicatorWidth}  ---- indicatorLeftMargin的值为:${this.indicatorLeftMargin}`)
                      }
                  })
              }
            .height('100%')
            .alignItems(VerticalAlign.Center).justifyContent(FlexAlign.Center)
            }
          }
            .height('100%')
            .padding({ left: 16 , right: 14 })
        }
        
    • 如何控制Swiper滑动后选中的Tab也滑动到屏幕中心位置呢?
      • 由于顶部是通过List实现,那么List提供的有Scroller控制器,可以调用scrollTo滑动到指定位置 或者 scrollToIndex滑动到指定Index(需要注意的是scrollToIndex默认帮我们实现了动画,并且动画时间在800ms,使用该Api会出现快速滑动时,顶部List的Tab文本信息区域发生改变数据无法拿到,所有指示器卡顿问题,这里只能通过scrollTo进行处理,自定义动画以及动画时常)。
        // 控制页签所在Scroll容器的偏移,保证选中页签居中显示,当选中页签左边的页签总宽度有限时,offset为0,右边同理
        private scrollIntoView(currentIndex: number): void {
            //获取对应角标位置的Tab区域信息
            const indexInfo = this.textInfos[currentIndex]
            //Tab的globalPosition(左侧位置)
            let currentIndexLeft = indexInfo[0];
             //Tab的width(总宽度)
            let currentIndexWidth = indexInfo[1];
            //获取屏幕宽度
            let screenWidth = this.getDisplayWidth();
            if (screenWidth < 100) {
              screenWidth = 300;
            }
           let targetLeft = screenWidth / 2 - currentIndexWidth / 2;
           const currentOffsetX: number = this.scroller.currentOffset().xOffset;
           this.scroller.scrollTo({
              // Scroll当前位移需移动”页签移动目标位移-页签当前位移“后将选中页签居中显示,因为Scroll位移和屏幕的正方向相反,所以此处是减去
              xOffset: currentOffsetX - (targetLeft - currentIndexLeft),
              yOffset: 0,
              animation: {
                duration: this.animationDuration,
                curve: Curve.Linear,
             }
           });
        }
        
    • Swiper的滑动如何控制指示器的宽度变化(先增在减呢)?
      • 需要监听Swiper的 页面滑动事件 来计算百分比,通过滑动的百分比进行计算增还是减,以及需要注意的是向左滑动还是向右滑动。
          private getCurrentIndicatorInfo(index: number): Record<string, number> {
              // console.error('ych' , `------- start --------`)
              let nextIndex = index
              // console.error('ych' , `nextIndex初始值:${nextIndex}`)
              if (index > 0 && this.scrollPercent < 0) {
                  nextIndex--
              } else if (index < this.tabList.length - 1 && this.scrollPercent > 0) {
                  nextIndex++
              }
            console.error('ych onContentDidScroll ------' , `nextIndex处理后值:${nextIndex}`)
            let indexInfo = this.textInfos[index]
            // console.error('ych' , `indexInfo值:${indexInfo}`)
            let nextIndexInfo = this.textInfos[nextIndex]
            // console.error('ych' , `nextIndexInfo值:${nextIndexInfo}`)
            let swipeRatio = 1 - Math.abs(this.scrollPercent)
            // console.error('ych onContentDidScroll' , `swipeRatio:${swipeRatio}`)
        
            let currentValue = (nextIndexInfo[0] + nextIndexInfo[1] / 2 - 10)  - (indexInfo[0] + indexInfo[1] / 2 - 10)
            console.error('ych onContentDidScroll' , `currentValue:${currentValue}`)
        
            let currentLeft:number = this.indicatorLeftMargin
            let currentWidth:number = 20
            //判断大于0.5和小于0.5的处理情况
            if (this.scrollPercent > 0){
              if (swipeRatio!= 1){
                if (swipeRatio <= 0.5) {
                    currentWidth = 20 + Math.abs(currentValue - 20) * swipeRatio * 2
                    currentLeft = this.textInfos[index][0] + this.textInfos[index][1] / 2 - 10 + 10 * swipeRatio * 2
                }else {
                    currentWidth = 20 + Math.abs(currentValue - 20) * (1 - swipeRatio) * 2
                    currentLeft = this.textInfos[index][0] + this.textInfos[index][1] / 2 - 10 + 10 * swipeRatio * 2 +             Math.abs(currentValue - currentWidth)
                }
               }
            }else {
                if (swipeRatio!= 1) {
                    if (swipeRatio <= 0.5) {
                        currentWidth = 20 + (Math.abs(currentValue) - 20) * swipeRatio * 2
                        currentLeft = this.textInfos[index][0] + this.textInfos[index][1] / 2 - 10 - (Math.abs(currentValue) - 20) * swipeRatio * 2 - 10 * swipeRatio * 2
                    } else {
                        currentWidth = 20 + (Math.abs(currentValue) - 20) * (1 - swipeRatio) * 2
                        currentLeft = this.textInfos[nextIndex][0] + this.textInfos[nextIndex][1] / 2 - 10 * (swipeRatio - 0.5) * 2
                  }
                }
            }
            console.error('ych onContentDidScroll' , `currentLeft:${currentLeft}`)
            console.error('ych onContentDidScroll' , `currentWidth:${currentWidth}`)
            // console.error('ych' , `------- end --------`)
            return { 'index': index, 'left': currentLeft, 'width': currentWidth }
          }
        }
        

3.FAQ

问题:为什么不采用scroller.scrollToIndex这个api来实现List的滚动,而是使用scrollTo改变滚动位置?

原因:由于scrollToIndex是帮我们封装好的方法,它指定移动的角标位置,并且默认携带动画,并且动画的执行时间在800ms,支持开启和关闭,所以如果使用scrollToIndex,那么只能在动画结束后才能获取到Tab的真实的区域信息,从而会导致指示器无法获取Tab真实位置信息,无法进行移动的处理,所以需要通过scrollTo自己实现动画以及滚动位置。

问题:Swiper快速滑动时,回调函数onContentDidScroll的中的参数 position(百分比)值会出现大于的情况

这里我们开发中需要注意,我们需要进行过滤处理一下。

问题:计算指示器位置的时候需要注意增加/减去指示器宽度的一半

原因:因为指示器的显示位置是以Tab中心位置为基准的,所以左侧需要减去指示器宽度的一半才是要显示的左侧位置。右侧同理。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容