news 2026/5/20 18:37:30

harmony-仿飞书导航-移动背景

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
harmony-仿飞书导航-移动背景

api18

效果:

代码:

import { CommonConstants, fpConversion, NavigationBarView, PopViewUtil, primaryColor, vpConversion } from 'common'; import { BuilderNameConstants, RouterModule } from 'routers'; import { componentUtils } from '@kit.ArkUI'; import { DietModel, DietModelPetdietData, } from '../model/DietModel'; import { AppUtil } from '@pura/harmony-utils'; import { util } from '@kit.ArkTS'; import { DataAdapter } from '../model/DataAdapter'; import { DietDialog } from '../components/DietDialog'; @Extend(Text) function dietText() { .fontSize(fpConversion(13)) .height(vpConversion(36)) .width(vpConversion(86)) .textAlign(TextAlign.Center) } export enum DietTabType { ok = 0, possible, impossible } export const TabItemData: TabItem[] = [ { id: DietTabType.ok, name: '猫咪可以吃', data: [] }, { id: DietTabType.possible, name: '猫咪谨慎吃', data: [] }, { id: DietTabType.impossible, name: '猫咪不能吃', data: [] } ]; export interface TabItem { id: number name: string | Resource data: DietModelPetdietData[] } /** * 宠物饮食 */ @ComponentV2 export struct DietView { @Local focusIndex: number = 0; //当前选中tab @Local indicatorLeftMargin: number = 10; //指示条左边距(控制位置) private controller: TabsController = new TabsController(); //控制tabs切换 @Local tabArray: Array<TabItem> = []; //数据源 private tabsWidth: number = 0; // tabs宽度 private tabWidth: number = 0; // 单tab宽度 private iteration: number = 1; private swipeRatio: number = 0.5; private leftPadding: number = 20; //居中给的左边距 adapter: DataAdapter<DietModelPetdietData> = new DataAdapter<DietModelPetdietData>(); @Local source: DietModelPetdietData[] = [] aboutToAppear(): void { // this.tabArray = TabItemData; this.indicatorLeftMargin = this.leftPadding + (this.focusIndex * this.tabWidth); this.initTabContext() } async initTabContext() { const catOk = await AppUtil.getContext().resourceManager.getRawFileContent('CanEatData.json') const catPossible = await AppUtil.getContext().resourceManager.getRawFileContent('CarefulToEatData.json') const catImpossible = await AppUtil.getContext().resourceManager.getRawFileContent('CanNotEatData.json') let textDecode = util.TextDecoder.create('utf-8') const okResult = JSON.parse(textDecode.decodeToString(catOk)) as DietModel const possibleResult = JSON.parse(textDecode.decodeToString(catPossible)) as DietModel const impossibleResult = JSON.parse(textDecode.decodeToString(catImpossible)) as DietModel const source = TabItemData source[DietTabType.ok].data = okResult.Petdiet.data source[DietTabType.possible].data = possibleResult.Petdiet.data source[DietTabType.impossible].data = impossibleResult.Petdiet.data this.tabArray = source } @Builder Tab(tabName: string | Resource, tabIndex: number) { Row() { Text(tabName) .dietText() .id(tabIndex.toString()) .fontColor(tabIndex === this.focusIndex ? Color.White : $r('app.color.color_333333')) .onAreaChange((_oldValue: Area, newValue: Area) => { if (this.focusIndex === tabIndex && (this.indicatorLeftMargin === 0)) { if (newValue.position.x !== undefined) { let positionX = Number.parseFloat(newValue.position.x.toString()) this.indicatorLeftMargin = Number.isNaN(positionX) ? 0 : positionX; let width = Number.parseFloat(newValue.width.toString()); this.tabWidth = Number.isNaN(width) ? 0 : width; } } }) } .justifyContent(FlexAlign.Center) .height(vpConversion(36)) .onClick(() => { this.focusIndex = tabIndex; this.controller.changeIndex(tabIndex); }) } @Builder TabContent(item: DietModelPetdietData) { Column({ space: vpConversion(8) }) { Row() { Image(item.img) .width(vpConversion(98)) .height(vpConversion(80)) } .width(CommonConstants.FULL_SCREEN) .justifyContent(FlexAlign.Center) Divider() .backgroundColor($r('app.color.color_ECECEC')) .width(CommonConstants.FULL_SCREEN) .height(vpConversion(1)) Text(item.name) .width(CommonConstants.FULL_SCREEN) .fontColor($r('app.color.color_323334')) .fontSize(fpConversion(16)) .fontWeight(FontWeight.Bold) Divider() .backgroundColor(primaryColor()) .width(vpConversion(26)) .height(vpConversion(3)) .borderRadius(vpConversion(10)) Text(item.content) .fontSize(fpConversion(13)) .width(CommonConstants.FULL_SCREEN) .maxLines(3) .ellipsisMode(EllipsisMode.END) .fontColor($r('app.color.color_A1A1A1')) } .alignItems(HorizontalAlign.Start) .backgroundColor(Color.White) .borderRadius(vpConversion(10)) .width(CommonConstants.FULL_SCREEN) .height(vpConversion(200)) .justifyContent(FlexAlign.Start) .padding({ left: vpConversion(10), right: vpConversion(10) }) .onClick(() => { PopViewUtil.showPopView<DietModelPetdietData>(wrapBuilder(DietDialog), item, { alignment: DialogAlignment.Center, autoCancel: true, isModal: true, }) }) } build() { NavigationBarView({ barTitle: $r('app.string.diet') }) { Column() { Stack({ alignContent: Alignment.TopStart }) { Row() .backgroundColor($r('app.color.color_E6E6E6')) .height(vpConversion(36)) .borderRadius(vpConversion(6)) .width('calc(100% - 40vp)') .margin({ left: vpConversion(20) }) Column() .height(vpConversion(36)) .width(vpConversion(86)) .margin({ left: this.indicatorLeftMargin }) .backgroundColor(primaryColor()) .borderRadius(vpConversion(6)) Row() { ForEach(this.tabArray, (item: TabItem, index: number) => { this.Tab(item.name, index) }, (item: TabItem, index: number) => JSON.stringify(item) + index) } .padding({ left: 20, right: 20 }) .alignItems(VerticalAlign.Center) .justifyContent(FlexAlign.SpaceBetween) .borderRadius(vpConversion(6)) .height(vpConversion(36)) .width(CommonConstants.FULL_SCREEN) } .width(CommonConstants.FULL_SCREEN) Tabs({ barPosition: BarPosition.Start, controller: this.controller }) { ForEach(this.tabArray, (item: TabItem) => { TabContent() { Grid() { ForEach(item.data, (data: DietModelPetdietData) => { GridItem() { this.TabContent(data) } }) } .padding({ left: vpConversion(10), right: vpConversion(10) }) .columnsTemplate('1fr 1fr') .columnsGap(vpConversion(20)) .rowsGap(vpConversion(20)) .width(CommonConstants.FULL_SCREEN) .height(CommonConstants.FULL_SCREEN) .edgeEffect(EdgeEffect.None) } }, (item: TabItem, index: number) => JSON.stringify(item) + index) } .margin({ top: vpConversion(20) }) .onAreaChange((_oldValue: Area, newValue: Area) => { let width = Number.parseFloat(newValue.width.toString()); this.tabsWidth = Number.isNaN(width) ? 0 : width; }) .width(CommonConstants.FULL_SCREEN) .barHeight(0) .animationDuration(300) .onAnimationStart((index: number, targetIndex: number) => { this.focusIndex = targetIndex; let targetIndexInfo = this.getTextInfo(targetIndex); this.startAnimateTo(300, targetIndexInfo.left); }) .onAnimationEnd((index: number, event: TabsAnimationEvent) => { let currentIndicatorInfo = this.getCurrentIndicatorInfo(index, event); this.startAnimateTo(0, currentIndicatorInfo.left); }) .onGestureSwipe((index: number, event: TabsAnimationEvent) => { let currentIndicatorInfo = this.getCurrentIndicatorInfo(index, event); this.focusIndex = currentIndicatorInfo.index; this.indicatorLeftMargin = currentIndicatorInfo.left; this.tabWidth = currentIndicatorInfo.width; }) } .padding({ top: vpConversion(20) }) .backgroundColor($r('app.color.color_F7F7F7')) .width(CommonConstants.FULL_SCREEN) } } private getTextInfo(index: number): Record<string, number> { let modePosition: componentUtils.ComponentInfo = this.getUIContext().getComponentUtils().getRectangleById(index.toString()); return { 'left': this.getUIContext().px2vp(modePosition.windowOffset.x), 'width': this.getUIContext().px2vp(modePosition.size.width) } } private getCurrentIndicatorInfo(index: number, event: TabsAnimationEvent): Record<string, number> { let nextIndex = index; if (index > 0 && event.currentOffset > 0) { nextIndex--; } else if (index < this.tabArray.length - 1 && event.currentOffset < 0) { nextIndex++; } let indexInfo = this.getTextInfo(index); let nextIndexInfo = this.getTextInfo(nextIndex); let swipeRatio = Math.abs(event.currentOffset / this.tabsWidth); let currentIndex = swipeRatio > this.swipeRatio ? nextIndex : index; let currentLeft = indexInfo.left + (nextIndexInfo.left - indexInfo.left) * swipeRatio; let currentWidth = indexInfo.width + (nextIndexInfo.width - indexInfo.width) * swipeRatio; return { 'index': currentIndex, 'left': currentLeft, 'width': currentWidth }; } private startAnimateTo(duration: number, leftMargin: number) { this.getUIContext().animateTo({ duration: duration, curve: Curve.Linear, iterations: this.iteration, playMode: PlayMode.Normal, }, () => { this.indicatorLeftMargin = leftMargin; }) } } // --- 路由 if (!RouterModule.getBuilder(BuilderNameConstants.DIET_PAGE)) { let builder: WrappedBuilder<[object]> = wrapBuilder(EncyclopediaDetailBuilder) RouterModule.registerBuilder(BuilderNameConstants.DIET_PAGE, builder) } @Builder export function EncyclopediaDetailBuilder(params: object) { DietView() }

原理:

动态改变左边距

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/20 18:37:15

如何快速配置Moonlight游戏串流:面向开发者的完整实践指南

如何快速配置Moonlight游戏串流&#xff1a;面向开发者的完整实践指南 【免费下载链接】moonlight-ios GameStream client for iOS/tvOS 项目地址: https://gitcode.com/gh_mirrors/mo/moonlight-ios Moonlight iOS/tvOS是一款专业的游戏串流客户端&#xff0c;让你在iO…

作者头像 李华
网站建设 2026/5/20 18:36:30

Solid服务器安全配置:SSL证书、认证策略与防护措施

Solid服务器安全配置&#xff1a;SSL证书、认证策略与防护措施 【免费下载链接】node-solid-server Solid server on top of the file-system in NodeJS 项目地址: https://gitcode.com/gh_mirrors/no/node-solid-server Solid服务器作为基于Node.js的文件系统服务器&am…

作者头像 李华
网站建设 2026/5/20 18:35:36

告别单选!用PyQt6的QComboBox实现带‘全选’的多选下拉框(附完整代码)

PyQt6实战&#xff1a;打造支持多选与全选的智能下拉框 下拉框是桌面应用开发中最常用的控件之一&#xff0c;但标准的QComboBox只能单选&#xff0c;这在需要批量操作的场景中显得力不从心。本文将带你从零开始&#xff0c;构建一个支持多选和全选功能的增强型下拉框控件。 1.…

作者头像 李华
网站建设 2026/5/20 18:28:53

微信单向好友检测:如何发现谁悄悄删除了你?

微信单向好友检测&#xff1a;如何发现谁悄悄删除了你&#xff1f; 【免费下载链接】WechatRealFriends 微信好友关系一键检测&#xff0c;基于微信ipad协议&#xff0c;看看有没有朋友偷偷删掉或者拉黑你 项目地址: https://gitcode.com/gh_mirrors/we/WechatRealFriends …

作者头像 李华
网站建设 2026/5/20 18:28:38

DDT4All:终极免费开源汽车诊断工具 - 解锁车辆ECU深层访问权限

DDT4All&#xff1a;终极免费开源汽车诊断工具 - 解锁车辆ECU深层访问权限 【免费下载链接】ddt4all OBD tool 项目地址: https://gitcode.com/gh_mirrors/dd/ddt4all DDT4All是一款专业的开源汽车诊断工具&#xff0c;为汽车爱好者和专业技师提供了强大的ECU调参和CAN总…

作者头像 李华