鸿蒙新特性——Search 搜索组件详解
一、引言
搜索是移动端应用中使用频率最高的功能之一。联系人搜索、城市查找、商品检索、内容过滤——几乎所有包含数据列表的应用都需要搜索功能。在传统开发中,实现一个搜索框需要组合 TextInput + 搜索图标 + 清除按钮 + 取消按钮,然后手动处理输入防抖、结果过滤、搜索历史管理等逻辑,工作量不低。
HarmonyOS 提供了 Search 组件——一个开箱即用的搜索框,内置搜索图标、清除按钮、搜索按钮,通过 onChange 支持实时过滤、onSubmit 支持确认搜索,配合 SearchController 实现焦点控制和光标定位。开发者只需关注数据的过滤逻辑,搜索交互的所有细节都由 Search 组件处理。
本文通过一个城市搜索 Demo 深入讲解 Search 组件的核心用法:onChange 和 onSubmit 的配合模式?搜索历史如何管理?三态视图如何切换?以及热门推荐与关键词匹配的实现。
阅读完本文,你将能够:
- 使用 Search 组件替代自建搜索框方案
- 区分
onChange(实时过滤)和onSubmit(确认搜索)的适用场景 - 实现搜索历史的去重存储和清空管理
- 构建"搜索前 → 搜索中 → 空结果"的三态视图切换
- 实现城市数据的实时过滤与结果展示
二、Search 组件 API 总览
2.1 构造函数
Search(options?: SearchOptions)
interface SearchOptions {
value?: string; // 搜索框当前文本
placeholder?: ResourceStr; // 占位提示文本
icon?: ResourceStr; // 搜索图标(可替换为自定义图标)
controller?: SearchController; // 控制器(焦点、光标等)
}
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
value |
string | - | 绑定搜索文本,支持 @State 联动 |
placeholder |
ResourceStr | - | 占位提示,如"搜索城市名称" |
icon |
ResourceStr | 系统搜索图标 | 左侧搜索图标,可替换为自定义 |
controller |
SearchController | - | 控制器,用于程序化控制焦点等 |
2.2 链式方法
// 搜索按钮文字(右侧)
.searchButton(value: string): SearchAttribute
// 搜索按钮样式
.searchButtonStyle(value: SearchButtonStyle): SearchAttribute
// 占位文本颜色
.placeholderColor(value: ResourceColor): SearchAttribute
// 文本颜色
.fontColor(value: ResourceColor): SearchAttribute
// 输入变化回调
.onChange(callback: Callback<string>): SearchAttribute
// 提交搜索回调
.onSubmit(callback: Callback<string>): SearchAttribute
// 通用样式
.height, .width, .backgroundColor, .borderRadius
| 方法 | 说明 |
|---|---|
.searchButton(string) |
右侧搜索按钮的文字,不设则为默认图标 |
.placeholderColor(ResourceColor) |
占位文本的颜色 |
.fontColor(ResourceColor) |
输入文本的颜色 |
.onChange(Callback<string>) |
每次文本变化时触发,用于实时过滤 |
.onSubmit(Callback<string>) |
点击搜索按钮或键盘回车时触发,用于确认搜索 |
2.3 onChange vs onSubmit 的区别
| 特性 | onChange | onSubmit |
|---|---|---|
| 触发时机 | 每次文本变化(输入/删除每个字) | 点击搜索按钮/键盘回车 |
| 适用场景 | 实时过滤本地数据 | 发起网络搜索、提交搜索请求 |
| 频率 | 高频(每次按键) | 低频(用户明确确认) |
| 性能考量 | 过滤逻辑需轻量(O(n) 遍历) | 可执行重量级操作(网络请求) |
| 参数 | EditableTextChangeEvent | string |
在本 Demo 中,onChange 用于实时过滤城市列表(轻量操作),onSubmit 用于确认搜索并添加到搜索历史。
2.4 SearchController
interface SearchController {
caretPosition(position: number): void; // 控制光标位置
}
SearchController 用于程序化控制搜索框——如在用户点击搜索历史关键词后自动聚焦到搜索框并设置光标位置。它是一个普通类(不能作为 @State),用 private 声明即可。
三、Demo 设计:城市搜索
3.1 功能概述
Demo 是一个城市搜索页面,模拟中国城市查找功能:
- 实时过滤:输入城市名或省份名,列表实时过滤匹配结果
- 确认搜索:点击搜索按钮或回车,关键词添加到搜索历史
- 搜索历史:显示最近 10 条搜索关键词,点击可重新搜索,支持清空
- 热门城市:8 个热门城市快捷入口,点击直接搜索
- 三态视图:初始状态(历史+热门)→ 搜索状态(结果列表)→ 空结果状态
3.2 交互点
| # | 交互 | 说明 |
|---|---|---|
| 1 | onChange 实时过滤 | 每输入一个字符,城市列表立即过滤更新 |
| 2 | onSubmit 确认搜索 | 点击搜索按钮,关键词存入搜索历史 |
| 3 | 搜索历史点击 | 点击历史关键词重新搜索,结果列表刷新 |
| 4 | 热门城市点击 | 点击热门城市卡片直接搜索 |
| 5 | 清空搜索历史 | 一键清除全部历史记录 |
![]() |
四、完整代码实现
4.1 数据模型
interface CityItem {
name: string; // 城市名
province: string; // 所属省份
}
private allCities: CityItem[] = [
{ name: '北京', province: '北京' },
{ name: '上海', province: '上海' },
{ name: '广州', province: '广东' },
// ... 共 20 个城市
];
使用 CityItem 接口定义城市数据结构,allCities 作为原始数据源不被修改,过滤结果通过 getFilteredCities() 方法动态计算。
4.2 状态变量
@State searchText: string = '';
@State searchHistory: string[] = [];
@State isSearching: boolean = false;
private searchController: SearchController = new SearchController();
关键状态:
searchText:与 Search 组件的value绑定,用户输入直接反映到此变量searchHistory:去重后的搜索历史数组,最多 10 条,新搜索在前isSearching:控制三态视图切换的标志——false显示历史和热门,true显示搜索结果searchController:private而非@State——SearchController 是普通类实例,不需要响应式
4.3 Search 组件配置
Search({
placeholder: '搜索城市名称或省份...',
value: this.searchText,
controller: this.searchController
})
.searchButton('搜索')
.placeholderColor('#BBBBCC')
.fontColor('#1a1a2e')
.backgroundColor('#F8F9FA')
.borderRadius(22)
.height(44)
.width('100%')
.onChange((value: string) => {
this.searchText = value;
if (value.trim().length > 0) {
this.isSearching = true;
}
})
.onSubmit((value: string) => {
this.doSearch(value);
})
逐行解析:
placeholder:搜索框为空时显示的引导文字,提示用户输入城市名或省份名value: this.searchText:与@State变量双向绑定。用户输入 → onChange 更新 searchText → 结果列表重新过滤。代码中设置 searchText(如点击历史关键词)→ Search 组件文本同步更新controller:传入 SearchController 实例,用于程序化控制.searchButton('搜索'):在搜索框右侧显示"搜索"按钮。不设置此属性时显示默认搜索图标。用户点击此按钮或键盘回车时触发onSubmit.height(44).borderRadius(22):44vp 高度 + 22vp 圆角 = 胶囊形搜索框,圆角半径为高度的一半.onChange():每次文本变化时更新searchText并将isSearching置为true,切换到搜索状态视图。过滤逻辑在getFilteredCities()中完成.onSubmit():调用doSearch()执行确认搜索——将关键词添加到搜索历史
4.4 城市过滤逻辑
getFilteredCities(): CityItem[] {
const kw = this.searchText.trim().toLowerCase();
if (kw.length === 0) return [];
return this.allCities.filter(c =>
c.name.toLowerCase().includes(kw) || c.province.toLowerCase().includes(kw)
);
}
过滤逻辑同时匹配城市名和省份名:
- 输入"广"→ 匹配"广州"(城市名)和所有"广东"的城市(广州、深圳)
- 输入"北"→ 匹配"北京"(城市名)和所有"河北"的城市(如"石家庄"在河北)
- 输入"苏"→ 匹配"苏州"(城市名)和所有"江苏"的城市(南京、苏州)
- 大小写不敏感(
toLowerCase()),支持中英文混合
返回空数组时 UI 显示"未找到匹配的城市"空结果状态。
4.5 搜索历史管理
doSearch(text: string) {
if (text.trim().length === 0) return;
this.searchText = text;
this.isSearching = true;
// 去重后添加到历史头部,保留最近 10 条
const trimmed = text.trim();
const filtered = this.searchHistory.filter(h => h !== trimmed);
this.searchHistory = [trimmed].concat(filtered).slice(0, 10);
}
clearHistory() {
this.searchHistory = [];
}
历史管理的关键逻辑:
- 空值拦截:纯空格输入不参与搜索
- 去重 + 前移:先用
filter移除历史中已有的相同关键词,再concat到数组头部。这样重复搜索"北京"时,"北京"被移动到第一条 - 上限控制:
.slice(0, 10)保留最近 10 条,防止历史列表无限增长 - 不可变更新:使用
filter+concat+slice创建新数组,符合@State的不可变更新要求
4.6 三态视图切换
// 搜索态:显示结果列表或空结果
if (this.isSearching && this.searchText.trim().length > 0) {
// 有结果:ForEach 渲染过滤后的城市列表
// 无结果:显示"未找到匹配的城市"空状态
}
// 初始态:显示搜索历史 + 热门城市
if (!this.isSearching || this.searchText.trim().length === 0) {
// 搜索历史列表(可清空)
// 热门城市标签(可点击直接搜索)
}
三态切换的核心是 isSearching 标志:
isSearching = false+ 空文本 → 搜索历史 + 热门城市(初始态)isSearching = true+ 有文本 + 有结果 → 过滤后的城市列表(结果态)isSearching = true+ 有文本 + 无结果 → 空结果提示(空结果态)
注意第二个条件 this.searchText.trim().length > 0:如果用户清空了搜索框(文本为空),应该回到初始态而非保持搜索态。
4.7 热门城市
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(['北京', '上海', '广州', '深圳', '杭州', '成都', '南京', '武汉'], (city: string) => {
Text(city)
.fontSize(13)
.fontColor('#1a1a2e')
.padding({ top: 6, bottom: 6, left: 16, right: 16 })
.borderRadius(16)
.backgroundColor('#F2F3F5')
.margin({ right: 8, bottom: 8 })
.onClick(() => {
this.searchText = city;
this.isSearching = true;
})
})
}
热门城市使用 Flex({ wrap: FlexWrap.Wrap }) 实现自动换行(不能用 Row.flexWrap()——ArkTS 不支持 Row 的 flexWrap 属性)。点击任意城市标签直接设置 searchText 并切换到搜索态,立即显示该城市的搜索结果。
五、关键技术点详解
5.1 onChange vs onSubmit 的配合模式
在 Search 组件中,onChange 和 onSubmit 不是二选一的关系,而是配合使用的:
// 模式 1:纯 onChange(实时过滤本地数据)
Search({ value: this.text })
.onChange((value: string) => { this.filterData(value); })
// 不设 onSubmit
// 模式 2:纯 onSubmit(网络搜索)
Search({ value: this.text })
.onSubmit((value: string) => { this.searchAPI(value); })
// 不设 onChange
// 模式 3:onChange + onSubmit(本 Demo)
Search({ value: this.text })
.onChange((value: string) => { this.text = value; }) // 实时过滤
.onSubmit((value: string) => { this.saveHistory(value); }) // 保存历史
选择准则:本地数据过滤用 onChange,网络请求搜索用 onSubmit,两者同时使用可同时实现实时过滤 + 历史管理。
5.2 searchButton 的文本定制
.searchButton('搜索') 在搜索框右侧显示"搜索"文字按钮。不设置此属性时,右侧默认显示搜索图标。searchButton 支持传入任意字符串,如"查找"“Go”“Submit”。
注意:searchButton 和 cancelButton(API 11+)是两个不同的按钮。searchButton 是搜索确认按钮,cancelButton 是取消搜索按钮。在移动端搜索场景中,通常只使用 searchButton 或两者都不使用。
5.3 SearchController 的使用方式
private searchController: SearchController = new SearchController();
// 在用户点击搜索历史后,自动聚焦到搜索框
onHistoryClick(keyword: string) {
this.searchText = keyword;
this.isSearching = true;
this.searchController.caretPosition(keyword.length); // 光标移到末尾
}
SearchController 不是 @State 装饰的响应式变量,而是一个普通类实例——它不需要驱动 UI,只用于程序化操作。常见用法是控制光标位置。
5.4 搜索防抖的简化处理
在 Demo 中,onChange 回调直接执行过滤 getFilteredCities(),没有做防抖处理。这是因为:
- 数据量小(20 个城市),
filter操作耗时可忽略 - 过滤逻辑是纯计算(无网络请求),不会产生副作用
在数据量更大或涉及网络请求的场景中,需要防抖处理:
// 防抖模式(伪代码)
private debounceTimer: number = -1;
.onChange((value: string) => {
this.text = value;
clearTimeout(this.debounceTimer);
this.debounceTimer = setTimeout(() => {
this.searchAPI(value);
}, 300); // 停止输入 300ms 后触发搜索
})
5.5 搜索历史的数据持久化
Demo 中的搜索历史存储在 @State 中,App 退出后丢失。实际应用中,搜索历史应持久化:
import { preferences } from '@kit.ArkData';
// 加载历史
async loadHistory() {
const prefs = await preferences.getPreferences(this.context, 'search_store');
const history = await prefs.get('history', []) as string[];
this.searchHistory = history;
}
// 保存历史
async saveHistoryToDisk() {
const prefs = await preferences.getPreferences(this.context, 'search_store');
await prefs.put('history', this.searchHistory);
await prefs.flush();
}
六、运行效果
6.1 初始状态
搜索框为空,占位文字"搜索城市名称或省份…"。下方显示搜索历史(初次使用为空)和 8 个热门城市标签(北京、上海、广州、深圳、杭州、成都、南京、武汉)。
6.2 实时过滤
输入"广"——列表立即过滤显示"广州"和"深圳"(两者都属广东),标题栏显示"2 个城市"。输入"州"——列表过滤出"广州"“杭州”“苏州”“郑州”(城市名含"州")。
6.3 确认搜索与历史
点击搜索按钮或键盘回车——“广州"添加到搜索历史第一条。再次搜索"深圳”——“深圳"添加到历史第一条,“广州"变为第二条。点击搜索历史中的"广州”——搜索框填入"广州”,列表立即显示"广州"和"深圳"。
6.4 空结果与热门
输入"xyz"——列表显示"未找到匹配的城市"空状态。清空搜索框——回到初始态,搜索历史和热门城市重新出现。
七、最佳实践与注意事项
7.1 何时使用 Search 组件
- 本地数据过滤:城市、联系人、商品、文章的实时搜索
- 搜索历史管理:记录用户搜索行为,提升重复搜索效率
- 热门推荐入口:引导用户快速发现热门内容
7.2 性能建议
- 本地数据量 < 1000 条:直接
filter,无需优化 - 1000-10000 条:考虑使用
LazyForEach+ 搜索索引 -
10000 条或涉及网络请求:使用
onSubmit+ 后端搜索 API - 避免在
onChange中执行setTimeout等待 300ms 后再过滤——对于小数据量反而增加了延迟感
7.3 常见问题
Q: Search 可以隐藏搜索图标吗?
A: 可以,将 icon 设置为空字符串:Search({ icon: '' })。
Q: onChange 和 onSubmit 的回调参数类型一样吗?
A: 不完全相同。onChange 的参数类型是 string(当前文本值),onSubmit 的参数类型也是 string。两者在 API 24 中参数类型已统一为 string。
Q: 如何设置搜索框的文本字号?
A: Search 组件不支持 .fontSize() 方法。如果需要调整字号,可以通过 .font() 方法或系统主题设置。在本实现中,字号由系统默认值决定。
八、总结
本文通过一个城市搜索的实战 Demo,深入讲解了 HarmonyOS Search 搜索组件的核心用法:
- Search 构造函数:
value绑定文本、placeholder占位提示、controller程序化控制 - onChange vs onSubmit:实时过滤(轻量)vs 确认搜索(重量),两者可配合使用
- 搜索历史管理:去重 + 前移 + 上限控制,使用不可变更新模式
- 三态视图切换:初始态(历史+推荐)→ 搜索态(结果列表)→ 空结果态
Search 是 ArkUI 搜索场景的标准解决方案。从联系人搜索到城市查找,从商品检索到内容过滤,Search 以其开箱即用的交互设计和灵活的 API,帮助开发者用极简代码实现完整的搜索体验。希望本文能帮助你在实际项目中高效运用 Search 组件。
本文基于 HarmonyOS NEXT API 24 编写,代码经 DevEco Studio 6.1.1 编译验证通过。
更多推荐



所有评论(0)