尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
HarmonyOS社交通讯应用开发20 : 底部工具栏 BottomToolbar
底部工具栏 BottomToolbar引言发布页最底部是BottomToolbar一个集位置选择 快捷图标于一体的工具栏。它的职责可以分成三块位置添加行显示Add Location或已选位置带定位图标和取消按钮位置列表定位/逆地理编码得到的候选位置横向排列点击选中收起后还能用characterElision按断点智能截断超长位置名图标行表情、键盘/列表、发送、话题四个SymbolGlyph图标后三个是演示占位。它是发布页里状态与 UI 联动最密集的区域isAddLocalInfo是否已加位置、isShowLocalInfo是否展示位置列表、selectLocalInfo选中的位置文本三个全局状态在这里被反复读写并借助addAppStorage()显式同步回 AppStorage。本篇按这三块 UI 逐一拆解并解释characterElision的断点截断逻辑。知识点讲解Link父组件传参的双向绑定BottomToolbar不持有页面级状态的原件而是通过Link接收父组件ContentEdithttps://i-blog.csdnimg.cn/devpress/blog/1c1c7e06641240e4932baed2b60ef058.png#left)Page传下来的状态引用LinkisAddLocalInfo: boolean;LinkselectLocalInfo: string | Resource;LinkisShowLocalInfo: boolean;LinkisKeyboard: boolean;Link的语义是引用父组件状态子组件里对它赋值父组件及其StorageLink对应的 AppStorage 键同步更新。发布页之所以又额外写addAppStorage()是把状态已变化这件事再次显式写入全局存储保证任何依赖 AppStorage 的模块如接续打包都能拿到最新值——双保险。顺便理清三个易混装饰器的分工State是组件私有状态自己写自己读Link是父传子的双向引用父子共写共读StorageLink是全局双向同步跨页面读写。BottomToolbar 恰好三种都在用currentLocalInfo是State、isKeyboard等是Link、currentBreakpoint/bottomHeight是StorageLink。**一个组件里装饰器混用是常态关键是清楚每条数据的归属**私有数据用 State、父子共享用 Link、全局共享用 StorageLink。SymbolGlyph系统符号图标SymbolGlyph是 ArkUI 的系统图标组件通过$r(sys.symbol.xxx)引用系统符号资源天然支持多色、可随字体粗细变化无需自带图片文件。本项目工具栏图标face_smiling、keyboard、list_bullet、arrowshape_turn_up_right、topic、xmark全部用它实现。注意区分两类图标资源$r(sys.symbol.xxx)是系统 Symbol矢量、可着色$r(app.media.xxx)是应用自带图片如ic_location、ic_public_cancel两张 SVG。定位图标因为需要灰/蓝两态切换工程里准备了两张资源图ic_location.svg、ic_location_blue.svg用条件表达式切换而键盘/列表图标直接利用 Symbol 的随状态换符号特性this.isKeyboard ? list_bullet : keyboard一行搞定。能用系统 Symbol 就不带图是减少包体的第一原则。Visibility显隐的三种状态.visibility()控制组件显隐取值有Visible显示并占位、Hidden隐藏但占位、None隐藏且不占位。本项目位置列表用None——收起时完全退出布局流工具栏高度随之收缩位置行里的取消图标用Visible/None切换。与if/else条件渲染相比visibility的优点是组件实例常驻、状态不丢失适合频繁切换的场景缺点是隐藏的组件仍在状态树里数量多时开销大。位置列表就 10 个左右条目用visibility完全够用。List 横向排列与选中ArkUIList通过.listDirection(Axis.Horizontal)横向排列子项配合.alignListItem(ListItemAlign.Center)让子项垂直居中。位置列表里每个ListItem是带圆角背景的Text点击后把自身文本写入selectLocalInfo。位置候选的类型是联合类型ResourceStr[] | geoLocationManager.GeoAddress[]——默认是 10 条资源字符串CURRENT_LOCAL_INFO定位成功后换成GeoAddress[]展开的地址文本。两种形态共用同一个ForEach得益于联合类型在 ArkTS 里的灵活性。文本截断为什么需要 characterElision逆地理编码返回的位置名可能很长如湖北省武汉市洪山区珞喻路。在小屏上整段展示会撑爆一行不同断点sm/md能容纳的字符数不同。characterElision按断点把超长文本从第 0 位截到指定长度再补...兼顾可读性与空间。结合本项目源码分析组件状态与生命周期文件路径entry/src/main/ets/view/contentEditor/BottomToolbar.ets。Componentexport struct BottomToolbar {// 位置候选列表默认为示例数据10 条Building %d, XX Road, WuhanStatecurrentLocalInfo: ResourceStr[] | geoLocationManager.GeoAddress[] CommonConstants.CURRENT_LOCAL_INFO;// 当前定位到的位置名StatecurrentLocation: string ;// 是否仍是默认位置未定位/未选中StateisDefaultLocation: boolean true;StorageLink(BreakpointConstants.BREAKPOINT_NAME)currentBreakpoint: WidthBreakpoint WidthBreakpoint.WIDTH_LG;// 底部导航条高度键盘避让用StorageLink(bottomHeight)bottomHeight: number 0;// 系统语言Watch语言变化时重新定位见第 21 篇Watch(systemLanguage)StorageLink(CommonConstants.LANGUAGE)systemLanguages: string ;LinkisAddLocalInfo: boolean;LinkselectLocalInfo: string | Resource;LinkisShowLocalInfo: boolean;LinkisKeyboard: boolean;aboutToAppear():void{this.requestPermissions();// 进入页面即申请定位权限第 22 篇详讲}aboutToDisappear():void{LocationUtil.geolocationOff();// 离开页面停止定位} }生命周期钩子点明了这个组件的隐藏职责它是定位能力的生命周期宿主——出现时申请权限并开启定位监听消失时关闭监听防止后台空耗电量。位置添加行BuilderbottomToolbar() { Column() {// —— 第一块位置添加行 ——Column() { Row() {// 定位图标已添加位置时变蓝色高亮Image(this.isAddLocalInfo ? $r(app.media.ic_location_blue) : $r(app.media.ic_location)) .height($r(app.integer.location_icon_size)) .margin({ left: $r(app.integer.location_icon_left), right: $r(app.integer.location_icon_right) }) Text() {// 主文本三态——未添加 / 已添加默认位置/ 已添加截断显示Span(this.isAddLocalInfo ? (this.isDefaultLocation ?this.selectLocalInfo :this.characterElision()) : $r(app.string.add_local)) .fontSize($r(app.integer.text_size_body3)) .fontColor($r(sys.color.font_secondary)) .onClick(() {this.changeFocus(true);// 收起软键盘this.isShowLocalInfo !this.isShowLocalInfo;// 展开/收起位置列表this.addAppStorage(); })// 取消图标已添加位置时可见点击删除位置ImageSpan($r(app.media.ic_public_cancel)) .width($r(app.integer.cancel_icon_size_mini)) .height($r(app.integer.cancel_icon_size_mini)) .visibility(this.isAddLocalInfo ? Visibility.Visible : Visibility.None) .onClick(() {this.isAddLocalInfo false;// 清除已添加位置标记this.addAppStorage(); }) } .align(Alignment.Top) .margin({ right: $r(app.integer.location_row_right) }) } .backgroundColor($r(sys.color.background_secondary)) .height($r(app.integer.location_info_row_height)) .borderRadius($r(app.integer.border_radius5)) } ...要点图标切换ic_location灰与ic_location_blue蓝两张资源随isAddLocalInfo切换视觉上明确提示已选择位置。Span 组合文本Text() { Span(...); ImageSpan(...) }让文字和取消图标在同一行内排版ImageSpan是行内图片组件与Span并列。取消图标用visibility控制显隐——未添加位置时不存在添加后出现。点击语义点击主文本 切换位置列表显隐同时changeFocus(true)强制收键盘点击取消图标 清除已选位置。取消的边界取消只把isAddLocalInfo置 falseselectLocalInfo的文本保留在状态里下次再展开列表时仍可见——这是取消展示、不销毁数据的温和处理。位置列表区// —— 第二块位置列表默认隐藏 —— Flex({alignItems: ItemAlign.Center, justifyContent: FlexAlign.SpaceBetween}) { List({ space: CommonConstants.LIST_COMM_SPACE}) { ForEach(this.currentLocalInfo, (item: Resource, index: number) { ListItem() { Text(item).backgroundColor($r(sys.color.background_secondary)).fontColor($r(sys.color.font_secondary)).fontSize($r(app.integer.text_size_body3)).borderRadius($r(app.integer.border_radius28)) // 全圆角胶囊.height($r(app.integer.text_publish_height)).padding({ right: $r(app.integer.comm_padding_margin_mini), left: $r(app.integer.comm_padding_margin_mini) }) }.onClick(() { hilog.info(DOMAIN, TAG, FORMAT, Number of the current picture: ${index});this.isAddLocalInfo true; // 标记已添加位置 this.selectLocalInfo item; // 记录选中文本 this.addAppStorage(); // 同步全局存储 }) }, (item: string, index: number) JSON.stringify(item index)) }.alignListItem(ListItemAlign.Center).width(CommonConstants.LOCAL_INFO_LIST_WIDTH) // 95%.height(auto).listDirection(Axis.Horizontal) // 横向滚动.scrollBar(BarState.Off).backgroundColor($r(sys.color.background_secondary)) // 关闭按钮xmark SymbolGlyph($r(sys.symbol.xmark)).fontSize($r(app.integer.cancel_icon_size_mini1)).fontColor([$r(sys.color.font_primary)]).onClick(() { this.isShowLocalInfo !this.isShowLocalInfo; // 收起列表this.addAppStorage(); }) } .visibility(this.isShowLocalInfo ? Visibility.Visible: Visibility.None) // 整体显隐.borderWidth({ top: $r(app.integer.default_border_width), bottom: $r(app.integer.default_border_width) }).borderColor($r(app.color.bottom_border_color)).padding({ left: ..., right: ... }) // 断点响应式 padding.height($r(app.integer.location_info_height)) // 45vp.width(CommonConstants.FULL_PERCENT).backgroundColor($r(sys.color.background_secondary))这段的交互逻辑是闭环的展开靠位置行的点击isShowLocalInfo true选中靠列表项点击写入selectLocalInfo收起靠 xmark 或再次点击位置行。选中后Text(item)直接作为selectLocalInfo的值Resource 类型随后位置行主文本按isDefaultLocation决定是否走截断逻辑。列表外层Flex的布局参数也值得留意alignItems: ItemAlign.Center让列表与 xmark 垂直居中justifyContent: FlexAlign.SpaceBetween让两者沿水平方向两端对齐——列表占LOCAL_INFO_LIST_WIDTH95%xmark 紧贴右缘互不重叠。宽度用百分比而非固定值是容器自适应、内部按比例分配的典型写法无论在手机还是平板宽度下列表区与关闭按钮的相对位置都能保持一致剩余 5% 的空间则充当了 xmark 与列表之间的天然留白。图标行// —— 第三块图标行 ——Flex({justifyContent: FlexAlign.Start,alignItems: ItemAlign.Center }){// 表情图标演示占位SymbolGlyph($r(sys.symbol.face_smiling)) .fontSize($r(app.integer.bottom_icon_size)) .margin({ right:$r(app.integer.comm_padding_margin)}) .onClick(() { this.getUIContext().getPromptAction().showToast({message: $r(app.string.trigger_event)}); })// 键盘/列表二合一图标第 15 篇已详讲SymbolGlyph(this.isKeyboard? $r(sys.symbol.list_bullet):$r(sys.symbol.keyboard)) .fontSize($r(app.integer.bottom_icon_size)) .fontColor([$r(sys.color.font_primary)]) .focusable(true) .margin({ right:$r(app.integer.comm_padding_margin)}) .onClick(() { this.isShowLocalInfo false; this.isKeyboard !this.isKeyboard; this.changeFocus(false); })// 发送图标演示占位SymbolGlyph($r(sys.symbol.arrowshape_turn_up_right))...// 话题图标演示占位SymbolGlyph($r(sys.symbol.topic))...} .backgroundColor($r(sys.color.background_secondary)) .width(CommonConstants.FULL_PERCENT) .height($r(app.integer.bottom_icon_region_height))// 56vp.padding({ left: ..., right:...})四个图标里只有键盘/列表有真实逻辑键盘联动第 15 篇详讲其余三个点击仅弹 Function to be developed. 的 toast——示例工程用占位交互交代这些功能后续实现。关键方法一addAppStorage 显式同步addAppStorage(): void { AppStorage.set(isShowLocalInfo,this.isShowLocalInfo); AppStorage.set(isAddLocalInfo,this.isAddLocalInfo); AppStorage.set(selectLocalInfo,this.selectLocalInfo); }每次状态变化后调用把三个位置相关状态显式写进 AppStorage。回顾发布页isAddLocalInfo、isShowLocalInfo、selectLocalInfo在 ContentEditPage 里都是StorageLink所以这里AppStorage.set与父组件的Link赋值最终指向同一份数据——三层AppStorage / 父组件 StorageLink / 子组件 Link保持强一致接续打包时读到的一定是最新值。关键方法二characterElision 断点截断characterElision(): string { let str this.selectLocalInfoasstring; let length str.length;// sm 断点超过 20 个字符就截断到第 20 位if(length CommonConstants.END_SM_LOCAL_INFO this.currentBreakpoint WidthBreakpoint.WIDTH_SM) {returnstr.substring(CommonConstants.START_MD_LOCAL_INFO, CommonConstants.END_SM_LOCAL_INFO) CommonConstants.OMITTED_CHARACTER;// ...}elseif(length CommonConstants.END_MD_LOCAL_INFO this.currentBreakpoint WidthBreakpoint.WIDTH_MD) {// md 断点超过 40 个字符才截断returnstr.substring(CommonConstants.START_MD_LOCAL_INFO, CommonConstants.END_MD_LOCAL_INFO) CommonConstants.OMITTED_CHARACTER; }else{returnstr;// lg 或未超长完整显示} }对应常量CommonConstants.etsEND_SM_LOCAL_INFO 20、END_MD_LOCAL_INFO 40、START_MD_LOCAL_INFO 0、OMITTED_CHARACTER ...。逻辑是按断点分级截断手机屏sm容纳 20 字符平板md容纳 40 字符大屏lg不截断。因为currentBreakpoint是StorageLink窗口尺寸变化时如平板分屏变窄截断规则会自动重算实现响应式文案。键盘避让收尾} .padding({ bottom: this.isKeyboard ?0: this.bottomHeight })// 键盘弹出时不需让位导航条.backgroundColor($r(sys.color.background_secondary)) .justifyContent(FlexAlign.Center)与第 15 篇的bottomHeight呼应键盘弹出时工具栏已被键盘顶起无需再为底部导航条留白。小结BottomToolbar演示了一个UI 密集 状态联动的组件该如何组织三块 UI 分工明确位置行入口、位置列表选择、图标行快捷操作各自状态独立、显隐可控状态流转成环isShowLocalInfo控制列表显隐 → 列表选中写selectLocalInfoisAddLocalInfo→ 位置行展示/取消 →addAppStorage同步全局细节考究图标蓝灰切换、胶囊列表项、xmark 关闭、characterElision断点截断都是小屏幕友好的成熟做法。下一篇把镜头拉到数据层看BottomToolbar调用的定位工具LocationUtil是如何获取位置并逆地理编码的。本文引用源码entry/src/main/ets/view/contentEditor/BottomToolbar.ets、entry/src/main/ets/constants/CommonConstants.ets
RELATED

相关推荐

游戏矩阵与数组思维:从二维数组建模到游戏逻辑实现

游戏矩阵与数组思维:从二维数组建模到游戏逻辑实现

你是不是觉得“游戏矩阵”听起来很高大上,感觉是游戏引擎或者图形学大佬才玩得转的东西?而“数组”又太基础,不就是 int arr[10] 吗,这俩能扯上什么关系? 如果你这么想,那就错过了一个极其重要的编程思维…

📅 2026/9/20 13:39:02
LeetCode高效刷题实战:从工程化环境搭建到面试避坑全攻略

LeetCode高效刷题实战:从工程化环境搭建到面试避坑全攻略

刷 LeetCode 这件事,最怕的不是题目难,而是刷了半天,发现时间花了,题也做了,但面试一考就懵,或者换个问法就不会。很多人把 LeetCode 当成一个“刷题”任务,但真正有效的做法,是把它…

📅 2026/10/8 14:47:44
算法训练实战指南:从基础到面试技巧

算法训练实战指南:从基础到面试技巧

1. 算法题解析:从入门到精通的实战路径算法题是程序员成长道路上绕不开的必修课。无论是准备技术面试,还是提升编程能力,算法训练都能帮助我们培养严谨的逻辑思维和高效的问题解决能力。今天我想分享一套经过实战检验的算法题训练方法&#x…

📅 2026/8/27 1:55:27
MORE NEWS

更多资讯

📰

Java后端开发必看:收藏这份AI转型指南,轻松入局高薪AI赛道!

Java后端开发在AI时代面临冲击,但并非“凉了”。企业更需懂AI的复合型人才。建议Java开发者升级技能,学习AI应用与智能体开发,薪资有望上涨40%以上。 今年招聘市场变化真大,尤其是后端开发相关的岗位。有很多同学在社交平台发帖质…

📰

掌握RAG,让AI像“查资料”一样回答问题——新手程序员必备收藏指南!

本文深入讲解了RAG的核心原理、知识库构建流程及三种架构(2-Step、Agentic、Hybrid),帮助读者理解如何让AI基于知识库检索信息并生成准确回答。文章涵盖了从文档加载到向量存储的完整流程,并提供了实际应用场景及架构选择建议&…

📰

告别ChatGPT副业陷阱,真正能赚钱的AI玩法都在这里!

本文指出当前围绕ChatGPT等AI工具的赚钱热潮已逐渐降温,初期的红利期主要集中在掌握信息差和销售教程上。随着AI技术的成熟,重复性、标准化的编程任务将逐渐被AI取代。未来,真正的机会在于将AI与专业领域结合,通过AI提升效率、创造…

📰

10-04-互操作-NativeContainer深度解析

NativeContainer 深度解析篇章:10-核心技术-托管互操作篇 | 阅读时间:约 45 分钟一、引言NativeContainer 是 Unity 在非托管内存上实现的数据结构系列,是 Job System 和 ECS 的数据基础设施。它们在非托管堆上分配内存,完全绕过 …

📰

实战分享:Docker镜像构建优化技巧(进阶篇)全流程解析

本文深入探讨Docker镜像构建优化技巧(进阶篇),涵盖背景分析、原理剖析、实战步骤、配置示例、优化建议和避坑指南。随着业务规模增长,Docker镜像构建优化技巧(进阶篇)的重要性日益凸显。无论你是刚入门还是…

📰

GitOps工作流实践指南(进阶篇)——工程师必备知识

本文深入探讨GitOps工作流实践指南(进阶篇),涵盖背景分析、原理剖析、实战步骤、配置示例、优化建议和避坑指南。作为DevOps与运维从业者,掌握GitOps工作流实践指南(进阶篇)不仅能提升系统稳定性&#xff0…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬