大家好,欢迎来到IT知识分享网。
往期知识点整理
- 鸿蒙(HarmonyOS)北向开发知识点记录~
- 鸿蒙(OpenHarmony)南向开发保姆级知识点汇总~
- 被裁员后,踏上了鸿蒙开发求职之路
- 记录一场鸿蒙开发岗位面试经历~
- 持续更新中……
场景描述
在页面布局过程中,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; } }
推荐
经常有很多小伙伴抱怨说:不知道学习鸿蒙开发哪些技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?
为了能够帮助到大家能够有规划的学习,这里特别整理了一套纯血版鸿蒙(HarmonyOS Next)全栈开发技术的学习路线,包含了鸿蒙开发必掌握的核心知识要点,内容有(ArkTS、ArkUI开发组件、Stage模型、多端部署、分布式应用开发、WebGL、元服务、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、OpenHarmony驱动开发、系统定制移植等等)鸿蒙(HarmonyOS NEXT)技术知识点。
《鸿蒙 (Harmony OS)开发学习手册》(共计892页):https://gitcode.com/HarmonyOS_MN/733GH/overview
如何快速入门?
1.基本概念
2.构建第一个ArkTS应用
3.……
鸿蒙开发面试真题(含参考答案):
《OpenHarmony源码解析》:
- 搭建开发环境
- Windows 开发环境的搭建
- Ubuntu 开发环境搭建
- Linux 与 Windows 之间的文件共享
- ……
- 系统架构分析
- 构建子系统
- 启动流程
- 子系统
- 分布式任务调度子系统
- 分布式通信子系统
- 驱动子系统
- ……
OpenHarmony 设备开发学习手册:https://gitcode.com/HarmonyOS_MN/733GH/overview
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/140922.html