HarmonyOS社交通讯应用开发20 : 底部工具栏 BottomToolbar
2026/8/25 6:32:46 网站建设 项目流程

底部工具栏 BottomToolbar

引言

发布页最底部是BottomToolbar,一个集"位置选择 + 快捷图标"于一体的工具栏。它的职责可以分成三块:

  1. 位置添加行:显示"Add Location"或已选位置,带定位图标和取消按钮;
  2. 位置列表:定位/逆地理编码得到的候选位置横向排列,点击选中;收起后还能用characterElision按断点智能截断超长位置名;
  3. 图标行:表情、键盘/列表、发送、话题四个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@StateisKeyboard等是@LinkcurrentBreakpoint/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_locationic_public_cancel两张 SVG)。定位图标因为需要"灰/蓝"两态切换,工程里准备了两张资源图(ic_location.svgic_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, Wuhan")@StatecurrentLocalInfo: 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置 false,selectLocalInfo的文本保留在状态里,下次再展开列表时仍可见——这是"取消展示、不销毁数据"的温和处理。

位置列表区

// —— 第二块:位置列表(默认隐藏) —— 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_WIDTH('95%'),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。回顾发布页:isAddLocalInfoisShowLocalInfoselectLocalInfo在 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.ets):END_SM_LOCAL_INFO = 20END_MD_LOCAL_INFO = 40START_MD_LOCAL_INFO = 0OMITTED_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控制列表显隐 → 列表选中写selectLocalInfo+isAddLocalInfo→ 位置行展示/取消 →addAppStorage同步全局;
  • 细节考究:图标蓝灰切换、胶囊列表项、xmark 关闭、characterElision断点截断,都是"小屏幕友好"的成熟做法。

下一篇,把镜头拉到数据层,看BottomToolbar调用的定位工具LocationUtil是如何获取位置并逆地理编码的。

(本文引用源码:entry/src/main/ets/view/contentEditor/BottomToolbar.etsentry/src/main/ets/constants/CommonConstants.ets

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询