HarmonyOS应用<奇妙科学乐园>开发第51篇:分类筛选标签——Scroll横向滚动与选中态
📖 引言
在上一篇文章中,我们完成了科普知识列表页 Topics 中搜索栏组件的完整拆解,从 TextInput 的属性配置到 onChange 事件监听,再到 500ms 防抖定时器的进阶优化。搜索栏下方紧跟着的一排可横滑分类标签,是搜索与列表之间承上启下的关键交互组件。
分类筛选标签在移动端应用中极为常见:微信通讯录的字母索引、淘宝商品分类的横向标签栏、B站视频的分区筛选——本质上都是同一个交互模式:一组互斥的选项,用户点击后切换当前激活项,列表内容随之刷新。在《奇妙科学乐园》中,这排标签承载着"太空探索"、"自然世界"、"海洋生物"等六大科学分类,外加一个固定的"全部"标签,构成完整的分类筛选体系。
本文将深入解析 Topics 页面中分类筛选标签的完整实现,涵盖 Scroll 横向滚动的配置技巧、"全部"标签与动态分类标签的布局编排、选中/未选中两种视觉状态的条件渲染,以及搜索与分类筛选的联动逻辑。
源码仓库:https://atomgit.com/2301_79280419/WonderSciencePark
🎯 学习目标
完成本文后,你将能够:
- ✅ 掌握 Scroll 组件横向滚动的配置:scrollable、scrollBar、margin 技巧
- ✅ 理解 Row 嵌套在 Scroll 中的水平排列布局原理
- ✅ 实现选中/未选中两种视觉状态的条件渲染(backgroundColor、fontColor、border)
- ✅ 掌握"全部"静态标签与 ForEach 动态分类标签的混排方案
- ✅ 理解分类切换时清空搜索关键词的联动设计
- ✅ 掌握 ForEach 的 keyGenerator 函数与数据稳定性
- ✅ 了解 Scroll 横向滚动中右侧留白与边缘对齐的处理技巧
💡 需求分析
分类标签在页面中的位置
科普知识列表页 Topics ├── 页面标题栏(Row: "科普知识"+设置图标) ├── 搜索栏(Row: 搜索图标+TextInput) ├── 分类筛选标签(Scroll:Row+Column标签组) ← 本文主角 │ ├── "全部"(静态标签) │ ├── "太空探索"(动态标签) │ ├── "自然世界"(动态标签) │ ├── "海洋生物"(动态标签) │ ├── "科技发明"(动态标签) │ ├── "人体奥秘"(动态标签) │ └── "天气现象"(动态标签) ├── 文章计数("共 X 篇文章") ├── 文章卡片列表(LazyForEach+TopicCard) └── 空状态/错误状态功能模块设计
| 模块 | 功能描述 | 技术要点 |
|---|---|---|
| 横向滚动容器 | 超出屏幕宽度的标签支持横向滑动 | Scroll + scrollable(ScrollDirection.Horizontal) |
| "全部"标签 | 固定的第一个标签,点击显示全部文章 | 静态 Column + 条件样式 |
| 分类标签组 | 从 categories 数据动态生成的标签列表 | ForEach + Category 数据模型 |
| 选中高亮 | 当前选中标签的主题色填充 + 白色文字 | 三元表达式条件渲染样式 |
| 未选中样式 | 白色背景 + 深色文字 + 灰色边框 | 默认样式配置 |
| 搜索联动 | 切换分类时自动清空搜索关键词 | selectCategory 方法 |
| 骨架占位 | 加载中显示灰色矩形条 | 条件渲染 + 骨架色 |
分类切换的数据流
用户点击分类标签 ↓selectCategory(categoryId)↓ ├── 更新 currentCategory = categoryId ├── 清空 searchKeyword =''← 联动清空搜索 └── 调用 loadTopics() ↓getTopicsByCategory(currentCategory)↓ topicDataSource.setData(topicList) ↓ LazyForEach 刷新列表🛠️ 核心实现
步骤1: Scroll 横向滚动容器配置
功能说明
当分类标签数量较多、总宽度超出屏幕时,需要支持横向滑动。HarmonyOS 中通过Scroll组件的scrollable(ScrollDirection.Horizontal)属性实现水平滚动。本步骤聚焦于 Scroll 容器本身的关键配置。
完整代码
//entry/src/main/ets/pages/Topics.ets//分类标签区域——Scroll 横向滚动容器//分类标签 Scroll() { Row() {//"全部"标签 + ForEach 分类标签 } .padding({ right:16});//右侧内边距,保证最后一个标签不贴边 } .scrollable(ScrollDirection.Horizontal)//开启横向滚动 .scrollBar(BarState.Off)//隐藏滚动条 .margin({ left:0, right: -16, bottom:4});代码解析
1. 横向滚动方向设置
Scroll(){ ... }.scrollable(ScrollDirection.Horizontal)原理/说明:
ScrollDirection.Horizontal枚举值指定滚动方向为水平- Scroll 的默认滚动方向是垂直(Vertical),必须显式指定为 Horizontal
- 子容器(Row)的内容总宽度超过 Scroll 自身宽度时,才会出现滚动
2. 隐藏滚动条
.scrollBar(BarState.Off)原理/说明:
BarState.Off完全隐藏滚动条,保持界面简洁- 横向标签栏通常不需要显示滚动条,用户通过手指滑动即可感知可滚动
- 对于面向儿童的应用,隐藏滚动条还能减少视觉干扰
3. 右侧留白与边缘对齐
// Row 内部:右侧内边距.padding({right:16});// Scroll 外部:右边距为 -16.margin({left:0,right: -16,bottom:4});原理/说明:
- Row 的
padding({ right: 16 })确保最后一个标签右侧有 16vp 的间距 - Scroll 的
margin({ right: -16 })使用负边距,让 Scroll 的滚动区域向右延伸 16vp - 两者的配合效果:最后一个标签可以滚动到屏幕右边缘,而不是停留在距右侧 16vp 的位置
margin({ bottom: 4 })为标签栏与下方列表之间增加 4vp 的间距
负边距的对齐效果对比:
❌ 无负边距(right:0): ┌─────────────────────────────┐ │[全部][太空][自然][海洋]│ ← 最后标签距右边缘16vp └─────────────────────────────┘ ✅ 有负边距(right: -16): ┌─────────────────────────────┐ │[全部][太空][自然][海洋]│← 可继续滚动 → └─────────────────────────────┘ 滑动后: ┌─────────────────────────────┐[太空][自然][海洋][科技]│ ← 最后标签可以贴齐右边缘 └─────────────────────────────┘步骤2: "全部"标签——静态标签的条件样式
功能说明
"全部"标签是分类筛选栏的第一个固定标签,不参与 ForEach 动态渲染。它的categoryId固定为'all',点击后显示所有分类的文章。选中与未选中两种状态通过三元表达式切换背景色、文字颜色和边框颜色。
完整代码
// entry/src/main/ets/pages/Topics.ets// "全部"标签——静态渲染Column() { Text('全部') .fontSize(14) .fontWeight(FontWeight.Medium) .fontColor(this.currentCategory ==='all'? ThemeColors.TEXT_WHITE : ThemeColors.TEXT_PRIMARY ); } .padding({ left:16, right:16, top:8, bottom:8}) .backgroundColor(this.currentCategory ==='all'? ThemeColors.PRIMARY : ThemeColors.BG_PRIMARY ) .borderRadius(16) .border({ width:1, color:this.currentCategory ==='all'? ThemeColors.PRIMARY :'#e0e0e0'}) .onClick(() => {this.selectCategory('all'); });代码解析
1. 文字颜色条件切换
.fontColor( this.currentCategory ==='all'? ThemeColors.TEXT_WHITE//选中:白色文字#ffffff: ThemeColors.TEXT_PRIMARY//未选中:深色文字#333333)原理/说明:
this.currentCategory === 'all'判断当前分类是否为"全部"- 选中时使用
ThemeColors.TEXT_WHITE(白色),与主题色背景形成对比 - 未选中时使用
ThemeColors.TEXT_PRIMARY(深色),保证在白色背景上的可读性 - 三元表达式的两个分支都引用 ThemeColors 常量,不使用硬编码色值
2. 背景色条件切换
.backgroundColor( this.currentCategory ==='all'? ThemeColors.PRIMARY//选中:主题色#ff6b6b: ThemeColors.BG_PRIMARY//未选中:白色#ffffff)原理/说明:
- 选中时背景变为主题色(珊瑚红 #ff6b6b),形成醒目的高亮效果
- 未选中时背景为白色,与页面背景融为一体
- 主题色
ThemeColors.PRIMARY是应用的品牌色,贯穿所有页面的强调元素
3. 边框颜色条件切换
.border({width:1,color:this.currentCategory==='all'? ThemeColors.PRIMARY// 选中:主题色边框(与背景融为一体):'#e0e0e0'// 未选中:浅灰边框(勾勒轮廓)})原理/说明:
- 选中时边框色与背景色相同(都是 PRIMARY),视觉上边框"消失"
- 未选中时使用浅灰边框
#e0e0e0,勾勒出标签的轮廓 - 边框宽度固定为 1vp,既清晰又不显厚重
4. 点击事件
.onClick(()=>{ this.selectCategory('all'); })原理/说明:
- 点击"全部"标签时调用
selectCategory('all')方法 - 该方法会更新 currentCategory、清空搜索关键词、刷新列表(后文详述)
步骤3: 动态分类标签——ForEach 遍历渲染
功能说明
"全部"标签之后的分类标签(太空探索、自然世界、海洋生物等)由ForEach根据 categories 数据动态生成。每个标签的选中态判断依据是this.currentCategory === category.id。标签之间通过margin({ left: 8 })控制间距。
完整代码
//entry/src/main/ets/pages/Topics.ets//动态分类标签——ForEach 遍历 ForEach(this.categories, (category: Category)=> { Column(){ Text(category.name).fontSize(14).fontWeight(FontWeight.Medium).fontColor( this.currentCategory=== category.id ? ThemeColors.TEXT_WHITE : ThemeColors.TEXT_PRIMARY ); }.padding({ left: 16, right: 16, top: 8, bottom: 8 }).backgroundColor( this.currentCategory=== category.id ? ThemeColors.PRIMARY : ThemeColors.BG_PRIMARY ).borderRadius(16).border({ width: 1, color: this.currentCategory=== category.id ? ThemeColors.PRIMARY : '#e0e0e0' }).margin({ left: 8 })//标签之间的间距.onClick(()=> { this.selectCategory(category.id); }); },(category: Category)=> category.id);代码解析
1. ForEach 的数据源与 keyGenerator
ForEach(this.categories,// 数据源:Category 数组(category: Category) =>{ ... },// 子项生成函数(category: Category) =>category.id// key 生成函数);原理/说明:
- 第一个参数
this.categories是从scienceData.getAllCategories()获取的分类数据数组 - 第二个参数是每个分类标签的 UI 生成函数,参数
category为当前遍历到的分类对象 - 第三个参数是 key 生成函数,使用
category.id(如 'space'、'nature')作为唯一标识 - key 必须唯一且稳定,避免 ForEach 在数据更新时错误地复用或重建组件
2. 动态标签与"全部"标签的样式一致性
// "全部"标签的选中判断this.currentCategory ==='all'// 动态标签的选中判断this.currentCategory === category.id原理/说明:
- 两种标签的样式属性(fontSize、fontWeight、padding、borderRadius)完全一致
- 唯一的区别是选中判断条件:'all' vs category.id
- 这种一致性保证了视觉上的统一感,用户不会感知到"全部"和分类标签是不同的组件
3. 标签间距处理
.margin({ left:8})//动态标签才有左边距原理/说明:
- "全部"标签是第一个标签,不需要左边距
- 每个动态标签通过
margin({ left: 8 })与前一个标签保持 8vp 的间距 - 这种"第一个无间距、后续有间距"的模式是标签栏的常见做法
- 如果用 Row 的
space: 8属性,"全部"标签右侧也会有 8vp 间距,效果不如 margin 精确
步骤4: 选中态 vs 未选中态——完整样式对照
功能说明
每个分类标签有两种视觉状态,通过this.currentCategory === xxx的三元表达式进行切换。以下是对两种状态下所有样式属性的完整对照。
完整代码
//entry/src/main/ets/pages/Topics.ets//标签样式配置汇总//选中态(以"全部"为例,currentCategory ==='all') Column() { Text('全部') .fontSize(14) .fontWeight(FontWeight.Medium) .fontColor(ThemeColors.TEXT_WHITE);//白色文字 } .padding({ left:16, right:16, top:8, bottom:8}) .backgroundColor(ThemeColors.PRIMARY)//主题色背景#ff6b6b.borderRadius(16)//圆角 .border({ width:1, color: ThemeColors.PRIMARY });//主题色边框//未选中态(currentCategory !=='all') Column() { Text('全部') .fontSize(14) .fontWeight(FontWeight.Medium) .fontColor(ThemeColors.TEXT_PRIMARY);//深色文字#333} .padding({ left:16, right:16, top:8, bottom:8}) .backgroundColor(ThemeColors.BG_PRIMARY)//白色背景 .borderRadius(16) .border({ width:1, color:'#e0e0e0'});//浅灰边框代码解析
样式属性对照表
| 属性 | 选中态 | 未选中态 | 视觉效果 |
|---|---|---|---|
| fontColor | TEXT_WHITE (#fff) | TEXT_PRIMARY (#333) | 白字 vs 深色字 |
| backgroundColor | PRIMARY (#ff6b6b) | BG_PRIMARY (#fff) | 珊瑚红 vs 白色 |
| border.color | PRIMARY (#ff6b6b) | #e0e0e0 | 与背景同色 vs 浅灰 |
| fontSize | 14 | 14 | 一致 |
| fontWeight | Medium | Medium | 一致 |
| padding | 16/16/8/8 | 16/16/8/8 | 一致 |
| borderRadius | 16 | 16 | 一致 |
视觉效果示意:
选中态: 未选中态:┌──────────────┐ ┌──────────────┐ │ ● 全 部 │ ← 珊瑚红底 │ 太空探索 │ ← 白色底 │ 白色文字 │ 白色字 │ 深色文字 │ 深色字 └──────────────┘ 主题色边框 └──────────────┘ 灰色边框原理/说明:
- 选中态的边框色与背景色相同(都是 PRIMARY),边框在视觉上"隐形"
- 未选中态的浅灰边框勾勒出标签轮廓,与白色背景形成柔和的区分
- 两种状态切换时,背景色、文字色、边框色三个属性同时变化,形成明确的视觉反馈
步骤5: selectCategory 方法——分类切换与搜索联动
功能说明
selectCategory方法是分类标签点击事件的统一处理入口。它完成三件事:更新当前分类、清空搜索关键词、刷新文章列表。这种"切分类清搜索"的联动设计,避免了用户同时处于"搜索模式"和"分类模式"的混乱状态。
完整代码
//entry/src/main/ets/pages/Topics.ets//分类切换方法 selectCategory(categoryId: string) {//第一步:更新当前选中的分类 this.currentCategory = categoryId;//第二步:清空搜索关键词(退出搜索模式) this.searchKeyword ='';//第三步:按新分类重新加载文章列表 this.loadTopics(); }代码解析
1. 三步操作的设计逻辑
this.currentCategory = categoryId;// 状态更新this.searchKeyword ='';// 状态清理this.loadTopics();// 数据刷新原理/说明:
this.currentCategory = categoryId:将组件的分类状态更新为用户点击的分类 IDthis.searchKeyword = '':清空搜索关键词,确保从搜索模式切换回分类模式this.loadTopics():触发列表刷新,因为 searchKeyword 为空,会走getTopicsByCategory路径
2. 搜索与分类的互斥关系
loadTopics() {if(this.searchKeyword &&this.searchKeyword.trim() !=='') {// 搜索模式:忽略分类this.topicList = scienceData.searchTopics(this.searchKeyword); }else{// 分类模式:按分类过滤this.topicList = scienceData.getTopicsByCategory(this.currentCategory); }this.topicDataSource.setData(this.topicList); }原理/说明:
- 搜索模式和分类模式通过
searchKeyword是否为空来区分 - 有搜索词时,搜索结果覆盖分类筛选(搜索优先)
- 无搜索词时,按
currentCategory过滤 selectCategory中清空searchKeyword,确保切回分类模式
3. 交互流程示意
初始状态: currentCategory='all', searchKeyword=''↓ 点击"海洋生物"selectCategory('ocean')↓ currentCategory='ocean', searchKeyword=''↓loadTopics()→ getTopicsByCategory('ocean') ↓ 列表显示: 仅海洋生物分类的文章 用户在搜索栏输入"太阳"↓ onChange searchKeyword='太阳'↓loadTopics()→ searchTopics('太阳') ↓ 列表显示: 标题或分类名含"太阳"的文章(跨分类) 用户点击"自然世界"标签 ↓selectCategory('nature')↓ currentCategory='nature', searchKeyword=''← 搜索被清空 ↓loadTopics()→ getTopicsByCategory('nature') ↓ 列表显示: 仅自然世界分类的文章步骤6: aboutToUpdate 中的外部分类切换
功能说明
当用户从首页点击分类图标进入科普列表时,首页通过AppStorage.setOrCreate('topicsCategory', category.id)传递初始分类 ID。Topics 组件通过@Prop initialCategory接收这个值,在aboutToUpdate生命周期中检测变化并触发分类切换。
完整代码
// entry/src/main/ets/pages/Topics.ets// 外部分类切换检测@PropinitialCategory: string ='all';@StatecurrentCategory: string ='all'; aboutToUpdate() {// 检测外部传入的 initialCategory 是否发生变化// 且仅在非搜索状态下响应(避免覆盖用户的搜索操作)if(this.initialCategory &&this.initialCategory !==this.currentCategory &&this.searchKeyword ==='') {this.currentCategory =this.initialCategory;if(scienceData.getIsInitialized()) {this.loadTopics(); } } }代码解析
1. 三个条件的组合判断
if(this.initialCategory &&this.initialCategory !==this.currentCategory &&this.searchKeyword ==='')| 条件 | 作用 | 说明 |
|---|---|---|
this.initialCategory | 非空检查 | 确保有传入的分类 ID |
this.initialCategory !== this.currentCategory | 变化检测 | 避免重复加载相同分类 |
this.searchKeyword === '' | 搜索状态检查 | 用户正在搜索时不打断 |
原理/说明:
- 第一个条件排除
initialCategory为空或 undefined 的情况 - 第二个条件防止"相同分类重复加载"的无效操作
- 第三个条件是最关键的——如果用户正在搜索中,从首页点击分类进来不应该覆盖用户的搜索状态
- 三个条件缺一不可,共同保证了外部切换的安全性和用户体验
2. 首页传递分类 ID 的方式
//entry/src/main/ets/pages/Index.ets//首页点击分类图标时 goToCategory(category: Category): void {//通过 AppStorage 通知 MainTabs 切换到科普 Tab AppStorage.setOrCreate<string>('switchToTab','topics');//传递分类 ID 给 Topics 组件 AppStorage.setOrCreate<string>('topicsCategory', category.id); }原理/说明:
- 首页不直接使用 router 跳转,而是通过 AppStorage 传递消息
switchToTab通知 MainTabs 切换到科普知识 TabtopicsCategory传递目标分类 ID- Topics 组件的
@Prop initialCategory绑定了这个 AppStorage 值
步骤7: 骨架屏中的分类标签占位
功能说明
在数据加载过程中,分类标签区域显示一组灰色的矩形占位条,模拟真实标签的尺寸和间距,给用户一个"标签即将出现"的视觉预期。
完整代码
//entry/src/main/ets/pages/Topics.ets//加载中状态——分类标签骨架if(this.isLoading) { Column() {//...搜索栏骨架...//分类标签骨架 Row({ space:8}) { ForEach([1,2,3,4], (_: number) => { Column() .width(56) .height(32) .backgroundColor(SKELETON_COLOR)//#e2e8f0.borderRadius(16); }, (_: number, index: number) => index.toString()); } .width('100%') .margin({ bottom:16}); } }代码解析
1. 骨架标签的尺寸设计
| 属性 | 骨架占位 | 真实标签 | 说明 |
|---|---|---|---|
| width | 56 | 自适应(padding 16+16+文字宽度) | 骨架取中等宽度 |
| height | 32 | padding 8+8+文字高度 ≈ 32 | 高度一致 |
| borderRadius | 16 | 16 | 完全一致 |
| space | 8 | margin left: 8 | 间距一致 |
原理/说明:
- 骨架标签使用固定的 56x32 尺寸,接近真实标签的平均大小
- 生成 4 个骨架标签,与实际 6+1 个标签数量接近(不需要完全一致)
- borderRadius 与真实标签保持一致(16),确保加载完成后圆角过渡自然
Row({ space: 8 })使用 Row 的 space 属性简化骨架间距设置
⚠️ 常见问题与解决方案
问题1: Scroll 横向滚动不生效
现象:
分类标签设置了scrollable(ScrollDirection.Horizontal),但内容无法横向滑动。
错误代码:
// ❌ 错误: 使用 Column 作为 Scroll 的子容器(Column 是垂直布局)Scroll(){Column(){ForEach(this.categories, (category: Category)=> {...}); } } .scrollable(ScrollDirection.Horizontal);正确代码:
// ✅ 正确: 使用 Row 作为 Scroll 的子容器(Row 是水平布局)Scroll(){Row(){ForEach(this.categories, (category: Category)=> {...}); } } .scrollable(ScrollDirection.Horizontal);规则/建议:
- 横向滚动的 Scroll 必须搭配 Row 作为直接子容器
- Row 会将子元素水平排列,当总宽度超出 Scroll 宽度时触发滚动
- 如果使用 Column,子元素会垂直排列,横向滚动自然无法生效
问题2: 最后一个标签无法滚动到屏幕右侧
现象:
横向滑动到最右侧时,最后一个标签仍然距屏幕右边缘有一段空白,无法完全显示。
错误代码:
// ❌ 错误: 没有处理右侧留白,最后一个标签被"卡住"Scroll() { Row() {// 标签们...}// 缺少右侧内边距}.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off);正确代码:
// ✅ 正确: Row 内部 padding + Scroll 外部负 margin 配合Scroll(){ Row() {// 标签们...}.padding({right:16});// Row 内部右侧留白}.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).margin({right: -16});// Scroll 负边距补偿规则/建议:
- Row 的
padding({ right: 16 })确保最后一个标签右侧有呼吸空间 - Scroll 的
margin({ right: -16 })用负边距扩展滚动区域 - 两者数值必须一致(如都是 16),才能正确抵消
- 这是横向标签栏的"标准配方",建议直接复用
问题3: ForEach key 不稳定导致标签闪烁
现象:
切换分类后,标签出现短暂的闪烁或位置错乱,ForEach 错误地重建了组件。
错误代码:
// ❌ 错误: 使用 index 作为 keyForEach(this.categories, (category: Category)=> {// 标签 UI},(category: Category,index:number) =>index.toString());正确代码:
// ✅ 正确: 使用 category.id 作为唯一且稳定的 keyForEach(this.categories, (category: Category)=> {// 标签 UI},(category: Category) =>category.id);规则/建议:
- ForEach 的 key 必须是数据项的唯一标识,不能使用数组索引
- 使用 index 作为 key 时,如果数组顺序变化(如排序、过滤),会导致组件错误复用
category.id是每个分类的唯一标识(如 'space'、'nature'),不会随数据变化而改变- key 的类型应与原始数据类型一致(string 返回 string,不需要 toString())
问题4: 标签选中状态不同步
现象:
点击分类标签后,视觉上标签没有变为选中态,或者多个标签同时显示为选中态。
错误代码:
// ❌ 错误: 每个标签维护自己的 isSelected 状态@ComponentstructCategoryTag { @State isSelected: boolean =false; category: Category; build(){Column(){Text(this.category.name).fontColor(this.isSelected? '#fff' : '#333'); } .backgroundColor(this.isSelected? '#ff6b6b' : '#fff').onClick(()=> { this.isSelected =true;// 只更新自己的状态}); } }正确代码:
// ✅ 正确: 父组件统一管理 currentCategory,子标签通过比较判断选中态// 父组件@StatecurrentCategory: string ='all';// 子标签通过 this.currentCategory === category.id 判断.fontColor(this.currentCategory === category.id ? ThemeColors.TEXT_WHITE : ThemeColors.TEXT_PRIMARY ) .backgroundColor(this.currentCategory === category.id ? ThemeColors.PRIMARY : ThemeColors.BG_PRIMARY )规则/建议:
- 互斥选项组的状态应该由父组件统一管理,而不是每个子项各自维护
- 使用
currentCategory单一状态变量 + 条件表达式是最简洁的互斥实现 - 避免"分散状态"(每个标签一个 isSelected)导致的同步问题
问题5: 切换分类后搜索栏仍显示旧关键词
现象:
用户先搜索"太阳",然后点击"自然世界"分类,列表正确切换了,但搜索栏中仍然显示"太阳"二字。
错误代码:
// ❌ 错误: 切换分类时没有清空搜索关键词selectCategory(categoryId:string) {this.currentCategory= categoryId;// 缺少: this.searchKeyword = '';this.loadTopics(); }正确代码:
// ✅ 正确: 切换分类时同步清空搜索关键词selectCategory(categoryId: string) {this.currentCategory = categoryId;this.searchKeyword ='';// 清空搜索状态this.loadTopics(); }规则/建议:
- 分类和搜索是两种互斥的筛选模式,切换时必须清理另一种模式的状态
- 如果使用
@State searchKeyword绑定 TextInput,清空变量后输入框会自动同步清空 - 建议在 selectCategory 方法中显式清空,而非依赖 TextInput 的双向绑定
📝 本章小结
核心知识点
本文详细讲解了科普知识列表页中分类筛选标签的完整实现,主要包括:
1. Scroll 横向滚动配置
scrollable(ScrollDirection.Horizontal)开启水平滚动scrollBar(BarState.Off)隐藏滚动条保持界面简洁- Row
padding({ right: 16 })+ Scrollmargin({ right: -16 })的右侧留白配方
2. 选中态条件渲染
this.currentCategory === xxx三元表达式驱动三种样式属性切换- 选中态:主题色背景 + 白色文字 + 主题色边框
- 未选中态:白色背景 + 深色文字 + 灰色边框
3. 分类与搜索的联动设计
- selectCategory 方法中同时更新分类状态和清空搜索关键词
- loadTopics 根据 searchKeyword 是否为空决定走搜索路径还是分类路径
- aboutToUpdate 中三个条件组合检测外部分类切换
最佳实践总结
✅Scroll 横向滚动容器
Scroll(){ Row() {// 标签内容}.padding({right:16}); }.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).margin({left:0, right: -16, bottom:4});✅选中态条件样式
Column(){ Text(category.name).fontSize(14).fontWeight(FontWeight.Medium).fontColor(this.currentCategory=== category.id ? ThemeColors.TEXT_WHITE : ThemeColors.TEXT_PRIMARY); }.padding({ left: 16, right: 16, top: 8, bottom: 8 }).backgroundColor(this.currentCategory=== category.id ? ThemeColors.PRIMARY : ThemeColors.BG_PRIMARY).borderRadius(16).border({ width: 1, color: this.currentCategory=== category.id ? ThemeColors.PRIMARY : '#e0e0e0' });✅分类搜索联动
selectCategory(categoryId: string) {this.currentCategory = categoryId;this.searchKeyword ='';this.loadTopics(); }下一步预告
在下一篇文章中,我们将:
- 🎨 深入拆解首页 Index 头部区域的 linearGradient 渐变实现
- 📐 解析 135 度角度渐变与品牌色配色方案的设计思路
- ✂️ 探讨 clip(true) 圆角裁剪在渐变背景中的应用
🔗 相关链接
- 项目源码:Atomgit仓库
💡 提示: 建议结合项目源码中entry/src/main/ets/pages/Topics.ets的分类标签区域和entry/src/main/ets/constants/AppConstants.ets的 ThemeColors 定义对照阅读,理解主题色体系与条件渲染的配合方式!
