一、引言

搜索是移动端应用中使用频率最高的功能之一。联系人搜索、城市查找、商品检索、内容过滤——几乎所有包含数据列表的应用都需要搜索功能。在传统开发中,实现一个搜索框需要组合 TextInput + 搜索图标 + 清除按钮 + 取消按钮,然后手动处理输入防抖、结果过滤、搜索历史管理等逻辑,工作量不低。

HarmonyOS 提供了 Search 组件——一个开箱即用的搜索框,内置搜索图标、清除按钮、搜索按钮,通过 onChange 支持实时过滤、onSubmit 支持确认搜索,配合 SearchController 实现焦点控制和光标定位。开发者只需关注数据的过滤逻辑,搜索交互的所有细节都由 Search 组件处理。

本文通过一个城市搜索 Demo 深入讲解 Search 组件的核心用法:onChangeonSubmit 的配合模式?搜索历史如何管理?三态视图如何切换?以及热门推荐与关键词匹配的实现。

阅读完本文,你将能够:

  • 使用 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 是一个城市搜索页面,模拟中国城市查找功能:

  1. 实时过滤:输入城市名或省份名,列表实时过滤匹配结果
  2. 确认搜索:点击搜索按钮或回车,关键词添加到搜索历史
  3. 搜索历史:显示最近 10 条搜索关键词,点击可重新搜索,支持清空
  4. 热门城市:8 个热门城市快捷入口,点击直接搜索
  5. 三态视图:初始状态(历史+热门)→ 搜索状态(结果列表)→ 空结果状态

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 显示搜索结果
  • searchControllerprivate 而非 @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 = [];
}

历史管理的关键逻辑:

  1. 空值拦截:纯空格输入不参与搜索
  2. 去重 + 前移:先用 filter 移除历史中已有的相同关键词,再 concat 到数组头部。这样重复搜索"北京"时,"北京"被移动到第一条
  3. 上限控制.slice(0, 10) 保留最近 10 条,防止历史列表无限增长
  4. 不可变更新:使用 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 组件中,onChangeonSubmit 不是二选一的关系,而是配合使用的:

// 模式 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”。

注意searchButtoncancelButton(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(),没有做防抖处理。这是因为:

  1. 数据量小(20 个城市),filter 操作耗时可忽略
  2. 过滤逻辑是纯计算(无网络请求),不会产生副作用

在数据量更大或涉及网络请求的场景中,需要防抖处理:

// 防抖模式(伪代码)
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 搜索组件的核心用法:

  1. Search 构造函数value 绑定文本、placeholder 占位提示、controller 程序化控制
  2. onChange vs onSubmit:实时过滤(轻量)vs 确认搜索(重量),两者可配合使用
  3. 搜索历史管理:去重 + 前移 + 上限控制,使用不可变更新模式
  4. 三态视图切换:初始态(历史+推荐)→ 搜索态(结果列表)→ 空结果态

Search 是 ArkUI 搜索场景的标准解决方案。从联系人搜索到城市查找,从商品检索到内容过滤,Search 以其开箱即用的交互设计和灵活的 API,帮助开发者用极简代码实现完整的搜索体验。希望本文能帮助你在实际项目中高效运用 Search 组件。


本文基于 HarmonyOS NEXT API 24 编写,代码经 DevEco Studio 6.1.1 编译验证通过。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐