ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

HarmonyOS社交通讯应用开发20 : 底部工具栏 BottomToolbar

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
返回列表