ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

HarmonyOS应用<奇妙科学乐园>开发第51篇:分类筛选标签——Scroll横向滚动与选中态

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'});//浅灰边框
代码解析

样式属性对照表

属性选中态未选中态视觉效果
fontColorTEXT_WHITE (#fff)TEXT_PRIMARY (#333)白字 vs 深色字
backgroundColorPRIMARY (#ff6b6b)BG_PRIMARY (#fff)珊瑚红 vs 白色
border.colorPRIMARY (#ff6b6b)#e0e0e0与背景同色 vs 浅灰
fontSize1414一致
fontWeightMediumMedium一致
padding16/16/8/816/16/8/8一致
borderRadius1616一致

视觉效果示意:

选中态: 未选中态:┌──────────────┐ ┌──────────────┐ │ ● 全 部 │ ← 珊瑚红底 │ 太空探索 │ ← 白色底 │ 白色文字 │ 白色字 │ 深色文字 │ 深色字 └──────────────┘ 主题色边框 └──────────────┘ 灰色边框

原理/说明:

  • 选中态的边框色与背景色相同(都是 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:将组件的分类状态更新为用户点击的分类 ID
  • this.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 切换到科普知识 Tab
  • topicsCategory传递目标分类 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. 骨架标签的尺寸设计

属性骨架占位真实标签说明
width56自适应(padding 16+16+文字宽度)骨架取中等宽度
height32padding 8+8+文字高度 ≈ 32高度一致
borderRadius1616完全一致
space8margin 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)隐藏滚动条保持界面简洁
  • Rowpadding({ 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 定义对照阅读,理解主题色体系与条件渲染的配合方式!

返回列表