1.概述
最近在做一个项目结合小红书首页顶部Tab切换底部指示器的效果。
2.最终效果展示
indicator.gif
3.效果解析
- 抛出疑问
- 指示器的位置如何控制?
- 如何控制Swiper滑动后选中的Tab也滑动到屏幕中心位置呢?
- Swiper的滑动如何控制指示器的宽度变化(先增在减呢)?
- 实现方案选择
- 问题分析
- 指示器的位置如何控制?
- 指示器的位置需要结合标题文本的宽度计算来计算指示器显示位置。
- 标题位置如何获取?
- 组件区域变化事件 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, } }); }
- 由于顶部是通过List实现,那么List提供的有Scroller控制器,可以调用scrollTo滑动到指定位置 或者 scrollToIndex滑动到指定Index(需要注意的是scrollToIndex默认帮我们实现了动画,并且动画时间在800ms,使用该Api会出现快速滑动时,顶部List的Tab文本信息区域发生改变数据无法拿到,所有指示器卡顿问题,这里只能通过scrollTo进行处理,自定义动画以及动画时常)。
- 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 } } }
- 需要监听Swiper的 页面滑动事件 来计算百分比,通过滑动的百分比进行计算增还是减,以及需要注意的是向左滑动还是向右滑动。
- 指示器的位置如何控制?
3.FAQ
问题:为什么不采用scroller.scrollToIndex这个api来实现List的滚动,而是使用scrollTo改变滚动位置?
原因:由于scrollToIndex是帮我们封装好的方法,它指定移动的角标位置,并且默认携带动画,并且动画的执行时间在800ms,支持开启和关闭,所以如果使用scrollToIndex,那么只能在动画结束后才能获取到Tab的真实的区域信息,从而会导致指示器无法获取Tab真实位置信息,无法进行移动的处理,所以需要通过scrollTo自己实现动画以及滚动位置。
问题:Swiper快速滑动时,回调函数onContentDidScroll的中的参数 position(百分比)值会出现大于的情况
这里我们开发中需要注意,我们需要进行过滤处理一下。
问题:计算指示器位置的时候需要注意增加/减去指示器宽度的一半
原因:因为指示器的显示位置是以Tab中心位置为基准的,所以左侧需要减去指示器宽度的一半才是要显示的左侧位置。右侧同理。