搜索功能是音乐播放器中使用频率最高的功能之一。用户可以通过搜索快速找到想听的歌曲、歌手或专辑。本篇文章将详细介绍如何实现一个功能完善的搜索页面,包括搜索建议、热门搜索、搜索历史以及多类型搜索结果展示。
页面基础结构
搜索页面使用StatefulWidget,因为需要管理搜索框内容、搜索状态等多个状态变量。
import'package:flutter/material.dart';import'package:get/get.dart';classSearchPageextendsStatefulWidget{constSearchPage({super.key});@overrideState<SearchPage>createState()=>_SearchPageState();}页面继承自StatefulWidget,使用GetX进行路由管理。搜索页面的交互比较复杂,需要响应用户的输入和点击操作。
状态变量定义
搜索页面需要管理多个状态,包括输入控制器、Tab控制器和搜索状态标志。
class_SearchPageStateextendsState<SearchPage>withSingleTickerProviderStateMixin{finalTextEditingController_controller=TextEditingController();lateTabController_tabController;bool _showResult=false;_controller用于控制搜索输入框的内容,_tabController用于控制搜索结果的Tab切换,_showResult标志决定显示搜索建议还是搜索结果。混入SingleTickerProviderStateMixin是使用TabController的必要条件。
生命周期管理
在initState中初始化TabController,在dispose中释放所有控制器资源。
@overridevoidinitState(){super.initState();_tabController=TabController(length:5,vsync:this);}@overridevoiddispose(){_controller.dispose();_tabController.dispose();super.dispose();}TabController的length设置为5,对应单曲、歌手、专辑、歌单、MV五个搜索类型。dispose方法中同时释放输入控制器和Tab控制器,避免内存泄漏。
AppBar搜索框设计
搜索框直接放在AppBar的title位置,这是音乐类App常见的设计模式。
@overrideWidgetbuild(BuildContextcontext){returnScaffold(appBar:AppBar(title:TextField(controller:_controller,autofocus:true,style:constTextStyle(color:Colors.white),decoration:constInputDecoration(hintText:'搜索歌曲、歌手、专辑',hintStyle:TextStyle(color:Colors.white54),border:InputBorder.none,),TextField设置了autofocus为true,进入页面时自动弹出键盘。输入文字使用白色,提示文字使用半透明白色,与深色主题协调。border设置为none,让输入框与AppBar融为一体。
搜索提交处理
用户按下键盘确认键或点击搜索按钮时触发搜索。
onSubmitted:(v)=>setState(()=>_showResult=v.isNotEmpty),),actions:[TextButton(onPressed:()=>setState(()=>_showResult=_controller.text.isNotEmpty),child:constText('搜索',style:TextStyle(color:Color(0xFFE91E63))),),],),onSubmitted回调在用户按下键盘确认键时触发,actions区域放置搜索按钮。两种方式都会检查输入内容是否为空,不为空时切换到搜索结果视图。搜索按钮使用主题色,突出显示。
页面内容切换
根据_showResult状态决定显示搜索建议还是搜索结果。
body:_showResult?_buildSearchResult():_buildSearchSuggestion(),);}这种条件渲染的方式简洁明了。当用户还没有进行搜索时显示热门搜索和搜索历史,搜索后显示搜索结果列表。
搜索建议页面
搜索建议页面包含热门搜索和搜索历史两个部分。
Widget_buildSearchSuggestion(){returnSingleChildScrollView(padding:constEdgeInsets.all(16),child:Column(crossAxisAlignment:CrossAxisAlignment.start,children:[constText('热门搜索',style:TextStyle(fontSize:16,fontWeight:FontWeight.bold),),constSizedBox(height:12),使用SingleChildScrollView包裹整个内容,当内容超出屏幕时可以滚动。Column采用左对齐,标题使用粗体突出显示。
热门搜索标签
热门搜索使用Wrap组件实现流式布局的标签展示。
Wrap(spacing:8,runSpacing:8,children:List.generate(12,(i)=>GestureDetector(onTap:(){_controller.text='热门${i+1}';setState(()=>_showResult=true);},child:Chip(label:Text('热门${i+1}'),backgroundColor:constColor(0xFF1E1E1E),),),),),Wrap组件会自动换行,spacing和runSpacing分别设置水平和垂直间距。点击标签时会将标签内容填入搜索框并触发搜索。Chip组件使用深色背景,与整体主题一致。
搜索历史区域
搜索历史使用列表形式展示,方便用户快速选择之前搜索过的内容。
constSizedBox(height:24),constText('搜索历史',style:TextStyle(fontSize:16,fontWeight:FontWeight.bold),),constSizedBox(height:12),ListView.builder(shrinkWrap:true,physics:constNeverScrollableScrollPhysics(),itemCount:5,itemBuilder:(context,i)=>ListTile(leading:constIcon(Icons.history,color:Colors.grey),title:Text('历史搜索${i+1}'),trailing:constIcon(Icons.north_west,color:Colors.grey,size:16),),),],),);}ListView.builder设置shrinkWrap为true,让列表高度自适应内容。physics设置为NeverScrollableScrollPhysics,禁用列表自身的滚动,由外层SingleChildScrollView统一处理。每个历史记录前面有时钟图标,尾部的箭头图标表示点击可以填入搜索框。
搜索结果页面结构
搜索结果页面使用TabBar和TabBarView实现多类型结果切换。
Widget_buildSearchResult(){returnColumn(children:[TabBar(controller:_tabController,isScrollable:true,labelColor:constColor(0xFFE91E63),unselectedLabelColor:Colors.grey,indicatorColor:constColor(0xFFE91E63),tabs:const[Tab(text:'单曲'),Tab(text:'歌手'),Tab(text:'专辑'),Tab(text:'歌单'),Tab(text:'MV'),],),TabBar设置isScrollable为true,当Tab数量较多时可以横向滚动。选中的Tab使用主题色,未选中使用灰色。五个Tab分别对应不同类型的搜索结果。
TabBarView内容区域
TabBarView包含五个不同类型的搜索结果列表。
Expanded(child:TabBarView(controller:_tabController,children:[_buildSongList(),_buildArtistList(),_buildAlbumList(),_buildPlaylistList(),_buildMVGrid(),],),),],);}使用Expanded让TabBarView占据剩余空间。每个Tab对应一个构建方法,分别构建不同类型的结果列表。
单曲搜索结果
单曲列表展示搜索到的歌曲。
Widget_buildSongList(){returnListView.builder(itemCount:20,itemBuilder:(context,i)=>ListTile(leading:Text('${i+1}',style:constTextStyle(color:Colors.grey),),title:Text('搜索结果${i+1}'),subtitle:constText('歌手名'),trailing:constIcon(Icons.play_circle_outline,color:Color(0xFFE91E63),),),);}每首歌曲前面显示序号,中间显示歌曲名和歌手名,尾部是播放按钮。播放按钮使用主题色,点击可以直接播放歌曲。
歌手搜索结果
歌手列表使用圆形头像展示。
Widget_buildArtistList(){returnListView.builder(itemCount:10,itemBuilder:(context,i)=>ListTile(leading:CircleAvatar(backgroundColor:Colors.primaries[i%Colors.primaries.length].withOpacity(0.3),child:constIcon(Icons.person,color:Colors.white70),),title:Text('歌手${i+1}'),subtitle:Text('${(i+1)*100}首歌曲'),),);}CircleAvatar用于显示歌手头像,背景色根据索引变化。副标题显示歌手的歌曲数量,帮助用户了解歌手的作品规模。
专辑搜索结果
专辑列表使用方形封面展示。
Widget_buildAlbumList(){returnListView.builder(itemCount:10,itemBuilder:(context,i)=>ListTile(leading:Container(width:50,height:50,decoration:BoxDecoration(borderRadius:BorderRadius.circular(8),color:Colors.primaries[i%Colors.primaries.length].withOpacity(0.3),),child:constIcon(Icons.album,color:Colors.white70),),title:Text('专辑${i+1}'),subtitle:constText('歌手'),),);}专辑封面使用圆角矩形,与歌手的圆形头像形成区分。Container设置固定宽高,保证封面比例一致。
歌单搜索结果
歌单列表的样式与专辑类似,但图标不同。
Widget_buildPlaylistList(){returnListView.builder(itemCount:10,itemBuilder:(context,i)=>ListTile(leading:Container(width:50,height:50,decoration:BoxDecoration(borderRadius:BorderRadius.circular(8),color:Colors.primaries[i%Colors.primaries.length].withOpacity(0.3),),child:constIcon(Icons.queue_music,color:Colors.white70),),title:Text('歌单${i+1}'),subtitle:Text('${(i+1)*50}首'),),);}歌单使用queue_music图标,副标题显示歌单包含的歌曲数量。这种统一的列表样式让用户容易理解和操作。
MV搜索结果
MV使用网格布局展示,更适合视频类内容。
Widget_buildMVGrid(){returnGridView.builder(padding:constEdgeInsets.all(16),gridDelegate:constSliverGridDelegateWithFixedCrossAxisCount(crossAxisCount:2,childAspectRatio:1.5,crossAxisSpacing:12,mainAxisSpacing:12,),itemCount:10,itemBuilder:(context,i)=>Container(decoration:BoxDecoration(borderRadius:BorderRadius.circular(12),color:Colors.primaries[i%Colors.primaries.length].withOpacity(0.3),),child:constCenter(child:Icon(Icons.play_circle_filled,size:40,color:Colors.white70),),),);}GridView使用SliverGridDelegateWithFixedCrossAxisCount设置每行2个,childAspectRatio设置为1.5,让MV封面呈现横向矩形。每个MV卡片中间显示播放图标,点击可以播放MV。
搜索防抖处理
为了避免频繁请求接口,可以添加搜索防抖功能。
Timer?_debounceTimer;void_onSearchChanged(Stringvalue){_debounceTimer?.cancel();_debounceTimer=Timer(constDuration(milliseconds:500),(){if(value.isNotEmpty){_performSearch(value);}});}void_performSearch(Stringkeyword){setState(()=>_showResult=true);// 调用搜索接口}使用Timer实现防抖,用户停止输入500毫秒后才执行搜索。每次输入变化时先取消之前的定时器,避免重复请求。
清空搜索框
搜索框右侧可以添加清空按钮。
Widget_buildClearButton(){if(_controller.text.isEmpty)returnconstSizedBox.shrink();returnIconButton(icon:constIcon(Icons.clear,color:Colors.grey),onPressed:(){_controller.clear();setState(()=>_showResult=false);},);}只有当搜索框有内容时才显示清空按钮。点击后清空输入内容并返回搜索建议页面。SizedBox.shrink()返回一个零尺寸的Widget,不占用任何空间。
搜索历史管理
搜索历史可以使用SharedPreferences进行本地存储。
Future<void>_saveSearchHistory(Stringkeyword)async{finalprefs=awaitSharedPreferences.getInstance();List<String>history=prefs.getStringList('search_history')??[];history.remove(keyword);history.insert(0,keyword);if(history.length>20){history=history.sublist(0,20);}awaitprefs.setStringList('search_history',history);}新的搜索词插入到列表开头,如果已存在则先删除再插入,保证最新搜索的在最前面。限制历史记录最多20条,避免占用过多存储空间。
总结
搜索功能的实现涉及到多个Flutter组件的综合运用:TextField实现搜索输入、Wrap实现流式标签布局、TabBar和TabBarView实现多类型结果切换、ListView和GridView实现不同形式的列表展示。通过合理的状态管理和UI设计,为用户提供了流畅的搜索体验。在实际项目中,还需要对接后端搜索接口,实现真正的搜索功能。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net