【HarmonyOS】RN of HarmonyOS实战项目:搜索框样式与交互优化

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
项目概述:本文详细介绍在HarmonyOS平台上使用React Native实现精美的搜索框组件,涵盖多种样式风格、搜索历史、实时建议、语音输入等功能,提供从设计实现到用户体验优化的完整解决方案。
一、项目背景
搜索框是移动应用中最常见的组件之一,其样式和交互直接影响用户体验。在HarmonyOS平台上实现优质的搜索框需要考虑:
- 视觉设计:符合HarmonyOS设计规范的圆角、阴影、颜色
- 交互体验:聚焦动画、清除按钮、语音输入
- 功能增强:搜索历史、实时建议、热门搜索
- 性能优化:防抖处理、虚拟列表、缓存策略
二、设计规范
2.1 HarmonyOS搜索框设计规范
typescript
/**
* 搜索框样式配置
*/
interface SearchBarStyle {
height: number; // 高度,默认40-48
borderRadius: number; // 圆角,默认8-12
backgroundColor: string; // 背景色,默认#F5F5F5
iconColor: string; // 图标颜色,默认#999
textColor: string; // 文字颜色,默认#333
placeholderColor: string; // 占位符颜色,默认#999
shadowColor: string; // 阴影颜色
shadowOpacity: number; // 阴影透明度
shadowRadius: number; // 阴影半径
}
/**
* 搜索框状态
*/
type SearchBarState = 'default' | 'focused' | 'filled' | 'loading' | 'error';
2.2 搜索框样式类型
搜索框样式
默认样式
突出样式
极简样式
卡片样式
浅灰背景 + 圆角
深色背景 + 强调色
无边框 + 下划线
卡片阴影 + 悬浮效果
三、核心实现代码
3.1 基础搜索框组件
typescript
/**
* 搜索框组件
* 符合HarmonyOS设计规范的基础搜索框
*
* @platform HarmonyOS 2.0+
* @react-native 0.72+
*/
import React, { useState, useCallback, useRef, useEffect } from 'react';
import {
View,
TextInput,
Text,
TouchableOpacity,
StyleSheet,
Animated,
ActivityIndicator,
} from 'react-native';
interface SearchBarProps {
placeholder?: string;
onSearch?: (query: string) => void;
onChangeText?: (text: string) => void;
debounceMs?: number;
showVoiceInput?: boolean;
onVoiceInput?: () => void;
style?: any;
}
export const SearchBar: React.FC<SearchBarProps> = ({
placeholder = '搜索',
onSearch,
onChangeText,
debounceMs = 300,
showVoiceInput = false,
onVoiceInput,
style,
}) => {
const [query, setQuery] = useState('');
const [isFocused, setIsFocused] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const inputRef = useRef<TextInput>(null);
const debounceTimer = useRef<NodeJS.Timeout>();
const focusAnim = useRef(new Animated.Value(0));
/**
* 处理输入变化
*/
const handleChange = useCallback((text: string) => {
setQuery(text);
onChangeText?.(text);
if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
}
if (debounceMs > 0) {
setIsLoading(true);
debounceTimer.current = setTimeout(() => {
onSearch?.(text);
setIsLoading(false);
}, debounceMs);
} else {
onSearch?.(text);
}
}, [debounceMs, onChangeText, onSearch]);
/**
* 处理搜索提交
*/
const handleSubmit = useCallback(() => {
onSearch?.(query);
inputRef.current?.blur();
}, [query, onSearch]);
/**
* 清空搜索
*/
const handleClear = useCallback(() => {
setQuery('');
onChangeText?.('');
onSearch?.('');
inputRef.current?.focus();
}, [onChangeText, onSearch]);
/**
* 聚焦动画
*/
useEffect(() => {
Animated.timing(focusAnim.current, {
toValue: isFocused ? 1 : 0,
duration: 200,
useNativeDriver: false,
}).start();
}, [isFocused]);
/**
* 清理定时器
*/
useEffect(() => {
return () => {
if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
}
};
}, []);
const backgroundColor = focusAnim.current.interpolate({
inputRange: [0, 1],
outputRange: ['#F5F5F5', '#FFFFFF'],
});
const shadowOpacity = focusAnim.current.interpolate({
inputRange: [0, 1],
outputRange: [0, 0.15],
});
const borderColor = focusAnim.current.interpolate({
inputRange: [0, 1],
outputRange: ['transparent', '#007AFF'],
});
return (
<Animated.View
style={[
styles.container,
{
backgroundColor,
shadowOpacity,
borderColor,
},
style,
]}
>
{/* 搜索图标 */}
<View style={styles.iconContainer}>
<Text style={styles.searchIcon}>🔍</Text>
</View>
{/* 输入框 */}
<TextInput
ref={inputRef}
style={styles.input}
value={query}
onChangeText={handleChange}
onSubmitEditing={handleSubmit}
placeholder={placeholder}
placeholderTextColor="#999"
returnKeyType="search"
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
/>
{/* 右侧按钮组 */}
<View style={styles.rightContainer}>
{isLoading && (
<ActivityIndicator size="small" color="#007AFF" style={styles.spinner} />
)}
{query.length > 0 && !isLoading && (
<TouchableOpacity onPress={handleClear} style={styles.clearButton}>
<Text style={styles.clearIcon}>×</Text>
</TouchableOpacity>
)}
{showVoiceInput && !isLoading && (
<TouchableOpacity onPress={onVoiceInput} style={styles.voiceButton}>
<Text style={styles.voiceIcon}>🎤</Text>
</TouchableOpacity>
)}
</View>
</Animated.View>
);
};
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
height: 44,
borderRadius: 12,
borderWidth: 1,
paddingHorizontal: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowRadius: 8,
elevation: 2,
},
iconContainer: {
marginRight: 8,
},
searchIcon: {
fontSize: 16,
},
input: {
flex: 1,
height: '100%',
fontSize: 15,
color: '#333',
paddingVertical: 0,
},
rightContainer: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
clearButton: {
width: 28,
height: 28,
borderRadius: 14,
backgroundColor: '#E0E0E0',
justifyContent: 'center',
alignItems: 'center',
},
clearIcon: {
fontSize: 18,
color: '#666',
fontWeight: 'bold',
},
voiceButton: {
width: 32,
height: 32,
justifyContent: 'center',
alignItems: 'center',
},
voiceIcon: {
fontSize: 18,
},
spinner: {
marginRight: 4,
},
});
3.2 搜索历史组件
typescript
/**
* 搜索历史组件
* 显示和管理用户的搜索历史
*/
import React, { useState, useEffect, useCallback } from 'react';
import {
View,
Text,
TouchableOpacity,
StyleSheet,
FlatList,
Alert,
} from 'react-native';
interface SearchHistoryProps {
visible: boolean;
history: string[];
onSelect: (query: string) => void;
onClear: () => void;
onDelete?: (query: string) => void;
}
export const SearchHistory: React.FC<SearchHistoryProps> = ({
visible,
history,
onSelect,
onClear,
onDelete,
}) => {
if (!visible || history.length === 0) {
return null;
}
/**
* 渲染历史项
*/
const renderItem = useCallback(({ item, index }: { item: string; index: number }) => (
<TouchableOpacity
style={historyStyles.item}
onPress={() => onSelect(item)}
onLongPress={() => {
if (onDelete) {
Alert.alert(
'删除历史',
`确定删除"${item}"吗?`,
[
{ text: '取消', style: 'cancel' },
{
text: '删除',
style: 'destructive',
onPress: () => onDelete(item),
},
]
);
}
}}
>
<View style={historyStyles.itemContent}>
<Text style={historyStyles.clockIcon}>🕐</Text>
<Text style={historyStyles.itemText} numberOfLines={1}>
{item}
</Text>
</View>
<TouchableOpacity
style={historyStyles.deleteButton}
onPress={() => onDelete?.(item)}
>
<Text style={historyStyles.deleteIcon}>×</Text>
</TouchableOpacity>
</TouchableOpacity>
), [onDelete, onSelect]);
return (
<View style={historyStyles.container}>
<View style={historyStyles.header}>
<Text style={historyStyles.title}>搜索历史</Text>
<TouchableOpacity onPress={onClear}>
<Text style={historyStyles.clearText}>清空</Text>
</TouchableOpacity>
</View>
<FlatList
data={history}
renderItem={renderItem}
keyExtractor={(item, index) => `${item}-${index}`}
scrollEnabled={false}
ItemSeparatorComponent={() => <View style={historyStyles.separator} />}
/>
</View>
);
};
const historyStyles = StyleSheet.create({
container: {
backgroundColor: '#fff',
marginTop: 12,
borderRadius: 12,
overflow: 'hidden',
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: '#F5F5F5',
},
title: {
fontSize: 14,
fontWeight: '600',
color: '#333',
},
clearText: {
fontSize: 13,
color: '#007AFF',
},
item: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingVertical: 12,
},
itemContent: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
},
clockIcon: {
fontSize: 14,
marginRight: 10,
},
itemText: {
flex: 1,
fontSize: 14,
color: '#666',
},
deleteButton: {
width: 28,
height: 28,
justifyContent: 'center',
alignItems: 'center',
},
deleteIcon: {
fontSize: 20,
color: '#999',
},
separator: {
height: 1,
backgroundColor: '#F5F5F5',
marginLeft: 40,
},
});
3.3 搜索建议组件
typescript
/**
* 搜索建议组件
* 实时显示搜索建议和热门搜索
*/
import React, { memo, useCallback } from 'react';
import {
View,
Text,
TouchableOpacity,
StyleSheet,
FlatList,
} from 'react-native';
interface SuggestionItem {
id: string;
text: string;
type: 'suggestion' | 'history' | 'hot';
highlight?: string;
}
interface SearchSuggestionsProps {
visible: boolean;
suggestions: SuggestionItem[];
hotSearches?: string[];
onSelect: (text: string) => void;
}
export const SearchSuggestions: React.FC<SearchSuggestionsProps> = memo(({
visible,
suggestions,
hotSearches = [],
onSelect,
}) => {
if (!visible) {
return null;
}
/**
* 渲染建议项
*/
const renderSuggestion = useCallback(({ item }: { item: SuggestionItem }) => {
return (
<TouchableOpacity
style={suggestionStyles.item}
onPress={() => onSelect(item.text)}
>
<Text style={suggestionStyles.typeIcon}>
{item.type === 'history' ? '🕐' : item.type === 'hot' ? '🔥' : '🔍'}
</Text>
<Text style={suggestionStyles.itemText} numberOfLines={1}>
{item.text}
</Text>
</TouchableOpacity>
);
}, [onSelect]);
/**
* 渲染热门搜索
*/
const renderHotSearches = () => {
if (hotSearches.length === 0) return null;
return (
<View style={suggestionStyles.section}>
<Text style={suggestionStyles.sectionTitle}>🔥 热门搜索</Text>
<View style={suggestionStyles.tagContainer}>
{hotSearches.map((item, index) => (
<TouchableOpacity
key={index}
style={[
suggestionStyles.tag,
index < 3 && suggestionStyles.tagHot,
]}
onPress={() => onSelect(item)}
>
<Text
style={[
suggestionStyles.tagText,
index < 3 && suggestionStyles.tagHotText,
]}
numberOfLines={1}
>
{item}
</Text>
</TouchableOpacity>
))}
</View>
</View>
);
};
return (
<View style={suggestionStyles.container}>
{renderHotSearches()}
{suggestions.length > 0 && (
<FlatList
data={suggestions}
renderItem={renderSuggestion}
keyExtractor={(item) => item.id}
scrollEnabled={false}
ListHeaderComponent={
hotSearches.length > 0 ? (
<View style={suggestionStyles.sectionHeader}>
<Text style={suggestionStyles.sectionTitle}>搜索建议</Text>
</View>
) : null
}
ItemSeparatorComponent={() => (
<View style={suggestionStyles.separator} />
)}
/>
)}
</View>
);
});
const suggestionStyles = StyleSheet.create({
container: {
backgroundColor: '#fff',
marginTop: 12,
borderRadius: 12,
overflow: 'hidden',
},
section: {
padding: 16,
},
sectionHeader: {
paddingHorizontal: 16,
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: '#F5F5F5',
},
sectionTitle: {
fontSize: 14,
fontWeight: '600',
color: '#333',
marginBottom: 12,
},
tagContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
},
tag: {
backgroundColor: '#F5F5F5',
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: 16,
},
tagHot: {
backgroundColor: '#FFF3E0',
},
tagText: {
fontSize: 13,
color: '#666',
},
tagHotText: {
color: '#E65100',
fontWeight: '500',
},
item: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 12,
},
typeIcon: {
fontSize: 14,
marginRight: 12,
},
itemText: {
flex: 1,
fontSize: 14,
color: '#333',
},
separator: {
height: 1,
backgroundColor: '#F5F5F5',
marginLeft: 40,
},
});
3.4 卡片样式搜索框
typescript
/**
* 卡片样式搜索框
* 带阴影效果的悬浮搜索框
*/
import React, { useState } from 'react';
import {
View,
TextInput,
Text,
TouchableOpacity,
StyleSheet,
} from 'react-native';
interface CardSearchBarProps {
placeholder?: string;
onSearch?: (query: string) => void;
filterButton?: boolean;
onFilterPress?: () => void;
}
export const CardSearchBar: React.FC<CardSearchBarProps> = ({
placeholder = '搜索...',
onSearch,
filterButton = true,
onFilterPress,
}) => {
const [query, setQuery] = useState('');
return (
<View style={cardStyles.container}>
<View style={cardStyles.searchCard}>
<Text style={cardStyles.searchIcon}>🔍</Text>
<TextInput
style={cardStyles.input}
value={query}
onChangeText={setQuery}
onSubmitEditing={() => onSearch?.(query)}
placeholder={placeholder}
placeholderTextColor="#999"
returnKeyType="search"
/>
{query.length > 0 && (
<TouchableOpacity
onPress={() => setQuery('')}
style={cardStyles.clearButton}
>
<Text style={cardStyles.clearIcon}>×</Text>
</TouchableOpacity>
)}
</View>
{filterButton && (
<TouchableOpacity
style={cardStyles.filterButton}
onPress={onFilterPress}
>
<Text style={cardStyles.filterIcon}>⚙️</Text>
</TouchableOpacity>
)}
</View>
);
};
const cardStyles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
},
searchCard: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#fff',
borderRadius: 16,
paddingHorizontal: 16,
paddingVertical: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 12,
elevation: 4,
},
searchIcon: {
fontSize: 16,
marginRight: 10,
},
input: {
flex: 1,
fontSize: 15,
color: '#333',
paddingVertical: 0,
},
clearButton: {
width: 28,
height: 28,
borderRadius: 14,
backgroundColor: '#F5F5F5',
justifyContent: 'center',
alignItems: 'center',
},
clearIcon: {
fontSize: 16,
color: '#999',
fontWeight: 'bold',
},
filterButton: {
width: 48,
height: 48,
backgroundColor: '#007AFF',
borderRadius: 16,
justifyContent: 'center',
alignItems: 'center',
shadowColor: '#007AFF',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 3,
},
filterIcon: {
fontSize: 18,
},
});
3.5 极简样式搜索框
typescript
/**
* 极简样式搜索框
* 无边框、简洁设计的搜索框
*/
import React, { useState } from 'react';
import {
View,
TextInput,
Text,
StyleSheet,
} from 'react-native';
interface MinimalSearchBarProps {
placeholder?: string;
onSearch?: (query: string) => void;
underline?: boolean;
}
export const MinimalSearchBar: React.FC<MinimalSearchBarProps> = ({
placeholder = '搜索',
onSearch,
underline = true,
}) => {
const [query, setQuery] = useState('');
const [isFocused, setIsFocused] = useState(false);
return (
<View style={minimalStyles.container}>
<View style={minimalStyles.inputRow}>
<Text style={minimalStyles.icon}>🔍</Text>
<TextInput
style={minimalStyles.input}
value={query}
onChangeText={setQuery}
onSubmitEditing={() => onSearch?.(query)}
placeholder={placeholder}
placeholderTextColor="#999"
returnKeyType="search"
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
/>
{query.length > 0 && (
<Text
style={minimalStyles.clear}
onPress={() => setQuery('')}
>
×
</Text>
)}
</View>
{underline && (
<View
style={[
minimalStyles.underline,
isFocused && minimalStyles.underlineFocused,
]}
/>
)}
</View>
);
};
const minimalStyles = StyleSheet.create({
container: {
paddingVertical: 8,
},
inputRow: {
flexDirection: 'row',
alignItems: 'center',
},
icon: {
fontSize: 18,
marginRight: 10,
color: '#999',
},
input: {
flex: 1,
fontSize: 16,
color: '#333',
paddingVertical: 4,
},
clear: {
fontSize: 20,
color: '#999',
marginLeft: 10,
paddingHorizontal: 8,
},
underline: {
height: 1,
backgroundColor: '#E5E5E5',
marginTop: 4,
},
underlineFocused: {
backgroundColor: '#007AFF',
height: 2,
},
});
3.6 使用示例
typescript
/**
* 搜索框组件使用示例
*/
import React, { useState, useCallback } from 'react';
import {
View,
StyleSheet,
ScrollView,
SafeAreaView,
} from 'react-native';
import { SearchBar } from './SearchBar';
import { SearchHistory } from './SearchHistory';
import { SearchSuggestions } from './SearchSuggestions';
import { CardSearchBar } from './CardSearchBar';
import { MinimalSearchBar } from './MinimalSearchBar';
const SearchBarDemo: React.FC = () => {
const [query, setQuery] = useState('');
const [showHistory, setShowHistory] = useState(true);
const [showSuggestions, setShowSuggestions] = useState(false);
const [history, setHistory] = useState(['React Native', 'HarmonyOS', 'TypeScript']);
const [suggestions, setSuggestions] = useState<any[]>([]);
const handleSearch = useCallback((text: string) => {
setQuery(text);
setShowHistory(false);
// 模拟获取建议
if (text.length > 0) {
setSuggestions([
{ id: '1', text: `${text} 教程`, type: 'suggestion' },
{ id: '2', text: `${text} 入门`, type: 'suggestion' },
{ id: '3', text: `${text} 实战`, type: 'suggestion' },
]);
setShowSuggestions(true);
} else {
setSuggestions([]);
setShowSuggestions(false);
}
}, []);
const handleSelectSuggestion = useCallback((text: string) => {
setQuery(text);
setShowSuggestions(false);
// 添加到历史
if (!history.includes(text)) {
setHistory([text, ...history].slice(0, 10));
}
}, [history]);
const handleClearHistory = useCallback(() => {
setHistory([]);
}, []);
const handleDeleteHistory = useCallback((text: string) => {
setHistory(history.filter(h => h !== text));
}, [history]);
return (
<SafeAreaView style={demoStyles.container}>
<ScrollView contentContainerStyle={demoStyles.content}>
<Text style={demoStyles.title}>搜索框样式</Text>
{/* 默认样式 */}
<View style={demoStyles.section}>
<Text style={demoStyles.sectionTitle}>默认样式</Text>
<SearchBar
placeholder="搜索"
onSearch={handleSearch}
showVoiceInput
/>
</View>
{/* 卡片样式 */}
<View style={demoStyles.section}>
<Text style={demoStyles.sectionTitle}>卡片样式</Text>
<CardSearchBar
placeholder="搜索..."
onSearch={handleSearch}
/>
</View>
{/* 极简样式 */}
<View style={demoStyles.section}>
<Text style={demoStyles.sectionTitle}>极简样式</Text>
<MinimalSearchBar
placeholder="搜索"
onSearch={handleSearch}
/>
</View>
{/* 搜索历史 */}
<SearchHistory
visible={showHistory && query.length === 0}
history={history}
onSelect={handleSelectSuggestion}
onClear={handleClearHistory}
onDelete={handleDeleteHistory}
/>
{/* 搜索建议 */}
<SearchSuggestions
visible={showSuggestions}
suggestions={suggestions}
hotSearches={['React Native', 'HarmonyOS', 'TypeScript', 'OpenHarmony']}
onSelect={handleSelectSuggestion}
/>
</ScrollView>
</SafeAreaView>
);
};
const demoStyles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F5F5F5',
},
content: {
padding: 16,
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#333',
marginBottom: 30,
textAlign: 'center',
},
section: {
marginBottom: 20,
},
sectionTitle: {
fontSize: 14,
color: '#999',
marginBottom: 8,
},
});
export default SearchBarDemo;
四、最佳实践
| 功能 | 实现方式 | 效果 |
|---|---|---|
| 防抖处理 | 300ms延迟搜索 | 减少不必要的请求 |
| 聚焦动画 | 背景色和阴影渐变 | 平滑的交互体验 |
| 搜索历史 | 本地存储最近搜索 | 快速重复搜索 |
| 热门搜索 | 标签云展示 | 发现热门内容 |
| 语音输入 | 集成语音识别 | 解放双手 |
五、总结
本文详细介绍了在HarmonyOS平台上实现搜索框的完整方案,涵盖:
- 多种样式:默认、卡片、极简三种风格
- 功能增强:搜索历史、实时建议、热门搜索
- 交互优化:聚焦动画、防抖处理、语音输入
- 用户体验:清晰的视觉反馈、流畅的动画
完整项目代码 :AtomGitDemos