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

相关新闻

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

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

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

2026/8/25 6:32:55 阅读更多 →
LeetCode高效刷题实战:从工程化环境搭建到面试避坑全攻略

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

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

2026/8/25 6:32:55 阅读更多 →
2026年Java大厂面试趋势与AI协同编码实战

2026年Java大厂面试趋势与AI协同编码实战

1. 项目背景与核心价值2026年Java技术栈的就业市场竞争愈发激烈,大厂面试的考核维度正在发生显著变化。根据近半年头部互联网企业的实际面试反馈,单纯背诵八股文的通过率已降至27%,而能结合AI工具解决实际问题的候选人录用率高达83%。这份题库…

2026/8/25 6:32:55 阅读更多 →

最新新闻

TokenHub:大模型API智能路由与成本控制实战指南

TokenHub:大模型API智能路由与成本控制实战指南

1. 项目概述:为什么TokenHub能成为大模型服务的最优解?最近在折腾大模型应用开发的朋友,估计都绕不开一个核心问题:API调用成本。无论是调用OpenAI的接口,还是部署国内的混元、文心一言等模型,随着调用量的…

2026/8/25 7:25:22 阅读更多 →
OmniRoute:免费无限次AI编程助手部署与VS Code集成指南

OmniRoute:免费无限次AI编程助手部署与VS Code集成指南

这次我们来看一个能让你在 VS Code 里免费、无限次使用 AI 编程助手的方案:OmniRoute。如果你正在寻找 Claude Code 的替代品,或者厌倦了订阅制 AI 工具的限制和信用卡绑定,这个开源项目值得一试。它的核心思路很直接:通过一个聚合…

2026/8/25 7:25:22 阅读更多 →
0824晨间日记

0824晨间日记

# 0824晨间日记 - 关键词 - 上午- 分班考试- 送小孩去分班考试- 路上没有电了- 后来找AY- 最后打车的过去的- 生活就是这样:- 前期准备不足,没有电就特别的慢- 但是要及时的止损,切换赛道- 目标是不会变的,- 换个方式: 完成目标- …

2026/8/25 7:25:22 阅读更多 →
携程算法岗笔试攻略:高频考点与解题技巧

携程算法岗笔试攻略:高频考点与解题技巧

1. 题目背景与考察意图解析2026年3月12日携程算法岗的这场笔试,从时间节点来看属于春季招聘季的常规技术筛选环节。作为在线旅游行业的头部企业,携程算法岗的题目设计往往带有鲜明的业务特征——既包含计算机科学的通用算法考察,又会融入旅游…

2026/8/25 7:25:22 阅读更多 →
AI重点已死,人工智能崛起

AI重点已死,人工智能崛起

《AI重点已死,人工智能崛起》——上轮缺口靠钱追,本轮缺口靠时间生根八成企业掌门人自认在领导人工智能转型,其实只是在管理一系列举措。这两件事,隔着一条正在拉宽的鸿沟。最新调查显示,约八成掌门人不满AI项目进展&a…

2026/8/25 7:25:22 阅读更多 →
OpenClaw:42个AI技能协同的智能体操作系统实战解析

OpenClaw:42个AI技能协同的智能体操作系统实战解析

1. 从“单打独斗”到“军团作战”:AI Agent的范式革命最近在AI圈子里,一个叫OpenClaw的项目彻底火了。如果你还在用ChatGPT、Claude或者Midjourney这些工具,一个接一个地手动输入指令、等待结果、再手动整合,那你可能已经落后了半…

2026/8/25 7:24:13 阅读更多 →

日新闻

洛谷 P7912:[CSP-J 2021 T4] 小熊的果篮 ← 双向链表

洛谷 P7912:[CSP-J 2021 T4] 小熊的果篮 ← 双向链表

【题目来源】 https://www.luogu.com.cn/problem/P7912 【题目描述】 小熊的水果店里摆放着一排 n 个水果。每个水果只可能是苹果或桔子,从左到右依次用正整数 1,2,…,n 编号。连续排在一起的同一种水果称为一个“块”。小熊要把这一排水果挑到若干个果篮里&#x…

2026/8/25 0:00:34 阅读更多 →
Transformers.js 网页端图像抠图实战:零后端 3 行代码返回透明 PNG

Transformers.js 网页端图像抠图实战:零后端 3 行代码返回透明 PNG

Transformers.js 网页端图像抠图实战:零后端 3 行代码返回透明 PNG 【免费下载链接】transformers.js State-of-the-art Machine Learning for the web. Run 🤗 Transformers directly in your browser, with no need for a server! 项目地址: https:/…

2026/8/25 0:00:34 阅读更多 →
数学建模竞赛论文写作指南:从模型构建到学术表达的核心技能

数学建模竞赛论文写作指南:从模型构建到学术表达的核心技能

1. 项目概述:从“会做”到“会写”的竞赛核心跃迁“全国大学生数学建模竞赛”,这个名字对理工科学生来说,分量极重。每年,无数团队在三天三夜的时间里,为一个开放性问题绞尽脑汁,从建立模型、求解算法到编程…

2026/8/25 0:00:34 阅读更多 →

周新闻

[光学原理与应用-521]:对光的错误理解与纠偏

[光学原理与应用-521]:对光的错误理解与纠偏

首先光是一种能量的载体和形态,宏观上观察到的光是由无数个微观的光量子组成的,每个光子在产生的瞬间,其在真空的空间中以确定不变的速度沿着一个初始的方向一直向前,在微观层面,每个光量子的运动轨迹是以波函数所展现…

2026/8/25 3:38:12 阅读更多 →
SIP通话转接原理与REFER方法实战解析

SIP通话转接原理与REFER方法实战解析

1. 通话转接不是“挂断再拨号”,而是SIP会话的动态重定向你有没有遇到过这样的场景:客服坐席A正在和客户通电话,突然需要把这通对话无缝转给专家坐席B,客户完全感知不到中间的断连——既没听到忙音,也没被要求重新拨号…

2026/8/25 3:38:18 阅读更多 →
Kolla-ansible单节点OpenStack部署实战:从环境准备到排坑指南

Kolla-ansible单节点OpenStack部署实战:从环境准备到排坑指南

1. 为什么选择Kolla-ansible来部署单节点OpenStack?如果你正在寻找一种能把OpenStack从“概念”快速变成“可用的实验环境”的方法,那么Kolla-ansible几乎是当前最主流、最省心的选择。我见过太多人卡在手动编译依赖、配置服务、处理版本冲突的泥潭里&am…

2026/8/25 3:38:23 阅读更多 →

月新闻

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南 【免费下载链接】BaiduNetdiskPlugin-macOS For macOS.百度网盘 破解SVIP、下载速度限制~ 项目地址: https://gitcode.com/gh_mirrors/ba/BaiduNetdiskPlugin-macOS 还在为百度网盘macOS版的龟速下…

2026/8/24 20:22:44 阅读更多 →
终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 还在为网易云音乐下载的NCM格式文件无法在其他播放器播放而烦恼吗?ncmdump解密工具帮你轻松解决这个困…

2026/8/23 12:10:44 阅读更多 →
HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

AgentCard 智能体卡片:为英语学习 App 打造桌面级学习助手适用平台:HarmonyOS 7.0 (API 26 Beta)一、引言 HarmonyOS 7.0(API 26 Beta)新增了 AgentCard 智能体卡片能力,这是继 HMAF(鸿蒙智能体框架&#x…

2026/8/24 11:20:22 阅读更多 →