资讯动态

鸿蒙原生应用 ArkUI 实战:手把手实现社交发现页的兴趣多选与实时预览卡

发布时间:2026/9/7 21:05:28 来源:尧图企业网站定制
鸿蒙原生应用 ArkUI 实战手把手实现社交发现页的兴趣多选与实时预览卡App 18「校园社交匹配」发现 TabFunc1Tab是智能匹配偏好设置页——用户设置昵称/简介/兴趣/范围实时预览卡片同步更新。整页用 Header 基本信息表单昵称/简介10 个兴趣标签多选已选计数 匹配范围设置学院/年龄范围/匹配范围/匿名 Toggle实时预览卡片昵称/学院/匿名状态/简介/已选标签 4 个快捷模板 开始匹配按钮。本篇基于18-social-match/entry/src/main/ets/pages/Func1Tab.ets约 271 行逐段拆解附 4 张实机截图。一、整体结构7 个 Builder 2 个辅助方法的偏好表单Func1Tab 是本系列交互联动最丰富的表单页——7 个 Builder toggleInterest()/selectedCount()2 个辅助方法build() { Column() { this.Header() Scroll() { Column({ space: 14 }) { this.FormSection() this.InterestSection() this.SelectorSection() this.PreviewCard() this.TemplateSection() this.SubmitBtn() } .width(100%) .padding({ left: D.pad, right: D.pad, top: 14, bottom: D.pad this.safeBottom 20 }) } .layoutWeight(1).scrollBar(BarState.Off).align(Alignment.Top) } .width(100%).height(100%).backgroundColor(C.bg) }7 个块按填信息 → 选兴趣 → 设范围 → 看预览 → 用模板 → 开始匹配漏斗展开FormSection— 昵称 个人简介输入InterestSection— 10 个兴趣标签多选本页核心交互SelectorSection— 学院/年龄范围/匹配范围/匿名开关PreviewCard— 实时预览本页最大亮点TemplateSection— 4 个快捷模板SubmitBtn— 开始智能匹配与 App 13/17 表单的差异App 13/17 是单选类型/优先级App 18 是多选兴趣——社交匹配需要多标签一个人可以有多个兴趣这是多选 vs 单选的业务差异。项目源码开源https://gitee.com/codenestFlow/HarmonyOSHub二、FormSection昵称 个人简介FormSection 是基本信息输入昵称 TextInput 简介 TextAreaBuilder FormSection() { Column({ space: 14 }) { Text(基本信息).fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text).width(100%) Column({ space: 8 }) { Text(昵称).fontSize(12).fontColor(C.textDim).width(100%) TextInput({ placeholder: 请输入你的昵称, text: this.nickname }) .onChange((val: string) { this.nickname val; }) .height(44).borderRadius(D.rMd).backgroundColor(C.bg) .border({ width: 1, color: C.stroke }).placeholderColor(C.textDim).placeholderFont({ size: 13 }) }.width(100%) Column({ space: 8 }) { Text(个人简介).fontSize(12).fontColor(C.textDim).width(100%) TextArea({ placeholder: 介绍一下自己让大家更了解你..., text: this.bio }) .onChange((val: string) { this.bio val; }) .height(80).borderRadius(D.rMd).backgroundColor(C.bg) .border({ width: 1, color: C.stroke }).placeholderColor(C.textDim).placeholderFont({ size: 13 }) }.width(100%) } .width(100%).padding(14).backgroundColor(C.card).borderRadius(D.rMd).border({ width: 1, color: C.stroke }) }与 App 13/17 的差异背景C.bg浅灰而非C.cardSoft——输入框用页面背景色配 1vp 描边C.stroke——描边输入框风格区别于 App 13/17 的纯色块输入框标签在上Text(昵称)在 TextInput 上方而非左侧固定宽度——上标签布局更宽松适合移动端表单State nickname: string State bio: string 双向绑定——输入内容实时存入状态供 PreviewCard 读取。三、InterestSection10 个兴趣标签多选本页核心InterestSection 是 10 个兴趣标签的多选胶囊右上角实时显示已选数量Builder InterestSection() { Column({ space: 12 }) { Row() { Text(兴趣标签).fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text) Blank() Text(已选 this.selectedCount() 个).fontSize(12).fontColor(C.primary) }.width(100%) Flex({ wrap: FlexWrap.Wrap }) { ForEach(this.interests, (item: InterestTag) { Text(item.label) .fontSize(13) .fontColor(item.selected ? #FFFFFF : C.textSub) .backgroundColor(item.selected ? C.primary : C.cardSoft) .borderRadius(16) .padding({ left: 14, right: 14, top: 8, bottom: 8 }) .margin({ right: 8, bottom: 8 }) .border({ width: 1, color: item.selected ? C.primary : C.stroke }) .onClick(() { this.toggleInterest(item.id); }) }, (item: InterestTag) item.id.toString()) }.width(100%) } .width(100%).padding(14).backgroundColor(C.card).borderRadius(D.rMd).border({ width: 1, color: C.stroke }) }3.1 Flex 换行布局本页特色Flex({ wrap: FlexWrap.Wrap })是 Flex 布局的换行模式Flex({ wrap: FlexWrap.Wrap }) { ForEach(this.interests, (item: InterestTag) { Text(item.label) .fontSize(13) ... .margin({ right: 8, bottom: 8 }) ... }, (item: InterestTag) item.id.toString()) }.width(100%)FlexWrap.Wrap让标签自动换行——10 个标签不会挤在一行而是自动流式排列每行放不下的换到下一行。每个标签用margin({ right: 8, bottom: 8 })留出间距而不是 space因为 Flex 换行模式不支持 space。对比系列其他选择器App 13 类型用 Row 一行、App 16 商品用 Grid 3 列、App 18 兴趣用Flex 自动换行——标签数量多 长短不一 → Flex 换行最佳10 个标签长短不一Grid 固定列会留白Row 一行放不下。3.2 选中态三件套 toggleInterest().fontColor(item.selected ? #FFFFFF : C.textSub) .backgroundColor(item.selected ? C.primary : C.cardSoft) .borderRadius(16) ... .border({ width: 1, color: item.selected ? C.primary : C.stroke })选中粉红底 白字 粉红描边未选浅灰底 灰字 浅灰描边点击调用toggleInterest(item.id)toggleInterest(id: number): void { for (let i 0; i this.interests.length; i) { if (this.interests[i].id id) { this.interests[i] { id: this.interests[i].id, label: this.interests[i].label, selected: !this.interests[i].selected }; } } }toggleInterest是翻转选中逻辑——找到 id 匹配的标签selected: !selected取反。关键写法this.interests[i] { id: ..., label: ..., selected: !this.interests[i].selected };创建新对象赋值而非原地改字段——this.interests[i].selected !selected直接改会触发 ArkUI 的响应式不感知问题数组元素属性变更不触发重渲染整体替换元素才能触发 ForEach 重渲染。这是 ArkTS 响应式数组的经典写法State 数组里改元素要重建对象。3.3 selectedCount()已选计数selectedCount(): number { let count: number 0; for (let i 0; i this.interests.length; i) { if (this.interests[i].selected) { count; } } return count; }线性遍历统计已选数量——右上角已选 4 个初始 4 个编程/篮球/阅读/电影。State interests一变 →selectedCount()重算 → 文字更新。10 个兴趣数据编程(选)/动漫/篮球(选)/音乐/摄影/阅读(选)/旅行/美食/游戏/电影(选)——初始 4 个选中覆盖技术/运动/文艺/生活多类兴趣。四、SelectorSection匹配范围四件套SelectorSection 是学院/年龄范围/匹配范围/匿名开关四组设置Builder SelectorSection() { Column({ space: 14 }) { Text(匹配范围).fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text).width(100%) Column({ space: 10 }) { Text(选择学院).fontSize(12).fontColor(C.textDim).width(100%) Row({ space: 8 }) { ForEach(this.depts, (d: DeptItem) { Row({ space: 6 }) { Text(d.icon).fontSize(14) Text(d.name).fontSize(12) } .padding({ left: 10, right: 10, top: 8, bottom: 8 }) .borderRadius(D.rMd) .backgroundColor(this.selectedDept d.id ? C.primarySoft : C.cardSoft) .border({ width: 1, color: this.selectedDept d.id ? C.primary : C.stroke }) .onClick(() { this.selectedDept d.id; promptAction.showToast({ message: d.name }); }) }, (d: DeptItem) d.id.toString()) }.width(100%) }.width(100%) Column({ space: 10 }) { Text(年龄范围).fontSize(12).fontColor(C.textDim).width(100%) Row({ space: 8 }) { ForEach(this.ageRanges, (r: string, idx: number) { Text(r) .fontSize(12).fontColor(this.ageRange idx ? #FFFFFF : C.textSub) .backgroundColor(this.ageRange idx ? C.primary : C.cardSoft) .borderRadius(D.rMd) .padding({ left: 12, right: 12, top: 8, bottom: 8 }) .border({ width: 1, color: this.ageRange idx ? C.primary : C.stroke }) .onClick(() { this.ageRange idx; }) }, (r: string, idx: number) r idx) }.width(100%) }.width(100%) Column({ space: 10 }) { Text(匹配范围).fontSize(12).fontColor(C.textDim).width(100%) Row({ space: 8 }) { ForEach(this.scopes, (s: string, idx: number) { Text(s) .fontSize(12).fontColor(this.matchScope idx ? #FFFFFF : C.textSub) .backgroundColor(this.matchScope idx ? C.primary : C.cardSoft) .borderRadius(D.rMd) .padding({ left: 12, right: 12, top: 8, bottom: 8 }) .border({ width: 1, color: this.matchScope idx ? C.primary : C.stroke }) .onClick(() { this.matchScope idx; }) }, (s: string, idx: number) s idx) }.width(100%) }.width(100%) Row() { Text(匿名匹配).fontSize(13).fontColor(C.text).layoutWeight(1) Toggle({ type: ToggleType.Switch, isOn: this.anonymous }) .selectedColor(C.primary) .onChange((on: boolean) { this.anonymous on; }) }.width(100%) } .width(100%).padding(14).backgroundColor(C.card).borderRadius(D.rMd).border({ width: 1, color: C.stroke }) }4 组设置的 3 种交互形态学院选择emoji 文字胶囊——5 个学院不限/计算机/艺术/文/理selectedDept单选。点击还弹 Toastd.name——选择反馈年龄范围文字胶囊——3 档同龄±1岁/同年级/不限年龄ageRange单选匹配范围文字胶囊——3 档同学院/同校区/全校matchScope单选匿名匹配Toggle 开关——anonymous布尔3 个单选 1 个开关 4 个 StateselectedDept/ageRange/matchScope/anonymous——每个设置一个 State互不干扰。匿名匹配是社交 App 的特色功能——匿名状态让用户不敢打招呼时也能参与匹配Soul 的匿名匹配同款。五、PreviewCard实时预览本页最大亮点PreviewCard 是实时预览卡——昵称/学院/匿名状态/简介/已选标签全部实时联动Builder PreviewCard() { Column({ space: 12 }) { Row() { Text(预览卡片).fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text) Blank() Text(他人将看到).fontSize(11).fontColor(C.textDim) }.width(100%) Row({ space: 14 }) { Row() { Text().fontSize(40) } .width(64).height(64).backgroundColor(C.primarySoft).borderRadius(32).justifyContent(FlexAlign.Center) Column({ space: 4 }) { Text(this.nickname.length 0 ? this.nickname : 你的昵称).fontSize(16).fontWeight(FontWeight.Bold).fontColor(C.text) Text(this.depts[this.selectedDept].name).fontSize(12).fontColor(C.textDim) Text(this.anonymous ? 匿名模式 : 公开模式).fontSize(11).fontColor(this.anonymous ? C.warn : C.ok) }.alignItems(HorizontalAlign.Start).layoutWeight(1) }.width(100%) if (this.bio.length 0) { Text(this.bio).fontSize(12).fontColor(C.textSub).width(100%).maxLines(3).textOverflow({ overflow: TextOverflow.Ellipsis }) } Row({ space: 6 }) { ForEach(this.interests, (item: InterestTag) { if (item.selected) { Text(item.label).fontSize(11).fontColor(C.primary) .backgroundColor(C.primarySoft).borderRadius(4) .padding({ left: 8, right: 8, top: 4, bottom: 4 }) } }, (item: InterestTag) item.id.toString()) }.width(100%) } .width(100%).padding(14).backgroundColor(C.card).borderRadius(D.rLg).border({ width: 1, color: C.stroke }) }5.1 昵称实时联动Text(this.nickname.length 0 ? this.nickname : 你的昵称).fontSize(16).fontWeight(FontWeight.Bold).fontColor(C.text)昵称输入 →State nickname→ 预览卡实时更新没输入时显示占位你的昵称。输入即预览——用户边打字边看到别人将看到的我的卡片。5.2 学院 匿名状态联动Text(this.depts[this.selectedDept].name).fontSize(12).fontColor(C.textDim) Text(this.anonymous ? 匿名模式 : 公开模式).fontSize(11).fontColor(this.anonymous ? C.warn : C.ok)学院depts[selectedDept].name从数组取当前选中学院名id1 → 不限学院——下标取数联动。匿名状态anonymous ? 匿名模式 : 公开模式—— 橙warn 匿名模式 vs 绿ok 公开模式——/ 图标 文字 颜色三重表达。Toggle 一开预览卡立刻显示 匿名模式。5.3 简介条件渲染if (this.bio.length 0) { Text(this.bio).fontSize(12).fontColor(C.textSub).width(100%).maxLines(3).textOverflow({ overflow: TextOverflow.Ellipsis }) }if (this.bio.length 0)有简介才显示空简介不占位。maxLines(3)TextOverflow.Ellipsis超 3 行省略——简介截断。5.4 已选标签联动Row({ space: 6 }) { ForEach(this.interests, (item: InterestTag) { if (item.selected) { Text(item.label).fontSize(11).fontColor(C.primary) .backgroundColor(C.primarySoft).borderRadius(4) .padding({ left: 8, right: 8, top: 4, bottom: 4 }) } }, (item: InterestTag) item.id.toString()) }.width(100%)预览卡只显示已选标签if (item.selected)过滤——点选/取消兴趣预览卡的标签列表实时增删。多选联动预览是本页联动链的核心闭环点击兴趣标签 → toggleInterest() 翻转 selected → InterestSection 胶囊高亮变化 → selectedCount() 数字变化 → PreviewCard 标签列表增删if selected 过滤一个 Stateinterests驱动 4 处 UI 同步更新——状态 → 多视图联动是 ArkUI 响应式的精髓。六、TemplateSection SubmitBtnTemplateSection 是 4 个快捷模板学习搭子/运动伙伴/美食探店/文艺同好Builder TemplateSection() { Column({ space: 12 }) { Text(快捷模板).fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text).width(100%) Row({ space: 12 }) { ForEach(this.templates, (t: TemplateItem) { Column({ space: 6 }) { Row() { Text(t.icon).fontSize(24) } .width(44).height(44).backgroundColor(C.primarySoft).borderRadius(22).justifyContent(FlexAlign.Center) Text(t.title).fontSize(12).fontWeight(FontWeight.Bold).fontColor(C.text) Text(t.desc).fontSize(10).fontColor(C.textDim).maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }) } .layoutWeight(1).padding({ top: 10, bottom: 10 }) .backgroundColor(C.card).borderRadius(D.rMd).border({ width: 1, color: C.stroke }) .alignItems(HorizontalAlign.Center) .onClick(() { promptAction.showToast({ message: 应用模板: t.title }); }) }, (t: TemplateItem) t.id.toString()) }.width(100%) } .width(100%).padding(14).backgroundColor(C.card).borderRadius(D.rMd).border({ width: 1, color: C.stroke }) }4 个模板 4 种社交目的学习搭子一起自习、运动伙伴⚽约球跑步、美食探店发现美食、文艺同好电影音乐——模板即匹配场景。点击弹 Toast 应用模板: 学习搭子——真实项目应自动填充兴趣/范围。SubmitBtn开始智能匹配 50vp 高粉红按钮——点击弹 Toast 正在为你匹配同好...匹配流程占位。七、State 的 7 状态设计Func1Tab 有 7 个 State与 App 15/16 并列系列最多State类型用途联动nicknamestring昵称输入预览卡昵称biostring简介输入预览卡简介interestsInterestTag[]10 标签选中胶囊高亮/计数/预览标签selectedDeptnumber学院1-5学院胶囊 预览卡学院名ageRangenumber年龄范围0-2年龄胶囊matchScopenumber匹配范围0-2范围胶囊anonymousboolean匿名开关Toggle 预览卡匿名状态7 个 State含 anonymous覆盖 7 个交互点——State 数量 交互点数规律。interests是对象数组 State——修改要整体替换元素toggleInterest 的新对象写法——ArkTS 响应式数组的关键知识点直接改字段不触发重渲染。八、与 App 13/17 表单的对比维度App 13/17 表单App 18 发现页选择类型单选类型/优先级多选10 兴趣标签布局Row/Grid 固定列Flex 自动换行设置项开关入口3 单选 1 开关预览无实时预览卡核心联动单选联动按钮多选联动预览计数辅助方法getProducttoggleInterest/selectedCountApp 18 的进步① 多选交互toggleInterest 翻转②实时预览卡输入/选择/点标签全部实时反馈③ Flex 换行布局。偏好设置 实时预览模板可复用到婚恋交友、招聘求职技能多选、游戏匹配、租房筛选——任何设置偏好 → 看效果的场景。九、匹配范围的三组单选设计逻辑SelectorSection 的三组单选学院/年龄范围/匹配范围不是随意堆叠而是**匹配精准度的三层控制**维度选项控制什么学院不限/计算机/艺术/文/理领域匹配找同学院年龄同龄±1岁/同年级/不限代际匹配找同龄人范围同学院/同校区/全校空间匹配找多远的人三个维度组合 完整的匹配漏斗范围同学院/同校区/全校→ 领域不限/计算机/…→ 年龄同龄/同年级/不限范围最外、年龄最内——先限定空间全校/同校区再限定领域学院再限定代际年龄——漏斗式筛选是匹配引擎的经典设计第一层粗筛、第二层中筛、第三层精筛。默认值的设计学院默认不限学院selectedDept1id1年龄默认同年级ageRange1范围默认同学院matchScope0匿名默认关anonymousfalse默认值 保守起步——默认范围尽量宽不限学院、隐私尽量公开非匿名——先广撒网再收窄是社交 App 的推荐策略Soul/探探默认推荐全量。真实项目的进阶滑块选择年龄 18-25 滑动条比三档胶囊更精细位置权限匹配范围联动定位多维标签兴趣 性格 作息**三组单选 三层匹配控制**是本页 SelectorSection 的设计逻辑。十、兴趣标签的多选交互设计细节App 18 的兴趣多选10 个标签有几个值得细读的交互细节1. 为什么是 10 个——10 个兴趣覆盖校园主流兴趣编程/动漫/篮球/音乐/摄影/阅读/旅行/美食/游戏/电影10 选多既不多到选择困难、也不少到不够个性化。真实项目通常是 20-30 个分类更细。2. 为什么初始选 4 个——编程/篮球/阅读/电影初始选中——预选降低首次使用门槛用户不用从零选起改改就行——默认值降低门槛。3. 已选计数的反馈——右上角已选 4 个实时更新——计数反馈让用户知道当前选择量选太多/太少都有数。4. 点击的翻转语义——toggleInterest的选中↔未选翻转——胶囊即开关点一下选中、再点取消——多选的正确交互单选用点别的换选中多选用点击翻转。5. 选中的实时预览——预览卡标签列表同步增删——选择即所见用户选什么预览卡就显示什么。多选标签的完整交互闭环点击 → toggleInterest 翻转 → 胶囊高亮变 → 计数变 → 预览卡变——一次点击、四处联动。这是 ArkUI 响应式状态管理的最佳实践——状态驱动多视图。十一、匿名模式的隐私设计App 18 的匿名匹配开关是本系列第一个隐私功能Row() { Text(匿名匹配).fontSize(13).fontColor(C.text).layoutWeight(1) Toggle({ type: ToggleType.Switch, isOn: this.anonymous }) .selectedColor(C.primary) .onChange((on: boolean) { this.anonymous on; }) }.width(100%)匿名模式的业务价值降低社交压力——不暴露身份也能参与匹配内向用户的保护壳保护隐私——不想被同学知道我在用匹配 App自由表达——匿名状态下更敢表达真实兴趣预览卡联动Text(this.anonymous ? 匿名模式 : 公开模式).fontSize(11).fontColor(this.anonymous ? C.warn : C.ok) 匿名模式橙 公开模式绿——锁隐私、地球公开的隐喻 颜色区分。Toggle 一开预览卡立刻从 公开变 匿名——隐私状态的实时反馈。真实项目的匿名实现头像模糊匿名时头像打码/换默认昵称隐藏显示匿名用户真实身份不可搜索匿名用户的资料不出现在搜索结果举报保护匿名不意味着无责平台仍可追溯匿名开关 预览反馈是隐私功能的标准交互——用户打开开关立刻看到效果信任感强。十二、总结App 18 发现匹配页解析完毕。10 标签多选toggleInterest 翻转 实时预览卡5 处联动 Flex 换行 匿名开关是四大亮点。**输入即预览**让用户完整掌控别人将看到什么——这是社交匹配类 App 最核心的信任设计。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价